(二)Qt 那些事儿:事件循环机制
(二)Qt 那些事儿:事件循环机制
[toc]
1. concepts
1.1 什么是事件循环?
Qt 事件循环(Event Loop)是整个框架的核心,它基于生产者-消费者模式运作:
操作系统 → 事件队列 → Qt事件循环 → 控件处理
↑ ↑ ↑ ↑
生产事件 缓存事件 消费事件 响应事件
核心机制:
- 事件生产:操作系统捕获用户操作(鼠标、键盘等)
- 事件缓存:事件被放入队列等待处理
- 事件消费:Qt 从队列取出事件并分发
- 事件响应:目标控件处理事件
在 Qt 框架中,事件循环(Event Loop)处于核心地位,负责管理和处理应用程序中各类事件。无论是用户在界面上的点击、输入等操作,还是系统产生的定时器事件、状态变化通知等,都需由程序捕捉并做出响应。
操作系统会捕获这些事件,发送到程序和 os 之间的一个缓存区,也叫事件队列。程序会不定期从这个事件队列取出事件消费,所谓的程序未响应其实就是这个事件队列已满,无法继续添加事件,因此当前出于无法响应的状态。而 Qt 作为一个框架,在这个过程中屏蔽了不同平台产生的事件类型,不同平台的事件获取方式,并给出了统一的事件消费、事件分发机制。
又因为事件的处理(分发、消费)和事件的生成不是同一个主体,前者是程序自身,而后者是 os 的事情,因此即使事件在处理的过程中,事件也可以不停生产并添加到事件队列中。即上述的生产事件、缓存事件是os环境的事情,而消费事件、响应事件是则是qt程序中主线程一直在重复做的事情。
事件循环基于队列机制循环处理事件。当事件队列中有等待处理的事件时,事件循环被激活并处理这些事件;若队列为空,事件循环则进入阻塞状态,等待新事件发生。这种阻塞,让程序在等待事件发生时保持低功耗,仅在必要时唤醒执行相关操作,高效利用系统资源。
1.2 Qt 程序对事件的具体处理机制
关于操作系统如何产生事件并将其送入进程的事件队列(例如中断、信号注册、内核向进程投递等),属于操作系统的实现细节,此处不再展开。本文侧重讲解 Qt 如何接收、封装并消费这些事件。
在程序接收操作系统分发的事件(如鼠标点击、键盘输入、窗口大小变化等)后,借助事件循环处理并传递给相应控件。以 Qt 为例,详细过程如下:
-
事件的消费和加工
操作系统将事件消息发送至前台窗口。以 Windows 为例,Qt 内部使用
QWindowsEventDispatcher接收WM_MOUSEMOVE、WM_LBUTTONDOWN等消息,随后封装为 Qt 自己的事件对象(如QMouseEvent)。Qt 将平台事件统一加工成 Qt 事件,屏蔽平台差异:
// Windows: WM_MOUSEMOVE, WM_LBUTTONDOWN // Linux: X11 events // macOS: Cocoa events // ↓ Qt内部转换 // QMouseEvent, QKeyEvent, QPaintEventQt 程序启动时执行
QApplication::exec(),这是一个持续运行的循环函数,不断从事件队列消费事件:int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); // 启动事件循环 }事件循环基于操作系统消息机制,封装在
QEventLoop类中。Qt 将平台消息转换为QEvent对象并放入事件队列(由QCoreApplication管理)。执行app.exec()时启动事件循环机制,通过QEventLoop::exec()进行 while 循环,不断取出并处理事件。总结:
QApplication::exec()启动事件循环,持续从事件队列消费事件,调用QCoreApplication::notify()分发事件。 -
Qt 对事件的分发
Qt 的事件分发是直接传入目标控件,而非父对象逐层传递。当鼠标点击发生时,Qt 精准定位最终目标控件(如
QPushButton),直接发送事件给它。这种定位在QApplication::notify()中完成,借助QWidget::find()等机制确定接收事件的具体控件。取出事件后,通过QCoreApplication::notify(QObject *receiver, QEvent *event)确定接收事件的控件并分发。该函数负责将事件分发给接收者,并调用receiver->event(event)函数。不同事件的分发方式:- 鼠标事件:通过坐标映射确定对应控件接收
- 键盘事件:由获得焦点的控件接收
- 定时器事件:由设置了
QTimer的控件或对象接收 - 自定义事件:手动投递或发送 | 类型 | 分发方式 | | ——————- | —————————————— | | 鼠标/键盘/触摸事件 | 🚀 直接分发给目标控件 | | 绘制事件 PaintEvent | 🧱 由外层控件递归向子控件调用 | | 自定义事件 | 🚀 使用
postEvent/sendEvent手动指定目标 | | 拦截机制 | 🛑installEventFilter()先于正常分发 |
注意:普通输入事件直接发送至目标控件,但绘制事件(
QPaintEvent)不同。绘制事件由框架自动产生,分发顺序按控件层级从外到内调用,如MainWindow::paintEvent() → QWidget::paintEvent() → QPushButton::paintEvent()。以鼠标事件为例:
// 在 QApplication::notify 中 // Qt直接定位目标控件,不逐层传递 { QWidget *target = QWidget::find(widgetAt(mousePos)); QMouseEvent event(...); QCoreApplication::sendEvent(target, &event); } { QCoreApplication::sendEvent(QObject* receiver, QEvent* ev) { QCoreApplication::notify(receiver, ev); // 同步转交给 notify } } { bool QCoreApplication::notify(QObject* receiver, QEvent* ev) { if (!receiver) return false; for (QObject* filter : receiver->installedEventFilters()) { if (filter->eventFilter(receiver, ev)) { return true; } } bool handled = receiver->event(ev); // 对 QWidget 来说,会在这里进入 QWidget::event // 4. QWidget::event 根据类型分发到具体处理函数: // if (ev->type()==MouseButtonPress) -> widget->mousePressEvent(static_cast<QMouseEvent*>(ev)), ... return handled; } } -
控件对事件的消费
事件到达控件(QObject 派生类)后,先调用
event()函数,再分发给具体处理函数:bool QWidget::event(QEvent *e) { switch (e->type()) { case QEvent::MouseButtonPress: return mousePressEvent(static_cast<QMouseEvent *>(e)); case QEvent::KeyPress: return keyPressEvent(static_cast<QKeyEvent *>(e)); ... } return false; }event()是控件处理所有事件的统一入口,Qt 根据事件类型调用专门的函数。开发者可重写这些处理函数实现自定义行为。事件处理返回值的意义:
- 返回
true:事件已处理完毕,Qt 事件系统认为不需要再传递给其他对象(包括父控件) - 返回
false:当前控件未处理该事件,Qt 事件系统将继续分发给其他对象
返回值决定事件是否”终止传播”:
true终止传播,false允许事件继续传递。 - 返回
1.3 如何介入事件处理流程的时机与方式
使用事件过滤器(事件拦截机制)可在事件到达目标控件之前进行拦截处理:
// 事件过滤器类 MyFilter
bool MyFilter::eventFilter(QObject *watched, QEvent *event) {
if (event->type() == QEvent::KeyPress) {
qDebug() << "Key pressed!";
return true; // 返回 true 表示拦截该事件
}
return false; // 返回 false 表示不处理,将事件交给目标对象
}
// 在主代码中安装事件过滤器
targetWidget->installEventFilter(myFilter);
// 注意:此行为不会对子对象生效,如需对子对象安装eventFilter,需要找到子对象调用此方法
eventFilter()是QObject类的成员函数,通常在QObject或其子类中重写,用于过滤其他对象的事件。安装事件过滤器后,当目标对象接收事件时,eventFilter()函数优先被调用,可以决定是否拦截事件或进行特殊处理。
1.4 挂起事件的提前消费 - processEvents
processEvents()是在阻塞代码中促使 Qt 对已排队事件进行”抢先处理”,维持界面响应性的关键手段:
void MainWindow::onLoadClicked() {
// UI显示"正在加载"
ui->label->setText("Loading...");
// 这里不调用 processEvents,UI 可能不会立即刷新
// 开始加载一个大文件(阻塞主线程)
loadBigFile();
// 加载完再刷新 UI
ui->label->setText("Load done");
}
在此实现中,由于设置label文本后未调用processEvents(),执行loadBigFile()这一阻塞操作时,UI 可能无法及时刷新显示”Loading…“。
改进后的实现:
void MainWindow::onLoadClicked() {
ui->label->setText("Loading...");
qApp->processEvents(); // 立即处理事件,刷新界面
loadBigFile(); // 阻塞操作
ui->label->setText("Load done");
}
processEvents()方法处理事件队列中的所有挂起事件,包括用户输入、窗口更新、定时器事件等。通过调用qApp->processEvents(),在设置文本后立即处理事件,确保 UI 及时刷新。
使用注意事项:
- 性能问题:频繁调用可能引发性能下降,每次调用都会处理所有挂起事件
- 避免递归事件:在事件处理过程中再次调用可能导致递归,使逻辑复杂
- 替代方案:优先采用异步操作(如
QThread或QtConcurrent)防止阻塞事件循环
1.5 主动生成事件
在 Qt 中,事件(QEvent)是对象间传递信息的机制。除了外部事件(鼠标、键盘、窗口重绘等),还有系统内部事件(定时器消息、进程间消息等)。
事件的构造函数不是私有的,我们可以直接构造事件。对于新构造的事件,有两种消费方式:
(1) sendEvent(QObject *receiver, QEvent *event)
- 同步调用:事件立即发送到目标对象,调用链立刻进入
receiver->event() - 事件处理完成后才返回调用者
- 不进入 Qt 事件队列,不等待事件循环
- 适合需要立即执行的场景
- 因为立即执行,若在非 UI 线程调用,会违反线程亲和性,可能导致崩溃
- 会阻塞当前线程直到事件处理完成
QMouseEvent event(QEvent::MouseButtonPress, QPointF(10, 10), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QCoreApplication::sendEvent(widget, &event); // 立即调用 widget->event(&event)
(2) postEvent(QObject *receiver, QEvent *event)
- 异步调用:将事件放入 Qt 事件队列,等到事件循环处理时才调用
receiver->event() - 不会立即处理,返回时事件还未处理
- 适合跨线程通信、延迟处理、避免阻塞
-
postEvent会自动接管event的内存(Qt 事件队列负责 delete) - 目标对象销毁时,未处理的事件会被自动丢弃并释放
- 更安全地跨线程发送事件,Qt 会把事件派发到目标对象所在线程的事件循环
QMouseEvent *event = new QMouseEvent(QEvent::MouseButtonPress, QPointF(10, 10), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QCoreApplication::postEvent(widget, event); // 放入事件队列,稍后执行
2. 子事件循环
2.1 什么是子事件循环?
正如前所述,Qt 应用程序启动时会创建一个主事件循环(Main Event Loop),通过调用QApplication::exec()进入持续运行的循环状态,负责处理所有事件。但是当我处理某一个特定事件时,如果我不想继续再生产事件,添加到主事件循环呢?该怎么做呢?
首先,肯定会很奇怪这是个什么问题吧。很简单,给一个场景就是,如果我想做文件未保存,点击关闭,然后弹出一个对话框(确切来说,是模态对话框),问是否确定未保存就关闭的功能就需要这个功能。这个时候,点击任何原有功能按钮都不需要响应了,特别是如果这个时候如果能被我通过ctrl+s保存就更奇怪了,因为我已经保存了,但是还有一个提示我未保存的对话框在等着我点击。这个时候我就是不希望有任何其他事件被处理,直到我处理完这个对话框为止。这个时候只需要子事件循环。
我只需要复用事件循环逻辑,新增一个子事件循环(Sub Event Loop),来暂停当前操作流程,这个时候操作系统产生的事件依然会被放入事件队列中,但是主事件循环不会去处理它们,而是最顶层的子事件循环消费这些事件,主事件循环才会继续处理剩余事件。可以说子事件循环就是一种临时构建的事件处理机制。除了由QApplication::exec()启动的主事件循环外,开发者可以在函数中临时创建QEventLoop对象,通过执行loop.exec()来处理事件。
当然,子事件循环并不局限于模态对话框场景,任何需要暂停当前操作流程、等待用户输入或异步事件完成的场景都可以使用子事件循环。 常见应用场景包括:
-
暂停当前函数执行以显示模态对话框:如
QDialog::exec()利用子事件循环暂停当前操作流程,等待用户输入 -
等待异步事件完成:
QEventLoop loop; connect(task, &AsyncTask::done, &loop, &QEventLoop::quit); loop.exec(); // 等待异步信号触发 -
在不中断主事件循环的前提下维持 UI 响应性:
QEventLoop loop; while (!operationFinished) { doOneStepOfLongTask(); loop.processEvents(); // 处理用户输入和界面刷新 if (shouldCancelOperation) break; }
其中第三个场景可能一下子不是很理解,后面会继续补充。
QEventLoop主要接口:
| 接口名 | 说明 |
|---|---|
exec(QEventLoop::ProcessEventsFlags flags = AllEvents) | 启动事件循环 |
exit(int returnCode = 0) | 停止事件循环,并返回状态码 |
quit() | 信号:用于退出事件循环 |
isRunning() | 判断事件循环是否正在运行 |
processEvents(QEventLoop::ProcessEventsFlags flags = AllEvents) | 立即处理所有待处理事件 |
wakeUp() | 唤醒处于阻塞状态的事件循环,常用于线程间通信 |
-
wakeUp():主要用于唤醒因事件队列无事件而阻塞的事件循环,通常在多线程场景下用于线程间通信 -
isRunning():判断当前是否正在运行事件循环,避免重复调用exec()防止嵌套过深或死锁
3. 如何实现同步形式的UI 交互?
这个标题的名字,又是很难理解。以下是一个具体场景来阐述。在 UI 交互的处理中,常有一种如此要求的需求:
StartDrawLineCommand();
auto pnt1 = GetPoint("请选择起点"); // 等待用户点击
auto pnt2 = GetPoint("请选择终点"); // 再等待用户点击
DrawLine(pnt1, pnt2);
这种同步执行流程直观清晰:一行一行执行,GetPoint()会”停下来等待”用户点击,用户点击后函数返回再继续执行下一行。这种写法就像控制台程序或脚本那样,自然清晰,也便于维护。
可那如何实现GetPoint()这个函数呢?它需要等待用户点击,然后返回点击的坐标。在这个过程中界面不能卡死,比如说我鼠标移动了,界面要能刷新,按钮要能点,这些都不能卡死,而且又要让这个mouseClick事件是专门为了GetPoint处理,不让其他widget给消费,还又不能过分复杂化。特别是GetPoint()这个函数的调用方式是同步的,必须等用户点完才能返回。
只能说普通的主事件循环机制,让事件驱动的 GUI 框架,是没办法做到的。从某个角度来说,GUI 框架的事件驱动模型,和我们想要的”同步等待用户输入”的模型,是天然冲突的。具体来说:
- 程序启动后,Qt 启动一个事件循环(event loop)
- 鼠标点击、键盘输入、窗口重绘等都会生成”事件”
- Qt 把事件投递给响应的对象(按钮、窗口等)
- 我们通过信号槽、事件处理函数来响应这些事件
关键区别:事件不是你代码调用触发的,而是用户操作后 Qt 调用你的回调函数。所以无法写出像GetPoint()这样的阻塞函数,除非人为构造一个”同步等待”的机制。
3.1 使用 QEventLoop 启动“子事件循环”
这就需要前面提到的子事件循环机制。通过QEventLoop,它可以临时开启一个新的事件循环,让 UI 不会卡死,同时主流程“停下来等结果”,模拟同步行为。
示意代码如下:
QPoint GetPoint(MyCanvas *viewport, const QString &hint) {
QPoint point;
if (!viewport) return point;
QEventLoop loop;
QPointer<MyCanvas> vp(viewport);
QMetaObject::Connection conn;
conn = QObject::connect(vp.data(), &MyCanvas::clicked, [&](QPoint pt){
point = pt;
QObject::disconnect(conn);
loop.quit();
});
showHint(hint);
loop.exec();
return point;
}
你可以理解为:主流程暂时“挂起”,进入一个新的事件处理阶段,即loop.exec();用户点击后触发回调,退出循环,即loop.quit(),主流程恢复继续执行。这就实现了看起来是同步的等待,但实际上 Qt 仍然在正常处理事件,不会卡 UI。
底层由事件驱动 + 子事件循环机制支撑,表面上是“同步流程”,实际上是异步事件响应。一般生产级别代码,可能还会面临如下需求:
- 模态交互(如点选对象、选择区域);
- 取消机制(按 Esc 或右键取消当前命令);
- 多重嵌套(主命令中调用子命令或对话框);
- 跨模块的统一交互逻辑调度。
所以一般都需要再对loop去封装一下
| 目标 | 是否达成 |
|---|---|
| 看起来是同步的交互等待 | ✅ 使用 exec() + quit() 实现 |
| UI 保持响应性 | ✅ 子事件循环不阻塞 UI |
| 支持多级嵌套和弹窗调用 | ✅ 用 _loops 栈统一管理 |
| 支持中断退出、状态恢复 | ✅ earlyExit / interrupt() 控制 |
| 提供统一指令交互调度框架 | ✅ 所有命令逻辑都走统一事件循环封装 |
总结:Qt 是异步事件驱动模型,无法像控制台程序那样”等输入”。如果想在界面程序中”等用户点一下”,要么写异步状态机逻辑,要么用QEventLoop开个子事件循环,模拟同步流程而不阻塞 UI。
这种做法在 Qt 的机制上构建了一套统一的指令调度和交互系统,既保留异步驱动的优势,又提供可控、清晰的”同步式”交互体验。
3.2 协程方式实现同步的 ui 交互
随着 C++20 引入协程支持,我们可以使用更现代的方式来实现看似同步的 UI 交互,避免复杂的状态机逻辑和事件循环嵌套。
3.2.1 协程的基本概念与优势
协程(Coroutine)是一种可以被暂停和恢复的函数,它允许我们在函数执行过程中”让出控制权”,稍后再从暂停的地方继续执行。这非常适合实现异步等待用户输入的场景。
核心优势对比:
| 方式 | 代码结构 | 异常处理 | 维护性 | 嵌套支持 |
|---|---|---|---|---|
| 状态机方式 | 🔴 分散的回调 | 🔴 复杂 | 🔴 难维护 | 🔴 困难 |
| QEventLoop 方式 | 🟡 较清晰 | 🟡 一般 | 🟡 中等 | 🟡 需要管理 |
| 协程方式 | 🟢 线性同步式 | 🟢 自然 | 🟢 易维护 | 🟢 天然支持 |
3.2.2 Qt 协程 UI 交互框架实现
首先,我们需要定义一个协程框架来处理 UI 交互:
#include <coroutine>
#include <optional>
#include <QEventLoop>
#include <QTimer>
#include <QMouseEvent>
#include <QWidget>
#include <QApplication>
// 协程任务类型
template<typename T>
struct Task {
struct promise_type {
T value{};
std::exception_ptr exception = nullptr;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(T val) { value = std::move(val); }
void unhandled_exception() {
exception = std::current_exception();
}
};
std::coroutine_handle<promise_type> coro;
Task(std::coroutine_handle<promise_type> h) : coro(h) {}
~Task() {
if (coro) coro.destroy();
}
// 移动语义
Task(Task&& other) noexcept : coro(std::exchange(other.coro, {})) {}
Task& operator=(Task&& other) noexcept {
if (this != &other) {
if (coro) coro.destroy();
coro = std::exchange(other.coro, {});
}
return *this;
}
// 禁止拷贝
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
T get_result() {
if (coro.promise().exception) {
std::rethrow_exception(coro.promise().exception);
}
return coro.promise().value;
}
};
// 等待用户点击的 awaiter
class PointAwaiter : public QObject {
Q_OBJECT
public:
PointAwaiter(QWidget* widget, const QString& hint)
: widget_(widget), hint_(hint) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> handle) {
handle_ = handle;
showHint(hint_);
installEventFilter();
}
QPoint await_resume() {
removeEventFilter();
return result_point_;
}
private slots:
void onTimeout() {
removeEventFilter();
cancelled_ = true;
if (handle_) {
handle_.resume();
}
}
private:
QWidget* widget_;
QString hint_;
QPoint result_point_;
std::coroutine_handle<> handle_;
bool cancelled_ = false;
QTimer* timeout_timer_ = nullptr;
void showHint(const QString& text) {
if (widget_) {
widget_->setToolTip(text);
widget_->setStatusTip(text);
}
}
void installEventFilter() {
if (widget_) {
widget_->installEventFilter(this);
// 设置超时机制
timeout_timer_ = new QTimer(this);
timeout_timer_->setSingleShot(true);
connect(timeout_timer_, &QTimer::timeout, this, &PointAwaiter::onTimeout);
timeout_timer_->start(30000); // 30秒超时
}
}
void removeEventFilter() {
if (widget_) {
widget_->removeEventFilter(this);
}
if (timeout_timer_) {
timeout_timer_->stop();
timeout_timer_->deleteLater();
timeout_timer_ = nullptr;
}
}
bool eventFilter(QObject* obj, QEvent* event) override {
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
result_point_ = mouseEvent->pos();
removeEventFilter();
if (handle_) {
handle_.resume();
}
return true; // 事件已处理
}
} else if (event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Escape) {
cancelled_ = true;
removeEventFilter();
if (handle_) {
handle_.resume();
}
return true;
}
}
return QObject::eventFilter(obj, event);
}
};
// 等待按键的 awaiter
class KeyAwaiter : public QObject {
Q_OBJECT
public:
KeyAwaiter(QWidget* widget, const QString& hint)
: widget_(widget), hint_(hint) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> handle) {
handle_ = handle;
showHint(hint_);
installEventFilter();
}
int await_resume() {
removeEventFilter();
if (cancelled_) {
throw std::runtime_error("用户取消操作");
}
return result_key_;
}
private:
QWidget* widget_;
QString hint_;
int result_key_;
std::coroutine_handle<> handle_;
bool cancelled_ = false;
void showHint(const QString& text) {
if (widget_) {
widget_->setStatusTip(text);
}
}
void installEventFilter() {
if (widget_) {
widget_->installEventFilter(this);
}
}
void removeEventFilter() {
if (widget_) {
widget_->removeEventFilter(this);
}
}
bool eventFilter(QObject* obj, QEvent* event) override {
if (event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
result_key_ = keyEvent->key();
removeEventFilter();
if (handle_) {
handle_.resume();
}
return true;
}
return QObject::eventFilter(obj, event);
}
};
3.2.3 协程 UI 交互的实际应用
使用上述框架,我们可以实现非常自然的 UI 交互逻辑:
// 绘制线条的协程函数 - 代码看起来是同步的!
Task<bool> drawLineCommand(QWidget* canvas) {
try {
// 获取起点
QPoint startPoint = co_await PointAwaiter(canvas, "请选择起点");
// 显示临时标记
showTemporaryMarker(startPoint);
// 获取终点
QPoint endPoint = co_await PointAwaiter(canvas, "请选择终点");
// 绘制线条
drawLine(startPoint, endPoint);
co_return true;
} catch (const std::exception& e) {
qDebug() << "绘制线条被取消:" << e.what();
co_return false;
}
}
// 复杂的多步骤交互
Task<bool> drawRectangleCommand(QWidget* canvas) {
try {
// 第一个角点
QPoint corner1 = co_await PointAwaiter(canvas, "选择矩形第一个角点");
// 对角点
QPoint corner2 = co_await PointAwaiter(canvas, "选择矩形对角点");
// 让用户确认
int key = co_await KeyAwaiter(canvas, "按回车确认,按ESC取消");
if (key == Qt::Key_Return) {
drawRectangle(corner1, corner2);
co_return true;
} else {
co_return false; // 用户取消
}
} catch (const std::exception& e) {
qDebug() << "绘制矩形被取消:" << e.what();
co_return false;
}
}
// 嵌套命令的例子 - 组合多个协程
Task<bool> complexDrawingCommand(QWidget* canvas) {
// 先画一条线
bool lineResult = co_await drawLineCommand(canvas);
if (!lineResult) {
co_return false;
}
// 再画一个矩形
bool rectResult = co_await drawRectangleCommand(canvas);
co_return lineResult && rectResult;
}
3.2.4 协程任务的启动和管理
为了在 Qt 应用中使用协程,我们需要一个任务调度器:
class CoroutineScheduler : public QObject {
Q_OBJECT
public:
template<typename T>
void schedule(Task<T> task) {
// 使用QTimer让协程在事件循环中执行
QTimer::singleShot(0, this, [this, task = std::move(task)]() mutable {
try {
// 协程会在需要等待时自动暂停
// 当用户操作完成时会自动恢复
T result = task.get_result();
emit taskCompleted(QVariant::fromValue(result));
} catch (const std::exception& e) {
qDebug() << "协程执行异常:" << e.what();
emit taskFailed(QString::fromStdString(e.what()));
}
});
}
signals:
void taskCompleted(const QVariant& result);
void taskFailed(const QString& error);
private:
std::vector<std::unique_ptr<QObject>> activeTasks_;
};
// 在主窗口中使用
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) {
setupUI();
connect(&scheduler_, &CoroutineScheduler::taskCompleted,
this, &MainWindow::onTaskCompleted);
connect(&scheduler_, &CoroutineScheduler::taskFailed,
this, &MainWindow::onTaskFailed);
}
private slots:
void onDrawLineClicked() {
scheduler_.schedule(drawLineCommand(ui->canvas));
}
void onDrawRectClicked() {
scheduler_.schedule(drawRectangleCommand(ui->canvas));
}
void onComplexDrawClicked() {
scheduler_.schedule(complexDrawingCommand(ui->canvas));
}
void onTaskCompleted(const QVariant& result) {
statusBar()->showMessage("操作完成", 2000);
}
void onTaskFailed(const QString& error) {
statusBar()->showMessage(QString("操作失败: %1").arg(error), 5000);
}
private:
CoroutineScheduler scheduler_;
// UI 组件...
};
3.2.5 与传统方式的详细对比
1. 代码复杂度对比:
// 传统状态机方式 - 分散的逻辑
class TraditionalLineDrawer {
enum State { WaitingStart, WaitingEnd };
State state_ = WaitingStart;
QPoint startPoint_;
void onMouseClick(QPoint p) {
switch(state_) {
case WaitingStart:
startPoint_ = p;
state_ = WaitingEnd;
showHint("选择终点");
break;
case WaitingEnd:
drawLine(startPoint_, p);
state_ = WaitingStart;
showHint("选择起点");
break;
}
}
void onKeyPress(int key) {
if (key == Qt::Key_Escape) {
state_ = WaitingStart;
showHint("操作已取消");
}
}
};
// QEventLoop方式 - 需要手动管理循环
class EventLoopLineDrawer {
QPoint getPoint(const QString& hint) {
QEventLoop loop;
QPoint result;
bool got_point = false;
auto connection = connect(canvas, &Canvas::clicked,
[&](QPoint p) {
result = p;
got_point = true;
loop.quit();
});
showHint(hint);
loop.exec();
disconnect(connection);
return result;
}
void drawLine() {
QPoint start = getPoint("选择起点");
QPoint end = getPoint("选择终点");
drawLine(start, end);
}
};
// 协程方式 - 自然的线性逻辑
Task<void> coroutineLineDrawer(QWidget* canvas) {
QPoint start = co_await PointAwaiter(canvas, "选择起点");
QPoint end = co_await PointAwaiter(canvas, "选择终点");
drawLine(start, end);
}
2. 错误处理对比:
// 协程方式的异常处理更自然
Task<bool> robustDrawCommand(QWidget* canvas) {
try {
QPoint p1 = co_await PointAwaiter(canvas, "选择起点");
QPoint p2 = co_await PointAwaiter(canvas, "选择终点");
if (isValidLine(p1, p2)) {
drawLine(p1, p2);
co_return true;
} else {
throw std::invalid_argument("无效的线条参数");
}
} catch (const std::runtime_error& e) {
showError(QString("操作失败: %1").arg(e.what()));
co_return false;
} catch (const std::invalid_argument& e) {
showWarning(QString("参数错误: %1").arg(e.what()));
co_return false;
}
}
3.2.6 注意事项和最佳实践
1. 内存管理:
// 确保协程对象的生命周期管理
class SafeCoroutineScheduler {
struct TaskWrapper {
virtual ~TaskWrapper() = default;
virtual void execute() = 0;
};
template<typename T>
struct TypedTaskWrapper : TaskWrapper {
Task<T> task;
TypedTaskWrapper(Task<T> t) : task(std::move(t)) {}
void execute() override { /* 执行逻辑 */ }
};
std::vector<std::unique_ptr<TaskWrapper>> activeTasks_;
public:
template<typename T>
void schedule(Task<T> task) {
auto wrapper = std::make_unique<TypedTaskWrapper<T>>(std::move(task));
activeTasks_.push_back(std::move(wrapper));
// 任务完成后自动清理
}
};
2. 线程安全:
// 确保协程恢复在正确的线程中执行
void await_suspend(std::coroutine_handle<> handle) {
handle_ = handle;
// 使用 QMetaObject::invokeMethod 确保在主线程恢复
QMetaObject::invokeMethod(qApp, [this, handle]() {
// 设置事件过滤器等操作
installEventFilter();
}, Qt::QueuedConnection);
}
3. 取消机制:
// 支持取消的协程框架
class CancellableTask {
std::atomic<bool> cancelled_{false};
public:
void cancel() { cancelled_ = true; }
bool isCancelled() const { return cancelled_; }
template<typename Awaiter>
auto operator co_await(Awaiter awaiter) {
struct CancellableAwaiter {
Awaiter inner;
CancellableTask* task;
bool await_ready() {
return task->isCancelled() || inner.await_ready();
}
void await_suspend(std::coroutine_handle<> h) {
if (!task->isCancelled()) {
inner.await_suspend(h);
}
}
auto await_resume() {
if (task->isCancelled()) {
throw std::runtime_error("操作被取消");
}
return inner.await_resume();
}
};
return CancellableAwaiter{std::move(awaiter), this};
}
};
3.2.7 性能和兼容性考虑
编译要求:
- 需要 C++20 支持的编译器(GCC 10+, Clang 11+, MSVC 2019 16.8+)
- CMake 中需要设置:
set(CMAKE_CXX_STANDARD 20)
性能特点:
- 协程的内存开销通常比线程小得多
- 切换开销比线程上下文切换低
- 但比直接的状态机略有开销
适用场景:
✅ 复杂的多步骤用户交互
✅ 需要频繁嵌套的操作流程
✅ 对代码可读性要求高的项目
✅ 团队熟悉现代C++特性
❌ 简单的单步交互(过度设计)
❌ 对C++20支持有限制的项目
❌ 性能要求极其苛刻的场景
协程方式为 Qt UI 交互提供了一种现代化、高效且易于维护的解决方案,特别适合需要复杂用户输入流程的应用场景。通过合理的设计和实现,可以显著提高代码的可读性、可维护性和开发效率。
99. quiz
1. 什么是 qApp?
在 Qt 应用中,qApp 作为全局 “事件中枢”,扮演着举足轻重的角色。它负责接收来自操作系统的事件,并对 Qt 内部所有事件的分发、循环、退出以及信号槽的运行等进行调度管理。qApp 是 Qt 提供的宏,用于指代当前应用程序的全局实例,具体定义为:
#define qApp (static_cast<QCoreApplication *>(QCoreApplication::instance()))
通过 QCoreApplication::instance() 获取当前正在运行的 QCoreApplication 对象实例,再经 static_cast<QCoreApplication *>() 转换为 QCoreApplication 类型指针,从而为开发者提供了在程序各处便捷访问全局 QApplication 或 QCoreApplication 对象的途径。
qApp 犹如应用程序的大脑与心脏,不仅承接操作系统的事件输入,还主宰着调度、分发、事件循环、定时器管理、信号槽机制等全局行为。在实际开发中,qApp 具有诸多常见使用场景:
- 访问应用全局状态:如需设置全局字体、语言或路径等应用全局状态信息,可借助
qApp完成。例如设置全局字体为宋体:
QFont font("宋体");
qApp->setFont(font);
- 捕获所有控件的事件:在事件过滤器中,通过
qApp能够捕获所有控件的事件。假设存在全局事件过滤器类GlobalEventFilter,操作如下:
GlobalEventFilter filter;
qApp->installEventFilter(&filter);
如此,在 GlobalEventFilter 类的 eventFilter 函数中,便可处理应用中所有控件的事件。
- 进行跨模块通信:例如在子模块中,若要退出程序,可调用
qApp->quit()。假设在名为SubModule的子模块类中有函数exitApp:
void SubModule::exitApp() {
qApp->quit();
}
需注意,在主程序中必须创建 QApplication(非 GUI 应用则使用 QCoreApplication),示例代码如下:
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 主程序其他代码
return app.exec();
}
此 app 实例即 qApp 所指向的对象。
qApp 的职责广泛,具体可通过以下类比与实际功能示例说明:
| 类似角色 | 在 Qt 中的名字 | 职责描述 | 功能示例及应用场景 |
| ---- | ---- | ---- | ---- |
| 🧠 大脑 | `QCoreApplication` | 管理事件、线程以及应用程序的生命周期,是应用程序运行的核心 | `qApp->exec()` 启动应用程序的事件循环,使应用开始接收和处理事件;`qApp->quit()` 用于关闭应用程序,可在用户点击关闭按钮等场景下调用 |
| 💓 心跳 | `QEventLoop` | 维持事件循环持续运行,确保应用程序能不断接收和处理事件 | `qApp->processEvents()` 主动处理当前事件队列中的事件,适用于需立即处理事件的场景;`qApp->hasPendingEvents()` 检查是否有未处理的事件,可在循环中判断 |
| 🕹️ 调度员 | `notify()` | 决定将接收到的事件分发给哪个对象处理 | `qApp->notify(receiver, event)` 将事件 `event` 分发给指定接收者 `receiver`,自定义事件分发逻辑时可重写此函数 |
| 📬 邮局 | `postEvent()` | 管理事件队列,将事件放入队列等待处理 | `qApp->postEvent(obj, event)` 将事件 `event` 发送到对象 `obj` 的事件队列,常用于异步处理事件场景,如多线程编程中一个线程向另一个线程的对象发送事件 |
| 📅 日历 | `QTimer` | 提供统一定时机制,定时触发特定事件或任务 | 可通过 `QTimer::singleShot(time, qApp, [](){ /* 定时执行的代码 */ });` 实现单次定时任务,如应用启动后延迟执行操作;也可创建 `QTimer` 对象并连接其 `timeout` 信号到相应槽函数实现周期性任务,如定时更新界面数据 |
| 🧭 导航 | - | 获取当前路径、应用名称等全局信息,方便程序各处使用 | `qApp->applicationDirPath()` 获取应用程序安装目录路径,适用于读取应用配置文件等场景;`qApp->applicationName()` 获取应用程序名称,可用于日志记录或显示应用标题 |
| 🧵 线程管家 | `qApp->thread()` | 代表 GUI 线程,方便管理与 GUI 相关的线程操作 | 在需确保某些操作在 GUI 线程执行的场景下,可通过判断当前线程是否为 `qApp->thread()` 决定,如更新界面元素必须在 GUI 线程执行 |
Qt 中所有事件最初由操作系统发出,qApp(即 QApplication 实例)负责接收并分发给对应的 Qt 对象(控件、窗口等)。Qt 的事件循环每次仅处理一个事件,处理完一个事件后才取下一个事件。具体特点如下: ✔️ 每个事件按事件队列先进先出的顺序处理。 ✔️ 当前事件必须处理完毕,才会继续处理下一个事件。 ❌ 但并非“阻塞式的同步调用”那种一直等待结果才继续,而是类似任务队列,事件处理函数正常返回即可继续,此时事件进一步的处理可另起线程进行。
2. 多线程场景下的事件处理问题
在 Qt 中,只有主线程(即 GUI 线程)能够对 UI 控件进行操作,原因主要有以下两点:
-
事件循环的主线程限制:子线程不能直接调用 UI 控件的方法或访问 UI,否则会产生未定义行为,甚至导致程序崩溃。这是因为 UI 组件的创建与管理均在主线程中完成,其生命周期与主线程紧密相关。若子线程对 UI 组件进行操作,极有可能在 UI 组件尚未完全创建或已被销毁时进行访问,进而引发错误。
-
UI 框架的线程安全性:Qt 的 UI 框架并非线程安全。若多个线程同时对 UI 组件进行操作,极易引发数据竞争。例如,当两个线程同时试图修改同一个按钮的文本时,按钮的文本内容将处于不可预测状态,严重时会致使程序崩溃。
Qt 的 UI 更新依赖于事件循环,主事件循环运行在主线程,负责处理各类 UI 事件,如绘制、重绘、鼠标点击等。而子线程没有自身的主事件循环,无法正确处理这些 UI 事件。若在子线程中进行 UI 操作,很可能导致 UI 组件无法正确更新或重绘。
在 Qt 客户端程序中,子线程不能执行 UI 操作,可通过信号/槽机制(Qt::QueuedConnection)或使用 QMetaObject::invokeMethod 实现子线程与主线程的通信,从而安全地更新 UI。以下是示例代码:
#include <QApplication>
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
#include <QThread>
#include <QMetaObject>
class WorkerThread : public QThread {
Q_OBJECT
signals:
void updateSignal(const QString&);
public:
void run() override {
QThread::sleep(2);
emit updateSignal("Updated from thread");
}
};
class MainWindow : public QWidget {
Q_OBJECT
public:
MainWindow() {
QLabel* label = new QLabel("Initial text");
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(label);
WorkerThread* thread = new WorkerThread(this);
connect(thread, &WorkerThread::updateSignal, [label](const QString& text) {
label->setText(text);
});
thread->start();
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
在上述示例中,WorkerThread 子线程通过 updateSignal 信号将消息发送到主线程,主线程接收到信号后更新 QLabel 的文本。若直接在子线程中更新 QLabel 的文本,就会出现问题。
总之,为确保程序的稳定性与正确性,UI 操作应在主线程中进行,子线程可借助信号与槽机制或 QMetaObject::invokeMethod 与主线程通信,由主线程完成 UI 更新。
3. 何时需使用 QSignalBlocker?
QSignalBlocker 在以下情形中颇具效用:
- 状态修改但避免触发信号:当需对对象状态进行修改,同时又不希望该修改触发任何信号时,可使用 QSignalBlocker。比如,更新
QSpinBox的值,但不希望触发valueChanged信号。在此场景下,QSignalBlocker 可暂时阻断信号发射,确保状态修改过程不受信号处理逻辑干扰。 - 批量操作后统一处理信号:若要执行一系列操作,且这些操作可能触发多个信号,但期望仅在所有操作完成后再处理这些信号,QSignalBlocker 就派上用场了。例如,对
QStandardItemModel的多个项进行修改,只希望在全部修改完成后再更新视图。利用 QSignalBlocker 可在操作期间抑制信号发射,待所有操作结束后,再让信号正常触发,从而实现对信号处理时机的精准控制。 - 防止信号循环:当存在两个相互关联的控件,一个控件状态改变会引发另一个控件状态改变,反之亦然时,为避免在特定时间段内产生循环信号,可使用 QSignalBlocker。比如,在修改其中一个控件状态时,通过 QSignalBlocker 阻止其发射信号,有效避免无限信号循环的产生。
需留意的是,QSignalBlocker 会阻断所有信号,包括可能期望处理的信号,所以应谨慎使用。若有可能,应通过优化代码设计,从根源上避免不必要的信号触发,而非过度依赖 QSignalBlocker。
Enjoy Reading This Article?
Here are some more articles you might like to read next: