← 返回博客列表

一、事件系统概述

Qt 的事件系统是整个框架处理异步消息流的核心机制。无论是鼠标点击、键盘按键、窗口尺寸变化、定时器触发,还是跨线程通信的派发,最终都会被抽象为一个 QEvent 对象,并经由 事件循环 派发到目标 QObjectevent() 方法中处理。

事件系统的本质
· 事件(Event):描述「发生了什么」的对象,封装了事件类型、数据与目标对象
· 事件循环(Event Loop):不断从事件队列取出事件并派发的中央调度器
· 事件处理者(Handler):QObject 子类重写 event() 或具体的事件回调函数响应事件
· 事件过滤器(Event Filter):在事件到达目标前进行拦截、改写或转发的旁观者

1.1 事件与普通函数调用的区别

普通函数调用是同步、直接的:调用者直接跳转到被调函数执行,控制权立即转移。而事件机制是异步、解耦的:发送者只需构造事件并投递到队列,事件循环在合适的时机再派发给接收者,发送者与接收者互不感知。

1.2 事件来源的多样性

Qt 事件来源
① 窗口系统(鼠标、键盘、窗口暴露、重绘等)
② 定时器(QTimer 通过平台抽象触发 timerEvent)
③ 网络/IO(socket notifier 派发读写事件)
④ 用户代码(postEvent / sendEvent 投递)
⑤ 跨线程通信(QueuedConnection 派发为 QMetaCallEvent)
⑥ 系统服务(剪贴板、拖放、输入法等)

二、事件循环:Qt 应用的心跳

事件循环是 Qt 应用程序持续运转的「心脏」。它本质上是一个 while 循环,不断从底层消息队列取出事件、翻译成 QEvent、派发给目标对象,直到退出条件满足为止。所有 Qt GUI 程序的 app.exec() 都会进入这一循环。

2.1 事件循环的简化模型

QCoreApplication::exec()
+ exec() : int
+ quit() : void
+ processEvents() : void
——→
QEventLoop
+ exec(flags) : int
+ exit(returnCode) : void
+ processEvents(flags) : void
+ wakeUp() : void
——→
QAbstractEventDispatcher
+ processEvents(flags) : bool
+ registerTimer() : void
+ registerSocketNotifier()
# 平台事件源抽象

2.2 exec() 的等价伪代码

// QCoreApplication::exec() 的核心逻辑(简化伪代码)
int QCoreApplication::exec() {
    QEventLoop eventLoop(this);
    return eventLoop.exec();
}

int QEventLoop::exec(ProcessEventsFlags flags) {
    while (!d->exit) {
        // 1. 由平台派发器从系统消息队列拉取事件
        //    (Windows: GetMessage;Linux/X11: XPending;macOS: CFRunLoop)
        d->threadData->eventDispatcher->processEvents(flags);

        // 2. 处理本线程 Qt 投递队列中的事件(postEvent 队列)
        QCoreApplication::sendPostedEvents();

        // 3. 处理 deferred delete(deleteLater 投递的事件)
        QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
    }
    return d->returnCode;
}
关键点:事件循环不是「忙等」,而是借助平台原生 API(Windows 的 GetMessage、Linux 的 poll/epoll、macOS 的 CFRunLoop)阻塞等待,直到有事件到达才被唤醒。这避免了空转浪费 CPU,是 GUI 程序能在 0% CPU 占用下持续响应的原因。

2.3 嵌套事件循环

Qt 允许在事件处理过程中开启嵌套事件循环,这是 QDialog::exec()QMenu::exec()QEventLoopLocker、模态对话框等机制能「阻塞」调用同时仍然响应事件的原因。

// 在槽函数中开启嵌套事件循环(典型场景:模态等待)
void MainWindow::onFetchData() {
    QEventLoop loop;
    QNetworkReply *reply = nam->get(QNetworkRequest(url));

    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);

    loop.exec();  // 「阻塞」当前槽函数,但 UI 仍可响应其它事件

    if (reply->error() == QNetworkReply::NoError)
        handleData(reply->readAll());
    reply->deleteLater();
}
嵌套事件循环的陷阱:在嵌套循环激活期间,外层函数的栈帧仍然存在,但代码可能被重入。例如用户在等待网络请求时点击了「关闭窗口」按钮,可能导致对象在槽函数返回前就被 deleteLater 销毁。务必使用 QPointer<T> 守护或对依赖对象使用 QScopedPointer

2.4 processEvents:手动驱动事件循环

在耗时操作中(如大规模数据处理、解析大文件),可定期调用 processEvents() 让事件循环处理积压事件,避免界面卡死:

// 在长任务中定期让出 CPU,避免 UI 冻结
void Worker::parseLargeFile(const QString &path) {
    QFile file(path);
    file.open(QIODevice::ReadOnly);

    QTextStream in(&file);
    int lineCount = 0;
    while (!in.atEnd()) {
        processLine(in.readLine());

        if (++lineCount % 100 == 0) {
            // 处理 100ms 内积压的事件(鼠标、键盘、重绘等)
            QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
        }
    }
}
processEvents 的危险性:它会导致不可预测的代码重入——槽函数 A 调用 processEvents 时,可能触发另一个槽函数 B,而 B 又可能删除 A 正在使用的对象。在「正常」业务流程中应避免使用,优先考虑:
① 将耗时任务移到 QThread 子线程;
② 用 QtConcurrent::run 或 QThreadPool;
③ 拆分任务,用 QTimer::singleShot(0, ...) 分批处理。

三、关键类总览

Qt 事件系统涉及一组分工明确的类,理解它们的职责边界是掌握事件机制的前提。

类 / 方法 角色 核心职责
QEvent 事件载体 所有事件的基类,封装事件类型 Typeaccept()/ignore() 状态
QObject::event() 事件入口 每个 QObject 接收事件的总入口,按类型分发到具体回调(timerEvent、childEvent 等)
QCoreApplication::notify() 派发器 事件从队列派发到目标对象的「总闸」,先经事件过滤器再调用 event()
QCoreApplication / QGuiApplication / QApplication 应用对象 持有主线程事件循环,负责从平台消息队列拉取并翻译事件
QEventLoop 循环封装 封装一次事件循环过程,支持 exec/quit/exit,可嵌套
QAbstractEventDispatcher 平台抽象 抽象平台消息源(定时器、socket notifier、原生消息),由 QEventDispatcherWin/Unix 等实现
QCoreApplication::postEvent() 异步投递 把事件加入目标对象所在线程的队列,由该线程事件循环稍后派发(线程安全)
QCoreApplication::sendEvent() 同步派发 直接在当前线程调用 notify() 派发,立即返回(非线程安全)
QObject::installEventFilter() 过滤器 给目标对象安装事件过滤器,事件到达目标前先调用过滤器的 eventFilter()
QInputEvent / QMouseEvent / QKeyEvent 等子类 具体事件 携带特定输入数据(鼠标坐标、按键码、修饰键等)
QMetaCallEvent 跨线程信号 跨线程信号槽连接的内部载体,QueuedConnection 会被打包成此事件投递

3.1 QEvent 的核心结构

QEvent
- d : QEventPrivate*
- t : Type
+ type() : Type
+ accept() : void
+ ignore() : void
+ isAccepted() : bool
+ spontaneous() : bool
# setAccepted(bool)

继承
QInputEvent
+ modifiers() : Qt::KeyboardModifiers
+ timestamp() : ulong

继承
QMouseEvent
+ position() : QPointF
+ button() : Qt::MouseButton
+ buttons() : Qt::MouseButtons

3.2 accept / ignore 的语义

每个事件对象都有一个 accepted 标志位,默认构造时为 true(已被处理)。事件处理者通过 ignore() 表示「我不处理这个事件」,使其能够继续向父组件传递。这是 GUI 窗口事件传播(如快捷键、键盘焦点)的核心机制。

QEvent e(QEvent::KeyPress);
qDebug() « e.isAccepted();  // true,默认已被接受

e.ignore();
qDebug() « e.isAccepted();  // false,等待上层处理

e.accept();
qDebug() « e.isAccepted();  // true,本层处理完毕

3.3 spontaneous:区分系统事件与程序事件

spontaneous() 返回 true 表示事件来源于底层窗口系统(用户真实点击鼠标/按键),返回 false 表示事件由 Qt 程序代码通过 sendEvent/postEvent 合成。这在区分「用户操作」与「程序模拟」时有用,例如防止模拟点击触发审计日志。

四、事件流转 UML 图(PlantUML 语法)

下面以 PlantUML 语法展示 Qt 事件系统的核心类结构与事件流转时序,可直接复制到 PlantUML 在线渲染器(如 plantuml.com)查看渲染后的图。

4.1 核心类结构图(PlantUML Class Diagram)

Qt 事件系统核心类结构图

4.2 事件流转时序图(PlantUML Sequence Diagram)

下述时序图描述了一次鼠标点击事件从操作系统到目标 QObject 的完整流转路径,包括事件过滤器的拦截点与事件循环的派发过程。

Qt 事件流转时序图

4.3 postEvent 异步派发流程

postEvent 异步派发流程图

五、事件入口:notify / event

5.1 sendEvent 与 postEvent

Qt 投递事件有两种方式,它们的执行语义截然不同:

// 1. sendEvent:同步派发(立即执行)
QMouseEvent press(QEvent::MouseButtonPress, QPointF(100, 100),
                  Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QCoreApplication::sendEvent(targetWidget, &press);
// 函数返回时,事件已处理完毕(在当前线程调用 target->event())

// 2. postEvent:异步投递(加入目标线程队列)
QEvent *customEvent = new MyCustomEvent("payload");
QCoreApplication::postEvent(target, customEvent, Qt::HighEventPriority);
// 函数立即返回,事件由 target 所在线程的事件循环稍后派发
特性 sendEvent postEvent
调用时机 立即同步派发 入队,稍后异步派发
线程安全 否(必须自己确保目标对象可被当前线程访问) 是(可跨线程投递)
事件所有权 栈对象即可(不接管) 必须堆分配(Qt 自动 delete)
派发线程 调用者所在线程 目标对象 thread() 所在线程
性能 快(无锁、无队列) 稍慢(加锁、入队、唤醒)
postEvent 必须堆分配:因为 postEvent 会接管事件的所有权,事件被派发完后由 Qt 自动 delete。如果传入栈对象的地址,将导致双重释放崩溃。

5.2 QCoreApplication::notify()

notify() 是事件从循环派发到对象的「最后一公里」。它依次执行:检查事件过滤器 → 调用目标对象的 event()。重写 QCoreApplication 子类的 notify() 可实现全局事件监听。

// 自定义 Application,全局监听所有事件
class MyApplication : public QApplication {
    Q_OBJECT
public:
    using QApplication::QApplication;

    bool notify(QObject *receiver, QEvent *event) override {
        // 全局事件追踪(如性能统计、操作日志)
        if (event->type() == QEvent::MouseButtonPress) {
            logUserActivity(receiver, static_cast<QMouseEvent*>(event));
        }

        try {
            return QApplication::notify(receiver, event);
        } catch (const std::exception &e) {
            qCritical() « "Unhandled exception in event handler:" « e.what();
            return false;
        }
    }
};

// main.cpp
int main(int argc, char *argv[]) {
    MyApplication app(argc, argv);
    return app.exec();
}
性能提示:notify() 会被每一次事件派发调用(鼠标移动时每秒可达数百次),其中的代码必须极其轻量。重逻辑应放到具体的 event() 处理函数中,避免在 notify 全局拦截中拖累整个应用。

5.3 QObject::event()

每个 QObject 的 event() 是其接收事件的总入口。基类实现按事件类型分发到具体虚函数:timerEvent()childEvent()customEvent() 等。子类重写时可拦截特定类型事件。

class StatusWidget : public QWidget {
    Q_OBJECT
protected:
    bool event(QEvent *e) override {
        // 拦截工具提示事件,自定义显示
        if (e->type() == QEvent::ToolTip) {
            QHelpEvent *he = static_cast<QHelpEvent*>(e);
            QToolTip::showText(he->globalPos(), "自定义提示:" + toolTip());
            e->accept();
            return true;
        }

        // 拦截状态改变事件
        if (e->type() == QEvent::StatusTip) {
            // 阻止默认行为
            return true;
        }

        // 其他事件交给父类处理
        return QWidget::event(e);
    }
};
重写 event() 的常见错误:① 处理完事件后忘记调用 e->accept()return true,导致事件继续传播;② 直接返回 true 而不调用 QWidget::event(e),导致父类逻辑(如 mousePressEvent、paintEvent)全部失效。一般原则:未处理的事件必须转交父类

六、事件过滤器

事件过滤器(Event Filter)是 Qt 事件系统中最强大的扩展机制之一。它允许一个对象「监听」另一个对象的事件流,在事件到达目标前进行拦截、修改或转发,而无需修改目标对象的代码。

6.1 安装与回调

// 监听者必须重写 eventFilter()
class KeyLogger : public QObject {
    Q_OBJECT
protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *ke = static_cast<QKeyEvent*>(event);
            qDebug() « "Key pressed:" « ke->key();

            // 返回 true 表示事件已被处理,不再传递给 watched
            if (ke->key() == Qt::Key_Escape) {
                return true;  // 拦截 Esc 键
            }
        }
        // 返回 false 表示放行,事件继续传递给 watched 的 event()
        return QObject::eventFilter(watched, event);
    }
};

// 在主窗口构造时安装过滤器
MainWindow::MainWindow() {
    KeyLogger *logger = new KeyLogger(this);
    textEdit->installEventFilter(logger);

    // 一个过滤器可监听多个对象
    lineEdit1->installEventFilter(logger);
    lineEdit2->installEventFilter(logger);

    // 移除:在 logger 销毁前自动移除(Qt 内部保证)
    // 也可手动:textEdit->removeEventFilter(logger);
}

6.2 事件过滤器的调用链

当事件派发到一个对象时,Qt 先依次调用其安装的所有事件过滤器(按安装逆序),任何一个过滤器返回 true 都会中止传播。所有过滤器放行后,才调用目标对象自身的 event()

notify(target, event)
filter1.eventFilter
return true → 终止
return false → 继续
filter2.eventFilter
同上
target.event()
按类型分发

6.3 应用程序级事件过滤器

若想监听所有对象的事件(例如全局快捷键、全局鼠标追踪),可以在 QApplication 对象上安装过滤器:

class GlobalShortcutHandler : public QObject {
    Q_OBJECT
protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *ke = static_cast<QKeyEvent*>(event);
            if (ke->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier)
                && ke->key() == Qt::Key_K) {
                emit globalShortcutTriggered();
                return true;  // 全局拦截
            }
        }
        return QObject::eventFilter(watched, event);
    }
signals:
    void globalShortcutTriggered();
};

// 安装到 QApplication
GlobalShortcutHandler *handler = new GlobalShortcutHandler;
qApp->installEventFilter(handler);
性能陷阱:应用程序级过滤器会在每一次事件派发时被调用,包括鼠标移动、绘制、定时器等高频事件。务必在最开头做类型判断快速放行,不要在里面做复杂业务逻辑。

6.4 事件过滤器的典型应用场景

七、自定义事件

当 Qt 内置的事件类型无法满足需求时,可以派生 QEvent 创建自定义事件,并通过 postEvent / sendEvent 派发。自定义事件常用于:跨线程任务结果回传、自定义插件协议、业务状态通知等。

7.1 自定义事件三步走

步骤一:派生 QEvent 子类

// 1. 定义自定义事件类型常量(必须 > QEvent::User)
const QEvent::Type MyCustomEventType =
    static_cast<QEvent::Type>(QEvent::registerEventType());

// 2. 派生 QEvent
class MyCustomEvent : public QEvent {
public:
    MyCustomEvent(const QString &payload)
        : QEvent(MyCustomEventType), m_payload(payload) {}

    QString payload() const { return m_payload; }

    // 提供静态 type() 方便使用
    static inline const QEvent::Type type = MyCustomEventType;

private:
    QString m_payload;
};
registerEventType() 的作用:Qt 预留了 QEvent::User(1000)以上的整数值供用户使用。QEvent::registerEventType() 保证返回一个未被占用的事件类型 ID,避免多个库之间的冲突。强烈建议使用它而非直接写常量。

步骤二:在目标对象中处理事件

class Worker : public QObject {
    Q_OBJECT
protected:
    bool event(QEvent *e) override {
        if (e->type() == MyCustomEventType) {
            MyCustomEvent *ce = static_cast<MyCustomEvent*>(e);

            qDebug() « "收到自定义事件:" « ce->payload();
            emit customEventReceived(ce->payload());

            e->accept();
            return true;
        }
        return QObject::event(e);
    }
signals:
    void customEventReceived(const QString &payload);
};

步骤三:投递事件

// 异步投递(跨线程安全)
QCoreApplication::postEvent(worker, new MyCustomEvent("Hello from another thread"));

// 同步派发(同线程)
MyCustomEvent e("sync event");
QCoreApplication::sendEvent(worker, &e);

7.2 完整跨线程示例

class Worker : public QObject {
    Q_OBJECT
public slots:
    void doHeavyWork() {
        for (int i = 0; i < 10; ++i) {
            QThread::msleep(500);

            // 通过自定义事件通知主线程进度
            QCoreApplication::postEvent(parent(),
                new ProgressEvent(i + 1, 10));
        }
    }
};

class Controller : public QObject {
    Q_OBJECT
public:
    Controller() {
        m_worker.moveToThread(&m_thread);
        connect(&m_thread, &QThread::started, &m_worker, &Worker::doHeavyWork);
        m_thread.start();
    }

protected:
    bool event(QEvent *e) override {
        if (e->type() == ProgressEvent::type) {
            auto *pe = static_cast<ProgressEvent*>(e);
            qDebug() « "进度:" « pe->current() « "/" « pe->total();
            e->accept();
            return true;
        }
        return QObject::event(e);
    }

private:
    QThread m_thread;
    Worker m_worker;
};
设计建议:自定义事件类型应在头文件中通过 registerEventType() 注册一次(C++ 函数局部 static 保证线程安全初始化)。事件类应保持不可变(immutable),避免在派发过程中被多线程读取时发生数据竞争。

7.3 事件优先级

postEvent 的第三个参数指定事件优先级,高优先级事件会被先派发。Qt 内部使用的优先级:

注意:事件队列本质是按优先级排序的链表。同一优先级内保持 FIFO 顺序。优先级仅影响派发顺序,不能用于「打断正在执行的事件处理」——后者需要重新设计架构(如使用 QFuture / 协程)。

八、与信号槽的联系与区别

事件系统和信号槽是 Qt 中两个最核心的「对象通信机制」,初学者常混淆。它们在概念上互相关联,在实现和使用场景上又各司其职。

8.1 概念联系

底层联系:跨线程的信号槽(Qt::QueuedConnection)实际上就是把调用打包成 QMetaCallEvent 投递到目标线程的事件队列,由目标线程事件循环派发后调用槽函数。

换句话说:跨线程信号槽 = 自定义事件 + 元对象调度。这就是为什么跨线程信号槽依赖目标对象所在线程必须运行事件循环。

下面这行跨线程信号槽:

connect(worker, &Worker::resultReady, mainWnd, &MainWindow::onResult,
        Qt::QueuedConnection);

当 worker 子线程 emit resultReady 时,等价于:

// 元对象系统内部等价行为
QMetaCallEvent *ev = new QMetaCallEvent(slotObject, mainWnd, /* args */...);
QCoreApplication::postEvent(mainWnd, ev);
// mainWnd 所在线程事件循环取出该事件,调用 slotObject.invoke()
// 最终调用 MainWindow::onResult

8.2 概念区别

对比维度 事件系统 信号槽
通信模型 点对点(一个事件 → 一个目标对象) 一对多(一个信号 → 多个槽)
发送者感知 发送者需知道目标对象(postEvent 需要 receiver) 发送者完全不知道接收者(emit 时无需关心有无连接)
触发方向 「拉」模型:事件循环主动派发 「推」模型:emit 时立即触发(同步)或入队(异步)
处理者 目标对象的 event() 重写 任意对象的槽函数(connect 时绑定)
多接收者 一个事件只能被一个对象处理(事件过滤器可拦截) 一个信号可同时触发多个槽
跨线程 postEvent 原生支持 QueuedConnection 内部依赖事件系统
类型安全 弱类型(QEvent* 需要 static_cast 向下转型) 强类型(编译期检查,函数指针语法)
典型来源 窗口系统、定时器、网络、跨线程 用户代码中的状态变化通知
开销 较大(构造事件、入队、派发、虚函数调用) 同步连接较小(直接函数调用)
解耦程度 发送者与接收者耦合(需直接引用 receiver) 完全解耦(emit 与 connect 分离)

8.3 何时用事件,何时用信号槽

使用信号槽的场景:
① 对象状态变化通知(如 valueChanged、connected、finished)
② 跨对象的松耦合通信(如按钮点击 → 窗口响应)
③ 一对多广播(多个对象订阅同一信号)
④ 跨线程通信(自动封装为 QMetaCallEvent)

使用事件的场景:
① 与窗口系统交互(鼠标、键盘、窗口暴露、绘制)
② 自定义协议层(插件间低层通信)
③ 需要事件过滤器拦截的场景
④ 严格单线程内的任务队列化(postEvent 到自身线程)
⑤ 需要 accept/ignore 控制传播链的场景

8.4 自定义事件 vs 信号槽:如何选择

对于「对象间通信」这一需求,自定义事件和信号槽都可以实现。一般原则:

九、与 GUI 窗体的联系

Qt 事件系统在 GUI 窗体(QWidget 及其子类)中得到最充分的应用。QWidget 重写了 event(),将通用事件按类型分发为一系列具体的虚函数:mousePressEventkeyPressEventpaintEventresizeEvent 等。同时,许多事件类型在窗口系统中具有传播链行为。

9.1 QWidget::event() 的事件分发

QWidget 的 event() 是一个庞大的 switch-case 分发器,将 QEvent 按类型派发到对应的虚函数:

QWidget::event(QEvent*)
switch(e->type())
输入事件分支
MouseButtonPress → mousePressEvent
MouseButtonRelease → mouseReleaseEvent
MouseMove → mouseMoveEvent
KeyPress → keyPressEvent
KeyRelease → keyReleaseEvent
Wheel → wheelEvent
DragEnter → dragEnterEvent
窗口事件分支
Paint → paintEvent
Resize → resizeEvent
Show → showEvent
Hide → hideEvent
Close → closeEvent
FocusIn/Out → focus(In|Out)Event
HoverMove → hoverMoveEvent

用户重写某个具体事件虚函数即可响应,无需重写 event()

class DrawWidget : public QWidget {
    Q_OBJECT
protected:
    void mousePressEvent(QMouseEvent *e) override {
        if (e->button() == Qt::LeftButton) {
            m_lastPos = e->position();
            m_drawing = true;
            e->accept();
        } else {
            e->ignore();  // 让父类处理
        }
    }

    void paintEvent(QPaintEvent *) override {
        QPainter p(this);
        // 绘制内容...
    }
};

9.2 事件传播链

GUI 事件有两条重要的传播链:

① 父子窗口传播(事件冒泡):某些事件(如按键)如果当前窗口未 accept,会传递给父窗口处理,直到顶层窗口。这是快捷键能从子控件冒泡到主窗口的原理。

② 焦点链传播:键盘事件按 Tab 顺序在焦点控件间移动。Qt::Tab 键、Qt::Backtab 键默认会触发焦点切换,除非控件自行 accept 该事件。

QLineEdit (focus)
keyPressEvent(QEvent::Key_Tab)
ignore() 默认
↑ 冒泡
QWidget (parent)
未处理 → 默认焦点切换
QTextEdit (next focus)
setFocus()

9.3 鼠标事件与 grabber

鼠标事件通常派发给鼠标光标下的窗口。但当某个窗口调用 grabMouse() 后,所有鼠标事件都会派发给它,直到调用 releaseMouse()。键盘事件同理有 grabKeyboard()。这是模态对话框、拖拽操作能持续接收鼠标移动事件的底层机制。

9.4 paintEvent 与重绘事件

QEvent::Paint 是 GUI 系统中最重要的事件之一。窗口系统检测到窗口「脏区」时,会派发 Paint 事件,QWidget 在 paintEvent 中重绘。Qt 提供 update() 异步请求重绘、repaint() 立即重绘两种接口。

// 修改数据后请求重绘(推荐方式)
void CircleWidget::setRadius(double r) {
    if (qFuzzyCompare(m_radius, r)) return;
    m_radius = r;
    update();  // 投递 Paint 事件,由事件循环稍后合并多个 update 一次重绘
}

void CircleWidget::paintEvent(QPaintEvent *) {
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    p.drawEllipse(rect(), m_radius);
}
update() vs repaint():update() 把 Paint 事件加入队列,事件循环会把多个连续 update 合并为一次重绘,性能更优。repaint() 直接同步调用 paintEvent(),会立即触发重绘,应避免在循环中频繁调用。

9.5 事件传播与 accept/ignore 在 GUI 中的实际效果

以下是一个典型的快捷键拦截示例:在 MainWindow 中拦截 F1 键显示帮助。如果某个子控件(如 QTextEdit)本身已处理 F1(如显示上下文帮助),事件会被 accept,不会冒泡到 MainWindow。

class MainWindow : public QMainWindow {
    Q_OBJECT
protected:
    void keyPressEvent(QKeyEvent *e) override {
        if (e->key() == Qt::Key_F1) {
            showHelp();
            e->accept();  // 已处理,不再冒泡
            return;
        }
        QMainWindow::keyPressEvent(e);  // 其他键交给父类
    }
};
常见误区:很多人以为重写 keyPressEvent 默认就能拦截所有按键。实际上,Qt 的默认实现会处理 Tab、Backtab、Esc、Enter 等系统键,未 accept 的事件会自动冒泡。如果想完全接管键盘,必须显式 accept 或调用父类方法。

9.6 GUI 事件与线程模型

Qt 严格要求所有 GUI 事件必须在主线程派发与处理。QWidget 及其子类的事件处理函数(paintEvent、mousePressEvent 等)只能在主线程调用。这意味着:

违反 GUI 线程模型的后果:轻则界面更新失效、绘制错乱,重则程序崩溃(特别是在 Windows 上,跨线程访问 GUI 句柄会触发 GDI 断言)。

十、总结

Qt 事件系统是理解整个框架行为模式的另一把钥匙,与元对象系统并列为 Qt 两大基石。它的设计哲学是统一抽象 + 队列派发 + 钩子扩展,将所有异步消息流(窗口系统、定时器、跨线程、用户代码)抽象为统一的 QEvent,通过事件循环派发到目标 QObject 的 event() 方法,并支持事件过滤器、自定义事件、accept/ignore 等扩展机制。

核心要点回顾

实战建议

① 优先使用具体事件虚函数:重写 mousePressEvent 而非 event(),更清晰且不影响其他事件类型

② 谨慎使用 processEvents:它会导致不可预测的代码重入,优先用 QThread 或 QtConcurrent

③ 事件过滤器优于继承:监听非自身对象时用过滤器,避免修改目标类

④ 跨线程通信首选信号槽:自动封装为 QMetaCallEvent,类型安全且无需手动管理事件对象

⑤ GUI 更新走主线程:子线程通过信号槽通知主线程,主线程内调用 update()

⑥ 自定义事件用 registerEventType:避免事件类型冲突,特别是与第三方库共存

⑦ 嵌套事件循环警惕重入:模态对话框中操作外部对象前用 QPointer 守护

⑧ notify() 全局拦截要轻量:每次事件派发都被调用,重逻辑会拖慢整个应用

与元对象系统的关系

事件系统与元对象系统在 Qt 中是互补关系:元对象系统提供编译期反射与信号槽机制,事件系统提供运行时异步消息派发。两者的交集在跨线程信号槽——元对象系统将 emit 调用打包为 QMetaCallEvent,事件系统负责将其派发到目标线程。理解这一交集是掌握 Qt 多线程编程的关键。