Qt 信号槽机制底层原理学习

打印 上一主题 下一主题

主题 1820|帖子 1820|积分 5460

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
简介

Qt的信号和槽(Signals and Slots)是Qt开辟团队创造的一种特殊回调机制,提供了非常简便易用的变乱触发-函数调用机制。
原理学习

固然上层使用简单,但底层实现机制却复杂的不得了,这里简单的学习一下大概原理。
1. 信号槽 语法

signal 声明信号,slot 声明槽函数,emit 发射信号,QObject::connect() 连接信号槽。
一个简单示例:
  1. // 这里是 MyClass.h 头文件
  2. // include 语句此处暂时忽略
  3. class MyClass: public QObject
  4. {
  5.     Q_OBJECT
  6. signals:
  7.         void my_signal_test(QString text);
  8. public:
  9.     MyClass();
  10.     ~MyClass();
  11. public slots:
  12.     void my_slot_test(QString text);
  13. };
  14. //
  15. //---------------------------------------------------------------------
  16. // 这里是 MyClass.cpp 源文件
  17. // include 语句此处暂时忽略
  18. //构造函数
  19. MyClass::MyClass()
  20. {
  21.         connect(this, &MyClass::my_signal_test, this, &MyClass::my_slot_test);
  22. }
  23. //槽函数
  24. void MyClass::my_slot_test(QString text)
  25. {
  26.         qDebug() << "槽函数已执行";
  27.         qDebug() << text;
  28. }
  29. //
  30. //---------------------------------------------------------------------
  31. // 这里是 main.cpp 主源文件
  32. // include 语句此处暂时忽略
  33. int main(int argc, char *argv[])
  34. {
  35.     QCoreApplication a(argc, argv);
  36.         MyClass me;
  37.     emit me.my_signal_test("hello qt");
  38.     return a.exec();
  39. }
复制代码
这里,简单将MyClass自己的信号绑定到自己的槽,然后在 main 中手动触发 信号。
乍一看,好像只是简单的一连串函数调用,实则大有天机。
2. 元对象处理

上面例子中,我们只是声明了 void my_signal_test(QString text); 并没有实现该函数内容,为什么能在 main 中能 emit me.my_signal_test("hello qt"); ?因为我们写的Qt版C++ 不是标准C++,上面叠加了Qt自己的专属语法,像 signals、slots、emit 是Qt 专属关键字,c++编译器不能识别的,得先经过Qt专属编译器(moc:元对象编译器 meta object compiler)转换成标准C++文件,才气最终编译。
moc 会帮我们自动实现全部信号的函数体,也就是说,信号 和 槽 都是类的成员函数。信号的函数体内容是什么呢?
是大概类似下面的东东。
  1. // SIGNAL 0
  2. void MyClass::my_signal_test(QString _t1)
  3. {
  4.     void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
  5.     QMetaObject::activate(this, &staticMetaObject, 0, _a);
  6. }
复制代码
简单来说,moc 会把我们的类,添加一大堆源对象信息、运行时范例信息等。并且在 信号函数中,使用QMetaObject::activate() 进行激活信号,QMetaObject::activate() 会把传过来的元信息读取,查抄该信号与槽的连接情况、连接方式、吸收者对象、槽函数所在,然后使用对应方式进行槽函数的调用。
我们使用 QObject::connect() 进行连接信号槽时,包含连接信息的内容 最终总得保存在一个地方吧,要否则发射信号后需要调用槽函数时,上哪查去。答案就是保存在发射者对象中,moc 会为我们的类 额外添加一个成员变量数组,来保存全部已添加的 信号-槽函数 连接信息。除了可以使用 connect 自动添加 信号槽连接,也可以使用 QObject::disconnect() 函数取消连接,参数同 connect 一样。
3. 线程

在 Qt 中,只有 Qt 对象才有资格使用 Qt 的核心机制,如信号与槽、元对象体系、变乱处理等。
一个类要想成为 Qt 类,必须满足以下要求:


  • 类必须直接或间接继承自 QObject
  • 在类的声明中添加 Q_OBJECT 宏。
    上面提到的 一大堆元对象信息,就是 moc 将 Q_OBJECT 更换来的。
每个 Qt 对象都有自己的关联线程,初始情况下,对象在哪个线程中创建的,它的关联线程就是谁。
每个Qt对象都从 QObject 继承了 QObject::thread() 函数,调用此函数,可以获取自己关联线程的指针。
很多文章博客每每都会笼统地说“某某对象在哪个线程”,这么说不免会造成误解,其实欠好,在线程A中创建的对象obj,我也能有办法在线程B中使用,那我在线程B中使用此obj对象时,你能说obj在B线程中吗?,所以还是用 关联线程 这个概念好一些。
好。简单总结一下:

  • Qt对象才可以使用 Qt 的核心机制,如信号与槽、元对象体系、变乱处理等。
  • Qt对象才有关联线程的说法。你自己任意写的 int a, pair<int,int> 等普平凡通的类对象,就是一块死数据,不在整个Qt的大一统体制内。
  • 另外,Qt库中,不是说前面带Q的就是Qt对象类,例如 QString 也只是简单数据类,不算上面说的 Qt 对象。成为 Qt 对象类,必须满足上面那两条要求。
现在来看看 QThread 这个类,这个是Qt的线程类,比较特殊,首先 QThread 自身是个 Qt 对象类,在哪个线程中创建了它,它的关联线程就是谁,但是吧,他是个线程类,它自己内部管理着一个新的线程。要是在这个新线程中创建了Qt对象,那么此对象关联线程是 新线程。我们可以使用从 QObject 继承来的成员函数 QObject::moveToThread() 来修改关联线程。
看个示例:
  1. // test.h
  2. #ifndef TEST_H
  3. #define TEST_H
  4. #include <QObject>
  5. #include <QDebug>
  6. class Test : public QObject{
  7.     Q_OBJECT
  8.         // OK , Test 已经成为合格的 Qt 对象类了
  9. public:
  10.     void print_hello(){
  11.         qDebug() << "hello qt";
  12.     }
  13. };
  14. #endif
  15. //----------------------------------------------------------------------------------
  16. //main.cpp
  17. #include <QCoreApplication>
  18. #include <QThread>
  19. #include <QDebug>
  20. #include "test.h"
  21. int main(int argc, char *argv[])
  22. {
  23.     QCoreApplication a(argc, argv);
  24.     QThread new_thread;
  25.     Test mytest;
  26.     qDebug() << "mytest 的关联线程: " << mytest.thread();
  27.     qDebug() << "new_thread 的关联线程: " << new_thread.thread();
  28.     mytest.moveToThread(&new_thread);
  29.     qDebug() << "mytest 新 的关联线程: " <<  mytest.thread();
  30.     return a.exec();
  31. }
复制代码
输出结果:
  1. mytest 的关联线程:  QThread(0x1e6b5336030, name = "Qt mainThread")
  2. new_thread 的关联线程:  QThread(0x1e6b5336030, name = "Qt mainThread")
  3. mytest 新 的关联线程:  QThread(0xacc55ff890)
复制代码
可以看到,初始情况下,因为 mytest、new_thread 两个对象都是在主线程中创建的,所以它们的关联线程都是主线程,当我们使用 moveToThread() 函数转移后,被操纵对象的关联线程就随之改变。
new_thread 就好比在 A 公司上班的人,自己背地里还是 B 公司的老总,如果你要问 new_thread 是哪个公司的,他会回答你是 A 公司的,另外,别人也可以跳槽到 new_thread 掌管的 B 公司。
4. 变乱循环

变乱循环是一个无限循环,它从操纵体系或其他变乱源获取变乱,并将其分发给应用程序中的对象进行处理。变乱循环确保应用程序可以或许不断地响应用户输入和其他异步变乱。
上面 QCoreApplication a; return a.exec(); 启动的是主变乱循环,除此之外,Qt 中每个线程都有属于该线程的 一个变乱循环,还是用上面的例子,new_thread 也掌管着一个变乱循环,不外我们没启动,QThread::exec() 函数负责启动本线程变乱循环,但是这个方法是 preotected 的。不让直接调用,需要执行 new_thread.start() 函数启动变乱循环,start() 会调用 run(),run() 默认操纵是调用 exec()。
变乱循环不停地从变乱队列中取出变乱,并将其分发给目标对象处理。


  • 变乱过滤器:变乱首先传递给变乱过滤器,变乱过滤器可以选择处理变乱或将其传递给下一个处理器。
  • 变乱处理器:如果变乱没有被变乱过滤器处理,Qt 会调用目标对象的 event() 方法。event() 方法会根据变乱范例调用特定的变乱处理器方法,例如 mousePressEvent()、keyPressEvent() 等。
用一段代码展示一下(复制的别人的)
  1. #include <QCoreApplication>
  2. #include <QEvent>
  3. #include <QDebug>
  4. #include <QTimer>
  5. // 自定义事件类
  6. class MyCustomEvent : public QEvent {
  7. public:
  8.     static const QEvent::Type MyEventType = static_cast<QEvent::Type>(QEvent::User + 1);
  9.     MyCustomEvent(const QString &message)
  10.         : QEvent(MyEventType), message(message) {}
  11.     QString getMessage() const { return message; }
  12. private:
  13.     QString message;
  14. };
  15. // 自定义对象类
  16. class MyObject : public QObject {
  17.     Q_OBJECT
  18. protected:
  19.     // 重写 event() 方法,处理自定义事件
  20.     bool event(QEvent *event) override {
  21.         if (event->type() == MyCustomEvent::MyEventType) {
  22.             MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);
  23.             qDebug() << "Custom event received with message:" << myEvent->getMessage();
  24.             return true; // 事件已处理
  25.         }
  26.         return QObject::event(event); // 传递给父类处理
  27.     }
  28. };
  29. // 自定义事件过滤器类
  30. class MyEventFilter : public QObject {
  31.     Q_OBJECT
  32. protected:
  33.     // 重写 eventFilter() 方法,过滤自定义事件
  34.     bool eventFilter(QObject *obj, QEvent *event) override {
  35.         if (event->type() == MyCustomEvent::MyEventType) {
  36.             MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);
  37.             qDebug() << "Event filter caught custom event with message:" << myEvent->getMessage();
  38.             return true; // 阻止事件进一步传播
  39.         }
  40.         return QObject::eventFilter(obj, event); // 传递给父类处理
  41.     }
  42. };
  43. int main(int argc, char *argv[])
  44. {
  45.     QCoreApplication app(argc, argv);
  46.     MyObject obj;
  47.     MyEventFilter filter;
  48.     // 安装事件过滤器
  49.     obj.installEventFilter(&filter);
  50.     // 创建并发送自定义事件
  51.     MyCustomEvent *event = new MyCustomEvent("Hello, Qt!");
  52.     QCoreApplication::postEvent(&obj, event);
  53.     // 创建一个定时器,定时退出应用程序
  54.     QTimer::singleShot(5000, &app, &QCoreApplication::quit);
  55.     return app.exec(); // 进入事件循环
  56. }
复制代码
5. 信号槽的连接范例

前面铺垫了那么多,终于写到这里了,全文最想写的就是这部分了。
看一下 connect 函数声明,当然 connect 有好几个重载版本,这里拿最常用的一个来说。
  1. template <typename PointerToMemberFunction>
  2. static QMetaObject::Connection QObject::connect (
  3.         const QObject *sender,
  4.         PointerToMemberFunction signal,
  5.         const QObject *receiver,
  6.         PointerToMemberFunction method,
  7.         Qt::ConnectionType type = Qt::AutoConnection
  8. )
复制代码
五个参数分别是
1、信号发送者对象指针
2、信号成员函数指针
3、信号吸收者对象指针
4、槽函数成员函数指针
5、连接范例【默认值:Qt::AutoConnection】
连接范例有以下几种:
  1. Qt::AutoConnection
  2. Qt::DirectConnection
  3. Qt::QueuedConnection
  4. Qt::BlockingQueuedConnection
  5. Qt::UniqueConnection
  6. Qt::SingleShotConnection
复制代码

  • 先说 Qt:irectConnection ,这个是直接连接,当信号发射时,直接在信号发射线程调用吸收者的槽函数。注意是信号所在线程,就是说发射信号这个动作在哪个线程发生的,槽函数就在哪个线程执行,而且是同步阻塞的,也就是在发射信号的地方,遍历发射者的保存的连接列表,从中依次调用 全部连接本信号的 槽函数(仅限Qt:irectConnection范例的),因为是同一线程执行,所以必须等这些槽函数全部执行完返回后,发射语句 后面的逻辑才会继续执行。注意,这种连接方式和发射者(sender)的关联线程是哪个没有关系,网上一些博客,总是错误的描述成 Qt:irectConnection 方式 是在 发射者(sender)所在线程执行槽函数。
  • Qt:ueuedConnection,队列连接,当信号发射时,如果该信号的某个连接的范例是 Qt:ueuedConnection 的,则先获取 吸收者(receiver)的关联线程,再获取吸收者关联线程掌管的 变乱循环队列,然后将 信号打包成 Qt变乱,投递到 该变乱循环队列中。完事返回。这就好比快递员将货物直接放你家门口,然后走人,不等着你劈面签收。 当接受者关联线程的变乱循环处理到此变乱时,对应的槽函数才会执行。
  • Qt::BlockingQueuedConnection,阻塞队列连接,和上面差不多,只不外,快递员将货物直接放你家门口后,搁那不停等着你出现,直到你出面签收完了,快递员才走。对于实际程序则是 投递完变乱 后,信号线程把自己挂起,等该信号的全部槽函数执行完后,才将其叫醒,继续后面的逻辑。
  • Qt::AutoConnection,自动连接,信号发射时,先查抄一下 信号所在线程接受者的关联线程 是否是同一线程,如果是,则使用 Qt:irectConnection 方式,否则使用 Qt:ueuedConnection 方式。 注意:是比较 信号所在线程 和 吸收者的关联线程,而不是比较 发送者的关联线程 和 吸收者的关联线程
  • Qt::UniqueConnection, 去重连接,这个是和上面几种 搭配一起使用的,使用位或运算 | 组合,例如:Qt:ueuedConnection | Qt::UniqueConnection,不加这个的话,相同的连接可以被重复添加,例如我执行两遍
  1. connect(this, &MyClass::my_signal_test, this, &MyClass::my_slot_test);
  2. connect(this, &MyClass::my_signal_test, this, &MyClass::my_slot_test);
复制代码
当信号发出时,槽函数会被执行两次。 要是加了 Qt::UniqueConnection,那么添加连接时,会查抄相同的连接是不是已经有了,如果有了,就不添加了。
6. Qt::SingleShotConnection,单次连接,这个是和上面几种 搭配一起使用的,使用位或运算 | 组合,例如:Qt::AutoConnection | Qt::SingleShotConnection,加上这个的话,该连接使用一次后就自动删除。效果就是,不管发射了多少次信号,槽函数只执行一次,因为第一次用完就删掉了,后面再发射信号时,没有匹配的槽函数了。除非重新手动再 connect() 添加上去。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

万万哇

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表