012-Qt 的事件系统

012-Qt 的事件系统
abinng😶🌫️auther: abinng date: 2026-03-31 14:49
createDate:2026-03-31 14:49
由于我已经开始实习,所以本篇往后我可能并不会详细进行笔记记录,因为太累了
操作系统本身就有事件系统,但是Qt是跨平台的框架,所以Qt在操作系统上层又封装了一层QEvent,一套代码在不同操作系统上运行
可参考Qt 教程 | 爱编程的大丙 和 Qt应用开发.pdf
复习路线
这篇笔记要回答的问题是:Qt 收到一个外部动作,或者代码主动投递一个事件之后,是怎样把它变成某个对象上的函数调用的?
先记住这条主线:
1 | OS/Qt 产生事件 -> 事件循环 exec() 取出事件 -> notify() 准备分发 |
下次忘记时,可以按这个顺序复习:
- 先看“事件是什么”和
exec(),建立事件循环的概念。 - 再看
event()和责任链,理解事件怎么从通用QEvent分发到具体 handler。 - 看
InnerLabel示例,确认为什么调用基类event()后还能回到子类的mousePressEvent()。 - 看
accept/ignore,理解输入事件什么时候被当前控件消费,什么时候继续交给父 widget。 - 看事件过滤器,理解怎么在对象收到事件之前拦截。
- 看“完整流程复盘”,把前面的点串成一次鼠标点击的生命周期。
- 看
sendEvent()/postEvent()和自定义事件,理解代码主动发送事件的方式。 - 看事件循环、线程和
deleteLater(),理解事件系统和对象生命周期的关系。 - 最后看“信号与事件”,把事件系统和业务层连接起来。
1. 事件是什么
Qt 是一个基于 C++ 的跨平台框架,GUI
程序的大部分交互都围绕事件展开。用户操作、窗口状态变化、定时器触发等都会变成事件,再由
Qt
分发给对应的对象处理。窗口事件产生之后,大致会经过:事件派发 -> 事件过滤 -> 事件分发 -> 事件处理
几个阶段。Qt
对很多事件都有默认处理,如果有特殊需求,就在合适的位置重写或拦截对应的处理逻辑。
事件(event)是由系统或者 Qt 本身在不同的场景下发出的。当用户按下/移动鼠标、敲下键盘,或者是窗口关闭/大小发生变化/隐藏或显示都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如鼠标/键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
事件在Qt中产生之后的分发过程是这样的:
当事件产生之后,Qt使用用应用程序对象调用notify()函数将事件发送到指定的窗口:
1 | [override virtual] bool QApplication::notify(QObject *receiver, QEvent *e); |
当事件发送到指定窗口之后,窗口的事件分发器会对收到的事件进行分类:
1 | [override virtual protected] bool QWidget::event(QEvent *event); |
事件分发器会将分类之后的事件(鼠标事件、键盘事件、绘图事件……)分发给对应的事件处理器函数进行处理,每个事件处理器函数都有默认的处理动作(我们也可以重写这些事件处理器函数),比如:鼠标事件:
1 | // 鼠标按下 |
2. 事件循环:exec
他会使Qt应用程序进入事件循环,这样就可以使应用程序在运行时接受各种事件。一旦有事件发生,Qt便会构建一个相应的QEvent子类对象来表示它,然后将它传递给相应的QObject对象或其子对象。
3. 分发入口:event()
event() 函数在 Qt
中的角色是一个统一的分发入口。它接收通用的
QEvent,判断事件类型(Type),然后把它转换为具体的事件类,并调用对应的事件处理函数(Event
Handler)。
1 | bool QWidget::event(QEvent *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
QLabel 的 event
函数只关心它自己特有的事件。
1 | bool QLabel::event(QEvent *e) { |
第二层:QFrame::event
QFrame 只关心边框绘制相关的特殊逻辑。
1 | bool QFrame::event(QEvent *e) { |
第三层:QWidget::event
QWidget 是所有可视化控件的基类,它承担了几乎所有 UI
交互事件的基础分发工作。
1 | bool QWidget::event(QEvent *event) { |
第四层:QObject::event
如果事件一路漏到了这里,QObject
只处理最底层的核心机制,比如定时器事件(Timer)、动态属性改变(DynamicPropertyChange)等。
关于Qt窗口提供的其他事件处理器函数还有很多,感兴趣的话可以仔细阅读Qt的帮助文档,窗口的事件处理器函数非常好找,规律是这样的:
- 受保护的虚函数
- 函数名分为两部分: 事件描述+Event
- 函数带一个事件类型的参数
4. 子类 handler 示例
这一节用 InnerLabel 说明:调用基类 event()
后,为什么最终仍然会进入子类重写的 mousePressEvent()。
InnerLabel 类
点击展开
1 |
|
1 |
|
在 main.cpp 中创建该对象
点击展开
1 |
|
运行后,会发现有一个黑色的框框,点击其内部会有输出,该输出是 InnerLabel::mousePressEvent 中的语句
程序的逻辑是,进入 a.exec()
这个循环后开始看有什么事件产生,将事件加入队列,不断取出队列中的事件到
event 函数中进行分发
我们重写了 InnerLabel::event ,在启动直接一句
return QLabel::event(ev) ,相当于是交付于
InnerLabel 的基类 QLabel 的 event
进行分发。但是为什么又会调用 QLabel 的子类 InnerLabel 的
mousePressEvent 呢?
InnerLabel::event 中有隐藏参数 this 指针,这个都知道吧。
然后调用 QLabel::event ,其中也有隐藏参数 this
指针,其类型虽然是 QLabel *,但实际还是直接将
InnerLabel::event 的 this 指针给传过去了,所以还是指向 InnerLabel
的实例对象的。
现在回到鼠标点击发生时的调用过程:
鼠标点击,进入
InnerLabel::event。你调用
QLabel::event,this指针原封不动地传进去了。QLabel::event发现这不是 QLabel 特有的事件处理逻辑(比如富文本链接),于是调用QFrame::event。QFrame::event发现这不是 QFrame 特有的处理逻辑,于是调用QWidget::event。QWidget::event发现type是QEvent::MouseButtonPress,进入鼠标按下事件的分支。QWidget::event执行了this->mousePressEvent(ev)。这一整条调用链里,
this指针的值(你的InnerLabel实例地址)始终没有变过。C++ 的虚函数机制生效,最终调用的是实际对象类型里的
InnerLabel::mousePressEvent。
就是这么一个过程
5. accept 和
ignore
这一节主要看输入事件什么时候被当前控件消费,什么时候继续交给父 widget。
QEvent 是一个事件类,accept() 和 ignore()
本质上是在修改事件对象内部的状态。
1 | class QEvent { |
调用 accept() 和
ignore(),仅仅只是在修改这个布尔值,完全没有发生任何函数跳转或神奇的操作。
用一个例子来讲解 accept 和 ignore:
看这几个函数:
点击展开
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
先看输出,当我们注释掉 ev->ignore 后,输出为:
1 | virtual bool InnerLabel::event(QEvent*) |
解除 ev->ignore 的注释后,输出为:
1 | virtual bool InnerLabel::event(QEvent*) |
为什么呢???
顺一遍逻辑:
点击ui中 InnerLabel 的内部时,产生一个鼠标事件,被分发到
InnerLabel::event ,然后调用 QLabel::event
,再调用 QFrame::event ->
QWidget::event,其中通过 switch case
找到了对应事件类型的处理。
这里要注意:下面代码只是帮助理解的伪代码,不要把它当成 Qt
源码逐字记忆。对鼠标、键盘这类输入事件来说,事件对象通常会带有一个
accepted 状态;如果处理函数没有调用 ignore(),Qt
通常就认为当前控件消费了该输入事件。
1 | bool QWidget::event(QEvent *event) { |
对鼠标事件来说,更实用的记法是:默认情况下控件收到并处理了鼠标事件后,事件不会继续交给父控件;如果你希望父控件也有机会处理,就在事件处理函数里调用
ev->ignore()。
调用 InnerLabel::mousePressEvent
之后,QWidget::event 返回 true。
根据前面所学,我们只知道这里返回true后代表“认识这个事件类型并进行了处理“,后面呢?
不要忘了还有最外层的 QApplication。真实 Qt 实现会经过
QApplication::notify()、内部 helper
和针对不同事件类型的专门处理逻辑。下面仍然是针对鼠标/键盘这类输入事件传播行为的简化模型:
1 | // 针对输入事件传播行为的简化模型,不是 Qt 源码 |
我们经过一层层返回 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 | // 函数原型:开启主事件循环 |
当 exec()
内部从操作系统拿到一个物理鼠标点击时,它会做两件事:
翻译事件: 将系统消息打包成
QMouseEvent *mouseEvent;。坐标碰撞检测 (Hit Testing): 调用类似
QWidget *target = QApplication::widgetAt(x, y);的底层方法。根据点击坐标,目标对象被定位为最上层的
QLabel。此时,
target指针指向了QLabel实例。
然后,exec 把目标对象和事件交给 notify
进入分发流程:
1 | // 准备分发!将目标 QLabel 和 打包好的事件 送入总枢纽 |
6.1.2 分发模型
事件进入 notify() 后,会进入 Qt
内部的具体事件分发逻辑。这里用简化模型理解输入事件传播。
这里用一个简化模型说明输入事件传播。真实的 Qt 源码不是一个简单的
while(parent) 循环,notify() 会进入内部 helper
和不同事件类型的处理分支。下面这段代码只用来理解鼠标/键盘这类输入事件在
ignore 后可能继续交给父 widget的行为。
1 | // 输入事件传播的简化模型,不是 QApplication::notify 的真实源码。 |
这个模型只适合帮助理解输入事件的传播。不要把它推广到所有 Qt 事件;例如绘图、定时器、resize 等事件都有自己的语义,不应该理解成“ignore 后统一向父对象冒泡”。
6.1.3 代入示例代码
下面把前面的 InnerLabel / Widget
代码代入这个模型。
现在,对照着上面的输入事件传播模型,我们重演一遍你代码里发生的事:
起点:
notify(InnerLabel实例, 鼠标事件)被调用。receiver是InnerLabel。环节 A:
InnerLabel没有安装事件过滤器,继续分发。环节 B: 调用
QLabel::event(ev)。鼠标事件保持 accepted 状态,除非你的处理函数显式调用
ignore()。然后调用了你的
InnerLabel::mousePressEvent。你的代码强行把状态改成了
ev->ignore()。QLabel::event执行完毕,返回true(表示它处理了)。此时eventHandled=true。
环节 C:
if (eventHandled && event->isAccepted())判定为false(因为你 ignore 了)。环节 D:
receiver = QLabel->parentWidget();。此时receiver变成了你的Widget实例!第二轮 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”。例如
QTimerEvent、QPaintEvent
的主要语义就不是输入事件传播。
7.3.3 输入事件的三种情况
通过 recognized 和 event->isAccepted()
这两个维度,可以把输入事件分成三种常见情况:
false+(任意):当前对象不处理这个事件类型。比如你给
InnerLabel发了个它没见过的自定义事件,event()直接走到最底层返回false。动作: 当前对象没有处理。后续是否继续传递取决于具体事件类型。
true+true:当前对象处理并消费了这次事件。常规的按钮点击(默认 accept)。
动作: 对输入事件来说,通常到当前对象为止。
true+false:当前对象处理了事件,但没有消费它。你在
mousePressEvent里写了ignore()。代码执行了,你做了想做的事(比如打印了 Log),但这次事件仍允许父 widget 继续处理。动作: 对鼠标、键盘这类输入事件来说,父 widget 可能继续获得处理机会。
7.4 小结
从理解角度看,这里可以类比为 “拦截器模式(Interceptor)”与“责任链模式(Chain of Responsibility)”的结合。
recognized表示当前对象是否处理这个事件类型。isAccepted()在输入事件里表达这次事件是否被当前处理逻辑消费。
这样可以把 “框架默认分发路径” 和 “开发者对单次事件的处理决策” 分开理解。
8. 鼠标点击流程
我们以 “你在屏幕上点击了一下鼠标,导致一个按钮改变了颜色” 为例,从操作系统消息开始,完整追踪这一个事件的生命周期。
可以按下面这条链路看:
8.1 OS 消息
Qt 应用拿到的是操作系统分发过来的消息,而不是直接读取鼠标硬件。
物理产生: 你的手指按下鼠标左键,鼠标硬件向主板发送一个电信号(中断)。
OS 捕获: 操作系统(Windows/macOS/Linux)捕获这个中断,并计算出鼠标点击的屏幕坐标,发现点击落在了你的 Qt 程序窗口内。
推入系统队列: 操作系统生成一个底层系统消息(例如在 Windows 上是
WM_LBUTTONDOWN),并把它放入你的 Qt 程序的系统消息队列中。
8.2 exec() 事件循环
现在,轮到 Qt 出场了。你写的 return a.exec();
到底是个什么东西?
exec() 本质上就是一个死循环(Event
Loop,事件循环)。如果用最极简的伪代码来表示,它长这样:
1 | int QApplication::exec() { |
事件分发器 (
QAbstractEventDispatcher):exec()内部依赖平台相关的事件分发器,从操作系统消息队列中取出类似WM_LBUTTONDOWN的底层消息。转换成 QEvent: Qt 把平台相关的底层消息转换成跨平台的
QMouseEvent对象。定位目标: Qt 引擎根据坐标,在内存的 UI 树中寻找,发现鼠标点击的位置正是你创建的那个
InnerLabel对象。
(注:如果你自己在代码里调用
QApplication::postEvent(),事件也会进入 Qt
的事件队列,然后由 exec() 循环取出来处理。)
8.3 notify()
事件找到目标对象后,通常会进入
QCoreApplication::notify() /
QApplication::notify() 这条分发路径。
当事件被打包好,找到了目标对象(InnerLabel),通常会进入
QCoreApplication::notify() /
QApplication::notify() 这条分发路径。它是 Qt
事件分发的核心入口之一。
1 | bool QCoreApplication::notify(QObject *receiver, QEvent *event) { |
notify
是整个事件分发系统的重要枢纽。如果你重写了这个函数,可以观察或干预应用层面的事件分发;但这种做法影响范围很大,一般不建议用它处理普通业务逻辑。
8.4 事件过滤器
在目标对象正式处理事件之前,Qt 会先检查是否安装了事件过滤器。
Qt 会检查:有没有别的对象对这个
InnerLabel调用了installEventFilter()?如果有,就把事件先送给那个监听者(Filter)。
监听者可以检查这个事件,甚至可以返回
true直接拦截这个事件。如果被拦截,InnerLabel不会再收到这次事件。如果监听者返回
false(放行),或者根本没人监听,事件才会真正送到目标手里。
8.5 event() 分发
事件来到
InnerLabel::event(ev)。这就是对象内部的事件分发入口。
就像我们之前讨论的,
event()是事件分发入口。你调用了
QLabel::event(ev)->QWidget::event(ev)。QWidget::event内部的switch (ev->type())认出了这是鼠标点击。它将通用的
QEvent强制转换(static_cast)为更具体的QMouseEvent。对鼠标这类输入事件,如果你的处理函数没有调用
ignore(),通常就表示当前控件消费了这次事件。它调用了
this->mousePressEvent(ev)。
8.6 处理与传播
事件处理函数执行业务逻辑后,输入事件可能被消费,也可能继续交给父 widget。
现在,代码执行到了你重写的
InnerLabel::mousePressEvent(QMouseEvent *ev)
里。你可以在这里改变按钮颜色、打印 Log、发起网络请求。
执行完你的代码后,控制权原路返回到 Qt 的事件分发逻辑中。
对于鼠标这类输入事件,后续大致可以这样理解:
你执行了
ev->accept()(或默认没动): Qt 通常认为当前控件消费了这个事件,程序回到事件循环,继续等待下一次事件。你执行了
ev->ignore(): Qt 会让父 widget 也有机会处理这个输入事件。Qt 顺着
InnerLabel找到它的 UI 父亲Widget。父 widget 会经历自己的事件过滤器、
event()和对应的事件处理函数。这个传播行为主要用于输入事件,不要推广到所有
QEvent。
9. 主动发送事件
这一节比较 sendEvent() 和
postEvent():一个同步发送,一个异步投递。
前面主要讲的是系统事件,例如鼠标点击、键盘输入、窗口重绘。Qt
里也可以在代码中主动给某个对象发送事件,常见接口是
sendEvent() 和 postEvent()。
1 | QCoreApplication::sendEvent(receiver, event); |
它们的区别在于是否立即处理:
sendEvent()是同步发送,会立刻进入目标对象的event()。函数返回时,事件已经处理完。postEvent()是异步投递,会把事件放进目标线程的事件队列,等事件循环之后再处理。
使用时要注意事件对象的生命周期:
1 | QEvent ev(QEvent::User); |
postEvent() 投递出去的事件由 Qt 在处理后释放;不要再手动
delete。sendEvent() 不接管事件对象的所有权。
这两个函数的使用场景也不同:
- 想立刻让对象处理事件,用
sendEvent()。 - 想把处理推迟到事件循环中,或者从别的线程投递给某个对象,用
postEvent()。
postEvent()
依赖事件循环。如果目标线程没有运行事件循环,投递的事件就不会被正常处理。
10. 自定义事件
当信号槽不适合表达某些低层事件,或者你希望把一段逻辑放进事件队列里处理时,可以定义自己的事件。
常见写法有两种:
- 使用已有的
QEvent类型,例如QEvent::User。 - 继承
QEvent,定义自己的事件类型和数据。
简单示例:
1 | class MyEvent : public QEvent |
目标对象可以在 event() 中识别它:
1 | bool MyObject::event(QEvent *ev) |
发送时可以用 postEvent():
1 | QCoreApplication::postEvent(obj, new MyEvent("hello")); |
也可以重写 customEvent(),它专门用于处理自定义事件:
1 | void MyObject::customEvent(QEvent *ev) |
两种方式都可以。一般来说,如果对象已经重写了 event()
并且要统一处理多种事件,就放在 event()
里;如果只是处理自定义事件,用 customEvent() 更直接。
11. 线程与延迟删除
这一节说明事件循环和线程的关系,以及 deleteLater()
为什么依赖事件系统。
Qt 的事件循环和线程关系很密切。每个线程都可以有自己的事件循环,对象属于哪个线程,它收到的事件通常就在哪个线程的事件循环里处理。
主线程里一般是:
1 | QApplication app(argc, argv); |
子线程如果需要处理事件,也要运行事件循环。比如使用
QThread::exec():
1 | QThread thread; |
理解这一点后,postEvent()
和跨线程信号就更容易理解了:它们不是直接在当前调用点执行目标对象逻辑,而是把工作交给目标对象所属线程的事件循环。
deleteLater() 也依赖事件系统:
1 | obj->deleteLater(); |
它不会立刻删除对象,而是向对象所属线程的事件队列投递一个延迟删除事件。等事件循环处理到这个事件时,Qt 才真正删除对象。
这也是为什么 Qt
里经常推荐在对象可能仍处于事件处理过程、或者涉及跨线程时使用
deleteLater(),而不是直接
delete obj。前提是对象所属线程的事件循环还在运行,否则延迟删除事件也没有机会被处理。
12. 信号和事件
这一节区分事件系统和信号槽:事件偏底层输入/状态变化,信号偏对象对外通知。
事件和信号都可以触发后续代码执行,所以容易混在一起。
可以这样区分:事件通常表示底层输入或 Qt 内部状态变化;信号通常表示对象对外通知某个语义动作已经发生。
事件:通常来自操作系统底层(鼠标按键、键盘、窗口重绘)或 Qt
框架内部(定时器)。 信号:来自 Qt
对象内部的业务逻辑代码(比如代码里显式写了
emit clicked())。
12.1 怎么配合
常见情况是:事件先发生,控件在事件处理函数里更新内部状态;当它确认某个语义动作成立时,再发射信号。
以最常见的“点击按钮 (QPushButton)
”为例,看看底层的链路:
【系统触发事件】:你用鼠标点击了按钮,操作系统发来底层消息,
QApplication把它打包成QMouseEvent,送到了对应的按钮控件处。【底层接收事件】:进入
QPushButton::mousePressEvent(ev)。按钮在这里改变自己的 UI 状态(比如让自己凹陷下去),并消费这次鼠标按下事件。【系统再次触发】:你松开了鼠标,Qt 又送来一个
QMouseEvent(Release)。【事件转化为信号】:进入
QPushButton::mouseReleaseEvent(ev)。按钮在这里让自己弹起,恢复原状。同时,它内部判断鼠标按下和抬起都在按钮区域内,于是发射clicked()信号。【业务层接收信号】:你在主窗口里写好的
connect(btn, &QPushButton::clicked, this, &Widget::submitData);收到广播,开始向服务器提交数据。
总结: 控件内部通过重写 事件处理函数(虚函数) 来处理系统交互,在确认交互有效后,对外 发射信号(Signal) 来通知业务层。
12.2 选择依据
什么时候重写事件处理函数,什么时候连接 signal,主要看你是要改变控件行为,还是只响应业务动作。
需要改变控件默认行为时,重写事件处理函数
场景: 你想写一个只能输入数字、且输入字母时边框变红的输入框 (
QLineEdit)。做法: 继承
QLineEdit,重写keyPressEvent。拦截掉非数字按键 (ignore或直接return),重绘边框。因为你要干预控件的默认系统行为,必须在源头截断。
只关心业务动作时,连接信号
场景: 用户点击了登录按钮,你需要发送网络请求。
做法: 直接
connect按钮的clicked信号。不需要关心鼠标是左键点的还是右键点的、按下了多少毫秒,你只需要结果。
一般来说,定制控件交互或拦截默认行为时用事件;响应按钮点击、文本变化这类业务动作时优先用信号。









