PyQt5中的事件系统之事件(QEvent)的传递(分发)和处理
使用Qt编程,几乎不用考虑事件,因为当产生某种事件时,Qt窗口部件都会发射一个相应的信号(即Qt会把事件转换为一个对应的信号),比如按钮被按下时,会产生一个MouseButtonPress 事件, Qt 会处理这一事件,并且会发射一个 clicked()单击信号,程序员可以直接处理 clicked()信号,而不必处理底层的事件。
对于事件,我们不需要知道Qt是怎么把事件转换为QEvent或其子类类型的对象的,我们只需要知道怎么传递和处理这些事件即可。比如对于按下鼠标事件,不需要知道Qt是怎样把该事件转换为QMouseEvent类型的对象的,只需要知道怎么传递和处理该事件即可。
1. QApplication、 QGuiApplication、 QCoreApplication 简介
-
继承关系见下图,其中左侧为顶级父类
-
一个程 序中只能有一个 QCoreApplication 及其子类的对象
-
QCoreApplication:主要提供无 GUI 程序的事件循环
-
QGuiApplication:用于管理 GUI 程序的控制流和主要设置
-
QApplication:该类专门为 QGuiApplication 提供基于 QWidget 的程序所需的一些功能,主要用于处理部件的初始化、最终化。主要职责如下:
- 使用用户的桌面设置初始化应用程序。
- 执行事件处理,也就是说该类能从底层系统接收并分发事件。比如,使用
QCoreApplication::sendEvent()或 QCoreApplication::postEvent()函数分发自定义事件。 - 解析常用命令行参数并设置其内部状态。
- 定义了应用程序的界面外观,可使用 QApplication::setStyle()进行更改。
- 指定应程程序如何分配颜色。
- 使用 QCoreApplication::translate()函数对字符串进行转换。
- 通过 QApplication::desktop()函数处理桌面,通过 QCoreApplication::clipboard()函数处理剪贴板。
- 管理应用程序的鼠标光标。比如使用 QGuiApplication::setOverrideCuresor()函数设置
光标等。
2. 事件的传递
QApplication、 QGuiApplication、 QCoreApplication 简介
在调用QApplication类的exec()函数时,它会使Qt应用程序进入事件循环,这样就可以使应用程序在运行时接收到发生的各种事件。一旦有事件发生,Qt便会构建一个相应的QEvent子类的对象来表示它,然后将它传递给相应的QObject对象或其子对象。
事件传递的步骤:
-
基本规则:若事件未被目标对象处理,则把事件传递给其父对象处理,若父对象仍未处理,则再传递给父对象的父对象处理,重复这个过程,直至这个事件被处理或到达顶级对象为止。 注意:事件是在对象间传递的,这里是指对象的父子关系,而不是指类的父子关系。
-
在 Qt 中有一个事件循环,该循环负责从可能产生事件的地方捕获各种事件,并把这些事件转换为带有事件信息的对象,然后由 Qt 的事件处理流程分发给需要处理事件的对象来处理事件。
-
通过调用 QCoreApplication::exec()函数启动事件主循环,主循环从事件队列中获取事件,然后创建一个合适的 QEvent 对象或其子类类型的对象来表示该事件, 在此步骤中,事件循环首先处理所有发布的事件,直到队列为空,然后处理自发的事件,最后处理在自发事件期间产生的已发布事件。注意:发送的事件不由事件循环处理,该类事件会被直接传递给对象
-
然后, Qt 会调用 QCoreApplication::notify()函数对事件进行传递(或分发)
-
最后, QObject 对象调用 QObject::event()函数接收事件
-
event()函数是一个虚函数, 是 QObject 对象处理事件的入口
-
在 QObject 的子类中通常会重写 event()函数,比如 QWidget 类就重写了 event()函数。
-
event()函数与事件处理函数的关系: event()函数负责把事件传递给目标对象并调用对应的事件处理函数处理事件,比如调用 QWidget::keyPressEvent()函数处理键盘按下事件等,注意, QObject 中的 event()函数并不能实现该功能,这是通过QObject 的子类中重写的 event()函数实现的,比如调用 QWidget::keyPressEvent()函数,就是由在 QWidget 类中重写的 event()函数来完成的。
-
event()函数对大多数事件都调用了默认的处理函数,但并不能包括全部的事件,因此,有时我们需要重写该函数,这也是我们处理 Qt 事件的方式之一。
-
event()函数不会处理事件,他只是根据事件的类型进行事件的传递(或分发),若返回 true 则表示这个事件被接受并进行了处理,否则事件未被处理,需进行进一步传递或丢弃。
-
3. 事件的处理
一个事件由一个特定的QEvent子类来表示,但是有时候一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都是由QEvent类的枚举型QEvent::Type来表示,其中包含了一百多种事件类型,可以在QEvent类的帮助文档中进行查看。虽然QEvent的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?QCoreApplication 类的notify()方法的帮助文档给出了5种处理事件的方法:
-
方法一:重新实现部件的paintEvent()、mousePressEvent()等事件处理函数。这是最常用的一种方法,不过只能用来处理特定部件的特定事件。例如实现拖放操作就是用的这种方法。
-
方法二:子类化QApplication, 并重新实现QCoreApplication::notify()方法。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。重写该函数会很复杂,也因此此方法很少被使用。这是唯一能在事件过滤器之前捕获到事件的方式
-
方法三:向QApplication对象上安装(注册)事件过滤器。因为一个程序只有一个QApplication对象,所以这样实现的功能与使用nofity()方法是相同的,优点是可以同时处理多个事件。一旦在QApplication对象上注册了事件过滤器,则程序中每个对象的每个事件都会在发送到其他事件过滤器之前,发送到这个eventFilter()方法。该方式对于调试非常有用。
-
方法四:重新实现event()方法。QObject类的event方法可以在事件到达默认的事件处理函数之前获得该事件。该方式常被用来处理Tab键的默认意义。在重新实现event()方法时,必须对未明确处理的事件调用基类的event()方法,比如,若子类化QWidget,并重写了event()方法,但未调用父类的event(),则程序可能会不能正常显示界面。
-
方法五:在QObject对象上安装(注册)事件过滤器。对象一旦使用installEventFilter()注册,传递给目标对象的所有事件都会先传递给这个监视对象的eventFilter()方法。如果同一对象上安装了多个事件处理器,则按照安装的逆序依次激活这些事件处理器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自QApplication类,而方法三需要使用一个全局的事件过滤器,这将减缓事件的传递,所以虽然这两种方法功能很强大,但是却很少被用到。
4. 代码示例
事件处理方式代码示例:
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QEvent, qDebug
from PyQt5 import QtGui
import sys
class MyWidget(QWidget):
def __init__(self):
super().__init__()
def event(self, e: QEvent) -> bool:
'''
重写QObject::event
'''
if e.type() == QEvent.Type.KeyPress:
qDebug(f'key down')
return super().event(e)
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
'''
重写QWidget类的事件处理函数mousePressEvent
'''
qDebug(f'mouse down')
return super().mousePressEvent(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
my_widget = MyWidget()
my_widget.show()
sys.exit(app.exec_())
运行效果如下:
重写notify函数代码示例:
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import qDebug, QEvent, QObject
import sys
import typing
class MyWidget(QWidget):
def __init__(self):
super().__init__()
def event(self, e: QEvent) -> bool:
qDebug(f'my event...') # 验证该函数是否被执行
return super().event(e)
class MyApplication(QApplication):
def __init__(self, argv: typing.List[str]) -> None:
super().__init__(argv)
def notify(self, o: QObject, e: QEvent) -> bool:
qDebug(f'my notify...')
# 若对象为my_widget且为键盘按下事件
if o.objectName() == 'my_widget' and e.type() == QEvent.Type.KeyPress:
qDebug(f'keydown')
# 一定要调用父类的notify方法,否则本例中event不会被执行
return super().notify(o, e)
if __name__ == '__main__':
app = MyApplication(sys.argv)
my_widget = MyWidget()
my_widget.setObjectName('my_widget')
my_widget.show()
sys.exit(app.exec_())
运行效果如下:
本例中可以看到notify和event方法的执行顺序。
事件传递示例代码:
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtCore import qDebug, QEvent
import sys
class MyWidget(QWidget):
def __init__(self):
super().__init__()
def event(self, e: QEvent) -> bool:
if e.type() == QEvent.Type.KeyPress:
qDebug(f'{self.objectName()} : key down ')
if e.type() == QEvent.Type.MouseButtonPress:
qDebug(f'{self.objectName()} : mouse down')
if e.type() == QEvent.Type.MouseButtonRelease:
qDebug(f'{self.objectName()} : mouse release')
return super().event(e)
class MyButton(QPushButton):
def event(self, e: QEvent) -> bool:
if e.type() == QEvent.Type.KeyPress:
# 如果是键盘按下事件,将事件传递给父对象处理
qDebug(f'{self.objectName()} : key down')
return False
if e.type() == QEvent.Type.MouseButtonPress:
# 如果是鼠标按下事件,则处理完毕,事件不传递
qDebug(f'{self.objectName()} : mouse down')
return True
return super().event(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
my_widget = MyWidget()
my_btn = MyButton('test', my_widget)
# 设置对象名
my_widget.setObjectName('my_widget')
my_btn.setObjectName('my_btn')
my_widget.show()
sys.exit(app.exec_())
运行效果如下:
小手一抖,点个赞再走哦~~~