Introduction to Qt

Introduction to qt

By Charles Calkins, OCI Principal Software Engineer

JUNE 2017

Introduction

Qt (pronounced "cute") is a cross-platform framework for creating applications. It was originally conceived in 1990, with its first public release in 1995 for Linux. Over time, it developed support for embedded devices, such as cell phones, through desktop platforms, such as Windows and Mac OS X. This article describes the basics of creating a basic calculator application using Qt that can run on multiple devices.

Obtaining Qt

Qt is available both in Open Source and commercial versions. While the Open Source version may be used to develop applications, a commercial license is required for the development of commercial software where compliance with the GNU Lesser Public License (LGPL) is not possible. The commercial distribution also provides more optimized code in certain circumstances, as well as additional Qt software components that are not available in the Open Source distribution.

Qt may be downloaded from https://www.qt.io/download/.

Two major flavors exist — Qt for desktop and mobile applications, and Qt for embedded devices. For this article, version 5.8 of Qt for desktop and mobile applications is used.

While the source code for Qt may be downloaded and compiled manually, distributions of Qt may be obtained via a unified installer or by downloading platform-specific archives.

Archives are selected by the host architecture, such as Windows or Linux, and then by the platform to compile for.

A compiler toolchain is known as a kit, and multiple kits may be installed on the same machine. For instance, distributions available for a Windows host at the time of this writing include 32- and 64-bit distributions for Visual Studio 2013 and 2015, and 32-bit distributions for MinGW, WinRT with Visual Studio 2013 and 2015, and Android. With Mac OS X as the host, distributions that use Clang are available for Mac OS X, as well as iOS development. Linux distributions may use compilers such as GCC.

The main integrated development environment for Qt is Qt Creator, provided as part of the distribution. It provides an editor and debugger, as well as a kit management system to configure and allocate kits to the projects that they will compile.

Qt also provides a large library of example code and an extensive help system. Qt Creator's editor also provides helpful code editing features, such as method refactoring, underlining of unused variables, and other features expected from a modern editor.

QML Calc

To illustrate Qt, in this article we'll develop a basic calculator, inspired by code written in Java by Fred Swartz.

Prior to relatively recent versions of Qt, a Qt application had to be created completely in C++, but with the development of QML, the Qt Meta (or Markup) Language, the user interface may be developed independently in a special scripting language.

UI constructs are declared in QML, and behavior is implemented in JavaScript, meaning a workable Qt application can be written almost entirely without the need for C++. Our first version of the calculator will be implemented in this manner.

In Qt Creator, a new QML-enabled project may be created by selecting New Project -> Application -> Qt Quick Application. For this example, we'll uncheck the checkbox in the project creation options and not generate a ui.qml file (useful for using a provided UI layout editor) in order to more easily view how QML operates. The QML skeleton code that is generated creates a clickable window containing a text field, as follows, in the file main.qml:

  1. import QtQuick 2.6
  2. import QtQuick.Window 2.2
  3.  
  4. Window {
  5. visible: true
  6. width: 640
  7. height: 480
  8. title: qsTr("Hello World")
  9.  
  10. MouseArea {
  11. anchors.fill: parent
  12. onClicked: {
  13. console.log(qsTr('Clicked on background. Text: "' + textEdit.text + '"'))
  14. }
  15. }
  16.  
  17. TextEdit {
  18. id: textEdit
  19. text: qsTr("Enter some text...")
  20. verticalAlignment: Text.AlignVCenter
  21. anchors.top: parent.top
  22. anchors.horizontalCenter: parent.horizontalCenter
  23. anchors.topMargin: 20
  24. Rectangle {
  25. anchors.fill: parent
  26. anchors.margins: -10
  27. color: "transparent"
  28. border.width: 1
  29. }
  30. }
  31. }

QML code begins with import statements, each typically naming a Qt module and the version number of the module that is being imported. A difference in recent versions of Qt, for example, are version 1 vs. version 2 controls. While version 1 controls are still available for use, version 2 controls are richer and more performant and have a somewhat different syntax than version 1 controls.

QML supports a number of GUI elements.

Window is one that represents a basic application window. QML, with respect to layout, is hierarchical; after properties such as size and visibility are set (using the property name : property value syntax), the next elements are GUI elements contained within the Window.

MouseArea represents a clickable area. GUI elements are positioned in various ways, with this one filling its parents by anchoring all four of its sides to the sides of its parent, using the .fill shortcut. So, no matter where one clicks in the Window, the onClicked handler will be invoked.

Handlers are implemented in JavaScript, either inline, as in this case, or by calling a JavaScript function defined elsewhere. Here, clicking causes a message to be logged to the console.

Qt supports internationalization via the call qsTr(), although internationalization is not fully realized in this code skeleton.

TextEdit element provides a means for text entry.

The id property is used to identify an element for referencing elsewhere — here, the id is set to textEdit so its text property, the contents of the edit field, may be logged to the console on a mouse click.

As with the MouseArea element, the TextEdit element is also positioned via anchors; but instead of filling its parent, it is set such that the top of the edit control is 20 pixels below the top of the parent window (via the top and topMargin anchor properties), and is centered horizontally.

The Rectangle within the TextEdit element allows the look of the TextEdit element to be customized. Here, the rectangle is as large as the TextEdit element itself (via the anchors.fill: parent setting), and is drawn with a 1 pixel outline and transparent background.

The skeleton code generation also creates a minimal C++ file to load the QML in main.cpp:

  1. #include <QGuiApplication>
  2. #include <QQmlApplicationEngine>
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6. QGuiApplication app(argc, argv);
  7.  
  8. QQmlApplicationEngine engine;
  9. engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
  10.  
  11. return app.exec();
  12. }

This code creates a QGuiApplication, loads the QML file into a QQmlApplicationEngine, and then starts the application message loop by calling app.exec().

For the calculator example, we shall leave the C++ code unchanged, but erase the contents of main.qml and start fresh. The source code for this article is available in the accompanying code archive, and is described below.

We begin by importing various QML modules, and as the skeleton code did, we create a Window:

  1. import QtQuick 2.6
  2. import QtQuick.Window 2.2
  3. import QtQuick.Controls 2.1
  4. import QtQuick.Layouts 1.3
  5.  
  6. Window {
  7. visible: true
  8. width: 640
  9. height: 480
  10. title: qsTr("Calculator")

Next, reminiscent of the original Java code, we create several variables to store the state of the calculator. We use the property syntax to define them as custom properties of the window.

  1. property double currentTotal: 0
  2. property bool startNumber: true
  3. property string displayField: "0"
  4. property string previousOp: "="
  5. property string error: qsTr("ERROR")

The behavior of the calculator is implemented by a series of JavaScript functions that parallel the original Java, such as:

  1. function applyOp(op) {
  2. if (startNumber) {
  3. clear();
  4. displayField = error; // expecting a number
  5. } else {
  6. startNumber = true;
  7. if (previousOp === "=")
  8. setTotal(displayField);
  9. else if (previousOp === "+")
  10. add(displayField);
  11. else if (previousOp === "-")
  12. subtract(displayField);
  13. else if (previousOp === "*")
  14. multiply(displayField);
  15. else if (previousOp === "/")
  16. divide(displayField);
  17.  
  18. displayField = getTotal().toString();
  19. previousOp = op;
  20. }
  21. }

Individual methods perform the calculator mathematics, with applyOp() called when a user enters an operator, applyDigit() invoked when the user enters a digit, and other methods as appropriate.

As we want the calculator to consist of a display where one may see digits entered and the results of calculations above a set of buttons, we use a ColumnLayout to place the display field above the buttons. The ColumnLayout fills the entire parent window (the Window), and the display anchors itself to the left and right of the parent window so it will expand and contract as the window does. It does not need to anchor itself to the top of the window as the ColumnLayout manages its vertical position. The font is increased from the default via the font properties.

  1. ColumnLayout {
  2. anchors.fill: parent
  3.  
  4. TextEdit {
  5. id: textEdit
  6. text: displayField
  7. verticalAlignment: Text.AlignVCenter
  8. horizontalAlignment: Text.AlignRight
  9. anchors.left: parent.left
  10. anchors.right: parent.right
  11.  
  12. color: "#000000"
  13. font.family: "Abel"
  14. font.pointSize: 16
  15. anchors.margins: 10
  16.  
  17. Rectangle {
  18. anchors.fill: parent
  19. color: "transparent"
  20. border.width: 1
  21. }
  22. }

Below the display are the calculator buttons, arranged by using a GridLayout. That is, the ColumnLayout aligns two UI elements, a TextEdit above a GridLayout. The GridLayout is set to have four rows and four columns, and it, too, is anchored to the left and right sides of the display.

  1. GridLayout {
  2. id: grid
  3. rows: 4
  4. columns: 4
  5. anchors.left: parent.left
  6. anchors.right: parent.right
  7.  
  8. GridButton {
  9. text: "CE"
  10. onClicked: clearEntry()
  11. }
  12.  
  13. GridButton {
  14. text: "C"
  15. onClicked: clear()
  16. }

The GridLayout contains 16 different GridButtons, where a GridButton is a custom element.

Custom elements may be defined by creating additional QML files that have the same name as the element that is being defined. In our case, the file GridButton.qml contains the following definition:

  1. import QtQuick 2.0
  2. import QtQuick.Controls 2.1
  3. import QtQuick.Layouts 1.3
  4.  
  5. Button {
  6. Layout.fillHeight: true
  7. Layout.fillWidth: true
  8.  
  9. contentItem: Text {
  10. renderType: Text.NativeRendering
  11. verticalAlignment: Text.AlignVCenter
  12. horizontalAlignment: Text.AlignHCenter
  13. font.family: "Abel"
  14. font.pointSize: (parent.width > 0) ? parent.width/5 : 14
  15. text: parent.text
  16. }
  17. }

The file is written to be self-contained in that it, too, begins with import statements and contains QML elements.

The Button element represents a pushbutton, and as it is placed in a GridLayout, has Layout-related properties that can be set.

The fillHeight and fillWidth properties allow the buttons to expand to fill the grid cells, so as the window changes size, so do the grid buttons.

The contentItem of the button is Text, as the buttons will display numbers, operators, or other symbols.

The pointSize of the text adjusts based on the window size, but care must be taken to not resize the window too rapidly as the author has seen instances where the application will crash if the window refreshes too quickly. If the pointSize is set to a constant value, the crashes do not occur.

Returning to main.qml, one sees that if every GridButton were the full Button definition, the code may become too complex and repetitive to maintain. Moving common settings out into user-defined objects makes the code much cleaner.

Properties not specified in common, such as the text property representing the characters to show on the button, are set on each button individually. Incidentally, QML supports Unicode, so special characters, such as the left arrow symbol, may be represented.

  1. GridButton {
  2. text: "\u2190" // "<-"
  3. onClicked: backspace()
  4. }

C++ Calc

While much may be done in QML with JavaScript, applications often need functionality that may not be provided by the Javascript implementation. QML may interact with C++ in a relatively straightforward way. In this example, we move the functionality that was implemented in JavaScript to C++ in order to demonstrate the interactivity.

Qt's support for C++ is extensive. In order to provide its multi-platform capability, Qt includes many classes to isolate the application from the underlying operating system. These types range from basic entities such as QString and QList through more complex ones that manage Bluetooth connectivity, socket and serial port access, database queries, and the like.

By using what Qt provides, as well as portable C++ constructs, an application may compile on any Qt-supported platform without modification.

We need to create a C++ object which may be referenced by QML, and as the base class for many Qt objects is QObject, we must inherit from it as well. The Q_OBJECT macro must also be specified so various Qt support functions may be declared. Since the object we need to create supports calculator functions, we'll call it Calc, defined in calc.h and calc.cpp.

  1. #ifndef CALC_H
  2. #define CALC_H
  3.  
  4. #include <QObject>
  5. #include <QQmlApplicationEngine>
  6. #include <QString>
  7.  
  8. class Calc : public QObject
  9. {
  10. Q_OBJECT

Only one variable is needed that must be in common between C++ and QML — the calculator display. It is defined as a Q_PROPERTY so the correct annotations may be made automatically to allow the field to be accessible.

Part of the build process of a Qt application runs a preprocessor that generates additional code in files prefixed by moc_ based on macros, such as these to provide the runtime type support, message passing, and QML connectivity.

  1. Q_PROPERTY(QString displayField READ displayField WRITE setDisplayField NOTIFY displayFieldChanged)

Q_PROPERTY need not always specify read, write, or notification methods. For example, a property may only have a read method and be declared CONSTANT if the property does not change during runtime of the application.

For this calculator example, the read and write methods are standard getters and setters implemented in calc.cpp, and displayFieldChanged() is a signal, a special Qt construct used for message passing, identified by the special signals: section in a class definition. A signal may be explicitly attached to a slot, a signal handler function, by the QObject::connect() method; or as happens here, is implicitly associated with QML where QML code that uses the property will automatically update when the signal is emitted. These methods are declared in calc.h as:

  1. QString _displayField;
  1. QString displayField() const;
  2. void setDisplayField(QString displayField);
  1. signals:
  2. void displayFieldChanged();

...and in calc.cpp as:

  1. QString Calc::displayField() const {
  2. return _displayField;
  3. }
  4.  
  5. void Calc::setDisplayField(QString displayField) {
  6. if (_displayField != displayField) {
  7. _displayField = displayField;
  8. emit displayFieldChanged();
  9. }
  10. }

The displayFieldChange() signal is not emitted (via the emit keyword) unless the value of the display field has actually changed.

While Q_PROPERTY serves to create a variable that is seen by QML, Q_INVOKABLE marks a method that is callable from QML.

In our example, all of the methods that may be executed by button presses are marked in this way in the class definition in calc.h.

  1. Q_INVOKABLE double getTotal() const;
  2. Q_INVOKABLE void setTotal(QString total);
  3. Q_INVOKABLE void clear();
  4. Q_INVOKABLE void clearEntry();
  5. Q_INVOKABLE void backspace();
  6. Q_INVOKABLE void applyOp(QString op);
  7. Q_INVOKABLE void negate();
  8. Q_INVOKABLE void applyDigit(QString digit);

In calc.cpp they are implemented in a corresponding way to their implementation in JavaScript:

  1. void Calc::applyOp(QString op) {
  2. if (_startNumber) {
  3. clear();
  4. setDisplayField(_error); // expecting a number
  5. } else {
  6. _startNumber = true;
  7. if (_previousOp == "=")
  8. setTotal(displayField());
  9. else if (_previousOp == "+")
  10. add(displayField());
  11. else if (_previousOp == "-")
  12. subtract(displayField());
  13. else if (_previousOp == "*")
  14. multiply(displayField());
  15. else if (_previousOp == "/")
  16. divide(displayField());
  17.  
  18. setDisplayField(QString("%1").arg(getTotal()));
  19. _previousOp = op;
  20. }
  21. }

To make a Calc object available to QML, it is instantiated and set as a property in main.cpp:

  1. Calc calc;
  2. engine.rootContext()->setContextProperty("calc", &calc);

calc may now be accessed from QML and its properties and methods referenced directly. Properties appear as variables, as with displayField used as the text contents of the TextEdit element in main.qml:

  1. TextEdit {
  2. id: textEdit
  3. text: calc.displayField

Methods are referenced in a similar manner, but contain a parameter list:

  1. GridButton {
  2. text: "CE"
  3. onClicked: calc.clearEntry()
  4. }

Qt provides additional methods for C++/QML interaction, such as for referencing C++ types and enumerations from QML or for passing more advanced data structures, but the above is sufficient for many applications.

Building on Multiple Platforms

In Qt Creator, clicking on the Build and Run Kit Selector button displays the kits configured for the current project. After selecting the kit to use and the type of build to create (debug, release or profiling), clicking the buttons below the selector will run, run with debugging, or just compile the project, respectively.

Most kits do not require additional configuration; but some, such as the Android kit, allow additional properties to be specified if the project is fully set up, such as a keystore that contains a key used to produce a signed release build.

The images below depict the kits configured on the author's Windows, Linux and Mac OS X build systems, respectively.

Windows

Windows

Linux

Mac OS X

The following images illustrate the result of running the calculator on various platforms.

  • The first is a screenshot of the Windows build machine running the calculator natively on the left, and from within a Linux virtual machine on the right, where the calculator is built for 64-bit Ubuntu.
  • The next two images are photographs of an Android tablet running the calculator in both landscape and portrait orientations (automatically handled without any source code changes).
  • The last image is a screenshot of the Mac OS X build machine, showing the calculator running natively and within the iOS simulator.

Native Windows and Linux

Android

Native Mac OS X and iOS

Each of these platforms is running the identical C++ calculator example without any platform-specific modifications to the code, which is what makes Qt an effective framework for multi-platform development.

Of course, although the application runs everywhere, some tweaks may be necessary for proper appearance. For example, while the button sizes and fonts scale based on the window size, the TextEdit field for the display has a fixed pixel size. As the display resolution is different for each platform, the point size that was chosen is not the best choice everywhere, and should be scalable as well. In particular, the display under Android is almost too small to be readable while it is tolerable elsewhere. Keeping in mind issues such as this will lead to fully usable applications across various devices.

Summary

Qt is a handy framework for writing multi-platform applications and may be a way to quickly deploy to multiple architectures. As illustrated above, the same code can build and run without change on multiple systems and devices, thus reducing development time and effort.

References

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret