(二)Qt 那些事儿:事件循环机制

(二)Qt 那些事儿:事件循环机制

1. concepts

1.1 什么是事件循环?

Qt 事件循环(Event Loop)是整个框架的核心,它基于生产者-消费者模式运作:

操作系统 → 事件队列 → Qt事件循环 → 控件处理
   ↑          ↑          ↑         ↑
生产事件    缓存事件    消费事件   响应事件

核心机制:

  • 事件生产:操作系统捕获用户操作(鼠标、键盘等)
  • 事件缓存:事件被放入队列等待处理
  • 事件消费:Qt 从队列取出事件并分发
  • 事件响应:目标控件处理事件

在 Qt 框架中,事件循环(Event Loop)处于核心地位,负责管理和处理应用程序中各类事件。无论是用户在界面上的点击、输入等操作,还是系统产生的定时器事件、状态变化通知等,都需由程序捕捉并做出响应。

操作系统会捕获这些事件,发送到程序和 os 之间的一个缓存区,也叫事件队列。程序会不定期从这个事件队列取出事件消费,所谓的程序未响应其实就是这个事件队列已满,无法继续添加事件,因此当前出于无法响应的状态。而 Qt 作为一个框架,在这个过程中屏蔽了不同平台产生的事件类型,不同平台的事件获取方式,并给出了统一的事件消费、事件分发机制。

又因为事件的处理(分发、消费)和事件的生成不是同一个主体,前者是程序自身,而后者是 os 的事情,因此即使事件在处理的过程中,事件也可以不停生产并添加到事件队列中。

事件循环基于队列机制循环处理事件。当事件队列中有等待处理的事件时,事件循环被激活并处理这些事件;若队列为空,事件循环则进入阻塞状态,等待新事件发生。

借助事件循环,Qt 应用能够以非阻塞方式响应用户交互,使界面保持响应状态,同时处理其他异步事件。这种模型让程序在等待事件发生时保持低功耗,仅在必要时唤醒执行相关操作,高效利用系统资源。

1.2 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循环,不断取出并处理事件。

  2. Qt 对事件的分发

    取出事件后,通过QCoreApplication::notify(QObject *receiver, QEvent *event)确定接收事件的控件并分发。该函数负责将事件分发给接收者,并调用receiver->event(event)函数。

    不同事件的分发方式:

    • 鼠标事件:通过坐标映射确定对应控件接收
    • 键盘事件:由获得焦点的控件接收
    • 定时器事件:由设置了QTimer的控件或对象接收
    • 自定义事件:手动投递或发送

    以鼠标事件为例:

    // 在 QApplication::notify 中
    // Qt直接定位目标控件,不逐层传递
    QWidget *target = QWidget::find(widgetAt(mousePos));
    QMouseEvent event(...);
    QCoreApplication::sendEvent(target, &event);
    

    Qt的事件分发是直接传入目标控件,而非父对象逐层传递。当鼠标点击发生时,Qt精准定位最终目标控件(如QPushButton),直接发送事件给它。这种定位在QApplication::notify()中完成,借助QWidget::find()等机制确定接收事件的具体控件。

    假设控件结构如下:

    MainWindow
    └── QWidget (parent)
        └── QPushButton (child, 坐标区域覆盖鼠标)
    

    事件分发流程:

    1. Qt获取鼠标坐标(相对于屏幕或主窗口)
    2. 调用QWidget::childAt()找到该坐标下最深层的控件(QPushButton
    3. 直接将事件发送给QPushButton
    4. QPushButton接收事件后调用自身的event()函数,进而分发给mousePressEvent()处理

    不同类型事件的分发方式:

    类型 分发方式
    鼠标/键盘/触摸事件 🚀 直接分发给目标控件
    绘制事件 PaintEvent 🧱 由外层控件递归向子控件调用
    自定义事件 🚀 使用postEvent/sendEvent手动指定目标
    拦截机制 🛑 installEventFilter()先于正常分发

    注意:普通输入事件直接发送至目标控件,但绘制事件(QPaintEvent)不同。绘制事件由框架自动产生,分发顺序按控件层级从外到内调用,如MainWindow::paintEvent() → QWidget::paintEvent() → QPushButton::paintEvent()

  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 什么是子事件循环

子事件循环是一种临时构建的事件处理机制。除了由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()防止嵌套过深或死锁
QEventLoop loop;
if (!loop.isRunning()) {
    loop.exec();
}

2.2 多个 QEventLoop 同时存在的情况

Qt应用程序拥有一个由QApplication::exec()启动的主事件循环。当启动子事件循环时,主事件循环暂时暂停,子事件循环成为当前活动的事件循环。

事件处理特点:

  • 子事件循环活动时负责处理事件队列中的事件
  • 父事件循环(主事件循环)暂停,无法主动消费事件
  • 新事件仍可添加到事件队列中
  • 新事件会在当前活动的子循环中处理(若适用),或等待子循环退出后由父循环处理

子事件循环退出后,父事件循环恢复运行继续处理剩余事件。任一时刻仅有一个事件循环处于活动状态,但事件队列是共享的。

嵌套事件循环示例:

#include <QCoreApplication>
#include <QEventLoop>
#include <QTimer>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 外层事件循环
    QEventLoop outerLoop;
    // 内层事件循环
    QEventLoop innerLoop;

    // 定时器,用于退出内层事件循环
    QTimer innerTimer;
    QObject::connect(&innerTimer, &QTimer::timeout, &innerLoop, &QEventLoop::quit);
    innerTimer.start(1000); // 1秒后超时

    // 定时器,用于退出外层事件循环
    QTimer outerTimer;
    QObject::connect(&outerTimer, &QTimer::timeout, &outerLoop, &QEventLoop::quit);
    outerTimer.start(3000); // 3秒后超时

    qDebug() << "进入外层事件循环";
    outerLoop.exec();
    qDebug() << "外层事件循环结束";

    return a.exec();
}

注意:过多的嵌套事件循环会使代码逻辑复杂,可能引发死锁等问题。在嵌套事件循环中,所有事件依旧在主事件循环中处理,嵌套事件循环仅暂停当前代码执行,不影响事件分发和处理。

3. 同步方式实现的 UI 交互

在UI交互中,常见一种”顺序式”的逻辑:

StartLineCommand();
GetPoint("请选择起点");  // 等待用户点击
GetPoint("请选择终点");  // 再等待用户点击

这种同步执行流程直观清晰:一行一行执行,GetPoint()会”停下来等待”用户点击,用户点击后函数返回再继续执行下一行。这种写法就像控制台程序或脚本那样,自然清晰,也便于维护。

但这在Qt中行不通,因为Qt是事件驱动的GUI框架

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

关键区别:事件不是你代码调用触发的,而是用户操作后Qt调用你的回调函数。所以无法写出像GetPoint()这样的阻塞函数,除非人为构造一个”同步等待”的机制

3.1 Qt 原生方式 —— 异步+状态机

在 Qt 中,推荐的写法是通过事件驱动的回调进行处理。例如监听点击事件,通过状态记录当前是第几次点击:

// 假设 currentState 是一个成员变量,记录当前状态(0=等待起点,1=等待终点)
void onUserClicked(QPoint p) {
    if (currentState == 0) {
        startPoint = p;
        currentState = 1;
        showHint("请选择终点");
    } else if (currentState == 1) {
        endPoint = p;
        finishLine(startPoint, endPoint);
        currentState = 0;
        showHint("请选择起点");
    }
}

这是标准的事件驱动写法,但它有两个问题:(1)写起来不像“流程代码”,而是拆成多个回调;(2)无法清晰表达“阻塞等待用户输入”的语义。

3.2 为什么不能用 while() 等待用户点击?

QPoint p;
while (!gotUserClick) {
    // 等待点击
}

很多人第一反应也还是上面这种写法,这种写法会让程序直接“卡死”,因为你阻塞了主线程,Qt 无法处理 UI 事件 —— 鼠标、键盘、重绘等全部停摆,界面无响应,整个程序陷入死锁状态。

3.3 使用 QEventLoop 启动“子事件循环”

Qt 提供一个非常有用的类 QEventLoop,它可以临时开启一个新的事件循环,让 UI 不会卡死,同时主流程“停下来等结果”,模拟同步行为。

示意代码如下:

QEventLoop loop;

connect(viewport, &MyCanvas::clicked, [&loop](QPoint pt){
    point = pt;
    loop.quit();  // 退出子事件循环
});

showHint("请选择一个点");
loop.exec();       // 启动子事件循环,等待 quit 被调用

你可以理解为:主流程暂时“挂起”,进入一个新的事件处理阶段;用户点击后触发回调,退出循环,主流程恢复继续执行。这就实现了看起来是同步的等待,但实际上 Qt 仍然在正常处理事件,不会卡 UI。

底层由事件驱动 + 子事件循环机制支撑,表面上是“同步流程”,实际上是异步事件响应。

但是一个实际的系统,可能还会面临如下需求:

  • 模态交互(如点选对象、选择区域);
  • 取消机制(按 Esc 或右键取消当前命令);
  • 多重嵌套(主命令中调用子命令或对话框);
  • 跨模块的统一交互逻辑调度。

所以一般都需要再对loop去封装一下

目标 是否达成
看起来是同步的交互等待 ✅ 使用 exec() + quit() 实现
UI 保持响应性 ✅ 子事件循环不阻塞 UI
支持多级嵌套和弹窗调用 ✅ 用 _loops 栈统一管理
支持中断退出、状态恢复 earlyExit / interrupt() 控制
提供统一指令交互调度框架 ✅ 所有命令逻辑都走统一事件循环封装

总结:Qt是异步事件驱动模型,无法像控制台程序那样”等输入”。如果想在界面程序中”等用户点一下”,要么写异步状态机逻辑,要么用QEventLoop开个子事件循环,模拟同步流程而不阻塞UI。

这种做法在Qt的机制上构建了一套统一的指令调度和交互系统,既保留异步驱动的优势,又提供可控、清晰的”同步式”交互体验。

3.4 协程方式实现同步的 ui 交互

随着C++20引入协程支持,我们可以使用更现代的方式来实现看似同步的UI交互,避免复杂的状态机逻辑和事件循环嵌套。

3.4.1 协程的基本概念与优势

协程(Coroutine)是一种可以被暂停和恢复的函数,它允许我们在函数执行过程中”让出控制权”,稍后再从暂停的地方继续执行。这非常适合实现异步等待用户输入的场景。

核心优势对比:

方式 代码结构 异常处理 维护性 嵌套支持
状态机方式 🔴 分散的回调 🔴 复杂 🔴 难维护 🔴 困难
QEventLoop方式 🟡 较清晰 🟡 一般 🟡 中等 🟡 需要管理
协程方式 🟢 线性同步式 🟢 自然 🟢 易维护 🟢 天然支持
3.4.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.4.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.4.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.4.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.4.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.4.7 性能和兼容性考虑

编译要求:

  • 需要C++20支持的编译器(GCC 10+, Clang 11+, MSVC 2019 16.8+)
  • CMake中需要设置:set(CMAKE_CXX_STANDARD 20)

性能特点:

  • 协程的内存开销通常比线程小得多
  • 切换开销比线程上下文切换低
  • 但比直接的状态机略有开销

适用场景:

✅ 复杂的多步骤用户交互
✅ 需要频繁嵌套的操作流程
✅ 对代码可读性要求高的项目
✅ 团队熟悉现代C++特性

❌ 简单的单步交互(过度设计)
❌ 对C++20支持有限制的项目
❌ 性能要求极其苛刻的场景

协程方式为Qt UI交互提供了一种现代化、高效且易于维护的解决方案,特别适合需要复杂用户输入流程的应用场景。通过合理的设计和实现,可以显著提高代码的可读性、可维护性和开发效率。

#include “ui_interaction.moc” // 包含MOC生成的文件

4. Qt 事件循环源码分析

下面深入剖析 Qt 事件循环的源码。在事件循环处理事件前,事件通过 sendEventpostEvent 进行派发,前者使事件立即执行,后者将事件先插入队列,再由事件循环取出执行,接下来着重阐述事件循环获取并发送队列中事件的运转过程。

在 Qt GUI 应用程序入口 main 函数中,能看到应用启动与事件循环引入过程:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();
    return a.exec();
}

实例化 QApplication 为程序设置基本环境与参数,创建并显示主窗口后,调用 a.exec() 启动事件循环,使程序能响应鼠标点击、按键等事件。

  1. QCoreApplication::exec 函数
int QCoreApplication::exec()
{
   ...
    threadData->quitNow = false;
    QEventLoop eventLoop;
    self->d_func()->in_exec = true;
    self->d_func()->aboutToQuitEmitted = false;
    int returnCode = eventLoop.exec();
   ...
}

此函数创建并启动 QEventLoop 对象,通过调用其 exec 方法运行事件循环,该循环负责管理事件队列与处理事件。

  1. QEventLoop::exec 函数
int QEventLoop::exec(ProcessEventsFlags flags)
{
    //...
    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);
    ref.exceptionCaught = false;
    return d->returnCode.loadRelaxed();
}

QEventLoop::exec 包含循环,持续调用 processEvents 处理事件。WaitForMoreEvents 标志表明无新事件时,循环将阻塞等待。

  1. QCoreApplication::processEvents 函数
void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{
    QThreadData *data = QThreadData::current();
    if (!data->hasEventDispatcher())
        return;
    QElapsedTimer start;
    start.start();
    while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {
        if (start.elapsed() > ms)
            break;
    }
}

processEvents 方法调用当前线程的事件调度器 QAbstractEventDispatcher,不同系统平台有不同实现,以 Windows 平台的 QEventDispatcherWin32 为例分析。在长时间循环中周期性调用 processEvents(),可避免界面卡死,且能在不中断操作流程时处理用户输入、动画刷新等。

  1. QEventDispatcherWin32 的事件处理
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
    Q_D(QEventDispatcherWin32);
   ...
    // 防止死锁,每次迭代发送已发布事件
    sendPostedEvents();
   ...
}
  1. QEventDispatcherWin32::sendPostedEvents
void QEventDispatcherWin32::sendPostedEvents()
{
    Q_D(QEventDispatcherWin32);
    if (d->sendPostedEventsTimerId != 0)
        KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);
    d->sendPostedEventsTimerId = 0;

    d->wakeUps.storeRelaxed(0);

    QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}

该部分展示事件调度器处理事件队列,最终调用 QCoreApplicationsendPostEvents 方法。

  1. QCoreApplicationPrivate::sendPostedEvents:事件发送逻辑
void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
                                               QThreadData *data)
{
    //...
    if (receiver && receiver->d_func()->threadData != data) {
        qWarning("QCoreApplication::sendPostedEvents: Cannot send "
                 "posted events for objects in another thread");
        return;
    }
   ...
    while (i < data->postEventList.size()) {
      ...
        QEvent *e = pe.event;
        QObject * r = pe.receiver;
      ...
        QCoreApplication::sendEvent(r, e);
      ...
    }
}

代码先检查线程,确保事件在所属线程发送,然后遍历事件列表,调用 sendEvent 方法将事件发送给目标对象。

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 更新。




    Enjoy Reading This Article?

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

  • (七)内核那些事儿:操作系统对网络包的处理
  • (六)内核那些事儿:文件系统
  • (五)内核那些事儿:系统和程序的交互
  • (四)内核那些事儿:设备管理与驱动开发
  • (三)内核那些事儿:CPU中断和信号