(二)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 为例,详细过程如下:

  1. 事件的消费和加工

    操作系统将事件消息发送至前台窗口。以 Windows 为例,Qt 内部使用QWindowsEventDispatcher接收WM_MOUSEMOVEWM_LBUTTONDOWN等消息,随后封装为 Qt 自己的事件对象(如QMouseEvent)。

    Qt 将平台事件统一加工成 Qt 事件,屏蔽平台差异:

    // Windows: WM_MOUSEMOVE, WM_LBUTTONDOWN
    // Linux: X11 events
    // macOS: Cocoa events
    // ↓ Qt内部转换
    // QMouseEvent, QKeyEvent, QPaintEvent
    

    Qt 程序启动时执行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()分发事件。

  2. 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;
         }
    }
    
    
  3. 控件对事件的消费

    事件到达控件(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 及时刷新。

使用注意事项:

  1. 性能问题:频繁调用可能引发性能下降,每次调用都会处理所有挂起事件
  2. 避免递归事件:在事件处理过程中再次调用可能导致递归,使逻辑复杂
  3. 替代方案:优先采用异步操作(如QThreadQtConcurrent)防止阻塞事件循环

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()来处理事件。

当然,子事件循环并不局限于模态对话框场景,任何需要暂停当前操作流程、等待用户输入或异步事件完成的场景都可以使用子事件循环。 常见应用场景包括:

  1. 暂停当前函数执行以显示模态对话框:如QDialog::exec()利用子事件循环暂停当前操作流程,等待用户输入

  2. 等待异步事件完成

    QEventLoop loop;
    connect(task, &AsyncTask::done, &loop, &QEventLoop::quit);
    loop.exec();  // 等待异步信号触发
    
  3. 在不中断主事件循环的前提下维持 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 框架的事件驱动模型,和我们想要的”同步等待用户输入”的模型,是天然冲突的。具体来说:

  1. 程序启动后,Qt 启动一个事件循环(event loop)
  2. 鼠标点击、键盘输入、窗口重绘等都会生成”事件”
  3. Qt 把事件投递给响应的对象(按钮、窗口等)
  4. 我们通过信号槽、事件处理函数来响应这些事件

关键区别:事件不是你代码调用触发的,而是用户操作后 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 类型指针,从而为开发者提供了在程序各处便捷访问全局 QApplicationQCoreApplication 对象的途径。

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 控件进行操作,原因主要有以下两点:

  1. 事件循环的主线程限制:子线程不能直接调用 UI 控件的方法或访问 UI,否则会产生未定义行为,甚至导致程序崩溃。这是因为 UI 组件的创建与管理均在主线程中完成,其生命周期与主线程紧密相关。若子线程对 UI 组件进行操作,极有可能在 UI 组件尚未完全创建或已被销毁时进行访问,进而引发错误。

  2. 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 在以下情形中颇具效用:

  1. 状态修改但避免触发信号:当需对对象状态进行修改,同时又不希望该修改触发任何信号时,可使用 QSignalBlocker。比如,更新 QSpinBox 的值,但不希望触发 valueChanged 信号。在此场景下,QSignalBlocker 可暂时阻断信号发射,确保状态修改过程不受信号处理逻辑干扰。
  2. 批量操作后统一处理信号:若要执行一系列操作,且这些操作可能触发多个信号,但期望仅在所有操作完成后再处理这些信号,QSignalBlocker 就派上用场了。例如,对 QStandardItemModel 的多个项进行修改,只希望在全部修改完成后再更新视图。利用 QSignalBlocker 可在操作期间抑制信号发射,待所有操作结束后,再让信号正常触发,从而实现对信号处理时机的精准控制。
  3. 防止信号循环:当存在两个相互关联的控件,一个控件状态改变会引发另一个控件状态改变,反之亦然时,为避免在特定时间段内产生循环信号,可使用 QSignalBlocker。比如,在修改其中一个控件状态时,通过 QSignalBlocker 阻止其发射信号,有效避免无限信号循环的产生。

需留意的是,QSignalBlocker 会阻断所有信号,包括可能期望处理的信号,所以应谨慎使用。若有可能,应通过优化代码设计,从根源上避免不必要的信号触发,而非过度依赖 QSignalBlocker。




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • (三)内核那些事儿:CPU中断和信号
  • (二)内核那些事儿:程序启动到运行的完整过程
  • (一)内核那些事儿:从硬件抽象到系统服务的完整框架
  • (七)内核那些事儿:操作系统对网络包的处理
  • (五)内核那些事儿:系统和程序的交互