Qt5: GUIs with QtQuick and QML

0
12090

Business person pushing symbols on a touch screen interface_18471905_l

In the previous article on Qt5 (Part 2), we built a small server program that served a random fortune cookie to anyone who connected to it using TCP, and we tested it out by telneting into it. In the third and final part of this series, we’re going to build a small GUI client to go with the server.

We’ve been dabbling with Qt5 for some time now, without doing the one thing that Qt is famous for – the GUIs. Now, we’re finally going to build that GUI in the easy and fun new way using QtQuick and QML. We’ll be building a fairly complex GUI with a bit of configuration headroom, without touching a single line of C++ code for the GUI logic.
That’s not to say we won’t be using C++ at all –- we’ll use it to write a QML component that does the networking bits for us. In the bargain, you’ll also learn how to extend QML and QtQuick programs with C++ code. But first, here’s 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 it’s 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. That’s 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 Nokia’s Maemo and then MeeGo (and even today, it’s used to write applications for Sailfish OS). QtQuick 1.0 was quickly hacked together and did its job well, but wasn’t much of a performer. However, that didn’t stop it from being used outside the mobile development space; it was used widely in KDE’s 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 there’s 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 isn’t available on the system.
So that’s enough of theory. Let’s 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 you’ll be presented with a drop-down list of component sets to use for the application. You’ll 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 you’re done, hit Next.
Run through the rest of the wizard. Make sure Desktop is selected when you’re 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, you’ll be dropped into the editor with main.qml open. You’ll 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, you’ll see a bunch of import statements, like in Python. These lines import the QML components that you’ll 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 it’s basically JavaScript; it’s actually powered by Google’s V8, so it’s a full-blown programming language to work in) or C++. We’re 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. It’s very much like HTML. There’s 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 you’ve figured out what QML looks like (don’t worry if you still don’t understand how to write code yourself), we’re 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 won’t be needing it since we’ll 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 we’re going to have to do it a little differently.
First of all, you’re going to have to fetch all the source code for this article. It’s 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 let’s 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

We’re importing a whole bunch of QtQuick extensions, including Layouts, which we’ll use to build the layout of the GUI (as the name suggests).
The last thing we’re importing—Fortune 1.0—does not quite exist yet, so QtCreator is going to give it a red underline. We’ll 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 won’t reproduce the code in print, but it’s simple enough and one look will give you an idea of what’s going on. What you’ll find interesting is that there are ampersand characters in some of the strings. This is to set up keyboard shortcuts—the 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.
We’ve 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 won’t reproduce it in print.
At this point, you might find QML overwhelming—let’s face it, we’ve already dealt with about 10 components and a lot more properties of each. The truth is, QML’s component sets are pretty huge, and to study each one of them in-depth before writing code is impractical. Hence this—an 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, it’s just three lines. The engine provides a standard QtQuick environment, so all QML components that are installed on the user’s system are available for the programmer to use.
But what about custom components? We’ll need to use one, so let’s figure out how to inject one into the QQmlApplicationEngine. It turns out that like the QApplication instance (which works at the global level and doesn’t 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 don’t have to be visible GUI widgets only—if 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 that’s needed for painting on the screen already implemented, and it’s easier to get access to the OpenGL context through this.
If you don’t 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 won’t need any of the special functionality when we implement the fortune client, but we’ll derive from QQuickItem anyway.

Figure 1
Figure 1: Fortune cookie

The code
Let’s 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();
};

There’s a lot that’s important here.
Let’s 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 you’ll 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 let’s take a look at how those functions are implemented. The reader function, serverHost(), is declared const and simply returns a QString. That’s all you need to do – declare your function const, which means telling the compiler this function doesn’t change the state of the object, and return some data of the property type.
Now you’ll 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 you’re specifying a Q_PROPERTY, however, you can only use a standard int as its type, since all the different qint and quint types don’t 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 that’s 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 we’re using this inside QML.

Firing it up
We’ll 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, you’d 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, you’ll 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

LEAVE A REPLY

Please enter your comment!
Please enter your name here