对于对象间的通信问题,很多框架采用回调函数类解决。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 种链接方式:基于字符串的语法和基于函数的语法。

基于字符串的语法:

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

基于函数的语法:

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. 类型检查和隐式类型转换

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

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

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

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

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

    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 表达式和其它信号。

    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++ 编译器无法识别。

    // 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 表达式,在表达式内调用槽函数。

    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 宏。

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

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

    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. 信号或槽的参数为枚举量时必须全限定声明

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

    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

QT 的信号-槽机制的更多相关文章

  1. C++11实现Qt的信号槽机制

    概述 Qt的信号槽机制是Qt的核心机制,按钮点击的响应.线程间通信等都是通过信号槽来实现的,boost里也有信号槽,但和Qt提供的使用接口很不一样,本文主要是用C++11来实现一个简单的信号槽,该信号 ...

  2. VJGUI消息设计-兼谈MFC、QT和信号/槽机制

    星期六下午4点,还在公司加班.终于写完了下周要交工的一个程序. 郁闷,今天这几个小时写了有上千行代码吧?虽然大部分都是Ctrl-C+Ctrl-V,但还是郁闷. 作为一个有10年经验的MFC程序员,郁闷 ...

  3. Qt学习记录--02 Qt的信号槽机制介绍(含Qt5与Qt4的差异对比)

    一 闲谈: 熟悉Window下编程的小伙伴们,对其消息机制并不陌生, 话说:一切皆消息.它可以很方便实现不同窗体之间的通信,然而MFC库将很多底层的消息都屏蔽了,尽管使用户更加方便.简易地处理消息,但 ...

  4. 非Qt工程使用Qt的信号槽机制

    非Qt工程,使用Qt的信号槽机制,蛋疼不?反正我现在就是要做这样一件蛋疼的事. 要使用Qt的信号槽机制,下面是从Qt Assist里面关于 signal & slots 的一句介绍: All ...

  5. QT信号槽机制

    信号槽 信号槽是QT中用于对象间通信的一种机制,也是QT的核心机制.在GUI编程中,我们经常需要在改变一个组件的同时,通知另一个组件做出响应.例如: 一开始我们的Find按钮是未激活的,用户输入要查找 ...

  6. QT写hello world 以及信号槽机制

    QT是一个C++的库,不仅仅有GUI的库.首先写一个hello world吧.敲代码,从hello world 写起. #include<QtGui/QApplication> #incl ...

  7. Qt开发之信号槽机制

    一.信号槽机制原理 1.如何声明信号槽 Qt头文件中一段的简化版: class Example: public QObject { Q_OBJECT signals: void customSigna ...

  8. QT源码之Qt信号槽机制与事件机制的联系

    QT源码之Qt信号槽机制与事件机制的联系是本文要介绍的内容,通过解决一个问题,从中分析出的理论,先来看内容. 本文就是来解决一个问题,就是当signal和slot的连接为Qt::QueuedConne ...

  9. QT学习记录之理解信号槽机制

    作者:朱金灿 来源:http://blog.csdn.net/clever101 QT的事件机制采用的信号槽机制.所谓信号槽机制,简而言之就是将信号和信号处理函数绑定在一起,比如一个按钮被单击是一个信 ...

  10. Qt中父子页面切换隐藏实现方法 (利用信号槽机制实现)

    首先既然你打开了这篇文章,那你一定想到过,将子界面作为父界面的一个属性来实现,但是这样父界面通知子界面会很轻松,但子界面通知父界面怎么搞呢?很显然不能再子界面再实例化父界面(因为这样做会循环引用),那 ...

随机推荐

  1. 159:更改shell环境

  2. Sybaris pg walkthrough Intermediate 从redis 到 rce

    nmap ┌──(root㉿kali)-[~/lab] └─# nmap -p- -A 192.168.166.93 Starting Nmap 7.94SVN ( https://nmap.org ...

  3. 在私有化部署的 Gitlab 实例中开启内置的容器镜像仓库

    版本 极狐 GitLab v16.1.2-jh 步骤 如果使用 Let's Encrpt 集成,容器镜像仓库功能自动开启,访问地址为 your-gitlab-domain:5050. 否则,默认不开启 ...

  4. 安卓编译报错Execution failed for task ‘:expo-modules-core:prepareBoost‘. Not in GZIP format的解决方案

    作者: Kovli 重要通知:红宝书第5版2024年12月1日出炉了,感兴趣的可以去看看,https://u.jd.com/saQw1vP 红宝书第五版中文版 红宝书第五版英文原版pdf下载(访问密码 ...

  5. EasyExcel合并行处理并优化

    业务场景 由于业务需要导出如下图中订单数据和订单项信息,而一个订单对应多个订单项,所以会涉及到自定义合并行 1.简单处理项目使用的EasyExcel,经查找发现Excel种有个AbstractMerg ...

  6. SpringBoot整合富文本编辑器(UEditor)

    UEditro是一款比较好用的富文本编辑器,所谓的富文本编辑器就是和服务器交互的数据不是普通的字符串文件,而是一些内容包含比较广的字符串,一般是指的html页面,目前比较好用的是百度的UEditor, ...

  7. C# List LinQ Lambda 表达式

    ------------恢复内容开始------------ # 参考链接 : https://blog.csdn.net/wori/article/details/113144580 首先 => ...

  8. docker - [03] docker原理

    题记 一.docker是怎么工作的 docker是一个CS(Client - Server)结构的系统,docker的守护进程运行在主机上,通过Socket从客户端访问. docker Server接 ...

  9. Ansible - [01] 入门&安装部署

    自动化运维工具,可以批量远程其他主机并进行管理操作 一.什么是 Ansible Ansible首次发布于2012年,作者:Michael DeHaan,同时也是Cobbler的作者,Ansible于2 ...

  10. 面试题10- I. 斐波那契数列

    地址:https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof/ <?php /** 写一个函数,输入 n ,求斐波那契(Fibona ...