事件系统
事件
事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。Qt 中使用一个对象来表示一个事件,继承自 QEvent 类。
需要说明的是,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMouseEvent (不是按钮产生的 ),而因为按钮被按下了 ,所以它会发出 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者当单击按钮时让它产生别的效果,那么就要关心鼠标事件了。可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。
事件的处理
一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举型 QEvent::Type 来表示,其中包含了 一百多种事件类型,可以在 QEvent 类的帮助文档中查看。虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法:
- 方法一:重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
- 方法二:重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
- 方法三:向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
- 方法四:重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
- 方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。
事件的传递
在每个程序的 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进人事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。(示例放到后面几节,进行深入分析)
这个图不错(但其实这个图只是示意,实际上不是这样的,准确一点的话最外层应该是WidgetWindow。懒得重画了,看下面实际的解析):可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,例如这里的键盘按下事件函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如上图所示。注意,event() 函数和事件处理函数,是在该部件内进行重新定义的,而事件过滤器却是在该部件的父部件中进行定义的。
exec的执行
通过源码的debug,深入了解开启事件循环的exec的执行过程:
- 逐步深入,最后调用的是
QCoreApplication::
exec
()
。最后本质上是启动了一个局部对象eventLoop
的exec
.
QEventLoop::
exec
本质上也是调用了processEvents
。 启动了一个循环,不断处理事件,直到exit()被调用。这里loadAcquire使用的是内存模型同步。
QEventLoop::
processEvents
。根据官方文档:This function is simply a wrapper for QAbstractEventDispatcher::processEvents(). 这里根据不同平台,选择对应的eventDispatcher。顺便说一下源码中大量出现的d、Q_D,QObjectPrivateClass之类的,QT封装的数据的方式也很有意思。
- 总之,会调用具体分发器的事件处理. 当然Windows下也还有细分。
- 最终真正的实现是在
QEventDispatcherWin32::
processEvents
。核心代码也是一个事件处理的循环。
这就涉及Windows平台的事件系统,比如句柄、消息等了,就不往下展开了。
可简化上述模型为:
举一个例子分析:
我们来看一下比较典型的事件流程是什么样的。假设某个界面程序上有一个 QPushButton 按钮,它的clicked
信号被连接到了一个doWork()
槽函数上。当用户在按钮上点击鼠标时,操作系统会给程序发送一个鼠标点击的事件,事件循环被唤醒,并将其转换为 QMouseEvent 对象,然后添加到事件队列中。随后在下一次循环时,事件从队列中取出,发送给 QPushButton 对象进行处理,进而在 QPushButton 事件处理函数的内部触发了clicked
信号,最终使得doWork()
函数被调用。假设点击事件发生后没有其他外部事件产生,doWork()
函数执行完成后,也没有产生新的内部事件,那么此时队列没有其他事件可以继续处理,事件循环再次进入等待状态,否则就继续处理队列中的其他事件,直到将事件队列为空。 在事件循环中,事件只能一个接一个的进行处理,在前面的事件处理完成之前,后续的事件只能在队列中等待。在上面的例子中,如果doWork()
的执行时间很长会发生什么呢?事件循环必须等待doWork()
函数执行完返回后才能处理下一个事件,在这期间内,事件循环相当于被卡住了。卡住的事件循环无法处理任何事件,也无法收到新的外部事件,所以此时在这个程序内,界面无法更新,计时器也无法触发,网络 IO 也得不到任何反馈,表面上看就是“卡住了”。并且系统的窗口管理器也能检测到程序不再处理任何事件了,所以提示用户“程序失去响应”。
示例演示和分析
代码:
子控件类
父控件类
代码里主要包括:
- 子控件类重写事件处理函数,即前面事件处理的方法1.
MyLineEdit::
keyPressEvent
- 子控件类重写event函数,即前面事件处理的方法4。
MyLineEdit::
event
- 父控件类重写事件处理函数,即前面事件处理的方法1.
Widget::
keyPressEvent
- 父控件类重写事件过滤器函数,即前面事件处理的方法5.
Widget::
eventFilter
. 这个生效的前提是给要过滤事件的控件进行注册:lineEdit->installEventFilter(this);
运行代码,结果是:
下面结合源码调用堆栈对这四个顺序进行深入分析:
Widget::
eventFilter
从堆栈可以看到很多前面提到的函数调用。比如:
QEventDispatcherWin32::
processEvents
,具体是调用下面的代码。通过DispatchMessage
分发消息。然后内部调用USER32。事件系统的一系列处理接口,
QWindowsGuiEventDispatcher::
sendPostedEvents
、QGuiApplicationPrivate::processKeyEvent。
重要的接口
QApplication::
notify
(这时已经拆解出事件接收者和事件了,接受者是从QWindowSystemInterfacePrivate::WindowSystemEvent
里拆出来的window,这里就是WidgetWindow,即我们定义的Widget最外层的界面)notify的官方文档是”Sends event to receiver: receiver->event(event).”
因此接下来就调用WidgetWindow的event,然后调用
forwardEvent
向上抛出(抛给这个顶层界面内部的具有焦点的控件)然后再次调用
notify
.这时接收者已经是输入框的控件。在
notify_helper
中,有一段处理处理,返送给eventFilters处理。这段代码说明两点,一个是EventFilters
要起作用,那肯定是要添加到事件接收者那个控件内才对其起作用。第二是如果如果其中的eventFilter
返回了true,那就会直接捕捉完成这个事件,将不再继续处理该事件。MyLineEdit::
event
前面部分跟上一个都是一样的。只是在前面处理完成后,在
notify_helper
代码中后一行,就会调用事件接受者(MyLineEdit
)的event函数。注意event函数是处理所有事件的,所以重写event只需要判断其中自己关注的事件即可。MyLineEdit::
keyPressEvent
前面的同上一点。只是处理完自己的event函数后,往上返回父类的event,直到使用
QWidget::event
提供默认的各种事件处理函数,这里就是keyPressEvent
.然后调用这个虚事件处理函数。Widget::
keyPressEvent
前面的还是同上。处理完事件接收者上的所有事件处理方式后,如果这个事件还是还没标记为处理完,即使用责任链的方式,将这个事件传递给它的父亲控件处理。可以看到有一个While循环进行处理。最终同上一点一样,会调用到
keyPressEvent
函数。强制处理事件
from:浅谈Qt事件系统与事件编程 – 李拜六的博客 (imlb6.com)的讨论。
在实际开发中,确实经常有一些很耗时的操作需求,比如复制大量文件,读写大量数据等等。这种情况一般有两种实现方案,一种是开启子线程进行耗时逻辑处理,避免主线程的事件循环阻塞,这种方式本文不做详细讨论,这里主要解释另一种:强制让事件循环处理事件。
调用 QCoreApplication::processEvents() 函数可以强制使事件循环分发处理队列中剩下的事件,直到队列中没有事件为止,或者直到给定的时间结束。
还是用前面的例子,假设
doWork()
函数是要向某个文件写入大量数据,执行时间很长,如果不采取特别的措施,在doWork()
执行过程中肯定会阻塞事件循环,使界面失去响应。但如果在写入数据的过程中,没间隔固定一段,比如每写入 4MB,就调用一次processEvents()
,就相当于在这个非常耗时的逻辑中,不时的间隔暂定一下,转而去处理事件队列中的事件。这样一种方式,就能在执行耗时逻辑的同时,穿插处理事件,而不至于让界面失去响应。当然,这个间隔需要开发者自己把握,如果间隔太小调用太频繁,会导致效率降低,如果间隔太大调用又太少,还是会导致一段时间内无法无法处理事件,界面卡顿。调用
QCoreApplication::
processEvents
的原理是:记得事件循环前面的简化模型吗:
这里相当于在调用dispatch_next_event时,分发前面一个事件处理耗费时间太长了,然后自己手动调用
processEvents
,就相当于跳过了while循环中这一个事件的处理,然后继续处理其他事件了。测试代码:
堆栈就可以看出来。在MyLineEdit一个函数迟迟完不成,然后手动调用processEvents,就恢复到启动事件循环时的processEvents一样了。
特殊的事件处理
测试过程发现中文输入法下键盘输入无法正常被捕获为事件键盘事件,需特殊处理。具体参考:
- 作者:Olimi
- 链接:https://olimi.icu/article/1a07fb1b-e691-4147-915c-09ec4546a36b
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。