QObject事件机制
-
-
-
- 图片地址:百度网盘-链接不存在 (提取码:8888)
- 这里先简单了解事件机制(简单版本事件机制),后面会详细讲解
- 事件机制相比较于"信号与槽"机制,信号与槽机制是对事件机制的高级封装,事件机制更偏底层
- 事件机制的应用场景
- 一般情况下, 我们直接通过内置的"信号与槽"就可以解决一般通讯问题(例如:QPushButton的 clicked 等信号)
- 但是有些控件并没有提供给我们想要的信号, 我们就需要自己重写具体的事件函数, 来捕获产生的事件, 做相应的处理(例如:QLabel 并没有clicked信号)
- 某些场景并不会把我们想要捕获的事件传递给特定函数, 而是做了其他的额外处理, 此时, 我们可以重写事件的分发函数event(例如:想捕获用户Tab键的点击。当用户点击了Tab键, 默认是切换焦点,并不会把这个事件分发给keyPressEvent函数,此时, 就需要我们自己重写event, 来做分发处理)
- 如果想同时对多个不同的控件进行捕获Tab点击, 那么每个都重写event()函数, 也是非常麻烦的, 所以, 考虑 安装"事件过滤器"
- QApplication对象的事件过滤器, 可以拦截所有的QObject事件; 一般不怎么使用
- QApplication对象的notify()更不怎么用, 会大大降低程序性能
- 事件的传递
- 如果一个控件没有处理该事件, 则会自动传递给父控件进行处理
- 事件对象具备两个特殊方法
accept() # 自己处理了这个事件, 并告诉系统不要再向上层传递 ignore() # 自己忽略了这个事件, 告诉系统, 继续往后传递去
简单版事件机制图
-
- 分解
-
- 首先,我们用电脑打开一个应用软件
- 操作系统上运行一个应用软件
- 一个应用程序(Qt)对应有一个QApplication对象
- 一个应用程序会有几个消息队列(消息队列:存放用户操作系统时出现的很多消息,让这些消息按照一定的顺序进行排列)
- 应用程序的消息循环,就是在处理这些队列中的消息
-
- 当用户操作系统时(如鼠标点击登录、点击注册...)就会产生各种事件消息
- 第一个接收到消息的是操作系统,操作系统会将消息分发到对应的应用程序的“消息队列”中
- 应用程序有多个消息队列,其中最主要的有两个:
- 一个用来存放系统分发过来的消息(如鼠标事件、键盘事件、拖放事件......)
- 一个用来存放应用程序内部产生的消息(如:定时器事件、绘屏事件......)
- 当程序启动时调用app.exec__()方法,调用exec__()方法会开启一个消息循环。让其不断循环监测上面两个消息队列
- 这个消息循环主要干两件事情:
- 循环监测消息队列。如果监测过程中一直没有消息事件,那么这个消息循环将一直循环监测
- 当用户用鼠标点击登录按钮的时候,系统会将这个鼠标点击事件的消息分发到这个应用程序的消息队列中。这个时候消息循环就能够监测到这个消息事件,接下来就会将这个消息事件包装成"QEvent对象",并进行分发处理
- 包装成"QEvent对象",并进行分发处理:实际上是将事件接收者(receiver)和事件对象(evt)传递给QApplication对象的notify(receiver,evt)方法
notify(receiver,evt)
方法负责事件的分发,发送给对应的对象进行处理 - QApplication对象的notify()方法会进行二次分发,会将消息事件分发给receiver对象(事件接收者)的event(evt)方法
- receiver对象(事件接收者)的event()方法,再根据evt的事件类型分发给receiver(事件接收者)对象的具体事件函数
- receiver(事件接收者)对象的具体事件函数
mousePressEvent(evt) # 鼠标按下事件 mouseReleaseEvent(evt) # 鼠标释放事件 mouseClickEvent(evt) # 鼠标单击事件 . . .
- 部分控件在这些具体的事件函数(方法)中可能发射了对应的信号
比如 QPushButton控件的mouseClickEvent(evt)事件函数(方法)中就发射了clicked信号 我们就可以通过这个信号来连接槽函数 btn.clicked.connect(槽函数)
个人理解
-
- 当用户点击登录按钮时,操作系统最先接收到相关信息: *
接收对象(receiver) ——> 按钮(QPushButton对象) evt ——> 鼠标单击事件
- 操作系统将接收到的相关信息分发给应用程序,应用程序根据分发过来的evt事件类型(鼠标单击事件),将这个消息事件排列到相应的消息队列中(消息队列1)
- 消息循环(app.exec__())监测到消息队列1中这个消息事件后,再将接收到的receiver对象(QPushButton对象)和evt(鼠标单击事件)分发给应用程序中QApplication对象的notify()方法
- QApplication对象的notify()方法会根据接收到的receiver对象(QPushButton对象)调用receiver对象(QPushButton对象)的event()方法,并将evt事件类型(鼠标单击事件)传递进去
- receiver对象(QPushButton对象)的event()方法再根据接收到的evt事件类型(鼠标单击事件)将消息事件分发给到receiver对象(QPushButton对象)的具体事件函数(鼠标单击事件函数)
- 如果这个具体的事件函数设置了相应的信号,那么此时该事件函数执行,将发射对应的信号
通过代码演示Qt事件机制
- 第一步:创建一个模拟软件登录界面(一个窗口,两个按钮)
import sys from PyQt5.Qt import * app = QApplication(sys.argv) window = QWidget() btn1 = QPushButton(window) btn1.setText('登录') btn1.move(100, 100) btn2 = QPushButton(window) btn2.setText('取消') btn2.move(100, 150) window.show() sys.exit(app.exec_())
-
- 第二步:通过信号与槽的机制,响应用户点击按钮
import sys from PyQt5.Qt import app = QApplication(sys.argv) window = QWidget() btn1 = QPushButton(window) btn1.setText('登录') btn1.move(100, 100) btn2 = QPushButton(window) btn2.setText('取消') btn2.move(100, 150) def cao1(): print(f'用户按下了【登录】按钮—信号与槽机制') def cao2(): print(f'用户按下了【取消】按钮—信号与槽机制') btn1.pressed.connect(cao1) btn2.pressed.connect(cao2) window.show() sys.exit(app.exec_())
- 第三步:分步讲解从用户点击按钮到槽函数响应并打印出相应内容
- 软件程序打开后,操作系统(电脑)内就会有一个应用程序在运行
- 当用户点击这个应用程序的“登录”按钮时,就会产生一个事件消息
- 第一个接收到这个事件消息的是操作系统(电脑),此时操作系统会发现这个事件消息是产生自哪个应用程序
- 操作系统就会将这个事件消息分发给这个应用程序的消息队列
- 当应用程序启动的时候,就开启了一个消息循环(app.exec__()),这个消息循环在不间断的监测该应用程序的消息队列
- 当消息循环监测到该应用程序消息队列中的事件消息时,就会将这个事件消息包装成QEvent对象进行分发——>分发给QApplication对象的notify()方法
- 我们查看一下这个QApplication对象的notify()方法到底有没有接收到这个事件消息(那么我们就需要重写notify()这个方法)
- QApplication对象的notify()方法是系统方法,不能直接重写。那我们就创建一个类,继承自QApplication对象,此时子类什么都不写(pass)。此时执行程序是正常的。因为创建app对象的时候调用QApp类,此时QApp类里面什么方法都没有,那么就会去QApp类的父类里面去找
import sys from PyQt5.Qt import * class QApp(QApplication): pass app = QApp(sys.argv) window = QWidget() btn1 = QPushButton(window) btn1.setText('登录') btn1.move(100, 100) btn2 = QPushButton(window) btn2.setText('取消') btn2.move(100, 150) def cao1(): print(f'用户按下了【登录】按钮—信号与槽机制') def cao2(): print(f'用户按下了【取消】按钮—信号与槽机制') btn1.pressed.connect(cao1) btn2.pressed.connect(cao2) window.show() sys.exit(app.exec_())
- QApplication对象的notify()方法是系统方法,不能直接重写。那我们就创建一个类,继承自QApplication对象,此时子类什么都不写(pass)。此时执行程序是正常的。因为创建app对象的时候调用QApp类,此时QApp类里面什么方法都没有,那么就会去QApp类的父类里面去找
- QApplication对象的notify()方法无法重写,那么只能通过子类重写notify()方法
import sys from PyQt5.Qt import * class QApp(QApplication): def notify(self, receiver, evt): print('QApp对象里面的notify方法',receiver, evt) return super().notify(receiver, evt) app = QApp(sys.argv) window = QWidget() btn1 = QPushButton(window) btn1.setText('登录') btn1.move(100, 100) btn2 = QPushButton(window) btn2.setText('取消') btn2.move(100, 150) def cao1(): print(f'用户按下了【登录】按钮—信号与槽机制') def cao2(): print(f'用户按下了【取消】按钮—信号与槽机制') btn1.pressed.connect(cao1) btn2.pressed.connect(cao2) window.show() sys.exit(app.exec_())
-
此时运行程序,我们发现控制台会不断的打印内容,说明notify()方法在不断执行。是因为操作系统捕获到了很多跟这个应用程序相关的事件消息,就会不断的分发给这个应用程序的QApplication对象的notify()方法
-
-
此时我们只需要在接收到QPushButton对象事件消息的时候才打印相应内容,那么我们就需要对事件接收者对象(receiver)进行判断
class QApp(QApplication): def notify(self, receiver, evt): if receiver.inherits('QPushButton'): print('QApp对象里面的notify方法',receiver, evt) return super().notify(receiver, evt)
-
此时再运行的时候,就只有QPushButton对象的事件消息才会触发notify()方法中的print()方法进行打印了
-
此时接收到的QPushButton对象的所有事件消息都会触发这个打印,那么我们只需要在QPushButton的按下事件(MouseButtonPress)时才触发,那么我们就要继续对事件对象(evt)的类型进行判断了
class QApp(QApplication): def notify(self, receiver, evt): if receiver.inherits('QPushButton') and evt.type() == QEvent.MouseButtonPress: print('QApp对象里面的notify方法',receiver, evt) return super().notify(receiver, evt)
-
此时,我们再运行程序,就只有鼠标按下按钮的时候才会触发notify()方法中的打印了
-
-
- 如果我们对nogiry()方法再进行修改一下,如果接收事件消息对象是QPushButton对象,并且事件对象(evt)的类型是MouseButtonPress时,只打印指定内容,并不执行其他操作,如果不是才继续调用父类的notify()方法
class QApp(QApplication): def notify(self, receiver, evt): if receiver.inherits('QPushButton') and evt.type() == QEvent.MouseButtonPress: print('QApp对象里面的notify方法',receiver, evt) else: return super().notify(receiver, evt)
- 此时我们再运行程序,并按下按钮的时,槽函数就不会执行了。原因:当是按钮按下事件的时候,就只执行了打印操作,而不会执行父类的notify()方法了,那么事件就不能继续分发下去了,就无法触发QPushButton对象的Event()方法中的信号了,信号不触发,就无法连接槽函数了
- 这里,我们仅仅是做一个测试效果,我们再将代码修改回来。那么此时,所有的事件消息都会通过父类的notify()方法根据事件接收者(receiver)分发给到具体的对象(我们这个案例中的事件接收者(receiver)是QPushButton对象)。那么,我们想要拦截这个案例中事件接收者(receiver)的Event()方法,那么就需要重写QPushButton对象的Event()方法了。同样的QPushButton对象的Event()方法是系统方法,无法重写。只能通过子类继承的方法进行重写
- 创建Btn类,继承自QPushButton。应用程序中的按钮通过Btn类来创建
import sys from PyQt5.Qt import * class QApp(QApplication): def notify(self, receiver, evt): if receiver.inherits('QPushButton') and evt.type() == QEvent.MouseButtonPress: print('QApp对象里面的notify方法',receiver, evt) return super().notify(receiver, evt) class Btn(QPushButton): pass app = QApp(sys.argv) window = QWidget() btn1 = Btn(window) btn1.setText('登录') btn1.move(100, 100) btn2 = Btn(window) btn2.setText('取消') btn2.move(100, 150) def cao1(): print(f'用户按下了【登录】按钮—信号与槽机制') def cao2(): print(f'用户按下了【取消】按钮—信号与槽机制') btn1.pressed.connect(cao1) btn2.pressed.connect(cao2) window.show() sys.exit(app.exec_())
- 此时,我们再次运行程序。当鼠标按下按钮的时候,操作系统将事件消息分发给该应用程序。此时接收事件者(receiver)就成了Btn对象了
- 应用程序接收到操作系统分发过来的事件消息,再将这个时间消息排列到消息队列中
- 应用程序的消息循环监测到这个事件消息,就会将receiver事件接收者(Btn对象)和evt事件对象(MouseButtonPress)包装成"QEvent对象",分发给QApplication对象的notify()方法
- QApplication的notify()方法再根据接收到的receiver,将事件消息分发给Btn对象的event()方法
- 此时,我们重写Btn对象的event()方法
class Btn(QPushButton): def event(self, evt) -> bool: print('Btn中的event方法', evt) return super(Btn, self).event(evt)
- 运行程序可以看到,执行了很多个Btn对象event方法中的打印。原因:QPushButton对象有很多事件消息,这些事件消息分发过来的时候,都会分发给到QPushButton对象的event方法,从而执行event方法中的打印
-
- 运行程序可以看到,执行了很多个Btn对象event方法中的打印。原因:QPushButton对象有很多事件消息,这些事件消息分发过来的时候,都会分发给到QPushButton对象的event方法,从而执行event方法中的打印
- 此时,我们对分发过来的evt事件类型进行判断。当事件类型是按钮按下事件(MouseButtonPress)时,才执行打印
class Btn(QPushButton): def event(self, evt): if evt.type() == QEvent.MouseButtonPress: print('Btn中的event方法', evt) return super().event(evt)
- receiver事件接收者的envent()方法会根据接收到的evt事件类型将事件消息分发给receiver事件接收者(Btn对象)具体的事件函数(这个案例中是按钮按下事件函数mousePressEvent)
- 那么,我们重写Btn类的mousePressEvent方法,查看是否能接收到事件消息
class Btn(QPushButton): def event(self, evt): if evt.type() == QEvent.MouseButtonPress: print('Btn中的event方法', evt) return super().event(evt) def mousePressEvent(self, evt): print('Btn中的MouseButtonPress方法', evt)
- 此时运行程序,我们可以看到输出结果中打印了相关内容,但是槽函数确没有执行。原因:我们重写了mousePressEvent方法,仅仅只是做了print的工作,并没有做其他工作。
- 如果需要发射信号,那么就必须在重写的时候加上发射信号的功能,或者调用父类的mousePressEvent()方法,通过父类的mousePressEvent()方法来发射信号
class Btn(QPushButton): def event(self, evt): if evt.type() == QEvent.MouseButtonPress: print('Btn中的event方法', evt) return super().event(evt) def mousePressEvent(self, evt): print('Btn中的MouseButtonPress方法', evt) return super().mousePressEvent(evt)
- 此时,我们再来看一下运行结果
-
- 这里我们就可以明显的看出来一个事件消息是如何层层分发,最终到事件接收者的具体事件函数上
- 完整代码如下
import sys from PyQt5.Qt import * class QApp(QApplication): def notify(self, receiver, evt): if receiver.inherits('QPushButton') and evt.type() == QEvent.MouseButtonPress: print('QApp对象里面的notify方法',receiver, evt) return super().notify(receiver, evt) class Btn(QPushButton): def event(self, evt): if evt.type() == QEvent.MouseButtonPress: print('Btn中的event方法', evt) return super().event(evt) def mousePressEvent(self, evt): print('Btn中的MouseButtonPress方法', evt) return super().mousePressEvent(evt) app = QApp(sys.argv) window = QWidget() btn1 = Btn(window) btn1.setText('登录') btn1.move(100, 100) btn2 = QPushButton(window) btn2.setText('取消') btn2.move(100, 150) def cao1(): print(f'用户按下了【登录】按钮—信号与槽机制') def cao2(): print(f'用户按下了【取消】按钮—信号与槽机制') btn1.pressed.connect(cao1) btn2.pressed.connect(cao2) window.show() sys.exit(app.exec_())
- 案例中,btn1是通过Btn类创建的对象,btn2却是通过QApplication类创建的对象。那我们执行一下程序,分别点击btn1和btn2按钮,看下输出结果
-
- 这里明显可以看到,按下btn1和btn2的输出结果不一致。原因就在于btn2是QPushButton对象创建的,事件消息分发的时候,btn2的receiver事件接收者是QPushButton
- QPushButton的notify()方法分发事件消息的时候就将btn2的事件消息分发给QPushButton对象的event()方法,QPushButton对象的event方法并没有print打印功能,所以按下btn按钮时候,并不会有中间两次打印
- 最后我们再通过图来理顺一下简单版事件消息的分发过程
-