跳转到内容
新建笔记

Qt 事件:分发、过滤、默认行为与队列寿命

Qt 把输入、绘制、尺寸变化、定时器等通知表示为 QEvent 或其派生对象,再送给某个 QObject。接收者不一定是控件,也不是所有事件都会沿父对象链传播。按钮的 clicked 等信号通常是控件处理输入后发出的较高层通知;信号槽与事件处理各有职责。

类型、目标与分发路径

跳转到“类型、目标与分发路径”
常见事件相关事件对象常见处理入口
鼠标按下、释放、移动QMouseEventmousePressEvent、mouseReleaseEvent、mouseMoveEvent
键盘按下、释放QKeyEventkeyPressEvent、keyReleaseEvent;特殊 Tab 行为也可能需要 event()
滚轮、触摸QWheelEvent、QTouchEvent依具体控件与事件类型处理
绘制、尺寸变化QPaintEvent、QResizeEventpaintEvent、resizeEvent
关闭、显示、隐藏QCloseEvent 等closeEvent、showEvent、hideEvent
定时器QTimerEventtimerEvent;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 执行检查。它们验证分发、过滤、队列所有权和界面逻辑,不能替代实际平台上的键盘焦点、窗口管理器与输入设备测试。