找回密码
 立即注册
首页 业界区 业界 QT 的信号-槽机制

QT 的信号-槽机制

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

信号-槽机制与 moc 及元对象系统密切相关,在 QT 中使用信号-槽机制必须满足以下条件:

  • 继承 QObject 类(或其子类),而且必须是第一个继承类,否则 moc 不会认为它是 QObject 的子类;
  • 必须使用宏 Q_OBJECT,否则 moc 不会进行预编译,不会在元数据中生成对应的信号-槽代码;
  • 在类中使用 Q_SIGNAL(signal)/Q_SIGNALS(signals) 声明信号,不需要实现;
  • 在类中使用 Q_SLOT(slot)/Q_SLOTS(slots) 声明槽函数;
信号-槽的链接类型


  • Qt::AutoConnection (默认类型)
    如果发射的信号与接收对象在同一个线程,该类型的处理方式与Qt:irectConnection一样,否则与Qt:ueuedConnectio一样。
  • Qt:irectConnection
    当信号发射时立即调用槽函数(与回调函数一样)。槽函数在发射信号的线程中执行。
  • Qt:ueuedConnection
    槽函数在接收者线程内执行。当事件循环的控制权交给接收线程时执行槽函数。使用 Qt:ueuedConnection 类型时,信号及槽的参数类型必须是 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:irectConnection 类型时,如果信号的发送者和接收者不在同一个线程中,则不是线程安全的。
信号-槽的链接方式

一个信号可以链接多个槽函数,一个槽函数也可以链接多个信号,信号也可以直接链接到另一个信号。如果一个信号链接多个槽函数,当发射信号时,槽函数参照链接时的先后顺序进行调用执行。
QT 提供了 2 种链接方式:基于字符串的语法和基于函数的语法。
基于字符串的语法:
  1. QMetaObject::Connection QObject::connect(const QObject **sender*, const char **signal*, const QObject **receiver*, const char **method*, Qt::ConnectionType *type* = Qt::AutoConnection)
  2.     // 需要使用宏 SIGNAL() 和 SLOT() 声明信号和槽函数
复制代码
基于函数的语法:
  1. 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);
    2. auto doubleSpinBox = new QDoubleSpinBox(this);
    3. // OK: 编译器将 int 转为 double
    4. connect(slider, &QSlider::valueChanged,
    5.         doubleSpinBox, &QDoubleSpinBox::setValue);
    6. // ERROR: 字符串无法包含转换信息
    7. connect(slider, SIGNAL(valueChanged(int)),
    8.         doubleSpinBox, SLOT(setValue(double)));
    9.         auto audioInput = new QAudioInput(QAudioFormat(), this);
    10. auto widget = new QWidget(this);
    11. // OK
    12. connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
    13.         widget, SLOT(show()));
    14. // ERROR: 无法使用命名空间,字符串 "State" 与 "QAudio::State" 不匹配
    15. using namespace QAudio;
    16. connect(audioInput, SIGNAL(stateChanged(State)),
    17.         widget, SLOT(show()));
    18. // ...
    复制代码
<ol start="2">使用 lambda 表达式链接信号
基于函数的语法支持 C++ 11 的 lambda 表达式,也支持标准函数、非成员函数、指向函数的指针。但是为了提高可读性,信号应该链接到槽函数、 lambda 表达式和其它信号。
  1. class TextSender : public QWidget {
  2.     Q_OBJECT
  3.     QLineEdit *lineEdit;
  4.     QPushButton *button;
  5. signals:
  6.     void textCompleted(const QString& text) const;
  7. public:
  8.     TextSender(QWidget *parent = nullptr);
  9. };
  10. TextSender::TextSender(QWidget *parent) : QWidget(parent) {
  11.     lineEdit = new QLineEdit(this);
  12.     button = new QPushButton("Send", this);
  13.         // 使用 lambda 表达式作为槽函数
  14.     connect(button, &QPushButton::clicked, [=] {
  15.         emit textCompleted(lineEdit->text());
  16.     });
  17.     // ...
  18. }
复制代码
链接 C++ 对象与 QML 对象
基于字符串的语法可以链接 C++ 对象与 QML 对象,因为 QML 类型只在运行时进行解析, C++ 编译器无法识别。
  1. // QmlGui.qml 文件
  2. Rectangle {
  3.     width: 100; height: 100
  4.     signal qmlSignal(string sentMsg)
  5.     function qmlSlot(receivedMsg) {
  6.         console.log("QML received: " + receivedMsg)
  7.     }
  8.     MouseArea {
  9.         anchors.fill: parent
  10.         onClicked: qmlSignal("Hello from QML!")
  11.     }
  12. }
  13. // C++ 类文件
  14. class CppGui : public QWidget {
  15.     Q_OBJECT
  16.     QPushButton *button;
  17. signals:
  18.     void cppSignal(const QVariant& sentMsg) const;
  19. public slots:
  20.     void cppSlot(const QString& receivedMsg) const {
  21.         qDebug() << "C++ received:" << receivedMsg;
  22.     }
  23. public:
  24.     CppGui(QWidget *parent = nullptr) : QWidget(parent) {
  25.         button = new QPushButton("Click Me!", this);
  26.         connect(button, &QPushButton::clicked, [=] {
  27.             emit cppSignal("Hello from C++!");
  28.         });
  29.     }
  30. };
  31. auto cppObj = new CppGui(this);
  32. auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
  33. auto qmlObj = quickWidget->rootObject();
  34. // QML 信号链接到 C++ 槽函数
  35. connect(qmlObj, SIGNAL(qmlSignal(QString)), cppObj, SLOT(cppSlot(QString)));
  36. // C++ 信号链接到 QML 槽函数
  37. connect(cppObj, SIGNAL(cppSignal(QVariant)), qmlObj, SLOT(qmlSlot(QVariant)));
复制代码
解除信号与槽的连接

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

  • 解除与一个发射者所有信号的连接
    1. public slots:
    2.     void printNumber(int number = 42) {
    3.         qDebug() << "Lucky number" << number;
    4.     }
    5.    
    6.    
    7. DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) {
    8.     // OK: 调用 printNumber() 时使用默认值 42
    9.     connect(qApp, SIGNAL(aboutToQuit()),
    10.             this, SLOT(printNumber()));
    11.     // ERROR: 编译器要求参数匹配
    12.     connect(qApp, &QCoreApplication::aboutToQuit,
    13.             this, &DemoWidget::printNumber);
    14. }
    复制代码
  • 解除与一个特定信号的所有连接
    1. // 槽函数重载定义
    2. QLCDNumber::display(int)
    3. QLCDNumber::display(double)
    4. QLCDNumber::display(QString)
    5. auto slider = new QSlider(this);
    6. auto lcd = new QLCDNumber(this);
    7. // S基于字符串的链接语法
    8. connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
    9. // 基于函数的链接语法
    10. connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));
    复制代码
  • 解除与一个特定接收者的所有连接
    1. // btnProperty 是 QPushButton类型,作为信号的发射者,
    2. // 此方法为 click() 信号的槽函数,并使用了自动链接
    3. void Widget::on_btnProperty_clicked()
    4. {
    5.     //获取信号的发射者
    6.     QPushButton *btn= qobject_cast<QPushButton*>(sender());
    7.     bool isFlat= btn->property("flat").toBool();
    8.     btn->setProperty("flat", !isFlat);
    9. }
    复制代码
  • 解除特定的一个信号与槽的连接
    1. connect(action, &QAction::triggered, engine,
    2.         [=]() { engine->processAction(action->text()); });
    复制代码
信号-槽的一些规则


  • 继承 QObject 类
    只有继承 QObject 类才能使用信号-槽。有多重继承时,QObject 必须是第一个继承类,因为 moc 总是检查第一个继承的类是否为 QObject ,如果不是将不会生成moc文件。另外,模板类不能使用 Q_OBJECT 宏。
    1. // 静态函数形式
    2. disconnect(myObject, nullptr, nullptr, nullptr);
    3. // 成员函数形式
    4. myObject->disconnect();
    复制代码
  • 函数指针不能用作信号或槽的参数
    许多情况下可以使用继承或虚函数来代替函数指针。
    1. // 静态函数形式
    2. disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);
    3. // 成员函数形式
    4. myObject->disconnect(SIGNAL(mySignal()));
    复制代码
  • 信号或槽的参数为枚举量时必须全限定声明
    这主要是针对基于字符串的链接语法,因为它是依靠字符串匹配来识别数据类型的。
    1. // 静态函数形式
    2. disconnect(myObject, nullptr, myReceiver, nullptr);
    3. // 成员函数形式
    4. myObject->disconnect(myReceiver);
    复制代码
  • 嵌套类不会能使用信号或槽
    1. // 静态函数形式
    2. disconnect(lineEdit, &QLineEdit::textChanged, label, &QLabel::setText);
    复制代码
  • 不能引用信号或槽的反返回类型
    信号或槽虽然可以有返回类型,但是它们的返回引用会被当作 void 类型。
  • 类声明信号或槽的部分只能声明信号或槽
    moc 编译器会检查这部分的声明
集成第三方信号-槽

集成第三方信号-槽机制,需要避免 signal、slots、emit关键字与第三方(例如 Boost)冲突,主要是通过配置使 moc 不使用 signal、slots、emit关键字。需要做以下配置:对于使用 CMake 的项目,需要在工程文件中添加
  1. // WRONG
  2. class SomeTemplate<int> : public QFrame
  3. {
  4.     Q_OBJECT
  5.     ...
  6. signals:
  7.     void mySignal(int);
  8. };
  9. // correct
  10. class SomeClass : public QObject, public OtherClass
  11. {
  12.     ...
  13. };
复制代码
对于使用 qmake 的项目,需要在 .pro文件中添加
  1. class SomeClass : public QObject
  2. {
  3.    Q_OBJECT
  4. public slots:
  5.    void apply(void (*apply)(List *, void *), char *); // WRONG
  6. };
  7. // correct
  8. typedef void (*ApplyFunction)(List *, void *);
  9. class SomeClass : public QObject
  10. {
  11.    Q_OBJECT
  12. public slots:
  13.    void apply(ApplyFunction, char *);
  14. };
复制代码
源文件中相应的关键字要替换为 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 的信号-槽不适合高性能需求的场景(例如,核心算法、高频事件处理,游戏循环、音频处理等要求毫秒级响应的场景)。
属性绑定

信号-槽机制随然解耦了对象,但是某些场景下用起来也会比较繁琐。例如,2个对象有关联关系使,一个对象需要跟随另一个对象变动。此时,可以使用 QT 提供的可绑定属性来解决,具体参考:QT 的可绑定属性,简化信号、槽(SIGNAL、SLOT)机制的方法
【参考】:
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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册