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(),那代码就太散了。

事件过滤器就是干这个的。

使用步骤:

  1. 在目标对象上调用 installEventFilter() 安装过滤器。
  2. 在过滤器对象的 eventFilter() 里处理事件。
  3. 返回 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() 一调用,就进入了事件循环。它大致干这几件事:

  1. 从操作系统的事件队列里取事件(鼠标、键盘、窗口等)。
  2. 把系统事件转换成 QEvent 对象。
  3. 把事件压入Qt内部的事件队列。
  4. 从队列里取出事件,调用 QObject::event() 分发。
  5. event() 根据事件类型,调用对应的处理函数(比如 mousePressEvent())。
  6. 处理完一个,再取下一个——循环往复。

我画了一张图,帮你理清这个流程:

Qt 事件循环核心流程 系统事件队列 QEvent 转换 Qt 事件队列 QObject::event() 事件过滤器 mousePressEvent() 等 继续取下一个事件 说明: 事件过滤器在 QObject::event() 之前执行。如果过滤器返回 true,event() 就不会被调用。 整个循环是单线程的——一个事件处理完,才会处理下一个。

你看,事件过滤器在 event() 之前执行。这意味着你可以"先下手为强"——在事件到达目标对象之前就把它截住。

关键点:事件循环是单线程的。这意味着如果你在某个事件处理函数里写了一个死循环,或者一个耗时的同步操作,整个界面就会"卡死"。触摸屏上的滑动、点击,全都没反应。

我调试过一个触摸屏设备,用户反映"点着点着就卡住了"。后来发现是一个 mouseReleaseEvent 里做了数据库写入,写入时网络超时,阻塞了十几秒。解决方案很简单:把耗时操作扔到 QThread 或者 QtConcurrent::run 里。

3.4 自定义事件

有时候,Qt自带的事件不够用。比如你想在某个业务逻辑完成后,通知界面更新。这时候就可以自定义事件。

步骤很简单:

  1. 继承 QEvent,定义一个唯一的 Type(用 QEvent::registerEventType() 获取)。
  2. 用 QCoreApplication::postEvent() 或者 sendEvent() 发送事件。
  3. 在目标对象的 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() 手动处理。

好了,事件系统的核心就这些。理解了这个机制,后面调试触摸屏驱动时,你就能快速定位问题——是事件没发出来?还是发出来了但被拦截了?还是处理函数里卡住了?心里有数,下手才准。