QT 的信号-槽机制

对于对象间的通信问题,很多框架采用回调函数类解决。QT 使用信号-槽解决对象间的通信问题,只要继承 QObject 类就可以使用信号-槽机制。信号-槽使用起来非常简单、灵活,发射和接收对象实现了解耦。发射信号的对象不需要关注有哪些对象需要接收信号,只需要在状态改变时发射信号即可;接收对象也不需要关注何时发射信号,只需要关注槽函数的实现。与回调函数相比较,信号-槽效率会低一些。一般情况下,使用信号-槽机制要比直接调用槽函数慢10倍。

信号-槽的链接类型

  • Qt::AutoConnection (默认类型)

    如果发射的信号与接收对象在同一个线程,该类型的处理方式与Qt::DirectConnection一样,否则与Qt::QueuedConnectio一样。

  • Qt::DirectConnection

    当信号发射时立即调用槽函数(与回调函数一样)。槽函数在发射信号的线程中执行。

  • Qt::QueuedConnection

    槽函数在接收者线程内执行。当事件循环的控制权交给接收线程时执行槽函数。使用 Qt::QueuedConnection 类型时,信号及槽的参数类型必须是 QT 元对象系统的已知类型。因 为 QT 需要在后台将参数拷贝并存储到事件中。如果参数类型不是 QT 元对象系统的已知类型将触发以下错误:

    QObject::connect: Cannot queue arguments of type 'MyType'

    此时,在建立链接前需要调用 qRegisterMetaType() 方法类注册数据类型。

  • Qt::BlockingQueuedConnection

    信号发射后当前线程阻塞,直到唤醒槽函数所在线程并执行完毕。注意: 发射信号和接收槽函数的对象在同一线程内时,使用该类型将导致死锁。

  • Qt::UniqueConnection

    该类型可以与上述类型使用OR 操作联合使用。设置为该类型后,同一信号只能与同一一个对象的槽函数链接一次。如果链接已经存在,将不会再次建立链接,connect() 返回 false。注意:该类型对应的槽函数只能是类的成员函数,不能使用 lambda 表达式和非成员函数。

  • Qt::SingleShotConnection

    该类型可以与上述类型使用OR 操作联合使用。设置为该类型后,槽函数仅会调用1次。信号发射后会自动断开信号与槽的链接。QT 6.0 后引入该类型。

    QObject::connect() 本身是线程安全的,但是 Qt::DirectConnection 类型时,如果信号的发送者和接收者不在同一个线程中,则不是线程安全的。

信号-槽的链接方式

一个信号可以链接多个槽函数,一个槽函数也可以链接多个信号,信号也可以直接链接到另一个信号。如果一个信号链接多个槽函数,当发射信号时,槽函数参照链接时的先后顺序进行调用执行。

QT 提供了 2 种链接方式:基于字符串的语法和基于函数的语法。

基于字符串的语法:

C++ 复制代码
QMetaObject::Connection QObject::connect(const QObject **sender*, const char **signal*, const QObject **receiver*, const char **method*, Qt::ConnectionType *type* = Qt::AutoConnection)
    // 需要使用宏 SIGNAL() 和 SLOT() 声明信号和槽函数

基于函数的语法:

c++ 复制代码
QMetaObject::Connection  QObject::connect(const QObject **sender*, const QMetaMethod &*signal*, const QObject **receiver*, const QMetaMethod &*method*, Qt::ConnectionType *type* = Qt::AutoConnection)

它们的区别如下:

基于字符串 基于函数
类型检查 运行时 编译期
支持隐式类型转换
支持使用 lambda 表达式链接信号
支持槽的参数比信号的参数数量少
支持将 C++ 函数链接到 QML 函数
  1. 类型检查和隐式类型转换

    基于字符串的语法依赖元对象系统的反射功能,使用字符串匹配方式检查信号和槽函数,有如下局限性:

  • 链接错误只能在运行才能检查出来;

  • 不能使用隐式类型转换;

  • 不能解析类型定义和命名空间;

    基于函数的语法由编译器来检查,编译器在编译期就能检查出链接错误,并且支持隐式类型转换,还能识别出同一类型的不同名称(即类型定义)。

    C++ 复制代码
    auto slider = new QSlider(this);
    auto doubleSpinBox = new QDoubleSpinBox(this);
    
    // OK: 编译器将 int 转为 double
    connect(slider, &QSlider::valueChanged,
            doubleSpinBox, &QDoubleSpinBox::setValue);
    
    // ERROR: 字符串无法包含转换信息
    connect(slider, SIGNAL(valueChanged(int)),
            doubleSpinBox, SLOT(setValue(double)));
    
            auto audioInput = new QAudioInput(QAudioFormat(), this);
    auto widget = new QWidget(this);
    
    // OK
    connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
            widget, SLOT(show()));
    
    // ERROR: 无法使用命名空间,字符串 "State" 与 "QAudio::State" 不匹配
    using namespace QAudio;
    connect(audioInput, SIGNAL(stateChanged(State)),
            widget, SLOT(show()));
    
    // ...
  1. 使用 lambda 表达式链接信号

    基于函数的语法支持 C++ 11 的 lambda 表达式,也支持标准函数、非成员函数、指向函数的指针。但是为了提高可读性,信号应该链接到槽函数、 lambda 表达式和其它信号。

    C++ 复制代码
    class TextSender : public QWidget {
        Q_OBJECT
    
        QLineEdit *lineEdit;
        QPushButton *button;
    
    signals:
        void textCompleted(const QString& text) const;
    
    public:
        TextSender(QWidget *parent = nullptr);
    };
    
    TextSender::TextSender(QWidget *parent) : QWidget(parent) {
        lineEdit = new QLineEdit(this);
        button = new QPushButton("Send", this);
    	// 使用 lambda 表达式作为槽函数
        connect(button, &QPushButton::clicked, [=] {
            emit textCompleted(lineEdit->text());
        });
    
        // ...
    }
  2. 链接 C++ 对象与 QML 对象

    基于字符串的语法可以链接 C++ 对象与 QML 对象,因为 QML 类型只在运行时进行解析, C++ 编译器无法识别。

    C++ 复制代码
    // QmlGui.qml 文件
    Rectangle {
        width: 100; height: 100
    
        signal qmlSignal(string sentMsg)
        function qmlSlot(receivedMsg) {
            console.log("QML received: " + receivedMsg)
        }
    
        MouseArea {
            anchors.fill: parent
            onClicked: qmlSignal("Hello from QML!")
        }
    }
    
    // C++ 类文件
    class CppGui : public QWidget {
        Q_OBJECT
    
        QPushButton *button;
    
    signals:
        void cppSignal(const QVariant& sentMsg) const;
    
    public slots:
        void cppSlot(const QString& receivedMsg) const {
            qDebug() << "C++ received:" << receivedMsg;
        }
    
    public:
        CppGui(QWidget *parent = nullptr) : QWidget(parent) {
            button = new QPushButton("Click Me!", this);
            connect(button, &QPushButton::clicked, [=] {
                emit cppSignal("Hello from C++!");
            });
        }
    };
    
    
    auto cppObj = new CppGui(this);
    auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
    auto qmlObj = quickWidget->rootObject();
    
    // QML 信号链接到 C++ 槽函数
    connect(qmlObj, SIGNAL(qmlSignal(QString)), cppObj, SLOT(cppSlot(QString)));
    
    // C++ 信号链接到 QML 槽函数
    connect(cppObj, SIGNAL(cppSignal(QVariant)), qmlObj, SLOT(qmlSlot(QVariant)));
  3. 槽函数的参数个数

    一般情况下,槽函数的参数类型与信号声明的一致,数量等于或少于信号的参数。基于字符串的语法提供了一个变通规则:如果参函数有默认参数,那么发射的信号就可以省略这些参数。当发射信号的参数少于槽函数的参数, QT 将使用槽函数的默认参数。

    基于函数的语法无法直接链接此类信号-槽,但是可以将信号链接到 lambda 表达式,在表达式内调用槽函数。

    C++ 复制代码
    public slots:
        void printNumber(int number = 42) {
            qDebug() << "Lucky number" << number;
        }
        
        
    DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) {
    
        // OK: 调用 printNumber() 时使用默认值 42
        connect(qApp, SIGNAL(aboutToQuit()),
                this, SLOT(printNumber()));
    
        // ERROR: 编译器要求参数匹配
        connect(qApp, &QCoreApplication::aboutToQuit,
                this, &DemoWidget::printNumber);
    }
  4. 信号-槽的重载

    对于重载的信号或槽,基于字符串的链接语法可以显示声明参数类型,但是基于函数的链接语法无法告诉编译器使用哪个实力进行链接,这时,需要通过 qOverload 函数来指明参数类型。

    复制代码
    // 槽函数重载定义
    QLCDNumber::display(int)
    QLCDNumber::display(double)
    QLCDNumber::display(QString)
    
    
    auto slider = new QSlider(this);
    auto lcd = new QLCDNumber(this);
    
    // S基于字符串的链接语法
    connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
    
    // 基于函数的链接语法
    connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));

信号-槽的自动链接

信号槽可以在编译期或运行时手动或自动链接。QT 的元对象系统 (QMetaObject) 可以根据信号自动匹配名称匹配的槽。按照如下方式声明并实现槽函数时,uic (User Interface Compiler)会自动在 setupUi() 函数中建立信号与槽的链接。(通过 QT Creator 的 form 界面自动创建的槽函数都是如此)

void on_<object name>_<signal name>(<signal parameters>);

注意:当 form 中的 widgets 重命名时,槽函数的名称也需要相应的修改。

也可以使用下面的函数开启信号与槽的自动匹配:

QMetaObject::connectSlotsByName(this);

获取信号发射者

在槽函数里调用函数 QObject::sender() 可以获取信号发射者的 QObject 对象指针。如果知道信号发射者的类型,就可以将 QObject 指针转换为确定类型对象的指针,然后使用这个确定类的接口函数。

复制代码
// btnProperty 是 QPushButton类型,作为信号的发射者,
// 此方法为 click() 信号的槽函数,并使用了自动链接
void Widget::on_btnProperty_clicked()
{
    //获取信号的发射者
    QPushButton *btn= qobject_cast<QPushButton*>(sender());
    bool isFlat= btn->property("flat").toBool();
    btn->setProperty("flat", !isFlat);
}

如果槽函数是 lambda 表达式,获取信号发射者更简单,只需要传参即可。

复制代码
connect(action, &QAction::triggered, engine,
        [=]() { engine->processAction(action->text()); });

解除信号与槽的连接

函数 disconnect()用于解除信号与槽的连接,它有 2 种成员函数形式和 4 种静态函数形式,有以下几种使用方式,示意代码中 myObject 是发射信号的对象,myReceiver 是接收信号的对象。

  1. 解除与一个发射者所有信号的连接

    复制代码
    // 静态函数形式
    disconnect(myObject, nullptr, nullptr, nullptr);
    // 成员函数形式
    myObject->disconnect();
  2. 解除与一个特定信号的所有连接

    复制代码
    // 静态函数形式
    disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);
    // 成员函数形式
    myObject->disconnect(SIGNAL(mySignal()));
  3. 解除与一个特定接收者的所有连接

    复制代码
    // 静态函数形式
    disconnect(myObject, nullptr, myReceiver, nullptr);
    // 成员函数形式
    myObject->disconnect(myReceiver);
  4. 解除特定的一个信号与槽的连接

    复制代码
    // 静态函数形式
    disconnect(lineEdit, &QLineEdit::textChanged, label, &QLabel::setText);

信号-槽的一些规则

  1. 继承 QObject 类

    只有继承 QObject 类才能使用信号-槽。有多重继承时,QObject 必须是第一个继承类,因为 moc 总是检查第一个继承的类是否为 QObject ,如果不是将不会生成moc文件。另外,模板类不能使用 Q_OBJECT 宏。

    C++ 复制代码
    // WRONG
    class SomeTemplate<int> : public QFrame
    {
        Q_OBJECT
        ...
    
    signals:
        void mySignal(int);
    };
    
    // correct
    class SomeClass : public QObject, public OtherClass
    {
        ...
    };
  2. 函数指针不能用作信号或槽的参数

    许多情况下可以使用继承或虚函数来代替函数指针。

    C++ 复制代码
    class SomeClass : public QObject
    {
       Q_OBJECT
    
    public slots:
       void apply(void (*apply)(List *, void *), char *); // WRONG
    };
    
    
    
    // correct
    typedef void (*ApplyFunction)(List *, void *);
    
    class SomeClass : public QObject
    {
       Q_OBJECT
    
    public slots:
       void apply(ApplyFunction, char *);
    };
  3. 信号或槽的参数为枚举量时必须全限定声明

    这主要是针对基于字符串的链接语法,因为它是依靠字符串匹配来识别数据类型的。

    C++ 复制代码
    class MyClass : public QObject
    {
       Q_OBJECT
    
       enum Error {
           ConnectionRefused,
           RemoteHostClosed,
           UnknownError
       };
    
    signals:
       void stateChanged(MyClass::Error error);
    };
  4. 嵌套类不会能使用信号或槽

    复制代码
    class A
    {
    public:
        class B
        {
            Q_OBJECT
    
            public slots:   // WRONG
                void b();
        };
    };
  5. 不能引用信号或槽的反返回类型

    信号或槽虽然可以有返回类型,但是它们的返回引用会被当作 void 类型。

  6. 类声明信号或槽的部分只能声明信号或槽

    moc 编译器会检查这部分的声明

集成第三方信号-槽

集成第三方信号-槽机制,需要避免 signal、slots、emit关键字与第三方(例如 Boost)冲突,主要是通过配置使 moc 不使用 signal、slots、emit关键字。需要做以下配置:对于使用 CMake 的项目,需要在工程文件中添加

复制代码
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)

对于使用 qmake 的项目,需要在 .pro文件中添加

复制代码
CONFIG += no_keywords

源文件中相应的关键字要替换为 Q_SIGNALS(Q_SIGNAL), Q_SLOTS(Q_SLOT),Q_EMIT。

基于 Qt 的库的公共 API 应该使用关键字 Q_SIGNALS 和 Q_SLOTS,否则,很难在定义 QT_NO_KEYWORDS 的项目中使用此类库。可以在构建库时设置预处理器定义 QT_NO_SIGNALS_SLOTS_KEYWORDS 强制实施此限制。

信号-槽的性能

QT 的信号槽机制在性能上不如基于模板的解决方案(如:boost::signal2或自定义实现)。因为 QT 的信号-槽依赖元对象系统,编译器(MOC)生成额外的代码在运行时会动态查找信号与槽的关联;参数通过 QVariant 封装,增加了运行时检查。基于模板的解决方案发射一个信号的成本大约是调用 4 个函数的成本,但是 QT 需要花费大概相当于调用10 个函数的成本。虽然信号发射增加了时间开销,但是相对槽中代码的执行,这些开销可以忽略不计。QT 的信号-槽不适合高性能需求的场景(例如,核心算法、高频事件处理,游戏循环、音频处理等要求毫秒级响应的场景)。

【参考】:

Why Does Qt Use Moc for Signals and Slots?

Signals and Slots Across Threads

Differences between String-Based and Functor-Based Connections | Qt 6.8

Signals & Slots | Qt Core 6.8.3

相关推荐
2401_8582861114 分钟前
CD27.【C++ Dev】类和对象(18)友元和内部类
开发语言·c++·类和对象
(王子变青蛙)16 分钟前
C++初始
开发语言·c++·程序人生
莫有杯子的龙潭峡谷16 分钟前
4.15 代码随想录第四十四天打卡
c++·算法
灋✘逞_兇32 分钟前
快速幂+公共父节点
数据结构·c++·算法·leetcode
姜行运1 小时前
每日算法(双指针算法)(Day 1)
c++·算法·c#
十五年专注C++开发1 小时前
面试题:请描述一下你在项目中是如何进行性能优化的?针对哪些方面进行了优化,采取了哪些具体的措施?
开发语言·数据结构·c++·qt·设计模式·性能优化
爱奥尼欧2 小时前
C++基础语法(1)
c语言·c++
pixel32 小时前
C++ STL常见容器
c++
wuqingshun3141592 小时前
经典算法 判断一个图是不是树
数据结构·c++·算法·蓝桥杯·深度优先
EverestVIP2 小时前
Qt 的 plugins/codecs 目录
开发语言·qt