Book Image

Mastering Qt 5 - Second Edition

By : Guillaume Lazar, Robin Penea
Book Image

Mastering Qt 5 - Second Edition

By: Guillaume Lazar, Robin Penea

Overview of this book

Qt 5.11 is an app development framework that provides a great user experience and develops full capability applications with Qt Widgets, QML, and even Qt 3D. Whether you're building GUI prototypes or fully-fledged cross-platform GUI applications with a native look and feel, Mastering Qt 5 is your fastest, easiest, and most powerful solution. This book addresses various challenges and teaches you to successfully develop cross-platform applications using the Qt framework, with the help of well-organized projects. Working through this book, you will gain a better understanding of the Qt framework, as well as the tools required to resolve serious issues, such as linking, debugging, and multithreading. You'll start off your journey by discovering the new Qt 5.11 features, soon followed by exploring different platforms and learning to tame them. In addition to this, you'll interact with a gamepad using Qt Gamepad. Each chapter is a logical step for you to complete in order to master Qt. By the end of this book, you'll have created an application that has been tested and is ready to be shipped.
Table of Contents (16 chapters)

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 (the MainWindow) that the removeTaskButton QPushButton has been clicked. We'll implement this by defining a custom removed signal in the 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 signal is used only to notify another class, the public keyword is not needed (it even raises a compilation error). 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, lambda is the following part:

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

Here, we connected 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 trigger the connected slot with what we passed as the 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: Defines what variables will be visible inside the lambda scope.
  • params: This is the function parameter's 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 ret type.

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

  • Have a reference on the removed() function, which is 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.

capture-list relies on standard C++ semantics: capture variables by copy or by reference. Let's say that we wanted to print a log of the name constructor parameter 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 invalid once we get out of the Task constructor (more precisely, from the caller scope). The qDebug() function will then try to display an unreachable code and crash.

You really need 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 name by copy:

connect(ui->removeButton, &QPushButton::clicked, [this, name] { 
        qDebug() << "Trying to remove" << name; 
        this->emit removed(this); 
    }); 
  • You can capture by copy or reference all variables that are reachable in the function where you define your lambda with the = and & syntax.
  • The this variable is a special case of the capture list. You cannot capture it by the [&this] reference 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 a 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 a "functional zealot":

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

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

  • []: We performed no capture. 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 taskName parameter. 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 by passing the name parameter. In a single expression, we define it then 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 will soon see how we can do it with C++11. For now, let's finish our remove feature.

The 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); 
    delete task; 
} 

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. The last step is to delete Task. The destructor will unregister itself from centralWidget of MainWindow. In this case, we don't rely on the Qt hierarchical parent-children system for the QObject life cycle because we want to delete Task before the destruction of MainWindow.