QT Core Mechanism 3: Signals and Slots

written in front

This article is basically an understanding of the translation of some chapters of the official Qt documentation. The reason for translating these chapters is that I think these are the core things in Qt. The process of translation is the process of forcing myself to read them carefully. I will copy the original text word by word, but translate the key points according to my own understanding. After all, my purpose is to understand them and organize them in a way that I can use flexibly, rather than mechanically transforming them from one to the other. Language changes into another language. The original content of the official documents involved mainly includes the following chapters:

  1. The Meta-Object System meta-object system
  2. The Property System property system
  3. Signals & Slots Signals and Slots

It is agreed here that the translation of the original text uses normal fonts, and personal understanding uses italic fonts. Also, please forgive me for mixing functions and methods in the text, they have the same meaning here.

It is divided into three articles in total, this article is the translation of Signals & Slots signals and slots .

Links to all three translations:

QT Core Mechanism 1: Metasystem_lczdk's Blog-CSDN Blog

QT core mechanism 2: attribute system_lczdk's blog-CSDN blog

QT Core Mechanism 3: Signals and Slots - Programmer Sought

Signals and slots

Signals and slots are used for communication between objects. The signal and slot mechanism is the core feature of Qt, and it is also the biggest difference from other frameworks. Qt's meta-object system enables the signal and slot mechanism to be implemented.

introduction

In GUI programming, we often hope that when a control (widget) is changed by the user, another control is notified. Furthermore, we want objects of any type to be able to communicate with each other. For example, when the user clicks the close button control, we may want the window control's close() method to be called.

Some tools implement this communication in the form of callback functions. A callback is a pointer to a function. If we want a running function to notify us when an event occurs, we can pass a pointer to another function to the running function (this function is called a callback function). The running function will call our callback function at the appropriate time. Although there is indeed a successful framework using this method, the callback may not be intuitive * (When reading the code, I am most afraid of encountering function pointers. When the code is more complicated, it is difficult for you to find out which functions this function pointer points to at runtime, but This is indeed the easiest notification mechanism to implement. When developing a microcontroller, if you do not use an RTOS, this is the most commonly used notification mechanism, but you need to be more careful when using it)*, and it may be difficult to ensure the type correctness of the callback parameter Encounter problems.

Signals and slots

In Qt, we use the signal and slot mechanism instead of the callback mechanism. Signals are emitted when certain events occur. Qt's controls predefine many signals, of course we can also inherit these controls to define our own subclasses, and then add our own signals. Slots are methods that are called in response to certain signals. There are many predefined slots in Qt controls, but the usual practice is to inherit controls to generate your own subclasses, and then add your own slots, so that we can handle signals of interest by ourselves.image-20220529152344902

The above figure demonstrates how signals and slots are connected. We can see that a signal can correspond to multiple slots, and in fact, a slot can also be connected to multiple signals.

The signal and slot mechanism is type-safe: the parameters of the signal must match the parameters of the slot. (In fact a slot function may have fewer parameters than signals, which means that some additional parameters are ignored.) Since the parameters need to be compatible, when we use the function pointer-based syntax, the compiler can compile The stage helps us check if the parameters match. The string-based signals and slots syntax is checked at runtime. Signals and slots are loosely coupled: the class that emits the signal neither knows nor cares which slot receives the signal. Qt's signal and slot mechanism ensures that if you connect a signal to a slot, the slot will call the signal's arguments at the correct time. Signals and slots can accept any number of arguments of any type. They are completely type safe.

All classes that inherit from QObject or one of its subclasses (for example, QWidget) can contain signals and slots. Signals are emitted when an object changes its state in a way that other objects may be interested in. That's all the communication the objects do. It doesn't know or care if anything is receiving the signal it's sending out. This is true information encapsulation and ensures that objects can be used as software components.

Slots can be used to receive signals, but they are also ordinary member functions. Just as an object doesn't know who will receive its signal, a slot doesn't know which signal it will be connected to. This ensures that truly self-contained components can be created with Qt. ,

We can connect any number of signals to a single slot, and a signal can connect to any number of slots. It is even possible to connect one signal directly to another. (This will emit the second signal as soon as the first signal is emitted.)

Signals and slots together form a powerful component programming mechanism.

Signals

An object emits a signal when its internal state has changed in some way that may be of interest to the object's client or owner. Signals are public access functions* (public) and can be emitted from anywhere, but we recommend only emitting signals from classes that define signals and their subclasses (only recommend emitting signals in classes that define signals, or inherit from signal in a subclass of this class)*.

When a signal is emitted, the slot connected to it normally executes immediately, just like a normal function call. When this happens, the signals and slots mechanism is completely independent of any GUI event loop. Once all slots have returned, the code following the emit statement is executed. When using queued connections* (queued connections, which can be simply understood as using a queue to buffer the slot functions that need to be executed, so that the execution can be delayed, a bit like using an asynchronous method for file IO in Linux), the situation is slightly different; here In this case, the code following the emit keyword will continue immediately, and the slot will be executed later. (Generally, if you connect directly, the slot function will be executed in the receiver's thread immediately, while using a queued connection, the slot function will be executed when the object thread that receives the signal starts to execute.) *

If multiple slots are connected to a signal, when the signal is emitted, the slots will execute sequentially in the order they were connected. Signals are automatically generated by the moc (Meta Object Compiler) and cannot be implemented in .cpp files. They can never have a return type (i.e. use void).

A note about parameters: experience shows that signals and slots are easier to reuse if they don't use special types. If QScrollBar::valueChanged() uses a special type, such as the hypothetical QScrollBar::Range, it can only be connected to slots designed specifically for QScrollBar. It is not possible to connect different input components together. This means that when designing signals and slots, using more general parameter types (such as common primitive types) can increase their reusability.

Slots

The slot will be called when the signal it is connected to is emitted. Slots are just normal C++ methods, so they can be called normally, the only difference is that they can be connected to signals.

Since slots are ordinary member functions, they follow normal C++ rules when called directly. However, as slots, they can be called by any component via a signal-slot connection, regardless of its access level. This means that a signal emitted from an instance of an arbitrary class may cause a private slot to be called in an instance of an unrelated class. Even if a slot is declared private, when it is connected to a signal, the signal-emitting class can call the slot normally, which is a bit like exporting a private member variable of a class as a property so that instances of other classes can access it.

We can also define slots as virtual functions, which is very useful in practice.

Compared with callbacks, the signal and slot mechanism provides more flexibility, so it will be slightly slower in efficiency, but it is basically difficult to perceive in practical applications. In general, calling the connected slot by sending a signal will be about ten times slower than calling the slot function directly, which is when the slot function is not a virtual function. The extra overhead is for locating the objects connected to, safely traversing all connections (such as checking whether the object receiving the signal was destroyed when the signal was sent), and marshalling any parameters in a generic way. While 10 non-virtual function calls sounds like a lot, it's much less overhead than any new or delete operation. For example, once you perform a string, vector, or list (String, vector, list) operation that requires new or delete behind the scenes, the signal and slot overhead is a very small fraction of the overall function call cost. The same is true whenever you make a system call in a slot; or indirectly call 10+ functions. The simplicity and flexibility of the signals and slots mechanism is worth such an overhead that the user won't even notice it.

It is important to note that other libraries that define variables called signals or slots may cause compiler warnings and errors when compiled with Qt-based applications. To fix this, #undef the preprocessor symbol that produced the problem.

a simple example

A basic C++ class declaration might look like this:

class Counter
{
    
    
    publicf:
    	Counter() {
    
     m_value = 0 }
    	
    	int value() const {
    
     return m_value }
    	void setValue(int value);
    
    private:
    	int m_value;
}

A class based on the QObject class might look like this:

#include <QObject>

class Counter : public QObject
{
    
    
    Q_OBJECT

public:
    Counter() {
    
     m_value = 0; }

    int value() const {
    
     return m_value; }

public slots:
    void setValue(int value); // 槽函数

signals:
    void valueChanged(int newValue); //值改变信号,一把在执行setValue时emit发送出去

private:
    int m_value;
};

Compared with the basic version, the version based on the QObject class adds slot functions and signals, which allows it to send signals to notify other objects when the value changes, or receive signals from other objects and call the slot function setValue to update the internal value. We will find that this forms a signal chain. Other signals trigger the setValue slot function. When the setValue slot function is executed, the valueChanged signal will be issued, so that other slots connected to this signal instance will be executed again.

All classes containing signals or slots must reference Q_OBJECT at the top of their declaration. They must also be derived (directly or indirectly) from QObject.

Slot functions are implemented by the application writer, as follows is a possible Counter::setValue slot function implementation:

void Counter::setValue(int value)
{
    
    
    if (value != m_value) //只在值真的更新时才发出信号,这样可以节省不必要的开销
    {
    
    
        m_value = value;
        emit valueChanged(value);
    }
}

The above function only emits a signal when the value is actually updated, which saves unnecessary overhead and avoids infinite loop calls when two instances are connected to each other. This also reminds us that the signal should be emitted when the value actually changes, not as soon as the function is called.

In the following code snippet, we create two Counter objects, and then use the connect function to connect the valueChanged() signal of the first object to the setValue() slot function of the second object.

Counter a, b;
QObect::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);

a.setValue(12); //此时a和b的值都被更新为12,a先更新自己然后发出信号,b收到信号后也更新自己
b.setValue(48); //此时a和b的值都被更新为48,b先更新自己然后发出信号,a收到信号后也更新自己

Take a.setValue(12) as an example, the execution process is: the value of a is changed to 12->a sends a valueChanged signal->the signal triggers setValue of b to set the value of b to 12.

Note that the signal here is only sent when the value changes, which prevents an infinite loop from occurring when the signals of a and b are connected to the slot. (If we don't judge whether the value really changes when sending the signal, in the above example it will lead to the following situation: a instance setValue->a sends a valueChanged signal->b receives the valueChanged signal of a->b instance setValue-> b sends a valueChanged signal -> a receives b's valueChanged signal -> a instance setValue, the situation returns to the first step, and then it will continue to loop.)

By default, one signal is emitted for each connection you make; two signals are emitted for duplicate connections. You can disconnect all these connections with a single disconnect() call. If you pass Qt::UniqueConnection to the type parameter when calling the connect() function, the connection will only be established if it is not a duplicate. If there is already a duplicate (the exact same signal to the exact same slot of the same object), the connection will fail and connect will return false.

The above examples illustrate that objects can work together without knowing anything about each other. To achieve this, you only need to connect the objects together, which can be achieved by some simple QObject::connect() function calls or the automatic connection feature of uic (User Interface Compiler, user interface compiler, not touched yet) .

An example of a practical scenario

The following is the beginning of a simple control class with no member methods. The main purpose is to show how to use the signal and slot mechanism in your own application.

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    
    
    Q_OBJECT

The LcdNumber class inherits QObject by inheriting QFrame and QWidget, which uses most of the signals and slots. It is similar to the built-in QLCDNumber control.

The Q_OBJECT macro is expanded by the preprocessor to declare several member functions implemented by the moc; if you get a compiler error of "undefined reference to vtable for LcdNumber", you probably forgot to run moc or include the moc output in the link command. (When using Qt's integrated development environment, we basically don't encounter this problem).

public:
	LcdNumber(QWidget *parent = nullptr);

signals:
	void overflow();

As above, we declare the signals of the class after the class constructor and public members. When the LcdNumber class is asked to display an impossible value, a signal overflow() is emitted.

If we don't care whether overflow occurs, or know that overflow is unlikely to happen, we can ignore the overflow() signal, in other words, we don't connect this signal to any slots.

On the other hand, if you want to call two different error functions on number overflow, just connect the signal to two different slots. Qt will call these two functions (in the order they are connected).

public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    void setSmallDecimalPoint(bool point);
};

#endif

A slot is a receiving function used to obtain information about state changes of other controls. As shown in the above code, LcdNumber uses them to set the displayed number. Because display() is part of the class's interface to the rest of the program, the slots are public.

There are several sample programs that connect the valueChanged() signal of the QScrollBar control to the display() slot of the LcdNumber, so the numbers displayed on the LCD will change as the scroll bar changes.

Note that the display() method is overloaded (there are several implementations); Qt will choose the appropriate version when connecting to the signal. If callbacks are used, we have to decide for ourselves which version of the function to link to the function pointer.

Signals and slots with default parameters

The parameter lists of signals and slots may have parameters, and the parameters have default values, QObject::destroyed() below biru:

void destroyed(QObject* = nullptr);

When a QObject is deleted, it emits the QObject::destroyed() signal. We want to catch this signal so we can clear all references to it when it's destroyed (since the instance is destroyed, the references are invalidated, we need to clean up those references, otherwise accessing invalid references will cause problems)* . A suitable slot parameter list could be as follows:

void objectDestroyed(QObject *obj = nullptr);

To connect a signal to a slot we use QObject::connect(). There are several ways to connect signals and slots. The first is to use function pointers:

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

There are several advantages to using QObject::connect() with function pointers. First, it allows the compiler to check that the signal's arguments are compatible with the slot's arguments. The compiler can also convert parameters implicitly if necessary.

Of course, it can also be connected with the lambdas function of C++11:

connect(sender, &QObject::destroyed, this, [=](){
    
     this->m_objects.remove(sender); });

In the above two calling methods, we use this to locate the receiver's context in which the slot function should be executed when the signal is sent. In other words, the slot function will be executed in the thread of the instance pointed to by this.

In the case of a lambda as a slot function, the lambda function will be disconnected when the instance is destroyed or the specified execution context is destroyed. It should be noted that any objects used in the lambda are still alive when the signal is emitted. (I don't really understand the meaning of this sentence. The lambda will be disconnected when the sender or context is destroyed. You should take care that any objects used inside the functor are still alive when the signal is emitted. The original text is so).

Another way to connect a signal to a slot is to use QObject::connect() with the signal and slot macros. The basis for judging whether to include parameters in the SIGNAL() and SLOT() macros is: if the parameters have default values, the parameter list passed to the SIGNAL() macro must be no less than the parameter list passed to the SLOT() macro. (That is to say, the slot function can ignore the parameters attached to some signals, but the parameters attached to the signal emission cannot be less than the parameters required by the slot function.)

All of the following usages are correct:

// 信号和槽的参数列表一致
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));

//信号的参数数量多于槽的参数数量
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));

//信号和槽都不指定参数(即使信号实际是会附带参数的)
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

But calling like this doesn't work properly:

//信号附带的参数少于槽所需的参数
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

Since the slot will wait for a QObject the signal will not be sent. This connection will report a runtime error. Keep in mind that the compiler does not check the signal and slot parameters when using this QObject::connect() overload with the SIGNAL and SLOT macros.

To sum up, that is to say, there are two ways to use the connect function (two overloads): use function pointers to connect signals and slots, or use SIGNAL and SLOT macros to connect signals and slots in string form; the former can be checked at the compilation stage Whether the parameters of the signal and the slot are compatible, and automatically determine which version of the slot function to use when there is an overload of the slot function, but if the signal is overloaded, it is impossible to determine which version of the signal the slot should correspond to using this method. Only the second form of string concatenation based on the SIGNAL and SLOT macros can be used; the latter needs to check whether the signal and slot are compatible at runtime, but it can be used in situations where there are multiple overloads of the signal. At this time, the user needs to Specify the parameter list for signals and slots yourself.

Advanced usage of signals and slots

For situations where information about the sender of the signal is required, Qt provides the QObject::sender() function, which returns a pointer to the object that sent the signal.

Lambda expressions are a convenience method that can be used to pass user-defined parameters to a slot:

// 使用lambda表达式我们可以直接访问到发送者action,这样就可以不使用QObject::sender()函数了
connect(action, &QAction::triggered, engine, [=]() {
    
     engine->processAction(action->text()); });

It is also possible to use the signal and slot mechanism provided by a third party in Qt, and we can even use both mechanisms in the same project (the signal and slot mechanism that comes with Qt and the signal and slot mechanism provided by a third party). Just add the following line to your qmake project (.pro) file.

CONFIG += no_keywords

It tells Qt not to define the moc keywords signals, slots, and emit, because these names will be used by third-party libraries, such as Boost. Then, if you want to use Qt's own signals and slots with the no_keywords flag, just replace all Qt moc keywords in the source code with the corresponding Qt macros Q_SIGNALS (or Q_SIGNAL), Q_SLOTS (or Q_SLOT) and Q_EMIT.

Guess you like

Origin blog.csdn.net/lczdk/article/details/125034246