Mastering Qt 5

4.7 (21 reviews total)
By Guillaume Lazar , Robin Penea
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Get Your Qt Feet Wet

About this book

Qt 5.7 is an application development framework that provides a great user experience and develops full-capability applications with Qt Widgets, QML, and even Qt 3D.

This book will address challenges in successfully developing cross-platform applications with the Qt framework. Cross-platform development needs a well-organized project. Using this book, you will have a better understanding of the Qt framework and the tools to resolve serious issues such as linking, debugging, and multithreading. Your journey will start with the new Qt 5 features.

Then you will explore different platforms and learn to tame them. Every chapter along the way is a logical step that you must take to master Qt. The journey will end in an application that has been tested and is ready to be shipped.

Publication date:
December 2016
Publisher
Packt
Pages
526
ISBN
9781786467126

 

Chapter 1.  Get Your Qt Feet Wet

If you know C++ but have never touched Qt, or if you have made some intermediate Qt applications, this chapter will ensure that your Qt foundations are safe before studying advanced concepts in the following chapters.

We will teach you to create a simple todo application using Qt Creator. This application will display a list of tasks that you can create/update/delete. We will cover the Qt Creator and Qt Designer interfaces, an introduction to the signal/slot mechanism, the creation of a custom widget with custom signals/slots, and its integration into your application.

You will implement a todo app using new C++14 semantics: lambdas, auto variables, and for loops. Each one of these concepts will be explained in depth and will be used throughout the book.

At the end of this chapter, you will be able to create a desktop application with a flexible UI using Qt widgets and new C++ semantics.

In this chapter, we will cover the following topics:

  • Qt project basic structure

  • Qt Designer interface

  • UI fundamentals

  • Signals and slots

  • Custom QWidget

  • C++14 lambda, auto, for each

 

Creating a project


The first thing to do is to start Qt Creator.

In Qt Creator, you can create a new Qt project via File | New File or Project | Application | Qt Widgets Application | Choose.

The wizard will then guide you through four steps:

  1. Location: You must choose a project name and a location.

  2. Kits: Target platforms that your project aims at (Desktop, Android, and so on).

  3. Details: Base class information and name for the generated class.

  4. Summary: Allows you to configure your new project as a subproject and automatically add it to a version control system.

Even if all default values can be kept, please at least set a useful project name such as "todo" or "TodoApp." We won't blame you if you want to call it "Untitled" or "Hello world."

Once done, Qt Creator will generate several files that you can see in the Projects hierarchy view:

The .pro file is Qt's configuration project file. As Qt adds specific file formats and C++ keywords, an intermediate build step is performed, parsing all files to generate final files. This process is done by qmake, an executable from the Qt SDK. It will also generate the final Makefiles for your project.

A basic .pro file generally contains:

  • Qt modules used (core, gui, and so on)

  • Target name (todo, todo.exe, and so on)

  • Project template (app, lib, and so on)

  • Sources, headers, and forms

There are some great features that come with Qt and C++14. This book will showcase them in all its projects. For GCC and CLANG compilers, you must add CONFIG += c++14 to the .pro file to enable C++14 on a Qt project, as shown in the following code:

QT       += core gui 
CONFIG   += c++14 
 
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 
 
TARGET = todo 
TEMPLATE = app 
 
SOURCES += main.cpp \ 
           MainWindow.cpp 
 
HEADERS  += MainWindow.h \ 
 
FORMS    += MainWindow.ui \ 

The MainWindow.h and MainWindow.cpp files are the headers/sources for the MainWindow class. These files contain the default GUI generated by the wizard.

The MainWindow.ui file is your UI design file in XML format. It can be edited more easily with Qt Designer. This tool is a WYSIWYG (What You See Is What You Get) editor that helps you to add and adjust your graphical components (widgets).

Here is the main.cpp file, with its well-known function:

#include "MainWindow.h" 
#include <QApplication> 
 
int main(int argc, char *argv[]) 
{ 
    QApplication a(argc, argv); 
    MainWindow w; 
    w.show(); 
 
    return a.exec(); 
} 

As usual, the main.cpp file contains the program entry point. It will, by default, perform two actions:

  • Instantiate and show your main window

  • Instantiate a QApplication and execute the blocking main event loop

This is the bottom-left toolbar for Qt Creator:

Use it to build and start your todo application in debug mode:

  1. Check that the project is in Debug build mode.

  2. Use the hammer button to build your project.

  3. Start debugging using the green Play button with a little blue bug.

You will discover a wonderful and beautifully empty window. We will rectify this after explaining how this MainWindow is constructed:

An empty MainWindow screenshot

Tip

Qt tip

  • Press Ctrl + B (for Windows/Linux) or Command + B (for Mac) to build your project

  • Press F5 (for Windows / Linux) or Command +R (for Mac) to run your application in debug mode

 

MainWindow structure


This generated class is a perfect yet simple example of Qt framework usage; we will dissect it together. As mentioned previously, the MainWindow.ui file describes your UI design and MainWindow.h/ MainWindow.cpp is the C++ object where you can manipulate the UI with code.

It is important to take a look at the header file MainWindow.h. Our MainWindow object inherits from Qt's QMainWindow class:

#include <QMainWindow> 
 
namespace Ui { 
class MainWindow; 
} 
 
class MainWindow : public QMainWindow 
{ 
    Q_OBJECT 
 
public: 
    explicit MainWindow(QWidget *parent = 0); 
    ~MainWindow(); 
private: 
    Ui::MainWindow *ui; 
}; 

As our class inherits from the QMainWindow class, on top of the header file, we add the corresponding include. The second part is the forward declaration of the Ui::MainWindow, as we only declare a pointer.

The Q_OBJECT can look a little strange to a non-Qt developer. This macro allows the class to define its own signals/slots and more globally Qt's meta-object system. These features will be covered later in this chapter.

This class defines a public constructor and destructor. The latter is pretty common. But the constructor takes a parameter parent. This parameter is a QWidget pointer that is null by default.

A QWidget is a UI component. It can be a label, a textbox, a button, and so on. If you define a parent-child relationship between your window, layout, and other UI widgets, memory management of your application will be easier. Indeed, in this case, deleting the parent is enough because its destructor will take care of also deleting its child, which in turn will delete its children and so on.

Our MainWindow class extends QMainWindow from the Qt framework. We have a ui member variable in the private fields. The type is a pointer of Ui::MainWindow, which is defined in the ui_MainWindow.h file generated by Qt. It's the C++ transcription of the UI design file MainWindow.ui. The ui member variable will allow you to interact with your UI components (QLabel, QPushButton, and so on) from C++, as shown in the following figure:

Tip

C++ tip

If your class only uses pointers or references for a class type, you can avoid including the header by using forward declaration. That will drastically reduce compilation time.

Now that the header part is done, we can talk about the MainWindow.cpp source file.

In the following code snippet, the first include is our class header. The second one is the include required by the generated class Ui::MainWindow. This include is required as we only use a forward declaration in the header:

#include "MainWindow.h" 
#include "ui_MainWindow.h" 
 
MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 

In many cases, Qt generates a good piece of code using the initializer list. The parent argument is used to call the superclass constructor QMainWindow. Our private member variable ui is also initialized now.

Now that ui is initialized, we must call the setupUi function to initialize all widgets used by the MainWindow.ui design file:

As we initialize a pointer in the constructor, it must be cleaned in the destructor:

MainWindow::~MainWindow() 
{ 
    delete ui; 
} 
 

Qt Designer


Qt Designer is a major tool for developing Qt applications. This WYSIWYG editor will help you easily design your GUI. If you switch between Edit mode and Design mode for the MainWindow.ui file, you will see the real XML content and the designer:

The designer displays several parts:

  • Form Editor: This is a visual representation of the form (empty for now)

  • Widget Box: This contains all widgets that can be used with your form

  • Object Inspector: This displays your form as a hierarchical tree

  • Property Editor: This enumerates the properties of the selected widget

  • Action Editor/Signal & Slots Editor: This handles connections between your objects

It's time to embellish this empty window! Let's drag and drop a Label widget from the Display Widgets section on the form. You can change the name and the text properties from the properties editor.

As we are making a todo application, we suggest these properties:

  • objectName: statusLabel

  • text: Status: 0 todo/0 done

This label will later display the count of todo tasks and the count of tasks already done. OK, save, build, and start your application. You should now see your new label in the window.

You can now add a push button with those properties:

  • objectName: addTaskButton

  • text: Add task

You should get a result close to this:

Tip

Qt tip

You can edit the text property of a widget directly on your form by double-clicking on it!

 

Signals and slots


The Qt framework brings a flexible message exchange mechanism through three concepts: signals, slots, and connections:

  • A signal is a message sent by an object

  • A slot is a function that will be called when this signal is triggered

  • The connect function specifies which signal is linked to which slot

Qt already provides signals and slots for its classes, which you can use in your application. For example, QPushButton has a signal clicked(), which will be triggered when the user clicks on the button. The QApplication class has a slot quit() function, which can be called when you want to terminate your application.

Here is why you will love Qt signals and slots:

  • A slot remains an ordinary function, so you can call it yourself

  • A single signal can be linked to different slots

  • A single slot can be called by different linked signals

  • A connection can be made between a signal and a slot from different objects, and even between objects living inside different threads!

Keep in mind that, to be able to connect a signal to a slot, their methods' signatures must match. The count, order, and type of arguments must be identical. Note that signals and slots never return values.

This is the syntax of a Qt connection:

connect(sender, &Sender::signalName,  
    receiver, &Receiver::slotName); 

The first test that we can do to use this wonderful mechanism is to connect an existing signal with an existing slot. We will add this connect call to the MainWindow constructor:

MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 
    connect(ui->addTaskButton, &QPushButton::clicked, 
    QApplication::instance(), &QApplication::quit); 
} 

Let's analyze how a connection is done:

  • sender: This is the object that will send the signal. In our example, it is the QPushButton named addTaskButton added from the UI designer.

  • &Sender::signalName: This is the pointer to the member signal function. Here, we want do something when the clicked signal is triggered.

  • receiver: This is the object that will receive and handle the signal. In our case, it is the QApplication object created in main.cpp.

  • &Receiver::slotName: This is a pointer to one of the receiver's member slot functions. In this example, we use the built-in quit() slot from Qapplication, which will exit the application.

You can now compile and run this short example. You will terminate the application if you click on the addTaskButton of your MainWindow.

Tip

Qt tip

You can connect a signal to another signal. The second signal will be emitted when the first one is triggered.

Now that you know how to connect a signal to an existing slot, let's see how to declare and implement a custom addTask() slot in our MainWindow class. This slot will be called when the user clicks on ui->addTaskButton.

This is the updated MainWindow.h:

class MainWindow : public QMainWindow 
{ 
    Q_OBJECT 
 
public: 
    explicit MainWindow(QWidget *parent = 0); 
    ~MainWindow(); 
 
public slots: 
    void addTask(); 
 
private: 
    Ui::MainWindow *ui; 
}; 

Qt uses a specific slot keyword to identify slots. Since a slot is a function, you can always adjust the visibility (public, protected or private) depending on your needs.

Add this slot implementation in the MainWindow.cpp file:

void MainWindow::addTask() 
{ 
    qDebug() << "User clicked on the button!"; 
} 

Qt provides an efficient way of displaying debug information with the QDebug class. An easy way to obtain a QDebug object is to call the qDebug() function. Then, you can use the stream operator to send your debug information.

Update the top of the file like this:

#include <QDebug> 
 
MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 
    connect(ui->addTaskButton, &QPushButton::clicked, 
    this, &MainWindow::addTask); 
} 

Since we now use qDebug() in out slot, we must include <QDebug>. The updated connect now calls our custom slot instead of quitting the application.

Build and run the application. If you click on the button, you will see your debug message inside the Qt Creator Application Output tab.

 

Custom QWidget


We now have to create the Task class that will hold our data (task name and completed status). This class will have its form file separated from MainWindow. Qt Creator provides an automatic tool to generate a base class and the associated form.

Click on File | New File or Project | Qt | Qt Designer Form Class. There are several form templates; you will recognize Main Window, which Qt Creator created for us when we started the todo app project. Select Widget and name the class Task, then click on Next. Here is a summary of what Qt Creator will do:

  1. Create a Task.h file and a Task.cpp file.

  2. Create the associated Task.ui and do the plumbing to connect it to Task.h.

  3. Add these three new files to todo.pro so they can be compiled.

Finish and, voilà, the Task class is ready to be filled. We will jump into the Task.ui first. Start by dragging and dropping a Check Box (put checkbox in the objectName) and a Push Button (objectName = removeButton):

My alignment looks great, let's ship this to the customers!

Unless you have a pixel-perfect eye, your items are not very well aligned. You need to indicate how your widgets should be laid out and how they should react when the window geometry changes (for example, when the user resizes the window). For this, Qt has several default layout classes:

  • Vertical Layout: In this layout, widgets are vertically stacked

  • Horizontal Layout: In this layout, widgets are horizontally stacked

  • Grid Layout: In this layout, widgets are arranged in a grid that can be subdivided into smaller cells

  • Form Layout: In this layout, widgets are arranged like a web form, a label, and an input

Every layout will try to constrain all widgets to occupy equal surfaces. It will either change the widgets' shape or add extra margins, depending on each widget's constraints. A Check Box will not be stretched but a Push Button will.

In our Task object, we want this to be horizontally stacked. In the Form Editor tab, right-click on the window and select Lay out | Lay out Horizontally. Each time you add a new widget in this layout, it will be arranged horizontally.

Now add a Push Button (objectName = editButton) line just after the checkbox object.

The Form Editor window offers a realistic preview of how your UI will render. If you stretch the window now, you can observe how each widget will react to this event. When resizing horizontally, you can note that the push buttons are stretched. It looks bad. We need something to "hint" to the layout that these buttons should not be stretched. Enter the Spacer widget. Take the Horizontal Spacer in the widget box and drop it after the checkbox object:

A spacer is a special widget that tries to push (horizontally or vertically) adjacent widgets to force them to take up as little space as possible. The editButton and removeButton objects now take up only the space of their text and will be pushed to the edge of the window when it is resized.

You can add sub layouts of any type in a form (vertical, horizontal, grid, form) and create a complex-looking application with a combination of widgets, spacers, and layouts. These tools are targeted at designing a good-looking desktop application that can react properly to different window geometries.

The Designer part is finished, so we can switch to the Task source code. Since we created a Qt Designer Form class, Task is closely linked to its UI. We will use this as leverage to store our model in a single place. When we create a Task object, it has to have a name:

#ifndef TASK_H 
#define TASK_H 
 
#include <QWidget> 
#include <QString> 
 
namespace Ui { 
class Task; 
} 
 
class Task : public QWidget 
{ 
    Q_OBJECT 
 
public: 
    explicit Task(const QString& name, QWidget *parent = 0); 
    ~Task(); 
 
    void setName(const QString& name); 
    QString name() const; 
    bool isCompleted() const; 
     
private: 
    Ui::Task *ui; 
}; 
 
#endif // TASK_H 

The constructor specifies a name, and as you can see there are no private fields storing any state of the object. All this will be done in the form part. We also added some getters and setters that will interact with the form. It is better to have a model completely separated from the UI, but our example is simple enough to merge them. Moreover, Task implementation details are hidden from the outside world and can still be refactored later on. Here is the content of the Task.cpp file:

#include "Task.h" 
#include "ui_Task.h" 
 
Task::Task(const QString& name, QWidget *parent) : 
        QWidget(parent), 
        ui(new Ui::Task) 
{ 
    ui->setupUi(this); 
    setName(name); 
} 
 
Task::~Task() 
{ 
    delete ui; 
} 
 
void Task::setName(const QString& name) 
{ 
    ui->checkbox->setText(name); 
} 
 
QString Task::name() const 
{ 
    return ui->checkbox->text(); 
} 
 
bool Task::isCompleted() const 
{ 
   return ui->checkbox->isChecked(); 
} 

The implementation is straightforward; we store the information in our ui->checkbox and the name() and isCompleted() getters take their data from the ui->checkbox.

 

Adding a task


We will now rearrange the layout of MainWindow to be able to display our todo tasks. Right now, there is no widget where we can display our tasks. Open the MainWindow.ui file and edit it to get the following result:

If we detail the content, we have:

  • A vertical layout for centralWidget containing the toolbarLayout file and the tasksLayout file.

  • A vertical spacer pushing these layouts to the top, forcing them to take up the smallest possible space.

  • We got rid of menuBar, mainToolBar, and statusBar. Qt Creator created them automatically, we simply don't need them for our purposes. You can guess their uses from their names.

Do not forget to rename the MainWindow title to Todo by selecting the MainWindow in the Object Inspector window and editing the Qwidget | windowTitle property. Your app deserves to be named properly.

Tip

Qt Tip

Press Shift + F4 in Designer mode to switch between the form editor and the source.

Now that the MainWindow UI is ready to welcome tasks, let's switch to the code part. The application has to keep track of the new tasks. Add the following in the MainWindow.h file:

#include <QVector> 
 
#include "Task.h" 
 
class MainWindow : public QMainWindow 
{ 
    // MAINWINDOW_H 
 
public slots: 
    void addTask(); 
 
private: 
    Ui::MainWindow *ui; 
    QVector<Task*> mTasks; 
}; 

The QVector is the Qt container class providing a dynamic array, which is an equivalent of the std::vector. As a general rule, STL containers are more customizable but might miss some features compared to Qt containers. If you use C++11 smart pointers, you should favor std containers, but we will get into that later.

In the Qt documentation of QVector, you might stumble upon the following statement: "For most purposes, QList is the right class to use". There is a debate about this in the Qt community:

  • Do you often need to insert objects larger than a pointer at the beginning or in the middle of your array? Use a QList class.

  • Need contiguous memory allocation? Less CPU and memory overhead? Use a QVector class.

The already added slot addTask() will now be called each time we want to add a new Task object to the mTasks function.

Let's fill our QVector tasks each time addTaskButton is clicked. First, we connect the clicked() signal in the MainWindow.cpp file:

MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow), 
    mTasks() 
{ 
    ui->setupUi(this); 
    connect(ui->addTaskButton, &QPushButton::clicked,  
    this, &MainWindow::addTask); 
}; 

Tip

C++ tip

As a best practice, try to always initialize member variables in the initializer list and respect the order of variable declarations. Your code will run faster and you will avoid unnecessary variable copies. Take a look at the standard C++ documentation at https://isocpp.org/wiki/faq/ctors#init-lists.

The body of the addTask() function should look like this:

void MainWindow::addTask() 
{ 
        qDebug() << "Adding new task"; 
        Task* task = new Task("Untitled task"); 
        mTasks.append(task); 
        ui->tasksLayout->addWidget(task); 
} 

We created a new task and added it to our mTask vector. Because Task is a QWidget, we also added it directly to the tasksLayout. An important thing to note here is that we never managed this new task's memory. Where is the delete task instruction? This is a key feature of the Qt Framework we started to broach earlier in the chapter; the QObject class parenting automatically handles object destruction.

In our case, the ui->tasksLayout->addWidget(task) call has an interesting side-effect; the ownership of the task is transferred to tasksLayout. The QObject* parent defined in Task constructor is now tasksLayout, and the Task destructor will be called when tasksLayout releases its own memory by recursively iterating through its children and calling their destructor.

This will happen at this precise moment:

MainWindow::~MainWindow() 
{ 
    delete ui; 
} 

When MainWindow is released (remember, it's a stack variable allocated in the main.cpp file), it will call delete ui, which in turn will bring down the whole QObject hierarchy. This feature has interesting consequences. First, if you use the QObject parenting model in your application, you will have much less memory to manage. Second, it can collide with some new C++11 semantics, specifically the smart pointers. We will get into that in later chapters.

 

Using a QDialog


We deserve something better than an untitled task. The user needs to define its name when it's created. The easiest path would be to display a dialog where the user can input the task name. Fortunately Qt offers us a very configurable dialog that fits perfectly in addTask():

#include <QInputDialog> 
... 
void MainWindow::addTask() 
{ 
    bool ok; 
    QString name = QInputDialog::getText(this,  
        tr("Add task"), 
        tr("Task name"), 
        QLineEdit::Normal, 
        tr("Untitled task"),               &ok); 
    if (ok && !name.isEmpty()) { 
        qDebug() << "Adding new task"; 
        Task* task = new Task(name); 
        mTasks.append(task); 
        ui->tasksLayout->addWidget(task); 
    } 
} 

The QinputDialog::getText function is a static blocking function that displays the dialog. When the user validates/cancels the dialog, the code continues. If we run the application and try to add a new task, we'll see this:

The QInputDialog::getText signature looks like this:

QString QinputDialog::getText( 
  QWidget* parent,  
      const QString& title,  
      const QString& label,  
      QLineEdit::EchoMode mode = QLineEdit::Normal,  
      const QString& text = QString(),  
      bool* ok = 0, ...)

Let's break it down:

  • parent: This is the parent widget (MainWindow) to which the QinputDialog is attached. This is another instance of the QObject class's parenting model.

  • title: This is the title displayed in the window title. In our example, we use tr("Add task"), which is how Qt handles i18n in your code. We will see later on how to provide multiple translations for a given string.

  • label: This is the label displayed right above the input text field.

  • mode: This is how the input field is rendered (password mode will hide the text).

  • ok: This is a pointer to a variable that is set to true if the user presses OK and to false if the user presses Cancel.

  • QString: The returned QString is what the user has typed.

There are a few more optional parameters we can safely ignore for our example.

 

Distributing code responsibility


Great, the user can now specify the task name when it's created. What if he makes an error when typing the name? The next logical step is to rename the task after we created it. We'll take a slightly different approach. We want our Task to be as autonomous as possible. If we attach it to another component (rather than MainWindow), this renaming feature has to keep working. Thus, this responsibility has to be given to the Task class:

// In Task.h 
public slots: 
    void rename(); 
 
// In Task.cpp 
#include <QInputDialog> 
 
Task::Task(const QString& name, QWidget *parent) : 
       QWidget(parent), 
       ui(new Ui::Task) 
{ 
   ui->setupUi(this); 
   setName(name); 
   connect(ui->editButton, &QPushButton::clicked, this, &Task::rename); 
} 
... 
void Task::rename() 
{ 
    bool ok; 
    QString value = QInputDialog::getText(this, tr("Edit task"), 
                                          tr("Task name"), 
                                          QLineEdit::Normal, 
                                          this->name(), &ok); 
    if (ok && !value.isEmpty()) { 
        setName(value); 
    } 
} 

We add a public slot rename() to connect it to a signal. The body of rename() reuses what we had previously covered with QInputDialog. The only difference is the QInputDialog default value, which is the current task name. When setName(value) is called, the UI is instantly refreshed with the new value; there's nothing to synchronize or update, the Qt main loop will do its job.

The nice thing is that Task::rename() is completely autonomous. Nothing has been modified in MainWindow, so we have effectively zero coupling between our Task and the parent QWidget.

 

Emitting a custom signal using lambdas


The remove task is straightforward to implement, but we'll study some new concepts along the way. The Task has to notify its owner and parent (MainWindow) that the removeTaskButtonQPushButton has been clicked. We'll implement this by defining a custom signal removed in Task.h files:

class Task : public QWidget 
{ 
    ... 
public slots: 
    void rename(); 
signals: 
    void removed(Task* task); 
   ... 
}; 

Like we did for the slots, we have to add the Qt keyword signals in our header. Since a signal is used only to notify another class, the public keyword is not needed (it even raises a compilation error). A signal is simply a notification sent to the receiver (the connected slot); it implies that there is no function body for the removed(Task* task) function. We added the task parameter to allow the receiver to know which task asked to be removed. The next step is to emit the removed signal upon the removeButton click. This is done in the Task.cpp file:

Task::Task(const QString& name, QWidget *parent) : 
        QWidget(parent), 
        ui(new Ui::Task) 
{ 
    ui->setupUi(this); 
    ... 
    connect(ui->removeButton, &QPushButton::clicked, [this] { 
        emit removed(this); 
    }); 
} 

This code excerpt shows a very interesting feature of C++11: lambdas. In our example, the lambda is the following part:

[this] { 
        emit removed(this); 
    }); 

What we did here is to connect the clicked signal to an anonymous inline function, a lambda. Qt allows signal relaying by connecting a signal to another signal if their signatures match. It's not the case here; the clicked signal has no parameter and the removed signal needs a Task*. A lambda avoids the declaration of a verbose slot in Task. Qt 5 accepts a lambda instead of a slot in a connect, and both syntaxes can be used.

Our lambda executes a single line of code: emit removed(this). Emit is a Qt macro that will immediately trigger the connected slot with what we passed in a parameter. As we said earlier, removed(Task* this) has no function body, its purpose is to notify the registered slot of an event.

Lambdas are a great addition to C++. They offer a very practical way of defining short functions in your code. Technically, a lambda is the construction of a closure capable of capturing variables in its scope. The full syntax goes like this:

[ capture-list ] ( params ) -> ret { body }

Let’s study each part of this statement:

  • capture-list: This defines what variables will be visible inside the lambda scope.

  • params: This is the function parameters type list that can be passed to the lambda scope. There are no parameters in our case, We might have written [this] () { ... }, but C++11 lets us skip the parentheses altogether.

  • ret: This is the return type of the lambda function. Just like params, this parameter can be omitted if the return type is void.

  • body: This is obviously your code body where you have access to your capture-list, and params, and which must return a variable with a type ret.

In our example, we captured the this pointer to be able to:

  • Have a reference on the removed() function, which is a part of the Task class. If we did not capture this, the compiler would have shouted error: 'this' was not captured for this lambda function emit removed(this);.

  • Pass this to the removed signal; the caller needs to know which task triggered removed.

The capture-list relies on standard C++ semantics: capture variables by copy or by reference. Let us say that we wanted to print a log of the constructor parameter name and we capture it by reference in our lambda:

connect(ui->removeButton, &QPushButton::clicked, [this, &name] { 
        qDebug() << "Trying to remove" << name; 
        this->emit removed(this); 
    }); 

This code will compile fine. Unfortunately, the runtime will crash with a dazzling segmentation fault when we try to remove a Task. What happened? As we said, our lambda is an anonymous function that will be executed when the clicked() signal has been emitted. We captured the name reference, but this reference may be - and is - invalid once we get out of Task constructor (more precisely, from the caller scope). The qDebug() function will then try to display an unreachable code and crash.

You really want to be careful with what you capture and the context in which your lambda will be executed. In this example, the segmentation fault can be amended by capturing the name by copy:

connect(ui->removeButton, &QPushButton::clicked, [this, name] { 
        qDebug() << "Trying to remove" << name; 
        this->emit removed(this); 
    }); 

Tip

C++ Tip

  • You can capture by copy or reference all variables that are reachable in the function where you define your lambda with the syntax [=] and [&].

  • The this variable is a special case of the capture list. You cannot capture it by reference [&this] and the compiler will warn you if you are in this situation: [=, this]. Don't do this. Kittens will die.

Our lambda is passed directly as a parameter to the connect function. In other words, the lambda is variable. This has many consequences: we can call it, assign it, and return it. To illustrate a "fully formed" lambda, we can define one that returns a formatted version of the task name. The sole purpose of this snippet is to investigate the lambda function's machinery. Don't include the following code in your todo app, your colleagues might call you something like a "functional zealot":

connect(ui->removeButton, &QPushButton::clicked, [this, name] { 
    qDebug() << "Trying to remove" << 
        [] (const QString& taskName) -> QString { 
            return "-------- " + taskName.toUpper(); 
    }(name); 
    this->emit removed(this); 
}); 

Here we did a tricky thing. We called qDebug(); inside this call we defined a lambda which is immediately executed. Let's analyze it:

  • []: We performed no capture. The lambda does not depend on the enclosing function.

  • (const Qstring& taskName): When this lambda is called, it will expect a QString to work on.

  • -> QString: The returned value of the lambda will be a QString.

  • return "------- " + taskName.toUpper(): the body of our lambda. We return a concatenation of a string and the uppercase version of the parameter taskName. As you can see, string manipulation becomes a lot easier with Qt.

  • (name): Here comes the catch. Now that the lambda function is defined, we can call it passing the name parameter. In a single instruction, we define it and call it. The QDebug() function will simply print the result.

The real benefit of this lambda will emerge if we are able to assign it to a variable and call it multiple times. C++ is statically typed, so we must provide the type of our lambda variable. In the language specification, a lambda type cannot be explicitly defined. We'll see soon how we can do it with C++11. For now, let's finish our remove feature.

Task now emits the removed() signal. This signal has to be consumed by MainWindow:

// in MainWindow.h 
public slots: 
    void addTask(); 
    void removeTask(Task* task); 
 
// In MainWindow.cpp 
void MainWindow::addTask() 
{ 
    ... 
    if (ok && !name.isEmpty()) { 
        qDebug() << "Adding new task"; 
        Task* task = new Task(name); 
        connect(task, &Task::removed,  
       this, &MainWindow::removeTask); 
    ... 
    } 
} 
 
void MainWindow::removeTask(Task* task) 
{ 
    mTasks.removeOne(task); 
    ui->tasksLayout->removeWidget(task); 
    task->setParent(0); 
    delete task; 
} 

The MainWindow::removeTask() must match the signal signature. The connection is made when the task is created. The interesting part comes in the implementation of  MainWindow::removeTask().

The task is first removed from the mTasks vector. It is then removed from tasksLayout. Here, tasksLayout releases its ownership of task (that is, tasksLayout ceases to be the task class's parent).

So far so good. The next two lines are interesting. The ownership transfer does not completely release the task class ownership. If we commented these lines, here is how removeTask() will look:

void MainWindow::removeTask(Task* task) 
{ 
    mTasks.removeOne(task); 
    ui->tasksLayout->removeWidget(task); 
    // task->setParent(0); 
    // delete task; 
} 

If you add a log message in Task destructor and execute the program, this log message will be displayed. Nonetheless, the Qt documentation tells us in Qlayout::removeWidget part: The ownership of a widget remains the same as when it was added.

Instead, what really happens is that the task class's parent becomes centralWidget, the tasksLayout class's parent. We want Qt to forget everything about task, that's why we call task->setParent(0). We can then safely delete it and call it a day.

 

Simplifying with the auto type and a range-based for loop


The final step to a complete CRUD of our tasks is to implement the completed task feature. We'll implement the following:

  • Click on the checkbox to mark the task as completed

  • Strike the task name

  • Update the status label in MainWindow

The checkbox click handling follows the same pattern as removed:

// In Task.h 
signals: 
    void removed(Task* task); 
    void statusChanged(Task* task); 
private slots: 
    void checked(bool checked); 
 
// in Task.cpp 
Task::Task(const QString& name, QWidget *parent) : 
        QWidget(parent), 
        ui(new Ui::Task) 
{ 
    ... 
 
    connect(ui->checkbox, &QCheckBox::toggled,  
    this, &Task::checked); 
} 
 
... 
 
void Task::checked(bool checked) 
{ 
    QFont font(ui->checkbox->font()); 
    font.setStrikeOut(checked); 
    ui->checkbox->setFont(font); 
    emit statusChanged(this); 
} 

We define a slot checked(bool checked) that will be connected to the checkbox::toggled signal. In our slot checked(), we strike out the checkbox text according to the bool checked value. This is done using the QFont class. We create a copy font from the checkbox->font(), modify it, and assign it back to ui->checkbox. If the original font was in bold, with a special size, its appearance would be guaranteed to stay the same.

Tip

Play around with the font object in Qt Designer. Select the checkbox in the Task.ui file and go to Properties Editor | QWidget | font.

The last instruction notifies MainWindow that the Task status has changed. The signal name is statusChanged, rather than checkboxChecked, to hide the implementation details of the task. Add the following code in the MainWindow.h file:

// In MainWindow.h 
public: 
    void updateStatus(); 
public slots: 
    void addTask(); 
    void removeTask(Task* task); 
    void taskStatusChanged(Task* task); 
 
// In MainWindow.cpp 
MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow), 
    mTasks() 
{ 
    ... 
    updateStatus(); 
    } 
} 
 
void MainWindow::addTask() 
{ 
   ... 
   if (ok && !name.isEmpty()) { 
       ... 
       connect(task, &Task::removed, this, 
               &MainWindow::removeTask); 
       connect(task, &Task::statusChanged, this, 
               &MainWindow::taskStatusChanged); 
       mTasks.append(task); 
       ui->tasksLayout->addWidget(task); 
       updateStatus(); 
   } 
} 
 
void MainWindow::removeTask(Task* task) 
{ 
   ... 
   delete task; 
   updateStatus(); 
} 
 
void MainWindow::taskStatusChanged(Task* /*task*/) 
{ 
    updateStatus(); 
} 
 
void MainWindow::updateStatus() 
{ 
    int completedCount = 0; 
    for(auto t : mTasks)  { 
        if (t->isCompleted()) { 
            completedCount++; 
        } 
    } 
    int todoCount = mTasks.size() - completedCount; 
 
    ui->statusLabel->setText( 
        QString("Status: %1 todo / %2 completed") 
                             .arg(todoCount) 
                             .arg(completedCount)); 
} 

We defined a slot taskStatusChanged, which is connected when a task is created. The single instruction of this slot is to call updateStatus(). This function iterates through the tasks and updates the statusLabel. The updateStatus() function is called upon task creation and deletion.

In updateStatus(), we meet more new C++11 semantics:

for(auto t : mTasks)  { 
    ...  
} 

The for keyword lets us loop over a range-based container. Because QVector is an iterable container, we can use it here. The range declaration (auto t) is the type and variable name that will be assigned at each iteration. The range expression (mTasks) is simply the container on which the process will be done. Qt provides a custom implementation of the for (namely foreach) loop targeted at prior versions of C++; you don't need it anymore.

The auto keyword is another great new semantic. The compiler deduces the variable type automatically based on the initializer. It relieves a lot of pain for cryptic iterators such as this:

std::vector::const_iterator iterator = mTasks.toStdVector() 
                                           .stdTasks.begin(); 
 
// how many neurones did you save? 
auto autoIter = stdTasks.begin(); 

Since C++14, auto can even be used for function return types. It's a fabulous tool, but use it sparingly. If you put auto, the type should be obvious from the signature name/variable name.

Tip

The auto keyword can be combined with const and references. You can write a for loop like this: for (const auto & t : mTasks) { ... }.

Remember our half bread lambda? With all the covered features, we can write:

auto prettyName = [] (const QString& taskName) -> QString { 
    return "-------- " + taskName.toUpper(); 
}; 
connect(ui->removeButton, &QPushButton::clicked,  
    [this, name, prettyName] { 
        qDebug() << "Trying to remove" << prettyName(name); 
        this->emit removed(this); 
}); 

Now that's something beautiful. Combining auto with lambda makes very readable code and opens up a world of possibilities.

The last item to study is the QString API. We used it in updateStatus():

ui->statusLabel->setText( 
        QString("Status: %1 todo / %2 completed") 
                             .arg(todoCount) 
                             .arg(completedCount)); 

The people behind Qt put a lot of work into making string manipulation bearable in C++. This is a perfect example, where we replace the classic C sprintf with a more modern and robust API. Arguments are position-based only, no need to specify the type (less error-prone), and the arg(...) function accepts all kinds of types.

Tip

Take some time to skim through the QString documentation at http://doc.qt.io/qt-5/qstring.html. It shows how much you can do with this class and you'll see yourself using fewer and fewer examples of std string or even cstring.

 

Summary


In this chapter, we created a desktop Qt application from scratch. Qt is well known for its signal/slot mechanism and you must be confident with this paradigm. We also introduced some important C++14 features that will be used all through this book.

It's now time to discover some qmake secrets and what is really done when you build your Qt project. In the next chapter, we will also talk about how to create and organize an application with some platform-dependent code that must run on Windows, Mac OS, and Linux.

About the Authors

  • Guillaume Lazar

    Guillaume Lazar is a software engineer living in France, near Paris. He has worked in different companies, from start-ups to multinationals, for the last 10 years. He took the opportunity to observe and learn many team organizations and technologies. In 2014, he founded his own software development company at the age of 27. The current hierarchical organization that applies to most companies seems obsolete to him. With his own company, he wants to try a different approach. Although he defines himself as a Qt framework lover, he likes to mix different technologies and platforms. He also spends time on game development, machine learning, and electronics, because "things" become "alive".

    Browse publications by this author
  • Robin Penea

    Robin Penea has been working in the software industry for a more than a decade. He worked in start-ups and large companies with many technologies that ranged from embedded software to web development. Armed with this experience, he wrote the Mastering Qt 5 book to spread what he loves the most about the programming craft: proper design and quality code. The teaching bug has bitten him, and he continues to share what he learned online using videos. When he is not tinkering with some new technology, he is either on a wall, rock-climbing, or playing music on his piano. You can reach him via Twitter @synapticrob.

    Browse publications by this author

Latest Reviews

(21 reviews total)
Excellent book with great material covering a lot of topics about Qt5.
Perfect book for Qt 5, in my opinion.
Very good book. Much valuable knowledge. However, like the other two Qt books I have reviewed, the author sometimes veers off and interrupts the flow of the project at hand. Still recommend it despite that.

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial