In the previous post, we saw the difficulty in reading a file with QML. Most resources helpfully point out that you can do this in C++. Because if there’s one thing every programmer hopes for, it’s to use C++ more often.
There seem to be two ways you can do this: you can write your program in C++, with a main.cpp
and everything, and then load the QML to be the GUI, or you can write a plugin in C++ that you can call from your QML program. Since I’m one of those rare programmers that doesn’t love C++, we’re going to go the second way.
I don’t know how to write such an extension or how to integrate it into a QML program, but Ubuntu thoughtfully bundles an example, the QML Extension Library + Tabbed Touch UI, in the templates that should show me what to do. Let’s select that and … boy there are a bunch of files. Let’s take a look at the README.
There’s no README.
Well, let’s start opening files and reading the comments.
There’s no comments.
Okay, let’s just build and run the thing to see what it does. Then maybe I can work backwards to figure out which files are doing what. Let’s hit that green arrow and….
…
New plan—we’re going to ignore the templates and look at the Qt documentation.
Somehow, I found a tutorial on writing QML extensions with C++. (I say “somehow” because I had a heck of a time finding it a second time. It’s not linked from the examples and tutorials page as one might expect.) This is for Qt5.1, but it also works for Qt5.0, as we have in Ubuntu. (The equivalent Qt5.0 example is the first time I’ve ever see Mad-Libs documentation. They give you the text with blanks for you to fill in the code. Thanks. That’s ever so useful.) Against all odds, chapter 6 of the tutorial is actually very helpful, showing both how to create a plugin and how to use it from QML. I’ll basically repeat that information below, as I describe how to write a C++ plugin to read files.
The Object Class
It seems that you can only import objects, and not functions, into QML. You need to create a .h
and a .cpp
file for this class. The header file is basically the same as it would be for a normal Qt object, except that methods need to be prefixed with Q_INVOKABLE
to be made visible to QML.
#ifndef FILEREADER_H
#define FILEREADER_H
#include <QObject>
class FileReader : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE QByteArray read(const QString &filename);
};
#endif // FILEREADER_H
The source file is unchanged from what it would be otherwise.
#include "filereader.h"
#include <QFile>
QByteArray FileReader::read(const QString &filename)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly))
return QByteArray();
return file.readAll();
}
The Plugin Class
You need to make another class to represent the plugin itself. The header file is just boilerplate.
#ifndef FILEREADERPLUGIN_H
#define FILEREADERPLUGIN_H
#include <QQmlExtensionPlugin>
class FileReaderPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.github.rschroll.FileReader")
public:
void registerTypes(const char *uri);
};
#endif // FILEREADERPLUGIN_H
I have no idea what Q_PLUGIN_METADATA
is needed for. The examples all use this Java-esque reversed domain name thing, so I made one up here. No problems so far.
#include "filereaderplugin.h"
#include "filereader.h"
#include <qqml.h>
void FileReaderPlugin::registerTypes(const char *uri)
{
qmlRegisterType<FileReader>(uri, 1, 0, "FileReader");
}
The important thing here is the call to qmlRegisterType
, which makes a specific class, in this case FileReader
, available to QML. The second and third arguments specify the major and minor version numbers of the plugin, and the last argument is the name under which the class is available to QML. If you’re exposing multiple classes to QML, you’ll need a qmlRegisterType
for each.
Building
Qt uses the qmake build system, which is controlled by .pro
files. I don’t claim to understand it, but this works for me.
TEMPLATE = lib
CONFIG += plugin
QT += qml quick
DESTDIR = File
TARGET = filereaderplugin
OBJECTS_DIR = tmp
MOC_DIR = tmp
HEADERS += filereader.h filereaderplugin.h
SOURCES += filereader.cpp filereaderplugin.cpp
OTHER_FILES += app.qml
DESTDIR
specifies a directory into which the library files go, and TARGET
gives the name of those files. Temporary build fiels are put into OBJECTS_DIR
and MOC_DIR
. OTHER_FILES
is just to let Qt Creator know to consider that file as part of the project.
Now you can build your plugin with
$ qmake
$ make
and you should get a folder named File
with libfilereaderplugin.so
in it. To this directory, you need to add a file named qmldir
with these contents:
module File
plugin filereaderplugin
The first line must give the name of the directory, as specified in DESTDIR
, while the second line must give the name of the library file as specified in TARGET
. This is not built automatically because requiring humans to add boilerplate by hand is a key to reliable software development.
(Actually, you can specify additional options in the qmldir
file. The library files can be put in another directory, specified by the plugin
line, for example.)
Using in QML
Now, you can import the plugin with import File 1.0
and have a FileReader
class with a read
method for your use. Here’s an example QML file.
import QtQuick 2.0
import File 1.0
Item {
width: 300; height: 200
FileReader {
id: filereader
}
Text {
anchors.centerIn: parent
text: "Click to read file into console"
}
MouseArea {
anchors.fill: parent
onClicked: console.log(filereader.read('app.qml'))
}
}
Run the program with
$ qmlscene -I . app.qml
The -I .
argument tells it to look in the current directory for extensions to be included. Note that you specify the directory that is the parent of the directory containing the qmldir
file.
Use with Qt Creator
If you open the filereader.pro
file with Qt Creator, it will open the whole project for you. It will give you a configuration screen; just accept the defaults. Qt Creator likes to use “shadow builds”, wherein the build happens in a completely separate directory. I can see places where this would be useful, but here it would interfere with the QML file finding the libraries. Disable them by clicking on the Project tab and deselecting Shadow build under General.
Now if you click the run arrow, everything should build, but you’ll get that dialog I showed at the beginning of this post. For Command, enter qmlscene
. For Arguments, -I . app.qml
. With a bit of luck, everything will just work.
Opening binary files
Those of you with a good memory may remember the reason we’re doing all of this work is that we can’t open binary files with directly with QML. But if we try opening a binary file with FileReader
, we find that it gets corrupted.
Argh! It seems that QML doesn’t have an equivalent type for QByteArray
, so it gets converted into a string when it becomes enters QML space. I’m guessing that it becomes Unicode with a UTF-8 encoding or something, but that’s not what we want. I suspect there’s no way around this. We saw before that QML doesn’t have an arraybuffer type, so there’s probably nothing that can represent an arbitrary sequence of bytes. The only thing we can do is to base64 encode the file before returning it to QML. It’s straightforward to add a read_b64
method to FileReader
.
Here’s a tarball with the whole project, including the base64 encoding. Enjoy!