对于对象间的通信问题,很多框架采用回调函数类解决。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 函数 | 是 | 否 |
-
类型检查和隐式类型转换
基于字符串的语法依赖元对象系统的反射功能,使用字符串匹配方式检查信号和槽函数,有如下局限性:
-
链接错误只能在运行才能检查出来;
-
不能使用隐式类型转换;
-
不能解析类型定义和命名空间;
基于函数的语法由编译器来检查,编译器在编译期就能检查出链接错误,并且支持隐式类型转换,还能识别出同一类型的不同名称(即类型定义)。
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())); // ...
-
使用 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()); }); // ... }
-
链接 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)));
-
槽函数的参数个数
一般情况下,槽函数的参数类型与信号声明的一致,数量等于或少于信号的参数。基于字符串的语法提供了一个变通规则:如果参函数有默认参数,那么发射的信号就可以省略这些参数。当发射信号的参数少于槽函数的参数, 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); }
-
信号-槽的重载
对于重载的信号或槽,基于字符串的链接语法可以显示声明参数类型,但是基于函数的链接语法无法告诉编译器使用哪个实力进行链接,这时,需要通过 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 是接收信号的对象。
-
解除与一个发射者所有信号的连接
// 静态函数形式 disconnect(myObject, nullptr, nullptr, nullptr); // 成员函数形式 myObject->disconnect();
-
解除与一个特定信号的所有连接
// 静态函数形式 disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr); // 成员函数形式 myObject->disconnect(SIGNAL(mySignal()));
-
解除与一个特定接收者的所有连接
// 静态函数形式 disconnect(myObject, nullptr, myReceiver, nullptr); // 成员函数形式 myObject->disconnect(myReceiver);
-
解除特定的一个信号与槽的连接
// 静态函数形式 disconnect(lineEdit, &QLineEdit::textChanged, label, &QLabel::setText);
信号-槽的一些规则
-
继承 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 { ... };
-
函数指针不能用作信号或槽的参数
许多情况下可以使用继承或虚函数来代替函数指针。
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 *); };
-
信号或槽的参数为枚举量时必须全限定声明
这主要是针对基于字符串的链接语法,因为它是依靠字符串匹配来识别数据类型的。
C++class MyClass : public QObject { Q_OBJECT enum Error { ConnectionRefused, RemoteHostClosed, UnknownError }; signals: void stateChanged(MyClass::Error error); };
-
嵌套类不会能使用信号或槽
class A { public: class B { Q_OBJECT public slots: // WRONG void b(); }; };
-
不能引用信号或槽的反返回类型
信号或槽虽然可以有返回类型,但是它们的返回引用会被当作 void 类型。
-
类声明信号或槽的部分只能声明信号或槽
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