Weve been dabbling with Qt5 for some time now, without doing the one thing that Qt is famous for the GUIs. Now, were finally going to build that GUI in the easy and fun new way using QtQuick and QML. Well be building a fairly complex GUI with a bit of configuration headroom, without touching a single line of C++ code for the GUI logic.
Thats not to say we wont be using C++ at all - well use it to write a QML component that does the networking bits for us. In the bargain, youll also learn how to extend QML and QtQuick programs with C++ code. But first, heres a primer on QtQuick and QML.
QtQuick and QML
QtQuick made its debut in Qt4.7 but only became the recommended way of building GUIs in Qt5. Originally, QUICK stood for Qt User Interface Construction Kit, but now its just called QtQuick.
So what is it? Well, the answer comes in two parts. The first is QML (again, originally Qt Markup Language, but now just called QML), a declarative language very much like JavaScript which can be used to declaratively describe the GUI and its logic. Thats right; you build the GUI and script it using nothing but JavaScript. The second part is QtQuick itself, which is a set of components (or controls) that you can make use of in your QML based GUIs.
QtQuick and QML were first conceived as a new way of writing UIs for Qt applications. These were meant for people without much of a programming background as, back then, Qt was used to write applications for Nokias Maemo and then MeeGo (and even today, its used to write applications for Sailfish OS). QtQuick 1.0 was quickly hacked together and did its job well, but wasnt much of a performer. However, that didnt stop it from being used outside the mobile development space; it was used widely in KDEs Plasma Active and eventually in parts of Plasma 4 itself.
QtQuick 2.0, which came with Qt5, was a major overhaul. Because QtQuick became so popular with developers on both desktops and mobile devices, its creators had to make it a first-class way to build GUIs. So with Qt5, they redid the entire GUI rendering pipeline, making the whole thing 3D based, and using OpenGL as a backend. The downside to this is that QtQuick 2.0 requires OpenGL support to run, even if the GUI consists only of 2D widgets, as these widgets are internally implemented as OpenGL surfaces. The upside is that QtQuick 2.0 GUIs are extremely fast.
So how do you run QML applications on Qt5 on systems where theres no 3D support? Well, QtQuick 1.0 still ships with Qt5, so you can use that. Or you can drop to using standard C++ GUIs, which can fall back to 2D rendering if 3D isnt available on the system.
So that’s enough of theory. Lets build something.
Starting off
Open up QtCreator and start a new project. This time, choose Qt Quick Application (and not Qt Quick UI, which creates a QML-only application with no C++ code). Hit Choose, set the location and hit Next.
Now youll be presented with a drop-down list of component sets to use for the application. Youll need to use the latest available Qt Quick Controls (at the time of writing, this is Qt Quick Controls 1.3). Qt Quick Controls is a set of desktop application widgets implemented on top of raw Qt Quick 2 (which is also available if you want to use it). Once youre done, hit Next.
Run through the rest of the wizard. Make sure Desktop is selected when youre selecting the kits to use for the application. Check the summary, and then finish the wizard to get started with the code.
QML
At this point, youll be dropped into the editor with main.qml open. Youll be able to inspect this file to figure out what QML applications look like. But let me help you out.
At the top of the code, youll see a bunch of import statements, like in Python. These lines import the QML components that youll be using to write your applications. In the sample main.qml that opens up, the import lines are:
import QtQuick 2.4 import QtQuick.Controls 1.3 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2
So first, QtQuick 2 itself is imported, followed by a bunch of extensions, including Controls, which implement GUI widgets; Dialogs, which are used to create pop-up dialogue boxes, and Window, which is used to create the main application window in QML.
QML components can be written in QML (remember that its basically JavaScript; its actually powered by Googles V8, so its a full-blown programming language to work in) or C++. Were going to write a component in C++ to make the QML GUI talk to the fortune cookie server.
The rest of the code describes the GUI. Its very much like HTML. Theres one root element (in this case, an ApplicationWindow, which encapsulates the rest of the element hierarchy). Notice that all elements look exactly like JSON objects, with properties that can either be other objects (like strings or numbers) or JavaScript functions.
Once youve figured out what QML looks like (dont worry if you still dont understand how to write code yourself), were ready to start building our own GUI.
Building the GUI
The first thing you should do is delete the MainForm.ui.qml file in the Projects pane. We wont be needing it since well be writing the entire UI inside main.qml.
Now comes the difficult bit. main.qml in our program is 150 lines long, which is a tad too long to print out in this magazine. So were going to have to do it a little differently.
First of all, youre going to have to fetch all the source code for this article. Its available on my GitHub account, at https://github.com/BaloneyGeek/FortuneClientExample. Like the last article, this is a fully functioning application, so you can just clone the repository, build it and run it.
But lets see some of the code at least. We start with the imports:
import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.3 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 import Fortune 1.0
Were importing a whole bunch of QtQuick extensions, including Layouts, which well use to build the layout of the GUI (as the name suggests).
The last thing were importingFortune 1.0does not quite exist yet, so QtCreator is going to give it a red underline. Well build this component in C++ later.
The next few lines set up the application window, and populate the menu bar and the status bar. We wont reproduce the code in print, but its simple enough and one look will give you an idea of whats going on. What youll find interesting is that there are ampersand characters in some of the strings. This is to set up keyboard shortcutsthe letter right after the ampersand symbol becomes the keyboard shortcut for that item.
Then we come to the main UI form of the program. The UI features one giant label in the centre top part of the window displaying the fortune cookie, and one button at the centre bottom that can be clicked to attempt to get a new fortune cookie:
Item { id: mainForm; anchors.fill: parent; GridLayout { rows: 2; columns: 1; rowSpacing: 32; anchors.centerIn: parent; Label { id: mainFortuneLabel; Layout.maximumWidth: 600; Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; text: qsTr(Set a server, and click the Get Fortune button); wrapMode: Text.Wrap; horizontalAlignment: Text.AlignHCenter; font.pointSize: 36; function setText(mtext) { text = qsTr(mtext); horizontalAlignment = Text.AlignLeft; horizontalAlignment = Text.AlignHCenter; } } Button { id: getFortuneButton; Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; text: Get Fortune; onClicked: fortuneClient.getNewFortune(); } }
This implements the main form. Notice that we have a setText() function as a part of the main label to change its text. This will be triggered by the Fortune component when it obtains a new fortune cookie from the server.
Weve also had to reset the horizontal alignment every time we change the text. This is because every time the text is changed, the new text is rendered with a very weird alignment. Whether this is a bug or by design is unclear, but resetting the alignment seems to fix this.
The next few lines implement the server selection dialogue box. Again, this is fairly simple but somewhat long, and to save space, we wont reproduce it in print.
At this point, you might find QML overwhelminglets face it, weve already dealt with about 10 components and a lot more properties of each. The truth is, QMLs component sets are pretty huge, and to study each one of them in-depth before writing code is impractical. Hence thisan application in QML to get you started, and the component reference manuals to refer to as you go along, which are linked to at the end of the article.
This brings us to the one component that we must build ourselves, in order to make this GUI talk to the fortune server. This is what it looks like:
FortuneClient { id: fortuneClient; onHaveFortune: mainFortuneLabel.setText(fortune); function setServerPort() { serverHost = serverTextField.text; serverPort = portTextField.text; statusBarText.text = qsTr(OK: Server set to %1:%2.arg(serverHost).arg(serverPort)); } }
And to explain how it works will involve another theory class and a bit of C++ coding.
The QQuickItem and the QQmlApplicationEngine
To load a QML-based UI into a Qt application, you start by creating a master QApplication instance (like all other Qt applications), but then you create a QQmlApplicationEngine instance, into which you load up your QML file. This engine executes your QML script and takes care of all the plumbing for you.
In actual code, it looks something like this:
QApplication app; QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral(qrc:/main.qml)));
True, its just three lines. The engine provides a standard QtQuick environment, so all QML components that are installed on the users system are available for the programmer to use.
But what about custom components? Well need to use one, so lets figure out how to inject one into the QQmlApplicationEngine. It turns out that like the QApplication instance (which works at the global level and doesnt need to be touched), the QQmlApplicationEngine is also a global object, into which we can inject custom components at will.
Now what about those components themselves? Well, technically, any object that inherits from QObject and implements the meta-object system can be used as a QML component. Remember, QML components dont have to be visible GUI widgets onlyif you use the HTML analogy, some QML components are like <script> tags and are not rendered on the screen.
If you do want to create a widget, you need to inherit from QQuickPaintedItem. This base class has a lot of glue logic thats needed for painting on the screen already implemented, and its easier to get access to the OpenGL context through this.
If you dont need screen access (like us), you can simply derive from QObject or if you want some of the glue taken care of, you can derive from QQuickItem. QQuickPaintedItem derives from this and adds the screen real estate management logic, but does handle screen events; so if you want a component that triggers certain actions within the application based on GUI events (without being visible), you need to derive from QQuickItem. We wont need any of the special functionality when we implement the fortune client, but well derive from QQuickItem anyway.
The code
Lets begin. This is what the FortuneClient class looks like:
#include <QQuickItem> #include <QString> #include <QByteArray> #include <QTcpSocket> class FortuneClient : public QQuickItem { Q_OBJECT Q_PROPERTY(QString serverHost READ serverHost WRITE setServerHost) Q_PROPERTY(int serverPort READ serverPort WRITE setServerPort) private: quint16 mPort; QString mHost; QTcpSocket * clientSocket; public: FortuneClient(); ~FortuneClient(); QString serverHost() const; void setServerHost(QString host); quint16 serverPort() const; void setServerPort(int port); signals: void haveFortune(QString fortune); public slots: void getNewFortune(); };
Theres a lot thats important here.
Lets begin with the Q_PROPERTY macros, which tell the MOC to expose properties to the QML environment. The minimal syntax looks like this:
Q_PROPERTY(property_type property_name READ read_function WRITE write_function)
So now youll know that the first Q_PROPERTY line exposes a QString property, called serverHost, to the QML environment. To read this property, the serverHost() function is called, and to write to this property, the setServerHost() function is called.
Now lets take a look at how those functions are implemented. The reader function, serverHost(), is declared const and simply returns a QString. Thats all you need to do – declare your function const, which means telling the compiler this function doesnt change the state of the object, and return some data of the property type.
Now youll have to do a little hackery with integer data. QML has an integer data type, and C++ has many integer data types, depending on how many bits you want to use. When youre specifying a Q_PROPERTY, however, you can only use a standard int as its type, since all the different qint and quint types dont have equivalents in the QML environment. So your handler functions must take care to check ranges and cast them into properly-sized integers.
The other very important bit that you need to know is how the signal and slot mechanisms work with QML.
First of all, there are two ways in which you can execute a C++ function from within the QML environment. The first is to precede a function declaration with the Q_INVOKABLE macro, as follows:
Q_INVOKABLE void someFunction(QString some_arg);
This makes the function visible from the QML environment. All functions, even public ones, are not visible from the QML environment, due to security concerns.
The second way is to simply declare the function as a public slot. All public slots are visible from the QML environment. And thats what we have done here with getNewFortune()declared it as a slot.
Now for the signals. When you define signals that are to be accessed from the QML environment, you must stick to some naming conventions. You must use camelCase(), with the initial letter being lower case. This is because in the QML environment, the signal gets turned into a property, with the name onCamelCase, to which you assign a function, JavaScript or C++, which gets executed when the signal is emitted. For example, here we have a signal called haveFortune(), which becomes the property onHaveFortune inside the QML component, and gets assigned an event handler in the QML file, as follows:
onHaveFortune: mainFortuneLabel.setText(fortune);
We can now take a look at the C++ code for the object:
#include fortuneclient.h FortuneClient::FortuneClient() { mPort = 0; clientSocket = new QTcpSocket(this); } FortuneClient::~FortuneClient() { clientSocket->disconnectFromHost(); clientSocket->deleteLater(); } /* accessors and mutators for the properties go first */ QString FortuneClient::serverHost() const { return mHost; } void FortuneClient::setServerHost(QString host) { mHost = host; } quint16 FortuneClient::serverPort() const { return mPort; } void FortuneClient::setServerPort(int port) { mPort = (quint16)port; } /* the slot we call to get a new fortune */ void FortuneClient::getNewFortune() { QByteArray fortune; clientSocket->connectToHost(mHost, mPort, QIODevice::ReadOnly); clientSocket->waitForReadyRead(); fortune = clientSocket->readAll(); clientSocket->disconnectFromHost(); emit haveFortune(QString(fortune)); }
You already know how were using this inside QML.
Firing it up
Well need to write main.cpp first, which is luckily less than five lines of actual code. Here it is:
#include <QQmlApplicationEngine> #include fortuneclient.h int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; qmlRegisterType<FortuneClient>(Fortune, 1, 0, FortuneClient); engine.load(QUrl(QStringLiteral(qrc:/main.qml))); return app.exec(); }
The critical line here is the qmlRegisterType() statement, which registers a component and injects it into the QQmlApplicationEngine. The syntax is:
qmlRegisterType<object_name>(namespace, major_version, minor_version, component_name);
So for the above line, youd import the Fortune 1.0 name space, and create a FortuneClient object within the QML code.
You can now go ahead and build the application, and run it. Run the server that we built last month, and in the GUI, go to Application->Set Server Details; fill in the host (127.0.0.1) and the port (56789), and hit OK. The status bar should now say OK: Server set to 127.0.0.1:56789. Go ahead and hit the Get Fortune button. If everything works fine, youll be greeted with your fortune cookie.
References
[1] The QML Documentation – http://doc.qt.io/qt-5/qtqml-index.html
[2] The QML Complete Reference – http://doc.qt.io/qt-5/qmlreference.html
[3] The QtQuick Documentation – http://doc.qt.io/qt-5/qtquick-index.html
[4] The QtQuick QML Types Reference – http://doc.qt.io/qt-5/qtquick-qmlmodule.html