接着上一篇文章我们继续讲解
五、信号与槽的高级特性
1. 信号与信号的连接
在某些场景中,我们可能渴望当一个对象发射某个信号时,自动触发另一个对象的信号,而不是直打仗发槽函数。也就是说,可以把一个信号“转发”成另一个信号。如许做可以让某些逻辑更清晰:上层只关心“信号 -> 信号”这一对照关系,而不必显式地调用槽函数。
(1) 实现信号的转发
- 基本思路:将发送者对象的信号 signalA 与汲取者对象的“信号” signalB 连接起来。
- 语法:和连接信号与槽一样,只是把“槽函数”换成另一个信号。
示例代码:
- #include <QApplication>
- #include <QObject>
- #include <QDebug>
- // A 对象:发射信号A
- class SenderA : public QObject
- {
- Q_OBJECT
- public:
- explicit SenderA(QObject *parent = nullptr) : QObject(parent) {}
- signals:
- void signalA(); // 用于演示的信号
- public slots:
- void emitSignalA()
- {
- qDebug() << "[SenderA] emit signalA()";
- emit signalA();
- }
- };
- // B 对象:也有自己的信号B
- class SenderB : public QObject
- {
- Q_OBJECT
- public:
- explicit SenderB(QObject *parent = nullptr) : QObject(parent) {}
- signals:
- void signalB(); // 转发用的信号
- // 注意这里没有定义slot,B纯粹做一个转发者也可以
- };
- int main(int argc, char *argv[])
- {
- QApplication app(argc, argv);
- SenderA a;
- SenderB b;
- // 将a的signalA 与 b的signalB 连接,这就相当于 signalA -> signalB
- QObject::connect(&a, &SenderA::signalA,
- &b, &SenderB::signalB);
- // 还可以再把 b 的signalB 连接到其他对象的槽函数,或再连接到其他信号
- QObject::connect(&b, &SenderB::signalB,
- [](/*可带参数*/){
- qDebug() << "[Lambda] Received signalB from b!";
- });
- // 触发A的信号 -> 导致B也发射signalB -> 导致Lambda被调用
- a.emitSignalA();
- return 0;
- }
- #include "main.moc"
复制代码 关键点:
- connect(&a, &SenderA::signalA, &b, &SenderB::signalB):此时 signalB 并不是槽函数,而是一个信号。Qt 允许如许做。
- 当 a 发射 signalA() 时,b 会收到这个信号并立刻转发 signalB()。
- 其他对象若对 b 的 signalB() 感兴趣,可以继续 connect 到自己的槽函数或信号。
(2) 信号与信号连接的应用场景
- 分层筹划:有时我们渴望中间对象不关心详细逻辑,只转发信号给更上层或其他模块。
- 解耦:如果直接 signalA -> slotX 会耦合到槽函数实现;signalA -> signalB -> slotX 可以让 B 模块灵活更换或增加逻辑,而无需改动 A 或 X。
- 事件聚合或分发:在复杂系统里,可用信号之间的转发来举行统一管理、路由或过滤。
2. 带参数的信号与槽
在很多情况下,信号和槽需要携带信息举行通信。比方:当按钮被点击时,需要把当前的坐标传给槽函数,或者当数据更新时,需要把新的数值发给槽函数。
(1) 带参数的信号声明与使用
- 在类的 signals: 区域中声明一个带参数的信号,如 void dataChanged(int newVal, QString desc);
- 在类的 slots: 或任意可用的函数(包罗lambda)中编写与之匹配的形参列表,如 void onDataChanged(int val, const QString &str).
- connect() 时,信号与槽的参数类型、次序必须对应,否则连接会失效或出现告诫。
示例代码:
- class DataObject : public QObject
- {
- Q_OBJECT
- public:
- explicit DataObject(QObject *parent = nullptr) : QObject(parent) {}
- void setData(int v, const QString &desc)
- {
- if (m_val != v || m_desc != desc) {
- m_val = v;
- m_desc = desc;
- // 发射带参数的信号
- emit dataChanged(m_val, m_desc);
- }
- }
- signals:
- void dataChanged(int newVal, const QString &info);
- private:
- int m_val = 0;
- QString m_desc;
- };
- // 槽函数示例
- class Handler : public QObject
- {
- Q_OBJECT
- public slots:
- void handleDataChanged(int val, const QString &str)
- {
- qDebug() << "[Handler] data changed => value:" << val << ", info:" << str;
- }
- };
- // 使用
- DataObject dataObj;
- Handler handler;
- QObject::connect(&dataObj, &DataObject::dataChanged,
- &handler, &Handler::handleDataChanged);
- dataObj.setData(100, "Temperature");
复制代码
- 当 dataObj.setData(100, "Temperature") 实行时,若值有变化,就会 emit dataChanged(100, "Temperature")。
- handler 收到这个信号,实行 handleDataChanged(100, "Temperature")。
(2) 参数类型的匹配标题
- 次序:信号和槽的参数列表次序必须同等,类型必须兼容。
- 数目:槽函数的参数数目可以小于等于信号的参数数目(前提是前面部分类型同等)。多出的参数会被忽略。
- 比方,信号 void someSignal(int, QString) 可以连接到槽 void someSlot(int).
- 引用/常量:最好使用 const QString & 等方式避免拷贝,提高效率。
3. 信号与槽的断开连接
在某些情况下,我们需要“取消”某个信号与槽的绑定,避免重复调用或在对象烧毁前后发买卖外访问。
(1) 为什么需要断开连接
- 对象烧毁:若对象 A 仍在发射信号,但对象 B 已经被烧毁,如果没有及时断开,可能导致对无效内存的访问(不过 Qt 在对象析构时也会自动断开和它相干的连接,这通常能避免瓦解)。
- 逻辑调整:有时步伐需要动态地切换槽函数或暂时停止汲取信号。
- 性能:如果有多个槽监听同一个信号,但不再需要某些槽,可断开以减少实行开销。
(2) 怎样使用 QObject::disconnect()
- 基本用法:与 connect() 对应,disconnect() 也有多种重载形式,可以指定发送者、信号、汲取者、槽来断开指定的连接。
- 完全断开:若不指定信号与槽,只写 disconnect(&objSender, nullptr, &objReceiver, nullptr),则会断开 objSender 与 objReceiver 之间的全部连接。
示例代码:
- QObject::disconnect(senderObj, &SenderClass::someSignal,
- receiverObj, &ReceiverClass::someSlot);
复制代码
- 仅在发送者、信号、汲取者、槽都与原先的 connect() 相匹配时才会断开对应的那条连接。
- 如果需要断开多个连接,需要调用多次 disconnect() 或使用更广泛的断开方式。
使用场景:
- 动态切换:先 disconnect() 再 connect() 到新的槽。
- 清理阶段:在某些复杂逻辑中,手动保证连接的排除,以免后续误调用。
小结
- 信号转信号:
- 允许将一个对象的信号转发成另一个对象的信号,形成“信号 -> 信号 -> 槽”链路;
- 应用场景是转发、分层或解耦,让中间模块只关心转发逻辑,不必直接调用槽。
- 带参数的信号与槽:
- 在类声明里使用 signals: 定义带参数的信号;
- 在槽函数中确保形参类型、次序与信号匹配;
- 可以让信号携带更多信息给槽函数,提高可扩展性和可读性。
- 断开连接:
- 使用 QObject::disconnect() 手动排除某个信号与槽的关联;
- 常见于对象烧毁前或需要暂时停止汲取信号的场景;
- 注意只要对象存在并未自动烧毁连接,正常情况下 Qt 会在对象析构时自动断开相干连接。
通过对高级特性的学习,你可以在更复杂的场景下使用信号与槽:不仅可以实现多对象的信号转发、携带多种类型的参数,还能根据需要灵活地断开或重新连接,进一步提升步伐的灵活度与可维护性。在现实开发中,如果你的信号与槽链路非常复杂,可以考虑使用注释、类图或文档来记录,以免日后调试时肴杂。
六、信号与槽机制的底层原理
在深入使用信号与槽之前,了解一下底层原理能资助我们更好地理解它的工作方式。主要涉及 Qt 的元对象系统(Meta-Object System)以及信号与槽在运行时的调用流程。
1. 元对象系统(Meta-Object System)
(1)元对象系统的作用
Qt 之所以可以或许提供信号与槽、属性系统、对象反射等特性,根本原因在于它的元对象系统。这个系统可以理解为一种“记录和管理类信息的机制”,包罗:
- 识别类中包含 Q_OBJECT 宏的部分
- 记录类名、信号、槽、属性等信息
- 支持运行时查询和调用(如根据对象指针和信号名找到已连接的槽函数)
通过这种机制,Qt 为每个使用 Q_OBJECT 宏的类天生相应的元数据,资助完成信号与槽、属性读写等功能。
(2)moc(Meta-Object Compiler)的工作流程
当我们在类中使用 Q_OBJECT 宏并包含信号或槽时,Qt 的元对象编译器(moc)会举行额外的处理:
- 扫描头文件,找到 Q_OBJECT 宏所在的类。
- 自动天生一个额外的 C++ 源文件(比方 moc_MyClass.cpp)。
- 这个文件包含以下内容:
- 类的元数据(如类名、信号列表、槽列表)
- 供运行时使用的辅助函数(比如信号发射后的槽查找)
- 最终与平凡源文件一起编译,成为应用步伐的一部分。
简而言之,moc 会把你的类中声明的信号、槽等信息收集起来,编进步伐,使得在运行时可以或许举行“对象 + 信号”到“槽函数”的查找和调用。
(3)Q_OBJECT 宏的重要性
Q_OBJECT 是让类具备“Qt 元对象本领”的关键标志。如果在类中声明白信号和槽,却没有写 Q_OBJECT,那么 moc 就不会天生对应的元数据,也就无法完成真正的信号与槽连接。
2. 信号与槽的调用流程
当我们在代码中使用 emit 关键字发射一个信号时,Qt 内部会通过元对象系统找到与该信号相连的全部槽,并按照肯定规则去调用它们。可以分为以下几个阶段:
(1)信号发出后的处理流程
- 对象发射信号
比方 emit someSignal(123);。虽然 emit 本质上是个空宏,但它能让代码更直观,告诉大家这里是在发射信号。
- 查找连接列表
Qt 在内部维护一个连接信息表,记录“对象指针 + 信号”对应了哪些“对象指针 + 槽函数”。当某个信号被发射时,会检索该列表,找到全部匹配的槽。
- 根据连接类型调用槽
- DirectConnection:信号发射处立刻调用槽函数(当火线程、当前调用栈)。
- QueuedConnection:将信号调用打包成事件,投递到汲取者所在线程的事件循环,然后在该线程下一个事件循环周期里实行槽函数。
- AutoConnection:如果发送者与汲取者处于同一线程,则相称于 Direct;否则就变成 Queued。
- 依次实行全部槽
连接信息里可能存在多个槽,Qt 会依次调用。每个槽汲取到的参数与发射信号时传入的同等。
(2)槽函数的调用过程
- DirectConnection(或同线程下的 AutoConnection)
信号发射后,会直接在发射的那一瞬间调用槽函数,等全部槽函数实行完才返回到原处。
- QueuedConnection(或跨线程的 AutoConnection)
发射信号时,Qt 会将参数举行序列化,然后将这个调用信息封装成事件,推送到目的线程的事件队列。当目的线程处理事件时,才会反序列化参数并调用槽函数。
(3)一个简朴示例
假设我们写了:
- connect(senderObj, &SenderClass::valueChanged,
- receiverObj, &ReceiverClass::updateValue);
复制代码 并在 senderObj 内部做了:
- void SenderClass::setValue(int v)
- {
- if (m_value != v) {
- m_value = v;
- emit valueChanged(v); // 发射信号
- }
- }
复制代码 当 setValue(10) 被调用后:
- 触发 emit valueChanged(10);
- Qt 内部查找 (senderObj, "valueChanged") 全部的槽函数列表
- 找到 (receiverObj, "updateValue(int)")
- 如果是直接连接,就立刻在当火线程调用 receiverObj->updateValue(10)。如果是队列连接,就投递事件给汲取者线程,下次事件循环实行。
小结
- 元对象系统
Qt 通过 moc 工具来扫描带有 Q_OBJECT 宏的类,自动天生元数据,记录信号、槽、属性等信息,为信号与槽机制提供支持。
- 信号与槽调用
当对象发射信号时,Qt 会根据连接信息查找并调用全部相干槽。连接类型决定了槽函数的调用机遇和所在线程。
- 线程间通信
如果发送者与汲取者在不同线程中,AutoConnection 会自动接纳队列连接的方式,确保线程安全。
- Q_OBJECT 必须加
如果类中要使用信号、槽或 Qt 的高级特性,就肯定要包含 Q_OBJECT 宏,否则 moc 无法天生对应的元对象代码。
通过这些底层原理,我们就明白为什么必须有 Q_OBJECT 宏,也理解了 Qt 信号与槽机制是怎样在运行时完成“对象之间通信”的。一样平常开发中,写完 connect(...) 就能实现功能,险些不消关心底层逻辑,但碰到复杂情况(如多线程、跨模块等),了解原理能资助我们排查标题、做更合理的筹划。
七、信号与槽调试实战:从踩坑到填坑指南
作为Qt开发者,你肯定碰到过如许的抓狂时刻:明明写了connect,但点击按钮死活没反应!别慌,这里总结了我多年踩坑经验,手把手教你怎样快速定位标题。
1. 信号不相应的五大元凶
|