3. Qt事件系统:QEvent类层次结构、事件过滤器、事件循环机制
事件系统,说白了就是Qt的"神经中枢"。你点一下屏幕,按一下按键,或者窗口需要重绘——所有这些动作最终都会变成一个个事件对象,在对象之间流转、分发、处理。
我刚开始接触Qt时,总觉得事件系统很玄乎。后来调试触摸屏驱动,发现很多诡异的问题——比如点击没反应、滑动卡顿、按键重复触发——追根溯源,全是事件处理环节出了岔子。从那以后,我花了不少功夫把事件系统彻底吃透了。
这一章,咱们就把它掰开揉碎,看看里面到底是怎么运作的。
3.1 QEvent类层次结构
Qt里所有事件都继承自 QEvent。它就像一棵树,根节点是 QEvent,下面分了很多子类。
核心概念:每个事件都有一个唯一的类型编号(Type),用 QEvent::Type 枚举表示。你可以在事件处理函数里通过 type() 方法拿到它,然后决定怎么处理。
我习惯把常用的事件类型分成几类:
| 类别 | 典型事件 | 说明 |
|---|---|---|
| 输入事件 | QMouseEvent, QKeyEvent, QTouchEvent | 鼠标、键盘、触摸屏 |
| 窗口事件 | QResizeEvent, QMoveEvent, QCloseEvent | 窗口状态变化 |
| 绘制事件 | QPaintEvent | 需要重绘时触发 |
| 定时器事件 | QTimerEvent | 定时器到期 |
| 自定义事件 | QEvent + 自定义Type | 你自个儿定义的事件 |
举个例子,你写一个按钮,用户点了一下——Qt会生成一个 QMouseEvent,类型是 QEvent::MouseButtonPress。这个事件会沿着对象树一路传递,直到被某个对象的 mousePressEvent() 接住。
// 看看事件类型
void MyWidget::mousePressEvent(QMouseEvent *event) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "鼠标按下,位置:" << event->pos();
}
}
嗯,这里要注意:QEvent 对象默认是栈分配的,别在事件处理函数里 delete 它。Qt的事件循环会自动管理它的生命周期。
3.2 事件过滤器
事件过滤器,说白了就是一个"拦截器"。你可以在某个对象上安装一个过滤器,所有发给这个对象的事件,都会先经过过滤器过一遍。
为什么要用事件过滤器?我遇到过这样一个场景:一个复杂的触摸屏界面,上面叠了很多层控件。我想在某个父容器里统一拦截所有触摸事件,做手势识别。如果每个子控件都去重写 touchEvent(),那代码就太散了。
事件过滤器就是干这个的。
使用步骤:
- 在目标对象上调用
installEventFilter()安装过滤器。 - 在过滤器对象的
eventFilter()里处理事件。 - 返回
true表示事件被拦截,不再往下传;返回false则继续传递。
// 在父容器里安装事件过滤器
void ParentWidget::setup() {
childButton->installEventFilter(this);
}
// 重写 eventFilter
bool ParentWidget::eventFilter(QObject *watched, QEvent *event) {
if (watched == childButton) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "按钮被点击了,但被我拦截了!";
// 做点别的事,比如播放音效
return true; // 事件不再往下传
}
}
return QWidget::eventFilter(watched, event);
}
我曾经踩过一个坑:在事件过滤器里修改了事件对象,然后返回 true,结果子控件收不到事件了。后来才明白,true 就是"我处理完了,你别管了"。如果你只是想"看看"事件,不改它,记得返回 false。
避坑指南:事件过滤器是同步调用的。如果你在过滤器里做耗时操作(比如读写文件、网络请求),整个事件循环会被卡住。触摸屏的响应就会变得"粘滞"——点一下,等半天才有反应。我曾经在嵌入式设备上吃过这个亏,后来把耗时操作扔到 QTimer::singleShot 里才解决。
3.3 事件循环机制
事件循环,就是Qt程序的"心脏"。它一直在跳,不停地从事件队列里取事件,分发给对应的对象。
你想想看,一个Qt程序启动后,app.exec() 一调用,就进入了事件循环。它大致干这几件事:
- 从操作系统的事件队列里取事件(鼠标、键盘、窗口等)。
- 把系统事件转换成
QEvent对象。 - 把事件压入Qt内部的事件队列。
- 从队列里取出事件,调用
QObject::event()分发。 event()根据事件类型,调用对应的处理函数(比如mousePressEvent())。- 处理完一个,再取下一个——循环往复。
我画了一张图,帮你理清这个流程:
你看,事件过滤器在 event() 之前执行。这意味着你可以"先下手为强"——在事件到达目标对象之前就把它截住。
关键点:事件循环是单线程的。这意味着如果你在某个事件处理函数里写了一个死循环,或者一个耗时的同步操作,整个界面就会"卡死"。触摸屏上的滑动、点击,全都没反应。
我调试过一个触摸屏设备,用户反映"点着点着就卡住了"。后来发现是一个 mouseReleaseEvent 里做了数据库写入,写入时网络超时,阻塞了十几秒。解决方案很简单:把耗时操作扔到 QThread 或者 QtConcurrent::run 里。
3.4 自定义事件
有时候,Qt自带的事件不够用。比如你想在某个业务逻辑完成后,通知界面更新。这时候就可以自定义事件。
步骤很简单:
- 继承
QEvent,定义一个唯一的 Type(用QEvent::registerEventType()获取)。 - 用
QCoreApplication::postEvent()或者sendEvent()发送事件。 - 在目标对象的
customEvent()里处理。
// 自定义事件类
class MyCustomEvent : public QEvent {
public:
static const QEvent::Type MyEventType =
static_cast<QEvent::Type>(QEvent::registerEventType());
MyCustomEvent(const QString &data)
: QEvent(MyEventType), m_data(data) {}
QString data() const { return m_data; }
private:
QString m_data;
};
// 发送事件
QCoreApplication::postEvent(receiver, new MyCustomEvent("hello"));
// 接收事件
void MyReceiver::customEvent(QEvent *event) {
if (event->type() == MyCustomEvent::MyEventType) {
MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);
qDebug() << "收到自定义事件:" << myEvent->data();
}
}
小技巧:postEvent() 是异步的,事件会进入队列,函数立即返回。sendEvent() 是同步的,会等事件处理完才返回。我一般用 postEvent(),避免阻塞调用方。
3.5 事件传播与接受
事件在Qt里是怎么"跑"的?我拿鼠标事件举个例子:
- 事件先发给
QApplication::notify()。 notify()把事件发给目标窗口的event()。event()先检查有没有事件过滤器,有的话先过一遍。- 然后根据事件类型,调用对应的处理函数(比如
mousePressEvent())。 - 如果处理函数调用了
event->accept(),事件就停止传播。 - 如果调用了
event->ignore(),事件会向上传递给父控件。
嗯,这里有个坑:默认情况下,QWidget::mousePressEvent() 会调用 event->ignore()。如果你在子控件里重写了 mousePressEvent() 但忘了调用 accept(),事件就会"冒泡"到父控件。我见过一个案例:按钮点击后,父窗口也跟着响应了——就是因为事件没被接受。
避坑指南:在触摸屏开发中,QTouchEvent 的传播机制和鼠标事件不太一样。触摸事件默认只发给被触摸的控件,不会自动向上传播。如果你需要父控件拦截触摸事件,得用事件过滤器,或者重写 event() 手动处理。
好了,事件系统的核心就这些。理解了这个机制,后面调试触摸屏驱动时,你就能快速定位问题——是事件没发出来?还是发出来了但被拦截了?还是处理函数里卡住了?心里有数,下手才准。