Qt 把输入、绘制、尺寸变化、定时器等通知表示为 QEvent 或其派生对象,再送给某个 QObject。接收者不一定是控件,也不是所有事件都会沿父对象链传播。按钮的 clicked 等信号通常是控件处理输入后发出的较高层通知;信号槽与事件处理各有职责。
类型、目标与分发路径
跳转到“类型、目标与分发路径”| 常见事件 | 相关事件对象 | 常见处理入口 |
|---|---|---|
| 鼠标按下、释放、移动 | QMouseEvent | mousePressEvent、mouseReleaseEvent、mouseMoveEvent |
| 键盘按下、释放 | QKeyEvent | keyPressEvent、keyReleaseEvent;特殊 Tab 行为也可能需要 event() |
| 滚轮、触摸 | QWheelEvent、QTouchEvent | 依具体控件与事件类型处理 |
| 绘制、尺寸变化 | QPaintEvent、QResizeEvent | paintEvent、resizeEvent |
| 关闭、显示、隐藏 | QCloseEvent 等 | closeEvent、showEvent、hideEvent |
| 定时器 | QTimerEvent | timerEvent;QTimer 处理到期后发出 timeout |
这些类型最终都属于 QEvent 体系。Qt 6 的鼠标、滚轮和触摸事件还经过指针事件等中间基类,不宜把简化示意当成精确的直接继承树。使用 event->type() 确认事件类型,再做与实际事件对象匹配的转换;不能仅凭希望收到某类事件就强制转换任意 QEvent*。
对典型 GUI 目标,可按“应用级过滤器 → 目标对象过滤器 → 目标 event() → 专门处理函数”理解主要路径。过滤器可以提前终止后续处理;event() 通常把事件分派给专门函数。普通按钮点击优先连接 clicked,定时任务优先连接 timeout,网络读取优先处理 readyRead;需要坐标、按键细节或拦截默认输入时才进入事件层。不要用未经测量的“事件总比信号快”来决定设计。Qt:事件系统
三种“处理结果”不能混为一谈
跳转到“三种“处理结果”不能混为一谈”| 接口 | 含义 |
|---|---|
eventFilter() 返回 true | 过滤器消费本次事件,后续过滤器和目标不再处理它 |
eventFilter() 返回 false | 放行到后续正常处理;不等于直接调用父窗口处理函数 |
event() 的布尔返回值 | 向分发机制报告本次事件是否已处理 |
event->accept() / ignore() | 修改事件的接受标志;具体含义由该类事件和分发规则决定 |
event() 返回 true 不会自动执行 event->accept()。鼠标事件可以在被忽略时向父控件传播,并受 WA_NoMousePropagation 等规则影响;这不能推广成自定义 QObject 事件、绘制事件等都按同样方式“冒泡”。没有重写处理函数,或没有显式写 accept(),也不等于事件未被处理:基类可能已经处理了它,事件本身也可能默认处于接受状态。Qt:鼠标传播规则、Qt:事件接受标志
程序一:验证过滤顺序和返回值
跳转到“程序一:验证过滤顺序和返回值”同一对象后来安装的过滤器先执行,应用级过滤器在对象过滤器之前。下面使用注册的自定义类型,避免与别的模块硬编码的 User + 1 冲突。目标故意设置 ignore() 后返回 true,展示两种状态相互独立;这不是建议所有业务事件都这样处理。
#include <QCoreApplication>#include <QEvent>#include <QObject>#include <QStringList>#include <cassert>#include <utility>
static QEvent::Type probeType() { static const int value = QEvent::registerEventType(); if (value < 0) qFatal("No event type available"); return static_cast<QEvent::Type>(value);}class Target final : public QObject {public: explicit Target(QStringList &entries) : trace(entries) {} int deliveries = 0;protected: bool event(QEvent *event) override { if (event->type() == probeType()) { trace.append("target"); ++deliveries; event->ignore(); return true; } return QObject::event(event); }private: QStringList &trace;};class Filter final : public QObject {public: Filter(QString label, QStringList &entries) : name(std::move(label)), trace(entries) {} bool block = false;protected: bool eventFilter(QObject *, QEvent *event) override { if (event->type() != probeType()) return false; trace.append(name); return block; }private: QString name; QStringList &trace;};int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList trace; Target target(trace); Filter first("first", trace), second("second", trace), global("global", trace); target.installEventFilter(&first); target.installEventFilter(&second); app.installEventFilter(&global); QEvent event(probeType()); const bool handled = QCoreApplication::sendEvent(&target, &event); assert(handled && !event.isAccepted()); assert((trace == QStringList{"global", "second", "first", "target"})); trace.clear(); second.block = true; QEvent blocked(probeType()); QCoreApplication::sendEvent(&target, &blocked); assert((trace == QStringList{"global", "second"})); assert(target.deliveries == 1); app.removeEventFilter(&global);}过滤器与被观察对象必须处于同一线程;应用级过滤器只接收主线程对象的事件,不是所有工作线程事件的统一入口。在过滤器中删除了目标对象,就必须终止该次事件继续访问目标。安装过滤器也不会自动递归安装到现有或以后创建的全部子控件。Qt:installEventFilter、Qt:notify
重写时保留哪一层默认行为
跳转到“重写时保留哪一层默认行为”若继承 QPushButton 并只添加观察逻辑,应调用 QPushButton::mousePressEvent();直接跳到 QWidget::mousePressEvent() 会绕过按钮自身的处理。这里的“基类”是 C++ 继承关系;调用基类实现不等于把事件送给 parentWidget()。
如果完全替换某种输入行为,可以处理后返回;未处理的情况委托直接基类。不存在“所有重写都必须无条件调用基类”的规则,也不应在基类已经处理后无条件 ignore() 改掉其结果。普通 QWidget 的默认按压实现主要涉及弹出窗口的特殊处理,不能笼统说它必然负责焦点、光标、拖动等所有行为。Qt:QWidget 鼠标处理
原来的自定义 clicked() 在每次左键释放时发射,会遗漏“先前是否确实按下”“是否在有效区域释放”等条件。需要标准按钮语义时,优先复用 QAbstractButton 的状态机;按下深灰、释放浅灰可以由按钮的 :pressed 样式状态表达。
程序二:观察按钮,与消费按钮输入
跳转到“程序二:观察按钮,与消费按钮输入”此程序中的父窗口包含标签和按钮。过滤器放行时,按钮继续产生 clicked;消费鼠标按下后,按钮不再走完整的默认按压流程。它还验证在按钮区域外释放不会被误算为一次点击。
#include <QApplication>#include <QLabel>#include <QMouseEvent>#include <QPushButton>#include <QWidget>#include <QtTest/QTest>#include <cassert>
class Parent final : public QWidget {public: int presses = 0;protected: void mousePressEvent(QMouseEvent *event) override { if (event->button() == Qt::LeftButton) { ++presses; event->accept(); return; } QWidget::mousePressEvent(event); }};class Button final : public QPushButton {public: using QPushButton::QPushButton; int presses = 0;protected: void mousePressEvent(QMouseEvent *event) override { ++presses; QPushButton::mousePressEvent(event); }};class Observer final : public QObject {public: bool consume = false; int presses = 0;protected: bool eventFilter(QObject *, QEvent *event) override { if (event->type() == QEvent::MouseButtonPress) { ++presses; return consume; } return false; }};int main(int argc, char *argv[]) { QApplication app(argc, argv); Parent parent; parent.resize(320, 180); QLabel label("Click Me", &parent); label.setGeometry(20, 10, 160, 30); Button button("Button", &parent); button.setGeometry(20, 60, 160, 40); button.setStyleSheet("QPushButton { background: lightgray; }" "QPushButton:pressed { background: darkgray; }"); int clicks = 0; QObject::connect(&button, &QPushButton::clicked, &parent, [&] { ++clicks; }); parent.show(); app.processEvents(); QTest::mouseClick(&button, Qt::LeftButton); assert(clicks == 1 && button.presses == 1 && parent.presses == 0); Observer observer; button.installEventFilter(&observer); QTest::mouseClick(&button, Qt::LeftButton); assert(observer.presses == 1 && clicks == 2); observer.consume = true; QTest::mouseClick(&button, Qt::LeftButton); assert(observer.presses == 2 && clicks == 2 && button.presses == 2); observer.consume = false; QTest::mousePress(&button, Qt::LeftButton); QTest::mouseRelease(&button, Qt::LeftButton, Qt::NoModifier, QPoint(-5, -5)); assert(clicks == 2); QTest::mouseClick(&label, Qt::LeftButton); assert(parent.presses == 1);}如果父窗口只是想知道按钮被激活,连接 clicked 即可;如果要记录鼠标按下,过滤器观察后放行即可。这两种需求都不需要伪造一次父窗口鼠标事件。手动重发同一个事件还可能导致坐标不匹配或递归分发。
自定义事件:同步发送与队列投递
跳转到“自定义事件:同步发送与队列投递”sendEvent() 在当前调用中同步分发,不接管事件对象,常用栈上事件。postEvent() 把堆分配的事件交给队列;调用之后不要再访问或删除它。相同优先级的已投递事件保持投递顺序,但不是所有系统输入、定时器和绘制更新都共享一个可推断的全局顺序。Qt:sendEvent 与 postEvent
程序三:携带进度、跨线程投递与销毁
跳转到“程序三:携带进度、跨线程投递与销毁”工作线程只复制进度数值到事件中,接收者的 event() 在主线程执行。接收对象在生产线程结束前一直存活;这条生命周期保证比“postEvent 是线程安全函数”更重要。下面用 sendPostedEvents() 定向处理队列,使短测试可确定地退出,实际应用通常由正常事件循环处理。
#include <QCoreApplication>#include <QEvent>#include <QObject>#include <QThread>#include <QVector>#include <atomic>#include <cassert>#include <thread>
class ProgressEvent final : public QEvent {public: static std::atomic<int> destroyed; static QEvent::Type eventType() { static const int value = QEvent::registerEventType(); if (value < 0) qFatal("No event type available"); return static_cast<QEvent::Type>(value); } explicit ProgressEvent(int progress) : QEvent(eventType()), value(progress) {} ~ProgressEvent() override { ++destroyed; } int value;};std::atomic<int> ProgressEvent::destroyed{0};class Receiver final : public QObject {public: QVector<int> values;protected: bool event(QEvent *event) override { if (event->type() == ProgressEvent::eventType()) { assert(QThread::currentThread() == thread()); values.append(static_cast<ProgressEvent *>(event)->value); event->accept(); return true; } return QObject::event(event); }};int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Receiver receiver; { ProgressEvent immediate(5); QCoreApplication::sendEvent(&receiver, &immediate); assert((receiver.values == QVector<int>{5})); assert(ProgressEvent::destroyed.load() == 0); } assert(ProgressEvent::destroyed.load() == 1); std::thread producer([&receiver] { for (int value = 0; value <= 100; value += 10) QCoreApplication::postEvent(&receiver, new ProgressEvent(value)); }); producer.join(); // 接收者仍存活,生产者已停止继续投递 assert(receiver.values.size() == 1); QCoreApplication::sendPostedEvents(&receiver, ProgressEvent::eventType()); assert(receiver.values.size() == 12 && receiver.values.back() == 100); assert(ProgressEvent::destroyed.load() == 12); auto *temporary = new Receiver; QCoreApplication::postEvent(temporary, new ProgressEvent(88)); delete temporary; // Qt 清理发给已销毁接收者的待处理事件 assert(ProgressEvent::destroyed.load() == 13);}不能把这个最后的清理规则误解成“可以继续向已释放的裸指针投递”。关闭接收者前,应先停止生产者或采用明确的协调协议。跨线程通知通常使用带接收上下文的排队信号槽更方便;它仍需要正确处理参数中对象的寿命。参见 [[10-knowledge/computing/software-development/application-development/desktop/qt/concurrency/future-completion-and-request-lifetime|异步请求与对象寿命]]。
高频输入:合并显示更新并限制诊断历史
跳转到“高频输入:合并显示更新并限制诊断历史”鼠标移动处理里只更新最新位置,再用单次定时器合并一次显示更新,可以减少重复工作。16 ms 是一个调度目标,不保证 60 FPS;若定时器回调仍执行耗时数据库访问或计算,GUI 仍会卡顿。需要后台执行的部分应另行调度。绘制时使用 QPaintEvent::region() 限制绘制区域,更新请求优先用 update()。
事件监控可以记录时间、类型、对象名和类名,限制只关注特定事件,并把历史限制为固定数量。按键的完整文本不是判断分发顺序所必需的诊断信息。避免保存长期悬空的控件裸指针;若另有受监控对象集合,要在对象销毁时移除,或者使用合适的观察指针。
程序四:单次更新与 1000 条历史上限
跳转到“程序四:单次更新与 1000 条历史上限”此程序给自身安装过滤器,保存最近 1000 次移动的元数据;移动处理只保存最新点,定时器回调安排一次重绘。普通启动可以移动鼠标观察点,--check 模式在一次调度间隔内送入 1005 次移动,验证只保留最新点和有界历史。
#include <QApplication>#include <QDateTime>#include <QEventLoop>#include <QMouseEvent>#include <QPaintEvent>#include <QPainter>#include <QQueue>#include <QTimer>#include <QWidget>#include <cassert>
struct EventInfo { QDateTime time; QString objectName; QString className; QEvent::Type type;};class Tracker final : public QWidget {public: QTimer timer; QQueue<EventInfo> history; QPoint latest; int flushes = 0; Tracker() { setObjectName("tracker"); setMouseTracking(true); installEventFilter(this); timer.setSingleShot(true); timer.setInterval(16); QObject::connect(&timer, &QTimer::timeout, this, [this] { ++flushes; update(); }); }protected: bool eventFilter(QObject *watched, QEvent *event) override { if (watched == this && event->type() == QEvent::MouseMove) { history.enqueue({QDateTime::currentDateTimeUtc(), objectName(), metaObject()->className(), event->type()}); if (history.size() > 1000) history.dequeue(); } return false; } void mouseMoveEvent(QMouseEvent *event) override { latest = event->position().toPoint(); if (!timer.isActive()) timer.start(); event->accept(); } void paintEvent(QPaintEvent *event) override { QPainter painter(this); painter.setClipRegion(event->region()); painter.drawEllipse(latest, 4, 4); }};int main(int argc, char *argv[]) { QApplication app(argc, argv); Tracker tracker; tracker.resize(300, 200); tracker.show(); app.processEvents(); if (app.arguments().contains("--check")) { tracker.history.clear(); tracker.timer.stop(); tracker.flushes = 0; for (int i = 0; i < 1005; ++i) { const QPoint local(i % 100, i % 100); const QPoint global = tracker.mapToGlobal(local); QMouseEvent event(QEvent::MouseMove, QPointF(local), QPointF(global), Qt::NoButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(&tracker, &event); } assert(tracker.history.size() == 1000); assert(tracker.latest == QPoint(4, 4) && tracker.flushes == 0); QEventLoop wait; QObject::connect(&tracker.timer, &QTimer::timeout, &wait, &QEventLoop::quit); QTimer::singleShot(2000, &wait, &QEventLoop::quit); wait.exec(); assert(tracker.flushes == 1 && !tracker.timer.isActive()); return 0; } return app.exec();}全局过滤器会增加主线程所有事件的检查成本。只有确实需要跨多个目标观察时才把过滤器装到应用对象;普通控件使用目标过滤器,并只处理所需类型即可。原来的只准数字键过滤方案会误吞退格、光标移动、快捷键、粘贴和输入法;输入范围约束应优先使用 QValidator、QSpinBox 等面向内容的机制。悬停提示可直接设置 toolTip,特殊 F1、Tab 等按键则按需要单独处理。Qt:验证器、Qt:定时器精度
构建和验证方式
跳转到“构建和验证方式”上面四个程序分别保存在独立目录的 main.cpp,不要把四个 main 加入同一目标。统一可使用以下构建文件;程序一、三只需 Core,程序二额外使用 Qt Test 生成输入,程序四使用 Widgets。这里一并链接三者以方便逐个构建。
cmake_minimum_required(VERSION 3.20)project(EventExample LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Core Widgets Test)add_executable(event_example main.cpp)target_link_libraries(event_example PRIVATE Qt6::Core Qt6::Widgets Qt6::Test)使用 Debug 构建保留断言。前三个程序完成检查后退出;第四个传入 --check 执行检查。它们验证分发、过滤、队列所有权和界面逻辑,不能替代实际平台上的键盘焦点、窗口管理器与输入设备测试。