当前位置: 首页 > news >正文

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

简介

Qt的信号和槽(Signals and Slots)是Qt开发团队创造的一种特殊回调机制,提供了非常简洁易用的事件触发-函数调用机制。

原理学习

虽然上层使用简单,但底层实现机制却复杂的不得了,这里简单的学习一下大概原理。

1. 信号槽 语法

signal 声明信号,slot 声明槽函数,emit 发射信号,QObject::connect() 连接信号槽。
一个简单示例:

// 这里是 MyClass.h 头文件
// include 语句此处暂时忽略
class MyClass: public QObject
{Q_OBJECT
signals:void my_signal_test(QString text);
public:MyClass();~MyClass();public slots:void my_slot_test(QString text);
};
//
//---------------------------------------------------------------------
// 这里是 MyClass.cpp 源文件
// include 语句此处暂时忽略
//构造函数
MyClass::MyClass()
{connect(this, &MyClass::my_signal_test, this, &MyClass::my_slot_test);
}
//槽函数
void MyClass::my_slot_test(QString text)
{qDebug() << "槽函数已执行";qDebug() << text;
}
//
//---------------------------------------------------------------------
// 这里是 main.cpp 主源文件
// include 语句此处暂时忽略
int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);MyClass me;emit me.my_signal_test("hello qt");return a.exec();
}

这里,简单将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 会帮我们自动实现所有信号的函数体,也就是说,信号 和 槽 都是类的成员函数。信号的函数体内容是什么呢?
是大概类似下面的东东。

// SIGNAL 0
void MyClass::my_signal_test(QString _t1)
{void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

简单来说,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线程中吗?,所以还是用 关联线程 这个概念好一些。

好。简单总结一下:

  1. Qt对象才可以使用 Qt 的核心机制,如信号与槽、元对象系统、事件处理等。
  2. Qt对象才有关联线程的说法。你自己随便写的 int a, pair<int,int> 等普普通通的类对象,就是一块死数据,不在整个Qt的大一统体制内。
  3. 另外,Qt库中,不是说前面带Q的就是Qt对象类,例如 QString 也只是简单数据类,不算上面说的 Qt 对象。成为 Qt 对象类,必须满足上面那两条要求。

现在来看看 QThread 这个类,这个是Qt的线程类,比较特殊,首先 QThread 自身是个 Qt 对象类,在哪个线程中创建了它,它的关联线程就是谁,但是吧,他是个线程类,它自己内部管理着一个新的线程。要是在这个新线程中创建了Qt对象,那么此对象关联线程是 新线程。我们可以使用从 QObject 继承来的成员函数 QObject::moveToThread() 来修改关联线程。

看个示例:

// test.h
#ifndef TEST_H
#define TEST_H
#include <QObject>
#include <QDebug>
class Test : public QObject{Q_OBJECT// OK , Test 已经成为合格的 Qt 对象类了
public:void print_hello(){qDebug() << "hello qt";}
};
#endif//----------------------------------------------------------------------------------//main.cpp#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include "test.h"int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);QThread new_thread;Test mytest;qDebug() << "mytest 的关联线程: " << mytest.thread();qDebug() << "new_thread 的关联线程: " << new_thread.thread();mytest.moveToThread(&new_thread);qDebug() << "mytest 新 的关联线程: " <<  mytest.thread();return a.exec();
}

输出结果:

mytest 的关联线程:  QThread(0x1e6b5336030, name = "Qt mainThread")
new_thread 的关联线程:  QThread(0x1e6b5336030, name = "Qt mainThread")
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() 等。

用一段代码展示一下(复制的别人的)

#include <QCoreApplication>
#include <QEvent>
#include <QDebug>
#include <QTimer>// 自定义事件类
class MyCustomEvent : public QEvent {
public:static const QEvent::Type MyEventType = static_cast<QEvent::Type>(QEvent::User + 1);MyCustomEvent(const QString &message): QEvent(MyEventType), message(message) {}QString getMessage() const { return message; }private:QString message;
};// 自定义对象类
class MyObject : public QObject {Q_OBJECTprotected:// 重写 event() 方法,处理自定义事件bool event(QEvent *event) override {if (event->type() == MyCustomEvent::MyEventType) {MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);qDebug() << "Custom event received with message:" << myEvent->getMessage();return true; // 事件已处理}return QObject::event(event); // 传递给父类处理}
};// 自定义事件过滤器类
class MyEventFilter : public QObject {Q_OBJECTprotected:// 重写 eventFilter() 方法,过滤自定义事件bool eventFilter(QObject *obj, QEvent *event) override {if (event->type() == MyCustomEvent::MyEventType) {MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);qDebug() << "Event filter caught custom event with message:" << myEvent->getMessage();return true; // 阻止事件进一步传播}return QObject::eventFilter(obj, event); // 传递给父类处理}
};int main(int argc, char *argv[])
{QCoreApplication app(argc, argv);MyObject obj;MyEventFilter filter;// 安装事件过滤器obj.installEventFilter(&filter);// 创建并发送自定义事件MyCustomEvent *event = new MyCustomEvent("Hello, Qt!");QCoreApplication::postEvent(&obj, event);// 创建一个定时器,定时退出应用程序QTimer::singleShot(5000, &app, &QCoreApplication::quit);return app.exec(); // 进入事件循环
}

5. 信号槽的连接类型

前面铺垫了那么多,终于写到这里了,全文最想写的就是这部分了。
看一下 connect 函数声明,当然 connect 有好几个重载版本,这里拿最常用的一个来说。

template <typename PointerToMemberFunction> 
static QMetaObject::Connection QObject::connect (const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection
)

五个参数分别是
1、信号发送者对象指针
2、信号成员函数指针
3、信号接收者对象指针
4、槽函数成员函数指针
5、连接类型【默认值:Qt::AutoConnection】

连接类型有以下几种:

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

当信号发出时,槽函数会被执行两次。 要是加了 Qt::UniqueConnection,那么添加连接时,会检查相同的连接是不是已经有了,如果有了,就不添加了。
6. Qt::SingleShotConnection,单次连接,这个是和上面几种 搭配一起使用的,使用位或运算 | 组合,例如:Qt::AutoConnection | Qt::SingleShotConnection,加上这个的话,该连接使用一次后就自动删除。效果就是,不管发射了多少次信号,槽函数只执行一次,因为第一次用完就删掉了,后面再发射信号时,没有匹配的槽函数了。除非重新手动再 connect() 添加上去。

相关文章:

  • 安装SDL和FFmpeg
  • 005-nlohmann/json 基础方法-C++开源库108杰
  • 性能测试之性能调优
  • 机器学习朴素贝叶斯算法
  • 0-1背包问题基础概念
  • 家政维修服务平台需求规格说明书
  • 记9(Torch
  • LeetCode 热题 100 17. 电话号码的字母组合
  • SQL常见误区
  • [低代码 + AI] 明道云与 Dify 的三种融合实践方式详解
  • 大模型学习专栏-导航页
  • Python字符串全解析:从基础操作到高级应用的技术指南
  • LeetCode:链表的中间结点
  • Python核心技巧 类与实例:面向对象编程的基石
  • 41.寻找缺失的第一个正数:原地哈希算法详解
  • 开元类双端互动组件部署实战全流程教程(第2部分:控制端协议拆解与机器人逻辑调试)
  • 精益数据分析(41/126):深入解读移动应用商业模式的关键指标与策略
  • Leetcode刷题记录32——搜索二维矩阵 II
  • SecureCRT 使用指南:安装、设置与高效操作
  • 判断题材持续性
  • 泰国培训十万网络安全人员加强网络防御打击电诈
  • 浙江医生举报3岁男童疑遭生父虐待,妇联:已跟爷爷奶奶回家
  • 全国铁路旅客发送量连续3天同比增幅超10%,今日预计发送1800万人次
  • 美国中央情报局计划裁员1200人
  • 韩代总统李周浩履职
  • 图忆|上海车展40年:中国人的梦中情车有哪些变化(下)