Qt信号槽与事件循环学习笔记

qt,信号,事件,循环,学习,笔记 · 浏览次数 : 17

小编点评

## Foo对象信号槽机制实现 **主要内容:** * 创建了两个Foo实例,并将foo2移动到另一个线程foo2thread中。 * 将foo的两个信号分别连接到foo2两个槽函数。 * 除了foo的finished()信号,还将foo的signal2和slot2之间是队列连接。 * 线程foo2thread的控制权已经回到事件循环处,并已经退出事件循环。 * 在发出信号finished()之后,程序会继续执行doStuff(),发出信号signal2(),这里注意,由于foo的signal2和slot2之间是直接连接,因此在发射signal2的同时,foo的slot2便阻塞执行了,而signal2和foo2的slot2之间是队列连接,线程foo2thread的控制权已经回到了事件循环处,并已经退出事件循环。 **其他内容:** * Qt::DirectConnection:信号被发射时,槽直接在信号发射的线程上调用,不涉及事件队列。 * Qt::QueuedConnection:信号被发射时,槽会被放入接收者对象的事件队列中,等待事件循环处理。 **示例代码:** ```cpp #include <QDebug>#include <QCoreApplication>#include <QTimer>#include <QThread>class Foo : public QObject {Q_OBJECTpublic: 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(); }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!\"; }}; ``` **运行结果:** ``` main thread id: 0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0x165c0

正文

事件与事件循环

在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 SystemQEvent Class,还推荐其他两篇文章Another Look at EventsQt源码阅读-事件循环

这里出现一个问题:次线程中创建的事件循环是否会处理Spontaneous事件?以下是GPT的回答,正确性未验证仅供参考,欢迎各位大佬指点。
GPT:在其他线程中创建的事件循环通常不会处理自发事件,除非明确地要求。如果需要在其他线程中处理自发事件,您需要自行创建和管理事件循环,并显式地设置相应的机制来触发事件的处理。也就是说,如果直接调用QThread::exec()函数开启事件循环,是不会处理Spontaneous事件的。

信号槽机制

信号槽signal-slot)和事件处理是两种不同的机制,都可以用于实现程序中不同对象之间的同步或异步通信。它们可以在Qt应用程序中一起使用,但它们在实现方式和应用范围上有一些区别。

信号槽机制:

  • 信号槽机制是Qt框架的独有特性,用于实现对象之间的松散耦合通信。
  • 发送信号并不需要一个特定的接收对象,信号可以被多个槽函数接收,类似于“广播”。
  • 信号槽机制允许对象在特定事件或状态变化时发射信号,通知其他对象执行相关操作。

事件和事件处理:

  • 事件和事件处理是一种通用的事件驱动编程范式,广泛应用于多种编程环境和框架,不仅限于Qt。
  • 事件发送和处理需要明确指定一个接收者对象,每个事件必须有一个确定的接收者,类似于“单播”。
  • 事件处理通常是通过重写对象的事件处理函数来实现,响应不同类型的事件,如用户输入事件、系统事件等。

Qt中信号槽的使用非常简单,使用connect将两个对象(必须是QObject的子类)的信号和槽连接起来即可。值得注意的是,connect函数的第5个参数为枚举类型Qt::ConnectionType,用于指定连接类型。以下是枚举类型Qt::ConnectionType的值:

  1. Qt::AutoConnection(默认值):Qt会根据信号和槽的所在线程来自动选择连接类型。如果信号和槽在同一线程,将采用 Qt::DirectConnection,否则采用 Qt::QueuedConnection。

  2. Qt::DirectConnection:信号被发射时,槽会直接在信号发射的线程上调用,不涉及事件队列。这通常在同一线程内的连接中使用,且是同步的。

  3. Qt::QueuedConnection:信号被发射时,槽会被放入接收者对象的事件队列中,等待事件循环处理。这用于在不同线程之间建立连接,因此是异步的。这里说明,信号槽机制的队列连接实现依赖事件循环机制。

  4. Qt::BlockingQueuedConnection:类似于 Qt::QueuedConnection,但不返回到信号发射者,阻塞直到槽函数完成执行。

  5. 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中,事件(event)被封装为QEvent类/子类对象,用来表示应用内部或外部发生的各种事情。事件可以被任何QObject子类的对象接收并处理。 根据事件的创建方式和调度方式,Qt中事件可分为三类,分别是: 自发事件(Spontaneous even

用 VS Code 搞 Qt6:让信号和槽自动建立连接

Qt 具备让某个对象的信号与符合要求的槽函数自动建立连接。弄起来也很简单,只要调用这个静态方法即可: QMetaObject::connectSlotsByName(...); connectSlotsByName 方法需要一个参数,此参数的指针指向一个实例,这个实例自身的信号,以及它的子级对象的信

逆向通达信 x 逆向微信 x 逆向Qt

本篇在博客园地址https://www.cnblogs.com/bbqzsl/p/18252961 本篇内容包括: win32窗口嵌入Qt UI。反斗玩转signal-slot。最后 通达信 x 微信 x Qt 做手术。 Qt Alien Widget是一种广义的DirectUI。 在UI技术中,D

Qt实现汽车仪表盘

在UI界面显示中,仪表盘的应用相对比较广泛,经常用于显示速度值,电压电流值等等,最终实现效果如下动态图片(文末提供给源工程下载): 主要包含以下绘制步骤: 绘制画布 /* * 绘制画布 */ void Widget::initCanvas(QPainter &painter) { //消除锯齿 pa

Qt 应用程序中自定义鼠标光标

在 Qt 应用程序中,你可以自定义鼠标光标。你可以使用 `QCursor` 类来设置不同类型的鼠标光标,比如内置样式或者自定义的图片。以下是一些使用示例: 使用内置光标样式 Qt 提供了一些内置的光标样式,你可以使用这些样式来改变光标的外观,例如箭头、手形、等待图标等等。 1 #include

QT中各控件的属性和方法

1.在QT6中,QLabel类具有许多属性和方法,以下是QLabel类的常见属性和调用方法:setText(const QString &text):设置标签的文本内容。setAlignment(Qt::Alignment align):设置文本在标签中的对齐方式。setPixmap(const Q

C++(Qt)-GIS开发-简易瓦片地图下载器

Qt-GIS开发-简易瓦片地图下载器 目录Qt-GIS开发-简易瓦片地图下载器1、概述2、安装openssl3、实现效果4、主要代码4.1 算法函数4.2 瓦片地图下载url拼接4.3 多线程下载5、源码地址6、参考 更多精彩内容 个人内容分类汇总 GIS开发 1、概述 支持单线

[Qt开发]当我们在开发兼容高分辨率和高缩放比、高DPI屏幕的软件时,我们在谈论什么。

前言 最近在开发有关高分辨率屏幕的软件,还是做了不少尝试的,当然我们也去网上查了不少资料,但是网上的资料也很零碎,说不明白,这样的话我就做个简单的总结,希望看到这的你可以一次解决你有关不同分辨率下的所有问题。 分辨率?DPI? 首先我们搞清楚我们现在到底面对的是什么场景。在开发高分屏的时候,实际上不

Qt OPC UA通信

介绍 OPC UA全称Open Platform Unified Architecture,开放平台统一架构,是工业自动化领域通用的数据交换协议,它有两套主要的通信机制:1.客户端-服务器通信;2.发布订阅。Qt对OPC UA通信标准也提供了支持,目前还是第三方库的形式(不过Qt官方貌似有文档了),

Qt开发技术:Q3D图表开发笔记(四):Q3DSurface三维曲面图颜色样式详解、Demo以及代码详解

前言 qt提供了q3d进行三维开发,虽然这个框架没有得到大量运用也不是那么成功,性能上也有很大的欠缺,但是普通的点到为止的应用展示还是可以的。 其中就包括华丽绚烂的三维图表,数据量不大的时候是可以使用的。 前面介绍了基础的q3d散点图、柱状图、三维曲面图,本片深入对三维曲面图支持的颜色表现方式进行探