012-Qt 的事件系统

auther: abinng date: 2026-03-31 14:49 createDate:2026-03-31 14:49

由于我已经开始实习,所以本篇往后我可能并不会详细进行笔记记录,因为太累了

操作系统本身就有事件系统,但是Qt是跨平台的框架,所以Qt在操作系统上层又封装了一层QEvent,一套代码在不同操作系统上运行

可参考Qt 教程 | 爱编程的大丙Qt应用开发.pdf


复习路线

这篇笔记要回答的问题是:Qt 收到一个外部动作,或者代码主动投递一个事件之后,是怎样把它变成某个对象上的函数调用的?

先记住这条主线:

1
2
3
OS/Qt 产生事件 -> 事件循环 exec() 取出事件 -> notify() 准备分发
-> eventFilter() 可提前拦截 -> event() 按类型分发
-> mousePressEvent/keyPressEvent/... 处理 -> 必要时发射 signal

下次忘记时,可以按这个顺序复习:

  1. 先看“事件是什么”和 exec(),建立事件循环的概念。
  2. 再看 event() 和责任链,理解事件怎么从通用 QEvent 分发到具体 handler。
  3. InnerLabel 示例,确认为什么调用基类 event() 后还能回到子类的 mousePressEvent()
  4. accept/ignore,理解输入事件什么时候被当前控件消费,什么时候继续交给父 widget。
  5. 看事件过滤器,理解怎么在对象收到事件之前拦截。
  6. 看“完整流程复盘”,把前面的点串成一次鼠标点击的生命周期。
  7. sendEvent() / postEvent() 和自定义事件,理解代码主动发送事件的方式。
  8. 看事件循环、线程和 deleteLater(),理解事件系统和对象生命周期的关系。
  9. 最后看“信号与事件”,把事件系统和业务层连接起来。

1. 事件是什么

Qt 是一个基于 C++ 的跨平台框架,GUI 程序的大部分交互都围绕事件展开。用户操作、窗口状态变化、定时器触发等都会变成事件,再由 Qt 分发给对应的对象处理。窗口事件产生之后,大致会经过:事件派发 -> 事件过滤 -> 事件分发 -> 事件处理 几个阶段。Qt 对很多事件都有默认处理,如果有特殊需求,就在合适的位置重写或拦截对应的处理逻辑。

事件(event)是由系统或者 Qt 本身在不同的场景下发出的。当用户按下/移动鼠标、敲下键盘,或者是窗口关闭/大小发生变化/隐藏或显示都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如鼠标/键盘事件等;另一些事件则是由系统自动发出,如计时器事件。

事件在Qt中产生之后的分发过程是这样的:

当事件产生之后,Qt使用用应用程序对象调用notify()函数将事件发送到指定的窗口:

1
2
3
4
5
[override virtual] bool QApplication::notify(QObject *receiver, QEvent *e);
// 事件在发送过程中可以通过事件过滤器进行过滤,默认不对任何产生的事件进行过滤。

// 需要先给窗口安装过滤器, 该事件才会触发
[virtual] bool QObject::eventFilter(QObject *watched, QEvent *event)

当事件发送到指定窗口之后,窗口的事件分发器会对收到的事件进行分类:

1
[override virtual protected] bool QWidget::event(QEvent *event);

事件分发器会将分类之后的事件(鼠标事件、键盘事件、绘图事件……)分发给对应的事件处理器函数进行处理,每个事件处理器函数都有默认的处理动作(我们也可以重写这些事件处理器函数),比如:鼠标事件:

1
2
3
4
5
6
// 鼠标按下
[virtual protected] void QWidget::mousePressEvent(QMouseEvent *event);
// 鼠标释放
[virtual protected] void QWidget::mouseReleaseEvent(QMouseEvent *event);
// 鼠标移动
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent *event);

2. 事件循环:exec

他会使Qt应用程序进入事件循环,这样就可以使应用程序在运行时接受各种事件。一旦有事件发生,Qt便会构建一个相应的QEvent子类对象来表示它,然后将它传递给相应的QObject对象或其子对象。

3. 分发入口:event()

event() 函数在 Qt 中的角色是一个统一的分发入口。它接收通用的 QEvent,判断事件类型(Type),然后把它转换为具体的事件类,并调用对应的事件处理函数(Event Handler)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool QWidget::event(QEvent *event) {
// 根据事件类型进行路由分发
switch (event->type()) {
case QEvent::MouseButtonPress: {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
this->mousePressEvent(mouseEvent); // <--- 核心就在这一句!
return true;
}
case QEvent::KeyPress: {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
this->keyPressEvent(keyEvent);
return true;
}
// ... 其他成百上千种事件的分发 ...
}
return QObject::event(event);
}

event() 的返回值不是“有没有执行完”,而是“当前对象是否处理了这个事件”。通常返回 true 表示这个事件已经被识别并处理;返回 false 表示当前对象不处理这个事件,调用者可以按 Qt 的规则继续走后续流程。

3.1 基类 event()

这一节主要解释:子类处理不了的事件,为什么要继续交给基类的 event()

可以看到 QWidget::event 中最后还调用了 QObject::event ,这是 Qt 事件处理机制的责任链模式,其理念在于 “DRY (Don’t Repeat Yourself - 不要重复你自己)” 也就是说并不是每一个Qt封装的组件类都包含了对每一个事件的处理,而是专注自己特有的逻辑

例如:既然“识别鼠标按下并调用 mousePressEvent”这个逻辑对所有的 UI 控件(Label, Button, Slider, ComboBox…)都是一样的,那就只在 QWidget::event 里写一次就够了。所有的子类只需要专注自己特有的逻辑,处理不了的就 return BaseClass::event(ev),让基类继续处理。这就形成了一套事件处理责任链。

假设调用了 QLabel::event ,调用链大致如下:

第一层:QLabel::event QLabelevent 函数只关心它自己特有的事件。

1
2
3
4
5
6
7
8
bool QLabel::event(QEvent *e) {
// 1. 处理富文本链接的悬停、点击
// 2. 处理 ToolTip(鼠标提示)
// 3. 处理快捷键覆盖

// 如果不是上面这些专属事件,或者处理完了还需要默认行为:
return QFrame::event(e); // 交给基类 QFrame 继续处理
}

第二层:QFrame::event QFrame 只关心边框绘制相关的特殊逻辑。

1
2
3
4
5
bool QFrame::event(QEvent *e) {
// 处理边框样式改变等逻辑...

return QWidget::event(e); // 继续踢给父类 QWidget
}

第三层:QWidget::event QWidget 是所有可视化控件的基类,它承担了几乎所有 UI 交互事件的基础分发工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool QWidget::event(QEvent *event) {
// 大量 switch-case 分支
switch (event->type()) {
case QEvent::MouseButtonPress: {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
this->mousePressEvent(mouseEvent); // 关键点:在这里进行了分发
return true;
}
case QEvent::KeyPress:
this->keyPressEvent((QKeyEvent*)event);
return true;
// ... 键盘、鼠标移动、滚轮、绘制(Paint) 等几十上百个分支 ...
}

// 如果连 QWidget 都不认识这个事件:
return QObject::event(event); // 最后交给 QObject
}

第四层:QObject::event 如果事件一路漏到了这里,QObject 只处理最底层的核心机制,比如定时器事件(Timer)、动态属性改变(DynamicPropertyChange)等。

关于Qt窗口提供的其他事件处理器函数还有很多,感兴趣的话可以仔细阅读Qt的帮助文档,窗口的事件处理器函数非常好找,规律是这样的:

  • 受保护的虚函数
  • 函数名分为两部分: 事件描述+Event
  • 函数带一个事件类型的参数

4. 子类 handler 示例

这一节用 InnerLabel 说明:调用基类 event() 后,为什么最终仍然会进入子类重写的 mousePressEvent()

InnerLabel

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef INNER_LABEL_H
#define INNER_LABEL_H
#include <QLabel>

class InnerLabel : public QLabel
{
Q_OBJECT
public:
InnerLabel(QWidget *parent = nullptr);
~InnerLabel() override = default;
protected:
bool event(QEvent *event) override;
void mousePressEvent(QMouseEvent *ev) override;

};

#endif // INNER_LABEL_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "inner_label.h"
#include <QDebug>
#include <QEvent>

InnerLabel::InnerLabel(QWidget *parent) : QLabel(parent)
{
setFrameStyle(QFrame::Panel | QFrame::Plain);
setLineWidth(3);
setMidLineWidth(1);

}

bool InnerLabel::event(QEvent *ev)
{
// qDebug() << ev->type();
return QLabel::event(ev);
}

void InnerLabel::mousePressEvent(QMouseEvent *ev)
{
qDebug() << "InnerLabel mousePressEvvent";
}

在 main.cpp 中创建该对象

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <QApplication>

#include "inner_label.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

QWidget* w = new QWidget();

w->setGeometry(100, 100, 500, 500);
InnerLabel* in_label = new InnerLabel(w);
in_label->resize(150, 150);
in_label->move(150, 150);
w->show();

return a.exec();
}

运行后,会发现有一个黑色的框框,点击其内部会有输出,该输出是 InnerLabel::mousePressEvent 中的语句

程序的逻辑是,进入 a.exec() 这个循环后开始看有什么事件产生,将事件加入队列,不断取出队列中的事件到 event 函数中进行分发

我们重写了 InnerLabel::event ,在启动直接一句 return QLabel::event(ev) ,相当于是交付于 InnerLabel 的基类 QLabelevent 进行分发。但是为什么又会调用 QLabel 的子类 InnerLabelmousePressEvent 呢?

InnerLabel::event 中有隐藏参数 this 指针,这个都知道吧。 然后调用 QLabel::event ,其中也有隐藏参数 this 指针,其类型虽然是 QLabel *,但实际还是直接将 InnerLabel::event 的 this 指针给传过去了,所以还是指向 InnerLabel 的实例对象的。

现在回到鼠标点击发生时的调用过程:

  1. 鼠标点击,进入 InnerLabel::event

  2. 你调用 QLabel::eventthis 指针原封不动地传进去了。

  3. QLabel::event 发现这不是 QLabel 特有的事件处理逻辑(比如富文本链接),于是调用 QFrame::event

  4. QFrame::event 发现这不是 QFrame 特有的处理逻辑,于是调用 QWidget::event

  5. QWidget::event 发现 typeQEvent::MouseButtonPress,进入鼠标按下事件的分支。

  6. QWidget::event 执行了 this->mousePressEvent(ev)

  7. 这一整条调用链里,this 指针的值(你的 InnerLabel 实例地址)始终没有变过

  8. C++ 的虚函数机制生效,最终调用的是实际对象类型里的 InnerLabel::mousePressEvent

就是这么一个过程

5. acceptignore

这一节主要看输入事件什么时候被当前控件消费,什么时候继续交给父 widget。

QEvent 是一个事件类,accept()ignore() 本质上是在修改事件对象内部的状态。

1
2
3
4
5
6
7
8
9
class QEvent {
private:
bool m_accepted; // <--- 就是这个小小的 bool 值

public:
void accept() { m_accepted = true; }
void ignore() { m_accepted = false; }
bool isAccepted() const { return m_accepted; }
};

调用 accept()ignore()仅仅只是在修改这个布尔值,完全没有发生任何函数跳转或神奇的操作。

用一个例子来讲解 acceptignore

看这几个函数:

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef INNER_LABEL_H
#define INNER_LABEL_H
#include <QLabel>

class InnerLabel : public QLabel
{
Q_OBJECT
public:
InnerLabel(QWidget *parent = nullptr);
~InnerLabel() override = default;
protected:
bool event(QEvent *event) override;
void mousePressEvent(QMouseEvent *ev) override;

};

#endif // INNER_LABEL_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "inner_label.h"
#include <QDebug>
#include <QMouseEvent>
#include <QEvent>

InnerLabel::InnerLabel(QWidget *parent) : QLabel(parent)
{
setFrameStyle(QFrame::Panel | QFrame::Plain);
setLineWidth(3);
setMidLineWidth(1);

}

bool InnerLabel::event(QEvent *ev)
{
// qDebug() << ev->type();
if (ev->type() == QEvent::MouseButtonPress) {
qDebug() << __PRETTY_FUNCTION__;
}
return QLabel::event(ev);
}

void InnerLabel::mousePressEvent(QMouseEvent *ev)
{
qDebug() << __PRETTY_FUNCTION__;
ev->ignore();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "inner_label.h"

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
Q_OBJECT

public:
Widget(QWidget *parent = nullptr);
~Widget();
protected:
bool event(QEvent *ev) override;
void mousePressEvent(QMouseEvent *ev) override;
private:
Ui::Widget *ui;
InnerLabel *in_label;
};
#endif // WIDGET_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
setGeometry(100, 100, 500, 500);
in_label = new InnerLabel(this);
in_label->resize(150, 150);
in_label->move(150, 150);
}

Widget::~Widget()
{
delete ui;
}

bool Widget::event(QEvent *ev)
{
if (ev->type() == QEvent::MouseButtonPress) {
qDebug() << __PRETTY_FUNCTION__;
}
return QWidget::event(ev);
}

void Widget::mousePressEvent(QMouseEvent *ev)
{
qDebug() << __PRETTY_FUNCTION__;
}
1
2
3
4
5
6
7
8
9
10
11
#include <QApplication>

#include "widget.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}

先看输出,当我们注释掉 ev->ignore 后,输出为:

1
2
virtual bool InnerLabel::event(QEvent*)
virtual void InnerLabel::mousePressEvent(QMouseEvent*)

解除 ev->ignore 的注释后,输出为:

1
2
3
4
5
virtual bool InnerLabel::event(QEvent*)
virtual void InnerLabel::mousePressEvent(QMouseEvent*)
// --- 因为 ignore(),事件传给了父组件 Widget ---
virtual bool Widget::event(QEvent*)
virtual void Widget::mousePressEvent(QMouseEvent*)

为什么呢???

顺一遍逻辑:

点击ui中 InnerLabel 的内部时,产生一个鼠标事件,被分发到 InnerLabel::event ,然后调用 QLabel::event ,再调用 QFrame::event -> QWidget::event,其中通过 switch case 找到了对应事件类型的处理。

这里要注意:下面代码只是帮助理解的伪代码,不要把它当成 Qt 源码逐字记忆。对鼠标、键盘这类输入事件来说,事件对象通常会带有一个 accepted 状态;如果处理函数没有调用 ignore(),Qt 通常就认为当前控件消费了该输入事件。

1
2
3
4
5
6
7
8
9
10
11
bool QWidget::event(QEvent *event) {
switch (event->type()) {
case QEvent::MouseButtonPress: {
// 调用你的重写函数;事件的 accepted 状态会影响后续是否继续传给父 widget
this->mousePressEvent((QMouseEvent*)event);

return true; // 返回 true 表示“我认识这个事件类型并进行了处理”
}
// ...
}
}

对鼠标事件来说,更实用的记法是:默认情况下控件收到并处理了鼠标事件后,事件不会继续交给父控件;如果你希望父控件也有机会处理,就在事件处理函数里调用 ev->ignore()

调用 InnerLabel::mousePressEvent 之后,QWidget::event 返回 true

根据前面所学,我们只知道这里返回true后代表“认识这个事件类型并进行了处理“,后面呢?

不要忘了还有最外层的 QApplication。真实 Qt 实现会经过 QApplication::notify()、内部 helper 和针对不同事件类型的专门处理逻辑。下面仍然是针对鼠标/键盘这类输入事件传播行为的简化模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 针对输入事件传播行为的简化模型,不是 Qt 源码
void deliverInputEvent(QWidget *targetObject, QInputEvent *event) {

// 核心循环:顺着 UI 树(父子关系)不断向上找
while (targetObject != nullptr) {

// 1. 调用当前对象的 event() 函数
// 这里会进入 InnerLabel::event -> QLabel::event -> QWidget::event -> mousePressEvent
// 注意:这是 C++ 类继承层面的调用
bool recognized = targetObject->event(event);

// 2. 函数执行完毕,回到分发逻辑中
// 此时再检查你刚才在 mousePressEvent 里设置的布尔值

if (recognized && event->isAccepted()) {
// 如果事件被识别,并且你没有调用 ignore()
break;
}

// 3. 如果你调用了 ignore(),event->isAccepted() 就是 false!
targetObject = targetObject->parentWidget();

// 拿着同一个 event,进入下一轮 while 循环,传给父对象
}
}

我们经过一层层返回 true,最终赋值给 recognized。对于这类输入事件,还要看事件对象的 accepted 状态:event() 返回值表示“这个类型我处理过”,accept/ignore 表示“这次事件我是否消费掉”。

当我们注释掉 ev->ignore 时,鼠标事件保持 accepted 状态,父组件不会再收到这次点击。

当我们释放掉 ev->ignore 时,InnerLabel::mousePressEvent 把这次鼠标事件标记为未消费。于是 Qt 会让父 widget 也有机会处理这个输入事件,也就是 InnerLabel 的父对象 Widget,之后调用 Widget::event -> QWidget::event -> Widget::mousePressEvent ,就会有多出的两行输出了。

为什么Qt要设计两个条件才能break呢?可以继续看“输入事件传播模型”这一节。

如果想从整体上复盘一次鼠标点击,可以直接跳到“完整流程复盘:一次鼠标点击的生命周期”。

6. 事件过滤器

事件过滤器用于在目标对象进入 event() 之前拦截事件。

当Qt的事件通过应用程序对象发送给相关窗口之后,窗口接收到数据之前这个期间可对事件进行过滤,过滤掉的事件就不能被继续处理了。QObject有一个 eventFilter() 函数,用于建立事件过滤器。函数原型如下:

1
[virtual] bool QObject::eventFilter(QObject *watched, QEvent *event);

参数:

  • watched:要过滤的事件的所有者对象
  • event:要过滤的具体的事件

返回值:如果想过滤掉这个事件,停止它被进一步处理,返回true,否则返回 false

既然要过滤传递中的事件,首当其冲还是要搞明白如何通过事件过滤器进行事件的过滤,主要分为两步:

给要被过滤事件的类对象安装事件过滤器

1
void QObject::installEventFilter(QObject *filterObj);

假设调用 installEventFilter() 函数的对象为当前对象,那么就可以基于参数指定的 filterObj 对象来过滤当前对象中的指定的事件了。

在要进行事件过滤的类中( filterObj 参数对应的类)重写从QObject类继承的虚函数 eventFilter()

6.1 过滤与传播示例

这一节把事件过滤器、event()ignore() 后继续交给父 widget 这几件事放在同一个例子里看。

Widget有一个QLabel类型的对象,在Widget中心,现在鼠标左键点击中心处

6.1.1 定位目标对象

OS 事件到达后,exec() 取出事件,Qt 根据坐标做 Hit Testing,找到目标 widget。

一切依然从事件循环开始。

1
2
// 函数原型:开启主事件循环
int QApplication::exec();

exec() 内部从操作系统拿到一个物理鼠标点击时,它会做两件事:

  1. 翻译事件: 将系统消息打包成 QMouseEvent *mouseEvent;

  2. 坐标碰撞检测 (Hit Testing): 调用类似 QWidget *target = QApplication::widgetAt(x, y); 的底层方法。

    • 根据点击坐标,目标对象被定位为最上层的 QLabel

    • 此时,target 指针指向了 QLabel 实例。

然后,exec 把目标对象和事件交给 notify 进入分发流程:

1
2
// 准备分发!将目标 QLabel 和 打包好的事件 送入总枢纽
qApp->notify(target, mouseEvent);

6.1.2 分发模型

事件进入 notify() 后,会进入 Qt 内部的具体事件分发逻辑。这里用简化模型理解输入事件传播。

这里用一个简化模型说明输入事件传播。真实的 Qt 源码不是一个简单的 while(parent) 循环,notify() 会进入内部 helper 和不同事件类型的处理分支。下面这段代码只用来理解鼠标/键盘这类输入事件在 ignore 后可能继续交给父 widget的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 输入事件传播的简化模型,不是 QApplication::notify 的真实源码。
// receiver: 当前接收者(一开始是 QLabel)
// event: 传递的输入事件(例如 QMouseEvent)
bool deliverInputEvent(QWidget *receiver, QInputEvent *event)
{
while (receiver != nullptr) {

bool eventHandled = false;

// ---------------------------------------------------------
// 环节 A:检查 Event Filter
// ---------------------------------------------------------
// 概念上:先检查当前 receiver 上安装的事件过滤器
for (QObject *filter : receiver->installedFilters()) {
// 原型:bool QObject::eventFilter(QObject *watched, QEvent *event);
if (filter->eventFilter(receiver, event)) {
// 如果 filter 返回 true,表示事件被拦截
return true; // 彻底终结!这次分发到此结束。
}
}

// ---------------------------------------------------------
// 环节 B:调用对象的 event() 分发入口
// ---------------------------------------------------------
// 如果过滤器没有拦截,调用对象自身的 event()
// 原型:virtual bool QObject::event(QEvent *e);
// 这里会进入 QLabel::event -> QWidget::event -> QLabel::mousePressEvent
eventHandled = receiver->event(event);

// ---------------------------------------------------------
// 环节 C:检查处理结果和 accepted 状态
// ---------------------------------------------------------
// 对输入事件传播来说,重点看两个条件:
// 1. receiver 的类层级认识这个事件吗?(eventHandled == true)
// 2. 这个事件最终保持 accepted 状态吗?(event->isAccepted() == true)
if (eventHandled && event->isAccepted()) {
// 事件已被当前对象处理并消费
return true;
}

// ---------------------------------------------------------
// 环节 D:继续交给父 widget
// ---------------------------------------------------------
// 如果代码走到这里,通常说明输入事件被 ignore 了,
// Qt 让父 widget 也有机会处理。

// 原型:QWidget* QWidget::parentWidget() const;
receiver = receiver->parentWidget();

// 使用同一个 event,回到 while 循环开头,对父组件再走一遍 A -> B -> C
}

return false; // 如果冒泡到最顶层的窗口还是被 ignore,返回 false
}

这个模型只适合帮助理解输入事件的传播。不要把它推广到所有 Qt 事件;例如绘图、定时器、resize 等事件都有自己的语义,不应该理解成“ignore 后统一向父对象冒泡”。


6.1.3 代入示例代码

下面把前面的 InnerLabel / Widget 代码代入这个模型。

现在,对照着上面的输入事件传播模型,我们重演一遍你代码里发生的事:

  1. 起点: notify(InnerLabel实例, 鼠标事件) 被调用。receiverInnerLabel

  2. 环节 A: InnerLabel 没有安装事件过滤器,继续分发。

  3. 环节 B: 调用 QLabel::event(ev)

    • 鼠标事件保持 accepted 状态,除非你的处理函数显式调用 ignore()

    • 然后调用了你的 InnerLabel::mousePressEvent

    • 你的代码强行把状态改成了 ev->ignore()

    • QLabel::event 执行完毕,返回 true(表示它处理了)。此时 eventHandled = true

  4. 环节 C: if (eventHandled && event->isAccepted()) 判定为 false(因为你 ignore 了)。

  5. 环节 D: receiver = QLabel->parentWidget();。此时 receiver 变成了你的 Widget 实例!

  6. 第二轮 while 循环:

    • 再次经过环节 A(检查 Widget 上安装的事件过滤器)。

    • 到达环节 B,调用 Widget::event(ev) -> Widget::mousePressEvent

    • 你的终端输出了 Widget 的打印信息。

下面是qt处理的流程图

7. 输入事件传播状态

这一节解释输入事件传播时需要同时看的两个状态:event() 返回值和 isAccepted()

先强调一下:这不是 Qt 所有事件的统一底层公式,而是理解鼠标、键盘等输入事件传播时很有用的模型。Qt 的事件类型很多,绘图、定时器、resize、自定义事件等都有自己的处理语义。

这两个状态表达的是两件不同的事:

7.1 recognized

recognized 是通过执行 targetObject->event(event) 得到的 bool 返回值。它表示当前对象是否处理这个事件类型。

  • Qt 里的事件成百上千: 除了鼠标、键盘,还有定时器事件 (QTimerEvent)、重绘事件 (QPaintEvent)、窗口改变大小事件 (QResizeEvent),甚至是你自己写的自定义事件。

  • 不按输入事件方式传播的事件: 很多事件不应该按“交给父 widget 冒泡”理解。比如 QPaintEvent 是系统要求某个 widget 重绘自己,QTimerEvent 是发给启动该 timer 的对象。

  • 对于这类事件,event() 返回 true 通常就表示当前对象已经处理;返回 false 则表示当前对象没有处理。后续怎么走,要看具体事件类型和 Qt 内部规则。

简单说,recognized 看的是“这个事件类型当前对象是否处理”。

7.2 isAccepted()

isAccepted() 表示当前处理逻辑是否消费这次事件。

这个状态主要在理解 UI 输入事件(QInputEvent 及其子类,如鼠标、键盘、触摸等) 时有意义。

对于鼠标点击这类事件,几乎所有的 QWidget 都能返回 true(因为基类 QWidget::event 里面有对应的 switch case,它能识别鼠标事件)。但是,能识别不代表一定要消费这次事件。

这就引出了你需要 ignore() 的场景(即 isAccepted() == false):当前对象虽然能处理这个事件类型,但本次处理逻辑决定不消费它。

简单说,isAccepted() 看的是“这一次输入事件是否被当前处理逻辑消费”。


7.3 两个状态的配合

单独看其中一个状态都不够,下面用两个反例说明原因。

可以从两个反例来理解为什么需要区分这两个状态:

7.3.1 只看 event() 返回值的问题

如果只看 event() 返回值,那么只要一个控件的 event() 函数处理了鼠标点击并返回 true,父 widget 就没有机会继续处理这个输入事件。这样会限制一些需要父子控件共同响应的场景。

7.3.2 只看 event->isAccepted() 的问题

这样也不可靠。QEvent 的子类在 API 上都有 accept/ignore 状态,但并不是每种事件都会用这个状态表达“是否继续交给父 widget”。例如 QTimerEventQPaintEvent 的主要语义就不是输入事件传播。

7.3.3 输入事件的三种情况

通过 recognizedevent->isAccepted() 这两个维度,可以把输入事件分成三种常见情况:

  1. false + (任意):当前对象不处理这个事件类型。

    • 比如你给 InnerLabel 发了个它没见过的自定义事件,event() 直接走到最底层返回 false

    • 动作: 当前对象没有处理。后续是否继续传递取决于具体事件类型。

  2. true + true:当前对象处理并消费了这次事件。

    • 常规的按钮点击(默认 accept)。

    • 动作: 对输入事件来说,通常到当前对象为止。

  3. true + false:当前对象处理了事件,但没有消费它。

    • 你在 mousePressEvent 里写了 ignore()。代码执行了,你做了想做的事(比如打印了 Log),但这次事件仍允许父 widget 继续处理。

    • 动作: 对鼠标、键盘这类输入事件来说,父 widget 可能继续获得处理机会。

7.4 小结

从理解角度看,这里可以类比为 “拦截器模式(Interceptor)”与“责任链模式(Chain of Responsibility)”的结合

  • recognized 表示当前对象是否处理这个事件类型。

  • isAccepted() 在输入事件里表达这次事件是否被当前处理逻辑消费。

这样可以把 “框架默认分发路径”“开发者对单次事件的处理决策” 分开理解。

8. 鼠标点击流程

我们以 “你在屏幕上点击了一下鼠标,导致一个按钮改变了颜色” 为例,从操作系统消息开始,完整追踪这一个事件的生命周期。

可以按下面这条链路看:


8.1 OS 消息

Qt 应用拿到的是操作系统分发过来的消息,而不是直接读取鼠标硬件。

  1. 物理产生: 你的手指按下鼠标左键,鼠标硬件向主板发送一个电信号(中断)。

  2. OS 捕获: 操作系统(Windows/macOS/Linux)捕获这个中断,并计算出鼠标点击的屏幕坐标,发现点击落在了你的 Qt 程序窗口内。

  3. 推入系统队列: 操作系统生成一个底层系统消息(例如在 Windows 上是 WM_LBUTTONDOWN),并把它放入你的 Qt 程序的系统消息队列中。

8.2 exec() 事件循环

现在,轮到 Qt 出场了。你写的 return a.exec(); 到底是个什么东西?

exec() 本质上就是一个死循环(Event Loop,事件循环)。如果用最极简的伪代码来表示,它长这样:

1
2
3
4
5
6
7
int QApplication::exec() {
while (!this->quit_flag) {
// 去系统的消息队列里看有没有新消息
processEvents();
}
return exit_code;
}
  1. 事件分发器 (QAbstractEventDispatcher): exec() 内部依赖平台相关的事件分发器,从操作系统消息队列中取出类似 WM_LBUTTONDOWN 的底层消息。

  2. 转换成 QEvent: Qt 把平台相关的底层消息转换成跨平台的 QMouseEvent 对象。

  3. 定位目标: Qt 引擎根据坐标,在内存的 UI 树中寻找,发现鼠标点击的位置正是你创建的那个 InnerLabel 对象。

(注:如果你自己在代码里调用 QApplication::postEvent(),事件也会进入 Qt 的事件队列,然后由 exec() 循环取出来处理。)

8.3 notify()

事件找到目标对象后,通常会进入 QCoreApplication::notify() / QApplication::notify() 这条分发路径。

当事件被打包好,找到了目标对象(InnerLabel),通常会进入 QCoreApplication::notify() / QApplication::notify() 这条分发路径。它是 Qt 事件分发的核心入口之一。

1
2
3
4
5
6
bool QCoreApplication::notify(QObject *receiver, QEvent *event) {
// receiver 就是你的 InnerLabel
// event 就是 QMouseEvent

// ... 准备发送 ...
}

notify 是整个事件分发系统的重要枢纽。如果你重写了这个函数,可以观察或干预应用层面的事件分发;但这种做法影响范围很大,一般不建议用它处理普通业务逻辑。

8.4 事件过滤器

在目标对象正式处理事件之前,Qt 会先检查是否安装了事件过滤器。

  1. Qt 会检查:有没有别的对象对这个 InnerLabel 调用了 installEventFilter()

  2. 如果有,就把事件先送给那个监听者(Filter)

  3. 监听者可以检查这个事件,甚至可以返回 true 直接拦截这个事件。如果被拦截,InnerLabel 不会再收到这次事件。

  4. 如果监听者返回 false(放行),或者根本没人监听,事件才会真正送到目标手里。

8.5 event() 分发

事件来到 InnerLabel::event(ev)。这就是对象内部的事件分发入口。

  1. 就像我们之前讨论的,event() 是事件分发入口。

  2. 你调用了 QLabel::event(ev) -> QWidget::event(ev)

  3. QWidget::event 内部的 switch (ev->type()) 认出了这是鼠标点击。

  4. 它将通用的 QEvent 强制转换(static_cast)为更具体的 QMouseEvent

  5. 对鼠标这类输入事件,如果你的处理函数没有调用 ignore(),通常就表示当前控件消费了这次事件。

  6. 它调用了 this->mousePressEvent(ev)

8.6 处理与传播

事件处理函数执行业务逻辑后,输入事件可能被消费,也可能继续交给父 widget。

现在,代码执行到了你重写的 InnerLabel::mousePressEvent(QMouseEvent *ev) 里。你可以在这里改变按钮颜色、打印 Log、发起网络请求。

执行完你的代码后,控制权原路返回到 Qt 的事件分发逻辑中。

对于鼠标这类输入事件,后续大致可以这样理解:

  1. 你执行了 ev->accept()(或默认没动): Qt 通常认为当前控件消费了这个事件,程序回到事件循环,继续等待下一次事件。

  2. 你执行了 ev->ignore() Qt 会让父 widget 也有机会处理这个输入事件。

    • Qt 顺着 InnerLabel 找到它的 UI 父亲 Widget

    • 父 widget 会经历自己的事件过滤器、event() 和对应的事件处理函数。

    • 这个传播行为主要用于输入事件,不要推广到所有 QEvent

9. 主动发送事件

这一节比较 sendEvent()postEvent():一个同步发送,一个异步投递。

前面主要讲的是系统事件,例如鼠标点击、键盘输入、窗口重绘。Qt 里也可以在代码中主动给某个对象发送事件,常见接口是 sendEvent()postEvent()

1
2
QCoreApplication::sendEvent(receiver, event);
QCoreApplication::postEvent(receiver, event);

它们的区别在于是否立即处理:

  • sendEvent() 是同步发送,会立刻进入目标对象的 event()。函数返回时,事件已经处理完。
  • postEvent() 是异步投递,会把事件放进目标线程的事件队列,等事件循环之后再处理。

使用时要注意事件对象的生命周期:

1
2
3
4
QEvent ev(QEvent::User);
QCoreApplication::sendEvent(obj, &ev); // 同步发送,栈对象可以

QCoreApplication::postEvent(obj, new QEvent(QEvent::User)); // 异步投递,通常使用 new

postEvent() 投递出去的事件由 Qt 在处理后释放;不要再手动 delete。sendEvent() 不接管事件对象的所有权。

这两个函数的使用场景也不同:

  • 想立刻让对象处理事件,用 sendEvent()
  • 想把处理推迟到事件循环中,或者从别的线程投递给某个对象,用 postEvent()

postEvent() 依赖事件循环。如果目标线程没有运行事件循环,投递的事件就不会被正常处理。

10. 自定义事件

当信号槽不适合表达某些低层事件,或者你希望把一段逻辑放进事件队列里处理时,可以定义自己的事件。

常见写法有两种:

  1. 使用已有的 QEvent 类型,例如 QEvent::User
  2. 继承 QEvent,定义自己的事件类型和数据。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyEvent : public QEvent
{
public:
static QEvent::Type type()
{
static int id = QEvent::registerEventType();
return static_cast<QEvent::Type>(id);
}

explicit MyEvent(QString text)
: QEvent(type()), text(std::move(text))
{
}

QString text;
};

目标对象可以在 event() 中识别它:

1
2
3
4
5
6
7
8
9
10
bool MyObject::event(QEvent *ev)
{
if (ev->type() == MyEvent::type()) {
auto *myEvent = static_cast<MyEvent *>(ev);
qDebug() << myEvent->text;
return true;
}

return QObject::event(ev);
}

发送时可以用 postEvent()

1
QCoreApplication::postEvent(obj, new MyEvent("hello"));

也可以重写 customEvent(),它专门用于处理自定义事件:

1
2
3
4
5
6
7
void MyObject::customEvent(QEvent *ev)
{
if (ev->type() == MyEvent::type()) {
auto *myEvent = static_cast<MyEvent *>(ev);
qDebug() << myEvent->text;
}
}

两种方式都可以。一般来说,如果对象已经重写了 event() 并且要统一处理多种事件,就放在 event() 里;如果只是处理自定义事件,用 customEvent() 更直接。

11. 线程与延迟删除

这一节说明事件循环和线程的关系,以及 deleteLater() 为什么依赖事件系统。

Qt 的事件循环和线程关系很密切。每个线程都可以有自己的事件循环,对象属于哪个线程,它收到的事件通常就在哪个线程的事件循环里处理。

主线程里一般是:

1
2
QApplication app(argc, argv);
return app.exec();

子线程如果需要处理事件,也要运行事件循环。比如使用 QThread::exec()

1
2
3
QThread thread;
worker->moveToThread(&thread);
thread.start(); // QThread 默认会在 run() 里调用 exec()

理解这一点后,postEvent() 和跨线程信号就更容易理解了:它们不是直接在当前调用点执行目标对象逻辑,而是把工作交给目标对象所属线程的事件循环。

deleteLater() 也依赖事件系统:

1
obj->deleteLater();

它不会立刻删除对象,而是向对象所属线程的事件队列投递一个延迟删除事件。等事件循环处理到这个事件时,Qt 才真正删除对象。

这也是为什么 Qt 里经常推荐在对象可能仍处于事件处理过程、或者涉及跨线程时使用 deleteLater(),而不是直接 delete obj。前提是对象所属线程的事件循环还在运行,否则延迟删除事件也没有机会被处理。

12. 信号和事件

这一节区分事件系统和信号槽:事件偏底层输入/状态变化,信号偏对象对外通知。

事件和信号都可以触发后续代码执行,所以容易混在一起。

可以这样区分:事件通常表示底层输入或 Qt 内部状态变化;信号通常表示对象对外通知某个语义动作已经发生。

事件:通常来自操作系统底层(鼠标按键、键盘、窗口重绘)或 Qt 框架内部(定时器)。 信号:来自 Qt 对象内部的业务逻辑代码(比如代码里显式写了 emit clicked())。

12.1 怎么配合

常见情况是:事件先发生,控件在事件处理函数里更新内部状态;当它确认某个语义动作成立时,再发射信号。

以最常见的“点击按钮 (QPushButton) ”为例,看看底层的链路:

  1. 【系统触发事件】:你用鼠标点击了按钮,操作系统发来底层消息,QApplication 把它打包成 QMouseEvent,送到了对应的按钮控件处。

  2. 【底层接收事件】:进入 QPushButton::mousePressEvent(ev)。按钮在这里改变自己的 UI 状态(比如让自己凹陷下去),并消费这次鼠标按下事件。

  3. 【系统再次触发】:你松开了鼠标,Qt 又送来一个 QMouseEvent (Release)。

  4. 【事件转化为信号】:进入 QPushButton::mouseReleaseEvent(ev)。按钮在这里让自己弹起,恢复原状。同时,它内部判断鼠标按下和抬起都在按钮区域内,于是发射 clicked() 信号。

  5. 【业务层接收信号】:你在主窗口里写好的 connect(btn, &QPushButton::clicked, this, &Widget::submitData); 收到广播,开始向服务器提交数据。

总结: 控件内部通过重写 事件处理函数(虚函数) 来处理系统交互,在确认交互有效后,对外 发射信号(Signal) 来通知业务层。

12.2 选择依据

什么时候重写事件处理函数,什么时候连接 signal,主要看你是要改变控件行为,还是只响应业务动作。

需要改变控件默认行为时,重写事件处理函数

  • 场景: 你想写一个只能输入数字、且输入字母时边框变红的输入框 (QLineEdit)。

  • 做法: 继承 QLineEdit,重写 keyPressEvent。拦截掉非数字按键 (ignore 或直接 return),重绘边框。因为你要干预控件的默认系统行为,必须在源头截断。

只关心业务动作时,连接信号

  • 场景: 用户点击了登录按钮,你需要发送网络请求。

  • 做法: 直接 connect 按钮的 clicked 信号。不需要关心鼠标是左键点的还是右键点的、按下了多少毫秒,你只需要结果。

一般来说,定制控件交互或拦截默认行为时用事件;响应按钮点击、文本变化这类业务动作时优先用信号。