laitimes

Qt Development - Recognize the nature of signal slots

author:QT Tutorials

Brief introduction

This time we discuss the knowledge points related to Qt signal-slots.

Signal-slots are the most core mechanism in the Qt framework and a skill that every Qt developer must master.

There are many articles on the network that introduce signal-slots, which can also be referenced.

Brother Tao's column is "Qt Advanced Road", if there is not even a signal-slot article, it will be soulless.

So this time Brother Tao will talk about the signal-trough from shallow to deep.

A story of cat and mouse

If a whole bunch of concepts and definitions are told at first, it is easy for the reader to read and fall asleep. So Brother Tao started with a story/scene.

When he was a child, he liked to watch the cartoon "Cat and Mouse", which has the story of Tom and Jerry fighting wits and courage...

Qt Development - Recognize the nature of signal slots

Now make a simple setting: Tom has a skill called "Meow", which is to make cat calls, and Jerry, who is stealing food, will run away when he hears the cat barking.

Qt Development - Recognize the nature of signal slots
Qt Development - Recognize the nature of signal slots

Let's try to describe this setting with C++ object-oriented ideas.

Let's start by defining both Tom and Jerry

//Tom的定义

class Tom
{
public:
    //猫叫
    void Miaow() 
    {
        cout << "喵!" << endl;
    }
    //省略其它
    ... 
};
//Jerry的定义
class Jerry
{
public:
    //逃跑
    void RunAway()
    {
        cout << "那只猫又来了,快溜!" << endl;
    }
    //省略其它
    ... 
};
           

Next, simulate the scenario

int main(int argc, char *argv[])
{
    //实例化tom
    Tom tom;

    //实例化jerry
    Jerry jerry;

    //tom发出叫声
    tom.Miaow();

    //jerry逃跑
    jerry.RunAway();

    return 0;
}
           

The scene looks simple, with Tom calling out and manually calling Jerry's escape.

Let's look at a few more slightly more complex scenarios:

Scenario 1:

Suppose Jerry escapes and comes back to steal food after a while. Tom screams again, Jerry runs away again...

This scene is repeated dozens of times. Can we achieve that jerry's RunAway is automatically called whenever tom's Miaow is called, rather than manually every time?

Scenario 2:

If Jerry is hiding behind a rice bag in a kitchen cupboard, it cannot be directly discovered (you cannot directly get the Jerry object and call its function).

In this case, how to build a model of "cat barking-mouse escape"?

Scenario 3:

If there are multiple jerry, when one Tom screams, all the jerry flees. How should this model be established?

If there are multiple Toms, when any one makes a cry, all Jerry flees. How should this model be established?

Scenario 4:

If you don't know the exact breed or name of the cat, and you don't know the breed or name of the mouse, as long as the cat makes a sound, the mouse animal will run away.

How can such a model be established?

There are many more scenes, so I won't repeat them.

The communication mechanism between objects

Here's an overview of what you want to achieve:

To provide a communication mechanism between objects. This mechanism should be able to establish a mapping relationship for functions in two different objects, and the latter can be called automatically when the former is called.

Going a little deeper, both objects are unaware of each other's existence and can still make connections. Even one-to-one mapping can be extended to many-to-many, and mapping between concrete objects can be extended between abstract concepts.

Try 1: Call directly

Some people should say, Miaow() function directly call RunAway() is not enough?

Obviously, scenario two passes this scheme.

The problem with direct calls is that the cat needs to know that the mouse has a function/interface called escape, and then actively calls it.

It's like Tom yelling, and then Tom takes the initiative to twist Jerry's leg to make him run. This is unreasonable. (Jerry said he was confused!)

The real logic is that the cat's cry travels in the air/medium, reaches the mouse's ears, and the mouse runs away. The cat and mouse don't see each other.

Try 2: Callback function + mapping table

Seems doable.

Thinking about it a little, we're going to do these two things:

1 Take out the RunAway function and store it somewhere

2 Establish a mapping relationship between the Miaow function and RunAway, and automatically call the latter when the former is called.

RunAway functions can be stored using function pointers|member function pointers or C++11-function, which can be called "callback functions".

(The following code is mainly written in C++11 function, and the writing of function pointers is slightly more complicated, the essence is the same)

Let's first use a simple Map to store the mapping relationship, and use a string as the name of the mapping relationship

std::map<std::string, std::function<void()>> callbackMap;
           

We also need to implement the "establish mapping" and "call" functions, so here encapsulates a Connections class

class Connections 
{
public:
    //按名称“建立映射关系”
    void connect(const std::string &name, const std::function<void()> &callback) 
    {
        m_callbackMap[name] = callback;
    }
    //按名称“调用”
    void invok(const std::string &name)
    {
        auto it = m_callbackMap.find(name);
        //迭代器判断
        if (it != m_callbackMap.end()) {
            //迭代器有效的情况,直接调用
            it->second();
        }
    }
private:
    std::map<std::string, std::function<void()>> m_callbackMap;
};
           

So where is this mapping stored? Apparently in a "context" shared by Tom and Jerry.

Let's use a global variable so that it can be easily simulated:

//全局共享的Connections。
static Connections s_connections;

//Tom的定义
class Tom
{
public:
    //猫叫
    void Miaow() 
    {
        cout << "喵!" << endl;
        //调用一下名字为mouse的回调
        s_connections.invok("mouse");
    }
    //省略其它
    ... 
};
//Jerry的定义
class Jerry
{
public:
    Jerry() 
    {
        //构造函数中,建立映射关系。std::bind属于基本用法。
        s_connections.connect("mouse", std::bind(&Jerry::RunAway, this));
    }
    //逃跑
    void RunAway()
    {
        cout << "那只猫又来了,快溜!" << endl;
    }
    //省略其它
    ... 
};
int main(int argc, char *argv[])
{
    //模拟嵌套层级很深的场景,外部不能直接访问到tom
    struct A {
        struct B {
            struct C {
                private:
                    //Tom在很深的结构中
                    Tom tom;
                public:
                    void MiaoMiaoMiao() 
                    {
                        tom.Miaow();
                    }
            }c;
            void MiaoMiao() 
            {
                c.MiaoMiaoMiao();
            }
        }b;
        void Miao() 
        {
            b.MiaoMiao();
        }
    }a;
    //模拟嵌套层级很深的场景,外部不能直接访问到jerry
    struct D {
        struct E {
            struct F {
                private:
                    //jerry在很深的结构中
                    Jerry jerry;
            }f;
        }e;
    }d;

    //A间接调用tom的MiaoW,发出猫叫声
    a.Miao();
    
    return 0;
}
           

RunAway is not called directly, but is triggered automatically.

Analysis: Here is the string "mouse" as the key to connecting Tom and Jerry. This is just a simple, crude example implementation.

Observer mode

In the GOF Four's book Design Patterns, there is an observer mode that can achieve the same function more elegantly.

(By the way, there are a total of 23 design patterns summarized by GOF, and Tao Ge once implemented a full set of them with C++11, and the github address is: https://github.com/jaredtao/DesignPattern)

The primary observer mode, Brother Tao will not repeat. Here Tao Ge uses C++11 with a little template trick to achieve a more general observer pattern.

It can also be called a publish-subscribe model.

//Subject.hpp
#pragma once
#include <vector>
#include <algorithm>

//Subject 事件或消息的主体。模板参数为观察者类型
template<typename ObserverType>
class Subject {
public:
    //订阅
    void subscibe(ObserverType *obs)
    {
        auto itor = std::find(m_observerList.begin(), m_observerList.end(), obs);
        if (m_observerList.end() == itor) {
            m_observerList.push_back(obs);
        }
    }
    //取消订阅
    void unSubscibe(ObserverType *obs)
    {
        m_observerList.erase(std::remove(m_observerList.begin(), m_observerList.end(), obs));
    }
    //发布。这里的模板参数为函数类型。
    template <typename FuncType>
    void publish(FuncType func)
    {
        for (auto obs: m_observerList)
        {
            //调用回调函数,将obs作为第一个参数传递
            func(obs);
        }
    }
private:
    std::vector<ObserverType *> m_observerList;
};
           
//main.cpp
#include "Subject.hpp"
#include <functional>
#include <iostream>

using std::cout;
using std::endl;

//CatObserver 接口 猫的观察者
class CatObserver {
public:
    //猫叫事件
    virtual void onMiaow() = 0;
public:
    virtual ~CatObserver() {}
};

//Tom 继承于Subject模板类,模板参数为CatObserver。这样Tom就拥有了订阅、发布的功能。
class Tom : public Subject<CatObserver>
{
public:
    void miaoW()
    {
        cout << "喵!" << endl;
        //发布"猫叫"。
        //这里取CatObserver类的成员函数指针onMiaow。而成员函数指针调用时,要传递一个对象的this指针才行的。
        //所以用std::bind 和 std::placeholders::_1将第一个参数 绑定为 函数被调用时的第一个参数,也就是前面Subject::publish中的obs
        publish(std::bind(&CatObserver::onMiaow, std::placeholders::_1));
    }
};
//Jerry 继承于 CatObserver
class Jerry: public CatObserver
{
public:
    //重写“猫叫事件”
    void onMiaow() override
    {
        //发生 “猫叫”时 调用 逃跑
        RunAway();
    }
    void RunAway()
    {
        cout << "那只猫又来了,快溜!" << endl;
    }
};
int main(int argc, char *argv[])
{
    Tom tom;
    Jerry jerry;

    //拿jerry去订阅Tom的 猫叫事件
    tom.subscibe(&jerry);

    tom.miaoW();
    return 0;
}
           

As long as any class inherits the Subject template class and provides observer parameters, it has the publish-subscribe function.

Qt's signal-slot

Introduction to signal-slots

Signal-slots is a Qt custom communication mechanism that differs from the standard C/C++ language.

The use of signal-slot is to add signal and slot tokens before the ordinary function declaration, and then connect the signal with the slot through the connect function.

Subsequently, as long as the signal function is called, the connected signal or slot function can be triggered.

Qt Development - Recognize the nature of signal slots

When connecting, the sender in front and the receiver in the back. Signals can also be connected to each other, in which case the receiver signal can be treated as a slot.

There are two types of signal-slots

Signal-slots should be viewed in two ways, one is a signal-slot within the same thread, and the other is a cross-thread signal-slot.

The signal-slot in the same thread is equivalent to a function call, similar to the previous observer pattern, except that the signal-slot has a slight performance loss (this will be discussed later).

Cross-thread signal-slot, when the signal is triggered, the sender thread converts the call of the slot function into a "call event" and puts it into the event loop.

When the receiver thread executes until the next event processing, it handles the "call event" and calls the appropriate function.

(For the event loop, you can refer to the previous article "Qt Practical Skills 3 - Understanding the Event Loop")

Implementation of the signal-slot meta-object compiler moc

Signal-slot implementation, with the help of a tool: Meta Object Compiler (MOC).

This tool is integrated into Qt's compilation toolchain qmake, and when starting to compile a Qt project, it will first execute the MOC, from the code

Parsing signals, slots, emit, etc. that do not exist in standard C/C++, as well as handling Q_OBJECT, Q_PROPERTY,

Q_INVOKABLE other related macros to generate a moc_xxx.cpp C++ file. (Use black magic to monetize grammar sugar)

For example, as long as the signal function is declared and does not need to be implemented by itself, it is automatically generated in this moc_xxx.cpp file.

MOC is followed by the regular C/C++ compilation and linking process.

The essence of MOC - reflection

The essence of MOC is actually a reflector. Standard C++ does not have reflection (and there will be in the future), so Qt implements reflection with moc.

What is reflex? Simply put, it is to obtain the constructor, member function, and member variables of the object during operation.

As an example, there is a class declaration like this:

class Tom {
public:
    Tom() {}
    const std::string & getName() const
    {
        return m_name;
    }
    void setName(const std::string &name) 
    {
        m_name = name;
    }
private:
    std::string m_name;
};           

The user of the class cannot see the declaration of the class, the header file cannot be obtained, and the constructor and member functions of the class cannot be directly called.

Once you have the string "Tom" from the profile/network, you need to create an object instance of the Tom class.

Then get a string of "setName" and call Tom's setName function.

In the face of this demand, it is necessary to store the constructor, member functions and other information of the Tom class, and it must be able to be called.

This information is "meta information", and users can "use this class" through "meta information". This is the reflection.

The "factory pattern" in the design pattern is a typical reflection case. However, the factory mode only solves the call of the constructor, and there is no member function, member variable and other information.

[Get the QT development tutorial learning materials, click the link below to receive the fee ↓↓, live first and don't get lost~]

Click here: Qt materials collection (video tutorial + documentation + code + project practice)