1. 重新实现event()
2. 使用事件过滤器
3. 全局事件过滤器
4. 处理耗时任务时保持窗口响应
在《快速掌握PyQt5》 事件处理这一章节中,笔者给大家介绍了一些比较常见的事件函数,并通过案例进行了演示。然而还并没有真正地体现出PyQt5事件功能的强大之处。在本章,笔者会带大家了解如何去编写一个更加个性化的事件函数以及其他一些在跟事件相关的知识点。
虽然PyQt5已经给我提供了许多内置的事件函数,如mousePressEvent(),closeEvent(),keyPressEvent()等等。但其实事件类型有很多,而一些事件类型并没有提供相应的内置函数。举个例子:我想让窗口响应鼠标进入事件,然而并没有mouseEnterEvent()。
所以这个时候我们应该通过重载event()的方式来实现我们想要的事件响应:
现在通过下面这个例子来演示下如何让窗口相应鼠标进入事件:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
def event(self, e):
if e.type() == e.Enter:
print('鼠标进入了窗口')
return True
return super(Demo, self).event(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
如果事件类型e.type()等于e.Enter的话,那么我们就捕获了这次事件,在控制台中打印消息,最后返回一个True。如果是其他事件类型的话,就交给父类的event()处理。
运行截图如下:
其实光从名字我们就可以猜出事件过滤器的作用——过滤掉我们不想要的事件。这个强大的过滤器功能可以让我们编写出更加个性化的事件处理。实现方法很简单,我们先重载这个eventFilter函数:
当这个函数重载完毕之后,我们再让相应对象调用installEventFilter就行了。如果不想要这个过滤器,那么就调用removeEventFilter卸载掉。
事件过滤器会在内置事件函数之前先捕获相应的事件,如果该事件在eventFilter中被过滤了,那么内置事件函数将不会再进行响应。比方说某个输入框的键盘事件在eventFilter中被过滤了,那么被该输入框的keyPressEvent()事件函数也就不会响应了。
我们通过一个案例来演示下:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.line = QLineEdit() # 1
self.btn = QPushButton('安装事件过滤器')
self.btn.clicked.connect(self.install_remove_slot)
v_layout = QVBoxLayout()
v_layout.addWidget(self.line)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)
def install_remove_slot(self):
if self.btn.text() == '安装事件过滤器':
self.line.installEventFilter(self)
self.btn.setText('卸载事件过滤器')
else:
self.line.removeEventFilter(self)
self.btn.setText('安装事件过滤器')
def eventFilter(self, watched, event): # 2
if watched == self.line and event.type() == event.KeyPress:
print('键盘事件被过滤')
return True
return super(Demo, self).eventFilter(watched, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 实例化一个QLineEdit输入框,该控件将被作为过滤器演示对象,而按钮用于执行给输入框控件安装或卸载过滤器的操作。
2. 在eventFilter函数中,我们进行一个判断,如果对象是之前实例化好的输入框,而且是键盘事件的话,就返回True将这个事件给过滤掉。注意未处理的事件我们是要返回给基类的eventFilter函数的,所以下面这行代码还需要加上:
return super(Demo, self).eventFilter(watched, event)
运行截图如下:
未安装事件过滤器,键盘事件可以接收,可以输入文本
安装事件过滤器后,键盘事件被过滤,无法输入任何文本
卸载事件过滤器,键盘事件可以再次接收,可以输入文本
我们可以将事件过滤器单独分离出来,使其全局可用:
import sys
from PyQt5.QtCore import QObject
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout
class KeyPressEater(QObject): # 1
def __init__(self):
super(KeyPressEater, self).__init__()
def eventFilter(self, watched, event):
if event.type() == event.KeyPress:
print('键盘事件被过滤')
return True
return super(KeyPressEater, self).eventFilter(watched, event)
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.line1 = QLineEdit()
self.line2 = QLineEdit()
self.line3 = QLineEdit()
self.line1.installEventFilter(keypress_eater)
self.line2.installEventFilter(keypress_eater)
self.line3.installEventFilter(keypress_eater)
v_layout = QVBoxLayout()
v_layout.addWidget(self.line1)
v_layout.addWidget(self.line2)
v_layout.addWidget(self.line3)
self.setLayout(v_layout)
if __name__ == '__main__':
keypress_eater = KeyPressEater() # 2
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 单独在一个类中编写eventFilter事件过滤函数,过滤所有的键盘事件。
2. 在程序入口处实例化键盘过滤器对象,然后给三个输入框全都安装上。
运行截图如下:
但是一个个调用installEventFilter函数也太麻烦了,毕竟控件数量太多。其实我们可以直接给QApplication直接安装上过滤器,这样的话应用程序中的每个对象就都不会接收到被过滤的事件了。这种操作在程序调试时非常有用,现在让我们修改下程序入口处的代码:
if __name__ == '__main__':
keypress_eater = KeyPressEater()
app = QApplication(sys.argv)
app.installEventFilter(keypress_eater)
demo = Demo()
demo.show()
sys.exit(app.exec_())
在入口处实例化一个过滤器对象,然后给QApplication安装上即可。
如果我们想让个别控件不被过滤器影响,那么直接改下eventFilter内容就行了:
import sys
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QWindow
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout
class KeyPressEater(QObject):
def __init__(self):
super(KeyPressEater, self).__init__()
def eventFilter(self, watched, event): # 1
if type(watched) != QWindow and watched.objectName() != 'line3':
if event.type() == event.KeyPress:
print('键盘事件被过滤')
return True
return super(KeyPressEater, self).eventFilter(watched, event)
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.line1 = QLineEdit()
self.line2 = QLineEdit()
self.line3 = QLineEdit()
self.line3.setObjectName('line3') # 2
v_layout = QVBoxLayout()
v_layout.addWidget(self.line1)
v_layout.addWidget(self.line2)
v_layout.addWidget(self.line3)
self.setLayout(v_layout)
if __name__ == '__main__':
keypress_eater = KeyPressEater()
app = QApplication(sys.argv)
app.installEventFilter(keypress_eater)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 判断控件的对象类型和名称。因为窗口会首先接收到事件,所以这里要先排除QWindow类型。如果watched是一个QLineEdit对象的话,那么需要判断它的对象名称,不能是'line3'。这样self.line3的键盘事件就不会被过滤,只会过滤self.line1和self.line2的。
2. 给self.line3控件设置一个对象名称。
运行截图如下:
通常我们会首先想到使用多线程来处理耗时任务,但其实还有中更加简单的办法就是调用QApplication.processEvents()。该函数会在程序执行耗时任务时去刷新界面,处理界面上待响应的事件,解决界面卡顿无响应的问题。
我们只需要在执行耗时操作的代码中调用QApplication.processEvents()即可,请看示例:
import sys
from PyQt5.QtCore import Qt, QEventLoop
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.count = 0
self.btn = QPushButton('计数', self)
self.btn.clicked.connect(self.count_slot)
self.label = QLabel('0', self)
self.label.setAlignment(Qt.AlignCenter)
v_layout = QVBoxLayout()
v_layout.addWidget(self.label)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)
def count_slot(self):
while True:
self.count += 1
self.label.setText(str(self.count))
QApplication.processEvents()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
按钮所连接的count_slot槽函数执行一个无限计数操作,并将数值通过self.label控件显示在界面上。如果代码中没有添加QApplication.processEvents()的话,那么当用户点击按钮时,界面就会卡顿甚至崩溃。
运行截图如下:
我们还可以往processEvents函数中传入一个flag,通过该flag来控制函数在更新界面时要处理的事件类型。flag可以取下面一些值:
取值 | 描述 |
---|---|
QEventLoop::AllEvents | 处理所有事件 |
QEventLoop::ExcludeUserInputEvents | 不处理用户输入事件,比如鼠标事件或键盘事件 |
QEventLoop::ExcludeSocketNotifiers | 不处理套接字监听事件 |
QEventLoop::WaitForMoreEvents | 如果没有待处理事件的话,则等待事件生成 |
我们把上面的例子修改下:
def count_slot(self):
while True:
self.count += 1
self.label.setText(str(self.count))
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
运行程序,按下按钮后我们会发现界面不再处理用户的鼠标事件,按钮无法再次按下,窗口也不能关闭了。如果你的程序包含一个耗时操作,而且不希望用户打断它,那么可以使用这个方法。