Qt信号槽与事件循环学习笔记
事件与事件循环
在Qt中,事件(event)被封装为QEvent类/子类对象,用来表示应用内部或外部发生的各种事情。事件可以被任何QObject子类的对象接收并处理。
根据事件的创建方式和调度方式,Qt中事件可分为三类,分别是:
- 自发事件(Spontaneous event)由窗口系统(window system)创建,随后加入事件队列,等待主事件循环处理(首先转换为QEvents实例,再分发给对应的QObjects实例)。
- 推送事件(Posted event)由Qt程序创建,并加入Qt的事件队列,等待事件循环分发。
- 发送事件(Sent event)由Qt程序创建,直接传递给目标对象。
Qt中的事件循环(event loop)是通过一个队列来循环处理事件的机制。当队列中有事件时,事件循环会处理事件;如果队列中没有事件,则事件循环会阻塞等待。
在Qt程序中,每个线程都可以有自己的事件循环(每个线程只能拥有一个活动的事件循环,并对应一个事件队列),事件循环在调用.exec()时进行启动。主线程/GUI线程对应的事件循环,被称为主事件循环(main event loop)。一般来说,在main()函数的末尾会调用QApplication::exec()函数,从而启动并进入Qt的主事件循环(更准确地说,Qt的主事件循环在主线程中调用QCoreApplication::exec()时启动),直到exit()函数被调用。
从概念上来说,事件循环可以理解为一些while循环:
while (!exit_was_called) {
while (!posted_event_queue_is_empty) {
process_next_posted_event();
}
while (!spontaneous_event_queue_is_empty) {
process_next_spontaneous_event();
}
while (!posted_event_queue_is_empty) {
process_next_posted_event();
}
}
首先,事件循环处理Posted事件,直到队列为空。然后,处理Spontaneous事件(首先转换为QEvents对象,然后发送给QObjects实例)。最后,处理在Spontaneous事件处理过程中生成的Posted事件。事件循环不会对Sent事件进行处理,Sent事件会直接传递给目标对象。
事件循环首先会将事件传递给对象树顶部的QObject实例,然后从根节点开始逐级向下传递事件,直到找到能够处理该事件的接收者对象。这个过程被称为事件传递(Event Propagation)。事件的处理通过调用QObject实例的event()函数来完成的。
在使用事件循环机制时,exec()用于开启事件循环,exit()或quit()用于退出事件循环,值得注意的是,的quit()或exit()函数并不会立即退出事件循环,而是等待控制权返回事件循环后,才会真正退出事件循环,并返回exec()调用处,这里有点费解,之后会通过一个例子说明。
更多关于Qt中事件系统的细节,除了参考Qt源码和官方文档The Event System、QEvent Class,还推荐其他两篇文章Another Look at Events、Qt源码阅读-事件循环
这里出现一个问题:次线程中创建的事件循环是否会处理Spontaneous事件?以下是GPT的回答,正确性未验证仅供参考,欢迎各位大佬指点。
GPT:在其他线程中创建的事件循环通常不会处理自发事件,除非明确地要求。如果需要在其他线程中处理自发事件,您需要自行创建和管理事件循环,并显式地设置相应的机制来触发事件的处理。也就是说,如果直接调用QThread::exec()函数开启事件循环,是不会处理Spontaneous事件的。
信号槽机制
信号槽(signal-slot)和事件处理是两种不同的机制,都可以用于实现程序中不同对象之间的同步或异步通信。它们可以在Qt应用程序中一起使用,但它们在实现方式和应用范围上有一些区别。
信号槽机制:
- 信号槽机制是Qt框架的独有特性,用于实现对象之间的松散耦合通信。
- 发送信号并不需要一个特定的接收对象,信号可以被多个槽函数接收,类似于“广播”。
- 信号槽机制允许对象在特定事件或状态变化时发射信号,通知其他对象执行相关操作。
事件和事件处理:
- 事件和事件处理是一种通用的事件驱动编程范式,广泛应用于多种编程环境和框架,不仅限于Qt。
- 事件发送和处理需要明确指定一个接收者对象,每个事件必须有一个确定的接收者,类似于“单播”。
- 事件处理通常是通过重写对象的事件处理函数来实现,响应不同类型的事件,如用户输入事件、系统事件等。
Qt中信号槽的使用非常简单,使用connect将两个对象(必须是QObject的子类)的信号和槽连接起来即可。值得注意的是,connect函数的第5个参数为枚举类型Qt::ConnectionType,用于指定连接类型。以下是枚举类型Qt::ConnectionType的值:
Qt::AutoConnection(默认值):Qt会根据信号和槽的所在线程来自动选择连接类型。如果信号和槽在同一线程,将采用 Qt::DirectConnection,否则采用 Qt::QueuedConnection。
Qt::DirectConnection:信号被发射时,槽会直接在信号发射的线程上调用,不涉及事件队列。这通常在同一线程内的连接中使用,且是同步的。
Qt::QueuedConnection:信号被发射时,槽会被放入接收者对象的事件队列中,等待事件循环处理。这用于在不同线程之间建立连接,因此是异步的。这里说明,信号槽机制的队列连接实现依赖事件循环机制。
Qt::BlockingQueuedConnection:类似于 Qt::QueuedConnection,但不返回到信号发射者,阻塞直到槽函数完成执行。
Qt::UniqueConnection:如果已经存在一个具有相同参数的连接,将不会建立新连接,而是返回一个无效的连接。
通常,直接使用默认值Qt::AutoConnection,即可满足大多数情况的需求,因为它会根据上下文自动选择合适的连接类型。特殊情况下,也可以手动指定连接类型,比如,指定同一个线程中的两个对象间为队列连接,或指定不同两个线程中的两个对象为直接连接。
下面用一个例子,详细解释以上所有特性。
#include <QDebug>
#include <QCoreApplication>
#include <QTimer>
#include <QThread>
class Foo : public QObject {
Q_OBJECT
public:
Foo(QObject *parent = nullptr) : QObject(parent) {}
private:
void doStuff() {
qDebug() << QThread::currentThreadId() << ": Emit signal one";
emit signal1();
qDebug() << QThread::currentThreadId() << ": Emit signal finished";
emit finished();
qDebug() << QThread::currentThreadId() << ": Emit signal two";
emit signal2();
}
signals:
void signal1();
void finished();
void signal2();
public slots:
void slot1() {
qDebug() << QThread::currentThreadId() << ": Execute slot one";
}
void slot2() {
qDebug() << QThread::currentThreadId() << ": Execute slot two";
}
void start() {
doStuff();
qDebug() << QThread::currentThreadId() << ": Bye!";
}
};
#include "main.moc"
int main(int argc, char **argv) {
qDebug() << "main thread id:" << QThread::currentThreadId();
QCoreApplication app(argc, argv);
Foo foo;
Foo foo2;
QThread *foo2thread = new QThread(&app);
foo2.moveToThread(foo2thread);
foo2thread->start();
QObject::connect(&foo, &Foo::signal1, &foo, &Foo::slot1);
QObject::connect(&foo, &Foo::signal1, &foo2, &Foo::slot1);
QObject::connect(&foo, &Foo::finished, &app, &QCoreApplication::quit);
QObject::connect(&foo, &Foo::finished, foo2thread, &QThread::quit);
QObject::connect(&foo, &Foo::signal2, &foo, &Foo::slot2); // Qt::DirectConnection
QObject::connect(&foo, &Foo::signal2, &foo2, &Foo::slot2); // Qt::QueuedConnection
QTimer::singleShot(0, &foo, &Foo::start);
return app.exec();
}
以下是运行结果:
main thread id: 0x165c
0x165c : Emit signal one
0x165c : Execute slot one
0x165c : Emit signal finished
0x5578 : Execute slot one
0x165c : Emit signal two
0x165c : Execute slot two
0x165c : Bye!
在这段代码中,
第1步:创建了两个Foo的实例foo和foo2,并将foo2移动到另一个线程foo2thread中。
第2步:将foo的两个信号分别连接到foo2两个槽函数。此外,还将foo的finished()信号,连接到app和foo2thread的quit函数上,以便在发出finished信号时,通知主事件循环和foo2thread线程的事件循环退出。
第3步:将单次定时器连接到foo的start() 函数,准备进入主事件循环。
第4步:启动并进入主事件循环。
当exec()函数被调用时,事件循环开始。发生的第一个事件是计时器在0毫秒后发出超时信号。信号timeout()连接到foo对象的start()槽函数。在轮询任何其他事件之前,start()槽函数将被执行完成。这导致了该doStuff()方法发出signal1(). 连接到该信号的槽slot1()将立即被执行。一旦控制返回到doStuff(),它就会发出第二个信号finished()。该信号连接到应用程序app和foo2thread线程的quit函数上,这是否意味着应用程序将立即退出?
答案是否定的。如前所述,QCoreApplication::quit()槽实际上调用QCoreApplication::exit(0),而分析后者的源码可以发现,其只是将事件循环的退出标志设为true。在控制权返回到主事件循环之前,实际的退出不会发生。
因此,在发出信号finished()之后,程序会继续执行doStuff(),发出信号signal2(),这里注意,由于foo的signal2和slot2之间是直接连接,因此在发射signal2的同时,foo的slot2便阻塞执行了,而signal2和foo2的slot2之间是队列连接,线程foo2thread的控制权已经回到了事件循环处,并已经退出事件循环。因此,foo2的slot2不会执行。
随后,返回start()。在start()退出之前,打印“Bye!”。
最后,回到主事件循环。
由于这时主事件循环退出标志设置为true,便会返回主函数中exec的调用处,随之程序结束。
如果手动指定QObject::connect(&foo, &Foo::signal2, &foo, &Foo::slot2)的连接类型为Qt::QueuedConnection,最后得到的结果会有所不同,感兴趣的读者可以自己试一试。
Qt信号槽与事件循环学习笔记的更多相关文章
- QT源码之Qt信号槽机制与事件机制的联系
QT源码之Qt信号槽机制与事件机制的联系是本文要介绍的内容,通过解决一个问题,从中分析出的理论,先来看内容. 本文就是来解决一个问题,就是当signal和slot的连接为Qt::QueuedConne ...
- Qt信号槽-原理分析
目录 一.问题 二.Moc 1.变量 2.Q_OBJECT展开后的函数声明 3.自定义信号 三.connect 四.信号触发 1.直连 2.队列连接 五.总结 六.推荐阅读 一.问题 学习Qt有一段时 ...
- Qt信号槽的一些事(第一次知道信号还有返回值,以及Qt::UniqueConnection)
注:此文是站在Qt5的角度说的,对于Qt4部分是不适用的. 1.先说Qt信号槽的几种连接方式和执行方式. 1)Qt信号槽给出了五种连接方式: Qt::AutoConnection 0 自动连接:默认的 ...
- QT信号槽详解
1 QT信号槽详解 1.1 信号和槽的定义 信号是触发信号,例如按钮的点击触发一个clicked信号,槽是用来接收信号,并处理信号,相当于信号响应函数.一个信号可以关联多个槽函数,信 ...
- Qt信号槽的一些事 Qt::带返回值的信号发射方式
一般来说,我们发出信号使用emit这个关键字来操作,但是会发现,emit并不算一个调用,所以它没有返回值.那么如果我们发出这个信号想获取一个返回值怎么办呢? 两个办法:1.通过出参形式返回,引用或者指 ...
- Qt信号槽的一些事
注:此文是站在Qt5的角度说的,对于Qt4部分是不适用的. 1.先说Qt信号槽的几种连接方式和执行方式. 1)Qt信号槽给出了五种连接方式: Qt::AutoConnection 0 自动连接:默认的 ...
- QT 信号槽connect中解决自定义数据类型或数组作为函数参数的问题——QT qRegisterMetaType 注册MetaType——关键:注册自定义数据类型或QMap等容器类
一般情况下信号槽直接连接方式不会出现问题,但是如果信号与槽在不同线程或Qt::QueuedConnection方式连接,可能会在连接期间报以下类似问题,如: QObject::connect: Can ...
- Qt 的线程与事件循环
Qt 的线程与事件循环
- Qt信号槽源码剖析(一)
大家好,我是IT文艺男,来自一线大厂的一线程序员 大家在使用Qt开发程序时,都知道怎么使用Qt的信号槽,但是Qt信号槽是怎么工作的? 大部分人仍然不知道:也就是说大家只知道怎么使用,却不知道基于什么原 ...
- Qt信号槽源码剖析(二)
大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的基本概念.元对象编译器.示例代码以及Qt宏:今天接着深入分析,进入Qt信号槽源码剖析系列的第二节视频. Qt信号槽的宏 ...
随机推荐
- 精讲Mybatis··#{}和${}
题目 笔记Notes 面试题目:#{}和${}的区别是什么? 网上的答案是:#{}是预编译处理,${}是字符串替换.mybatis在处理#{}时, 会将sql中的#{}替换为?号,调用Prepared ...
- SQL 查询 总结 【行子查询 ; 列子查询 ; 表子查询 ; 自链接 ; 内连接 ;外连接 ; 无规则链接 ……】
简单介绍一下连接方式: 1.1.使用无连接规则连接两表 无限规则 也就简单的 select * from tableA , tableB 即得到一个笛卡尔积. 什么是 笛卡尔积 在 我的 另外 ...
- Educational Codeforces Round 151 (Rated for Div. 2) A-D
A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n, ...
- 【技术积累】JavaScript中的基础语法【一】
Math对象 JavaScript中的Math对象是一个内置的数学对象,表示对数字进行数学运算的方法和属性的集合. Math对象不是一个构造函数,所以不能使用new关键字来创建一个Math对象的实例. ...
- 【原创】从Ubuntu-base构建ubuntu rootfs系统(以x86_64和arm为例)
版权声明:本文为本文为博主原创文章,转载请注明出处,博客地址:https://www.cnblogs.com/wsg1100/.如有错误,欢迎指正. 目录 1.介绍 2.目的 2.准备宿主系统 2.1 ...
- Sealos 私有化部署完全指南
Sealos 用了五年的时间从一个 K8s 一键安装工具蜕变成了一个真正的云操作系统,将产品体验提升到了极致,也收获了 10w+ 的社区用户. 一个多月前,Sealos 正式发布了公有云托管版本,社区 ...
- Java8 函数式编程stream流
1.初始环境准备 场景:现在有一个公司,公司部门有一级部门,二级部门甲和二级部门乙(其中二级部门甲和二级部门乙是一级部门的子部门), 一级部门下面有有001号员工小明,二级部门甲下面有002号员工 ...
- FPGA按键消抖
简介 按键 按键是输入设备,一般来说,按键在没有按下的时候是高电平:当按键按下的时候,为低电平. 在DE2-70 User Manual中 Each switch provides a high lo ...
- hive grouping set
reference https://www.cnblogs.com/erlou96/p/13564191.html data-demo 2015-03,2015-03-10,cookie1 2015- ...
- Thinkphp 5.x 远程代码执行漏洞利用小记
Thinkphp 5.x远程代码执行漏洞存在于Thinkphp 5.0版本及5.1版本,通过此漏洞,可以在远程执行任意代码,也可写入webshell 下面是对其进行的漏洞利用! 漏洞利用: 1,利用s ...