序章:读完你能掌握
Qt 的事件系统是每个 Qt 开发者绕不开的核心机制,但很多人在写业务代码时只是"能跑就行",一旦遇到界面卡顿、事件不响应、快捷键失效、控件吞掉事件等问题,就无从下手。本文从底层机制讲到实战代码,读完你将掌握以下 7 件事:
- 事件是什么 :理解
QEvent的本质,以及事件与信号槽的区别。 - 事件循环如何运转 :搞清楚
QApplication::exec()背后发生了什么,事件从哪里来、到哪里去。 - 一次点击的完整旅程 :从操作系统消息到
mousePressEvent被调用的全过程。 - 13 种常用事件类型:鼠标、键盘、滚轮、绘制、尺寸、关闭等事件速查。
- 重写事件处理函数 :掌握
paintEvent、mousePressEvent、keyPressEvent等正确写法,并完成一个画板 Demo。 - 事件过滤器 :用
installEventFilter实现局部监听和全局快捷键。 - 自定义事件与 10 个实战坑 :掌握
postEvent与sendEvent的差异,避开最常见的踩坑点。
本文所有代码基于 Qt 5 / Qt 6 通用写法,示例可直接编译运行。
一、事件系统全景
1.1 事件是什么
在 Qt 中,事件(Event)是应用程序内部或外部发生的事情的抽象表示 。它由 QEvent 类及其子类描述,例如鼠标按下是 QMouseEvent,键盘按下是 QKeyEvent,窗口重绘是 QPaintEvent。
事件对象通常包含三部分信息:
- 事件类型 :通过
QEvent::Type枚举标识,如QEvent::MouseButtonPress。 - 事件参数:如鼠标坐标、按键码、修饰键状态等。
- 接收者 :事件最终被派发给哪个
QObject。
需要强调的是,事件不是信号。事件是"消息",信号是"通知"。事件由 Qt 的事件循环派发,信号由对象在状态变化时主动发出。
1.2 事件循环
每个 Qt GUI 程序都从 QApplication::exec() 开始进入事件循环。它的核心逻辑可以简化为一个无限循环:
cpp
while (true) {
// 1. 从系统事件队列取出一个事件
QEvent *event = takeNextEvent();
if (!event) continue;
// 2. 找到目标接收者
QObject *receiver = event->receiver();
// 3. 派发事件
QCoreApplication::sendEvent(receiver, event);
// 4. 处理完毕,销毁事件对象
delete event;
}
实际实现要复杂得多,涉及平台事件队列、定时器、socket 通知、posted 事件队列等,但理解这个骨架就足够应对绝大多数问题。
事件循环的关键点:
- 单线程:GUI 事件循环运行在主线程,任何耗时操作都会阻塞事件派发,导致界面"假死"。
- posted 事件队列 :
postEvent投递的事件会进入队列,由事件循环在下一轮取出处理。 - sendEvent 是同步的 :它不经过队列,直接调用接收者的
event()函数。
1.3 一次点击的完整旅程
以用户点击窗口中的一个按钮为例,事件大致经历以下路径:
这条链路中有几个关键节点值得记住:
- QPA(Qt Platform Abstraction):负责把 Windows、X11、Wayland、macOS 的原生消息翻译成 Qt 事件。
- QApplication::notify:事件派发的总入口,也是事件过滤器生效的地方。
- QWidget::event :所有事件的统一入口,内部根据类型分发给具体的
xxxEvent函数。 - 信号发射:控件在事件处理完成后,才把"用户操作"转换成信号。
1.4 事件 vs 信号
这是初学者最容易混淆的一对概念,下面用表格对比:
| 对比项 | 事件(Event) | 信号(Signal) |
|---|---|---|
| 本质 | 消息对象,描述发生了什么 | 通知机制,描述状态变化 |
| 来源 | 系统、Qt 内部、用户代码 | 对象自身主动发出 |
| 传递方式 | 事件循环派发,或 sendEvent 同步调用 | 信号槽连接,可同步或异步 |
| 处理入口 | event() / xxxEvent() |
槽函数 / lambda |
| 典型用途 | 输入、绘制、窗口管理 | 业务逻辑解耦、组件通信 |
| 能否拦截 | 可以,通过事件过滤器或重写 event | 不能拦截,只能断开连接 |
一句话总结:事件是"输入",信号是"输出"。控件先把事件消化掉,再决定是否发出信号。
二、事件类型速查
Qt 定义了上百种事件类型,日常开发中高频使用的集中在以下 13 种:
| 事件类 | QEvent::Type | 触发时机 | 常用处理函数 |
|---|---|---|---|
QMouseEvent |
MouseButtonPress | 鼠标按下 | mousePressEvent |
QMouseEvent |
MouseButtonRelease | 鼠标释放 | mouseReleaseEvent |
QMouseEvent |
MouseMove | 鼠标移动 | mouseMoveEvent |
QMouseEvent |
MouseButtonDblClick | 鼠标双击 | mouseDoubleClickEvent |
QKeyEvent |
KeyPress | 键盘按下 | keyPressEvent |
QKeyEvent |
KeyRelease | 键盘释放 | keyReleaseEvent |
QWheelEvent |
Wheel | 滚轮滚动 | wheelEvent |
QPaintEvent |
Paint | 需要重绘 | paintEvent |
QResizeEvent |
Resize | 控件尺寸变化 | resizeEvent |
QMoveEvent |
Move | 控件位置变化 | moveEvent |
QCloseEvent |
Close | 窗口关闭 | closeEvent |
QFocusEvent |
FocusIn / FocusOut | 获得/失去焦点 | focusInEvent / focusOutEvent |
QTimerEvent |
Timer | 定时器超时 | timerEvent |
补充说明:
QMouseEvent的移动事件默认只在鼠标按下时触发,若需悬停追踪,要调用setMouseTracking(true)。QPaintEvent可能被合并,不要依赖它被调用的次数,绘制逻辑必须幂等。QCloseEvent中调用ignore()可以阻止窗口关闭,常用于"是否保存"提示。
三、重写事件处理函数
3.1 基本写法
重写事件处理函数是自定义控件最直接的方式。基本套路是:先处理自己的逻辑,必要时调用基类实现。
cpp
class MyWidget : public QWidget {
protected:
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
qDebug() << "left click at" << event->pos();
}
// 不调用基类,事件被"吞掉",不再向上传递
// QWidget::mousePressEvent(event);
}
};
是否调用基类实现,取决于你是否希望事件继续按默认逻辑处理。对于自定义控件,通常需要自己完全接管。
3.2 鼠标事件
cpp
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
m_lastPoint = event->pos();
}
}
void mouseMoveEvent(QMouseEvent *event) override {
if (event->buttons() & Qt::LeftButton) {
QPoint delta = event->pos() - m_lastPoint;
m_lastPoint = event->pos();
// 处理拖拽
}
}
void mouseReleaseEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
// 结束拖拽
}
}
注意 event->button() 表示"触发本次事件的按键",event->buttons() 表示"当前所有按下的按键",两者不要混用。
3.3 键盘事件
cpp
void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Escape) {
close();
return;
}
if (event->modifiers() & Qt::ControlModifier && event->key() == Qt::Key_S) {
save();
return;
}
QWidget::keyPressEvent(event);
}
键盘事件要求控件有焦点,必要时调用 setFocusPolicy(Qt::StrongFocus)。
3.4 滚轮事件
cpp
void wheelEvent(QWheelEvent *event) override {
int delta = event->angleDelta().y();
if (delta > 0) {
zoomIn();
} else if (delta < 0) {
zoomOut();
}
event->accept();
}
Qt 5.15 之后推荐使用 angleDelta(),delta() 已废弃。
3.5 paintEvent
cpp
void paintEvent(QPaintEvent *event) override {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), Qt::white);
for (const auto &line : m_lines) {
painter.drawLine(line);
}
}
绘制逻辑必须只依赖当前状态,不要在里面做耗时计算或修改数据。
3.6 resizeEvent 与 closeEvent
cpp
void resizeEvent(QResizeEvent *event) override {
QWidget::resizeEvent(event);
// 根据新尺寸调整内部布局或缓存
}
void closeEvent(QCloseEvent *event) override {
if (maybeSave()) {
event->accept();
} else {
event->ignore();
}
}
3.7 画板 Demo
下面把鼠标、绘制、尺寸事件组合成一个简易画板:
cpp
class PaintBoard : public QWidget {
public:
PaintBoard(QWidget *parent = nullptr) : QWidget(parent) {
setAttribute(Qt::WA_StaticContents);
setMouseTracking(true);
}
protected:
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
m_lastPoint = event->pos();
m_drawing = true;
}
}
void mouseMoveEvent(QMouseEvent *event) override {
if (m_drawing && (event->buttons() & Qt::LeftButton)) {
m_lines.append(QLine(m_lastPoint, event->pos()));
m_lastPoint = event->pos();
update();
}
}
void mouseReleaseEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
m_drawing = false;
}
}
void paintEvent(QPaintEvent *) override {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), Qt::white);
painter.setPen(QPen(Qt::black, 2));
for (const QLine &line : m_lines) {
painter.drawLine(line);
}
}
void resizeEvent(QResizeEvent *event) override {
QWidget::resizeEvent(event);
update();
}
private:
QList<QLine> m_lines;
QPoint m_lastPoint;
bool m_drawing = false;
};
这个 Demo 覆盖了鼠标按下、移动、释放、绘制和尺寸变化五类事件,是理解事件重写的最佳练手项目。
四、事件过滤器
4.1 原理
事件过滤器允许一个对象监视另一个对象的事件。被监视对象的所有事件会先经过监视者的 eventFilter,再决定是否继续派发。
它的调用时机在 QApplication::notify 之后、目标对象的 event() 之前,因此可以做到"先于目标对象处理事件"。
4.2 三步用法
- 在监视者类中重写
eventFilter(QObject *watched, QEvent *event)。 - 调用
watchedObject->installEventFilter(this)安装过滤器。 - 在
eventFilter中返回true表示拦截,返回false表示放行。
cpp
bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
if (watched == ui->lineEdit && event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Return) {
onSearch();
return true; // 拦截回车,不再传给 lineEdit
}
}
return QMainWindow::eventFilter(watched, event);
}
4.3 局部监听
只监听某个控件的某类事件,是最常见的用法。例如限制输入框只能输入数字:
cpp
bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
if (watched == ui->numberEdit && event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() < Qt::Key_0 || keyEvent->key() > Qt::Key_9) {
return true;
}
}
return QMainWindow::eventFilter(watched, event);
}
4.4 全局快捷键
在 QApplication 上安装事件过滤器,可以捕获整个应用范围内的事件,实现全局快捷键:
cpp
class GlobalShortcutFilter : public QObject {
protected:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->modifiers() == Qt::ControlModifier
&& keyEvent->key() == Qt::Key_K) {
emit quickSearchRequested();
return true;
}
}
return QObject::eventFilter(watched, event);
}
signals:
void quickSearchRequested();
};
// 安装
auto *filter = new GlobalShortcutFilter(qApp);
qApp->installEventFilter(filter);
注意:全局过滤器会影响所有控件,务必谨慎返回 true,否则可能吞掉正常输入。
五、自定义事件
5.1 自定义事件类
当内置事件类型不够用时,可以定义自己的事件。推荐使用 QEvent::User 之后的类型值:
cpp
class DataReadyEvent : public QEvent {
public:
static const QEvent::Type DataReadyType =
static_cast<QEvent::Type>(QEvent::User + 1);
explicit DataReadyEvent(const QByteArray &data)
: QEvent(DataReadyType), m_data(data) {}
QByteArray data() const { return m_data; }
private:
QByteArray m_data;
};
5.2 postEvent vs sendEvent
| 对比项 | postEvent | sendEvent |
|---|---|---|
| 是否经过事件队列 | 是 | 否 |
| 调用方式 | 异步,立即返回 | 同步,处理完才返回 |
| 事件对象所有权 | Qt 接管,处理完自动 delete | 调用方负责释放 |
| 适用场景 | 跨线程、延迟处理 | 立即处理、需要返回值 |
cpp
// 异步投递,事件对象由 Qt 管理
QCoreApplication::postEvent(receiver, new DataReadyEvent(data));
// 同步发送,事件对象由调用方管理
DataReadyEvent event(data);
QCoreApplication::sendEvent(receiver, &event);
跨线程通信时,postEvent 是安全的选择,因为事件会被投递到目标线程的事件队列。
5.3 event() 接收
自定义事件需要在目标对象的 event() 中接收,因为 xxxEvent 系列函数只覆盖内置类型:
cpp
bool MyWidget::event(QEvent *event) {
if (event->type() == DataReadyEvent::DataReadyType) {
auto *dataEvent = static_cast<DataReadyEvent *>(event);
handleData(dataEvent->data());
return true;
}
return QWidget::event(event);
}
如果忘记调用基类 event(),会导致所有内置事件失效,这是非常常见的错误。
六、10 个坑
坑 1:在事件处理中做耗时操作
事件处理函数运行在 GUI 线程,任何阻塞都会让界面卡死。耗时任务应放到工作线程,通过信号或 postEvent 回到主线程更新 UI。
坑 2:忘记调用基类实现
重写 event() 或 xxxEvent() 后不调用基类,可能导致控件默认行为丢失,例如输入框无法输入、按钮无法点击。
坑 3:mouseMoveEvent 不触发
默认情况下,只有鼠标按下时才会收到移动事件。需要悬停追踪时,必须调用 setMouseTracking(true)。
坑 4:事件被父控件吞掉
子控件未处理的事件会向上传递给父控件。如果父控件重写了 event() 并返回 true,子控件可能收不到事件。排查时优先检查父级的事件过滤器。
坑 5:事件过滤器返回 true 过度
在全局过滤器中对所有 KeyPress 返回 true,会导致整个应用无法输入。过滤器应尽量精确匹配 watched 和事件类型。
坑 6:postEvent 后手动 delete
postEvent 投递的事件对象由 Qt 接管,处理完后自动释放。手动 delete 会造成重复释放崩溃。
坑 7:sendEvent 使用栈对象却让 Qt 释放
sendEvent 不会接管事件对象所有权,使用栈对象是安全的;但如果误用 postEvent 传栈对象地址,会导致悬空指针。
坑 8:paintEvent 中触发重绘
在 paintEvent 中调用 update() 或修改会触发重绘的数据,会造成无限重绘、CPU 占满。绘制函数必须只读。
坑 9:跨线程直接操作 UI
Qt 的控件不是线程安全的。工作线程不能直接调用 setText 等方法,必须通过信号槽(自动排队)或 postEvent 回到主线程。
坑 10:closeEvent 中忘记 ignore
用户点击关闭时,如果需要在保存失败后阻止关闭,必须调用 event->ignore()。默认行为是接受关闭,窗口会直接消失。
总结
Qt 事件系统的核心可以浓缩为一条链路:系统消息 → QPA → 事件队列 → QApplication::notify → 事件过滤器 → QWidget::event → xxxEvent → 信号。理解这条链路,再配合事件过滤器与自定义事件,就能覆盖绝大多数交互需求。最后记住那 10 个坑,能帮你省下大量调试时间。