Signals & Slots及Events
Qt GUI 应用程序是事件驱动的。 事件主要由应用程序的用户生成。 但它们也可以通过其他方式生成; 例如 Internet 连接、窗口管理器或计时器。 当调用应用程序的 exec
或exec_
方法时,应用程序进入主循环。 主循环获取事件并将它们发送给对象。
在事件模型中,有三个参与者:
- 事件源
- 事件对象
- 事件目标
事件源是状态发生变化的对象。 它生成事件。 事件对象(event)封装了事件源中的状态变化。 事件目标是想要被通知的对象。 事件源对象将处理事件的任务委托给事件目标。
PyQt5 有一个独特的信号和槽机制来处理事件。 信号和槽用于对象之间的通信。 当特定事件发生时会发出信号。 插槽可以是任何 Python 可调用的。 发出连接信号时调用插槽。
在文中,将详细介绍PyQt的Singal、Slote和Event。
1、Signals & Slots
Signal(信号)
是Qt小部件在发生某些事情时发出的通知。例如,从按下按钮,到输入框的文本发生变化,再到窗口的文本发生变化。 许多信号是由用户操作发起的,但也可以是其他方式,例如Internet连接、窗口管理器或计数器。
除了通知发生的事情外,信号还可以发送数据以提供有关发生情况的额外上下文。
在Qt中,除了小部件自带的信号之外,还支持自定义信号,这将在后面详细介绍。
Slots
是 Qt 用于信号接收器的名称。 在 Python 中,应用程序中的任何函数(或方法)都可以用作Slot——只需将信号连接到它即可。 如果信号发送数据,那么接收函数也将接收该数据。 许多 Qt 小部件也有自己的内置插槽,这意味着可以直接将 Qt 小部件连接在一起。
让我们看一下 Qt 信号的基础知识,以及如何使用它们来连接小部件以使事情发生在应用程序中。
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("PyQt5")
# 创建QPushButton
button = QPushButton("Click Me!")
button.setCheckable(True)
# 连接clicked事件
button.clicked.connect(self.the_button_was_clicked)
self.setCentralWidget(button)
def the_button_was_clicked(self):
print("Clicked!")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
运行代码,当点击QPushButton时,控制台将输出如下消息:
Clicked!
Clicked!
Clicked!
Clicked!
1.1 接收数据
信号支持发送数据,在前面的例子中,我们设置QPushButton的checkable
属性设置为True
,这意味着,在clicked
信号触发时,会发送QPushbutton的checkable
值。为了接收该状态值,需要重新连接clicked
信号:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("PyQt5")
# 创建QPushButton
button = QPushButton("Click Me!")
# 设置可以点击
button.setCheckable(True)
# 连接clicked事件
button.clicked.connect(self.the_button_was_clicked)
button.clicked.connect(self.the_button_was_toggled)
self.setCentralWidget(button)
def the_button_was_clicked(self):
print("Clicked!")
def the_button_was_toggled(self, checked):
print("Checked?", checked)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
运行代码,点击QPushButton将得到如下结果:
Clicked!
Checked? True
Clicked!
Checked? False
1.2 数据储存
将小部件的当前状态存储在 Python 变量中通常很有用。 可以像使用任何其他 Python 变量一样使用这些值,而无需访问原始小部件。 可以将这些值存储为单个变量,也可以根据需要使用字典。 在下一个示例中,我们将按钮的选中值存储在 self.button_is_checked
的变量中。
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.button_is_checked = True
self.setWindowTitle("My App")
button = QPushButton("Press Me!")
button.setCheckable(True)
button.clicked.connect(self.the_button_was_toggled)
button.setChecked(self.button_is_checked)
self.setCentralWidget(button)
def the_button_was_toggled(self, checked):
self.button_is_checked = checked
print(self.button_is_checked)
1.3 复杂应用示例
为了更好地理解Signal和Slot,下面将给出一个复杂的示例:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
sld.valueChanged.connect(lcd.display)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal and slot')
self.show()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
运行结果如下:
在示例中,显示了一个 QtGui.QLCDNumber 和一个 QtGui.QSlider。 通过拖动滑块旋钮来更改 lcd 编号。
sld.valueChanged.connect(lcd.display)
这里我们将滑块的一个 valueChanged 信号连接到 lcd 数字的显示槽。
发送者是发送信号的对象。 接收器是接收信号的对象。 槽是对信号做出反应的方法。
2、Event
在 Qt 中,事件是派生自抽象 QEvent 类的对象,它们表示在应用程序内发生的事情或作为应用程序需要了解的外部活动的结果。 事件可以被 QObject 子类的任何实例接收和处理,但它们与小部件特别相关。 本文档描述了在典型应用程序中如何传递和处理事件。
2.1 如何传递事件
当一个事件发生时,Qt 通过构造一个适当的 QEvent 子类的实例来创建一个事件对象来表示它,并通过调用它的 event() 函数将它传递给一个特定的 QObject 实例(或其子类之一)。
该函数不处理事件本身; 根据传递的事件类型,它为该特定类型的事件调用事件处理程序,并根据事件是被接受还是被忽略来发送响应。
一些事件,例如 QMouseEvent 和 QKeyEvent ,来自窗口系统; 一些,例如 QTimerEvent ,来自其他来源; 有些来自应用程序本身。
2.2 事件类型
大多数事件类型都有特殊的类,特别是 QResizeEvent 、 QPaintEvent 、 QMouseEvent 、 QKeyEvent 和 QCloseEvent 。 每个类都是 QEvent 的子类,并添加了特定于事件的函数。 例如,QResizeEvent 添加 size() 和 oldSize() 以使小部件能够发现它们的尺寸是如何更改的。
一些类支持不止一种实际事件类型。 QMouseEvent 支持鼠标按键、双击、移动等相关操作。
每个事件都有一个关联类型,在 Type 中定义,这可以用作运行时类型信息的便捷来源,以快速确定给定事件对象是从哪个子类构造的。
由于程序需要以各种复杂的方式做出反应,Qt 的事件传递机制是灵活的。 notify() 的文档简明扼要地讲述了整个故事; Qt Quarterly 文章“Another Look at Events”对其进行了不太简洁的重新表述。 在这里,我们将对 95% 的应用程序进行足够的解释。
2.3 事件处理
通过定义自定义或扩展事件处理程序,可以更改小部件响应这些事件的方式。 事件处理程序的定义与任何其他方法一样,但名称特定于它们处理的事件类型。
小部件接收的主要事件之一是 QMouseEvent。 QMouseEvent 事件是为每个鼠标移动和按钮单击小部件创建的。
以下事件处理程序可用于处理鼠标事件:
事件 | 描述 |
---|---|
mouseMoveEvent | 鼠标移动 |
mousePressEvent | 鼠标按钮按下 |
mouseReleaseEvent | 鼠标按钮松开 |
mouseDoubleClickEvent | 检测到双击 |
例如,单击一个小部件将导致一个 QMouseEvent 被发送到该小部件上的 .mousePressEvent 事件处理程序。 此处理程序可以使用事件对象来查找有关发生的事情的信息,例如触发事件的原因以及发生的具体位置。
可以通过子类化和覆盖类上的处理程序方法来拦截事件。 可以选择过滤、修改或忽略事件,通过使用 super() 调用父类函数将它们传递给事件的正常处理程序。 这些可以添加到您的主窗口类中,如下所示。 在每种情况下,e
都会收到传入的事件。
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QTextEdit
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = QLabel("Click in this window")
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
self.label.setText("mouseMoveEvent")
def mousePressEvent(self, e):
self.label.setText("mousePressEvent")
def mouseReleaseEvent(self, e):
self.label.setText("mouseReleaseEvent")
def mouseDoubleClickEvent(self, e):
self.label.setText("mouseDoubleClickEvent")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
从上面示例代码可以看到鼠标移动事件仅在按下按钮时才会注册。 可以通过在窗口上调用 self.setMouseTracking(True) 来更改此设置。
从上面代码还注意到按下(单击)和双击事件都会在按下按钮时触发。 只有释放按钮时才会触发释放事件。 通常,要注册用户的点击,应该注意鼠标按下和释放。
在事件处理程序中,可以访问事件对象。 此对象包含有关事件的信息,可用于根据具体发生的情况做出不同的响应。 接下来我们将看看鼠标事件对象。
2.3.1 Mouse事件
Qt 中的所有鼠标事件都使用 QMouseEvent 对象进行跟踪,有关事件的信息可从以下事件方法中读取。
方法 | 返回值 |
---|---|
.button() | 触发此事件的特定按钮 |
.buttons() | 所有鼠标按钮的状态(或标志) |
.globalPos() | 应用程序全局位置作为“QPoint” |
.globalX() | 应用程序全局水平 X 位置 |
.globalY() | 应用程序全局垂直 Y 位置 |
.pos() | 作为 QPoint integer 的 Widget 相对位置 |
.posF() | 小部件相对位置作为 QPointF float |
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
# handle the left-button press in here
self.label.setText("mousePressEvent LEFT")
elif e.button() == Qt.MiddleButton:
# handle the middle-button press in here.
self.label.setText("mousePressEvent MIDDLE")
elif e.button() == Qt.RightButton:
# handle the right-button press in here.
self.label.setText("mousePressEvent RIGHT")
def mouseReleaseEvent(self, e):
if e.button() == Qt.LeftButton:
self.label.setText("mouseReleaseEvent LEFT")
elif e.button() == Qt.MiddleButton:
self.label.setText("mouseReleaseEvent MIDDLE")
elif e.button() == Qt.RightButton:
self.label.setText("mouseReleaseEvent RIGHT")
def mouseDoubleClickEvent(self, e):
if e.button() == Qt.LeftButton:
self.label.setText("mouseDoubleClickEvent LEFT")
elif e.button() == Qt.MiddleButton:
self.label.setText("mouseDoubleClickEvent MIDDLE")
elif e.button() == Qt.RightButton:
self.label.setText("mouseDoubleClickEvent RIGHT")
按钮标识符在 Qt 命名空间中定义,如下:
标识符 | 值(二进制) | 描述 |
---|---|---|
Qt.NoButton | 0 (000 ) | 没有按下按钮,或者事件与按钮按下无关 |
Qt.LeftButton | 1 (001 ) | 左键被按下 |
Qt.RightButton | 2 (010 ) | 按下右侧按钮 |
Qt.MiddleButton | 4 (100 ) | 按下中间按钮 |
2.3.2 键盘按键事件
当按键被按下或释放时,按键事件被发送到具有键盘输入焦点的小部件。
按键事件包含一个特殊的接受标志,指示接收器是否将处理按键事件。 默认情况下为 KeyPress 和 KeyRelease 设置此标志,因此在对键事件进行操作时无需调用 accept()。 对于 ShortcutOverride,接收者需要明确接受事件以触发覆盖。 在关键事件上调用 ignore() 会将其传播到父小部件。 事件沿父窗口小部件链向上传播,直到窗口小部件接受它或事件过滤器使用它。例如,在上面的例子中,在MainWindow类中添加如下代码,用于处理键盘按键事件:
def keyPressEvent(self, e):
self.label.setText("keyPressEvent")
def keyReleaseEvent(self,e):
self.label.setText("keyReleaseEvent")
2.4 事件过滤
有时,一个对象需要查看并可能拦截传递给另一个对象的事件。例如,对话框通常希望过滤某些小部件的按键;例如,修改 Return-key 处理。
installEventFilter()
函数通过设置事件过滤器来实现这一点,使指定的过滤器对象在其 eventFilter()
函数中接收目标对象的事件。事件过滤器在目标对象之前处理事件,允许它根据需要检查和丢弃事件。可以使用 removeEventFilter()
函数删除现有的事件过滤器。
当调用过滤器对象的 eventFilter()
实现时,它可以接受或拒绝事件,并允许或拒绝对事件的进一步处理。如果所有事件过滤器都允许对事件进行进一步处理(每个都返回 false),则该事件将被发送到目标对象本身。如果其中一个停止处理(通过返回 true),则目标和任何后续事件过滤器根本看不到该事件。如下面代码所示:
def eventFilter(self, QObject object, QEvent event):
if (object == target and event.type() == QEvent.KeyPress) {
keyEvent = QKeyEvent (event)
if (keyEvent.key() == Qt.Key_Tab) {
# Special tab handling
return True
else:
return False
return False
上面的代码显示了另一种拦截发送到特定目标小部件的 Tab 键按下事件的方法。 在这种情况下,过滤器会处理相关事件并返回 true 以阻止它们被进一步处理。 所有其他事件都被忽略,过滤器返回 false 以允许它们通过安装在其上的任何其他事件过滤器发送到目标小部件。
还可以通过在 QApplication 或 QCoreApplication 对象上安装事件过滤器来过滤整个应用程序的所有事件。 此类全局事件过滤器在特定于对象的过滤器之前调用。 这非常强大,但它也减慢了整个应用程序中每个事件的事件传递。
2.5 发送事件
许多应用程序想要创建和发送自己的事件。您可以通过构造合适的事件对象并使用 sendEvent()
和postEvent()
以与 Qt 自己的事件循环完全相同的方式发送事件。
sendEvent()
立即处理事件。当它返回时,事件过滤器和/或对象本身已经处理了事件。对于许多事件类,有一个名为 isAccepted() 的函数,它告诉您事件是被最后一个调用的处理程序接受还是拒绝。
postEvent()
将事件发布到队列中以供以后调度。下次 Qt 的主事件循环运行时,它会分派所有已发布的事件,并进行一些优化。例如,如果有多个调整大小事件,则将它们压缩为一个。这同样适用于绘制事件: update() 调用 postEvent() ,这消除了闪烁并通过避免多次重绘来提高速度。
postEvent()
也在对象初始化期间使用,因为发布的事件通常会在对象初始化完成后很快被调度。在实现一个小部件时,重要的是要意识到事件可以在其生命周期的早期交付,因此,在其构造函数中,请务必尽早初始化成员变量,然后才有可能接收到事件。
要创建自定义类型的事件,您需要定义一个事件编号,该编号必须大于 User
,并且您可能需要继承 QEvent 以传递有关您的自定义事件的特定信息。有关详细信息,请参阅 QEvent 文档。
2.6 事件复杂示例
1)确定事件源
import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn1 = QPushButton("Button 1", self)
btn1.move(30, 50)
btn2 = QPushButton("Button 2", self)
btn2.move(150, 50)
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
self.statusBar()
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Event sender')
self.show()
def buttonClicked(self):
sender = self.sender()
self.statusBar().showMessage(sender.text() + ' was pressed')
def main():
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
上面示例中有两个按钮。 在 buttonClicked 方法中,我们通过调用 sender
方法来确定我们点击了哪个按钮。
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
两个按钮都连接到同一个Slot
def buttonClicked(self):
sender = self.sender()
self.statusBar().showMessage(sender.text() + ' was pressed')
通过调用 sender 方法来确定信号源。 在应用程序的状态栏中,显示正在按下的按钮的标签。
运行结果如下:
2)信号发送
import sys
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtWidgets import QMainWindow, QApplication
class CustomSignal(QObject):
closeApp = pyqtSignal()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.c = CustomSignal()
self.c.closeApp.connect(self.close)
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Emit signal')
self.show()
def mousePressEvent(self, event):
self.c.closeApp.emit()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
在上面代码中,我们创建一个名为 closeApp 的新信号。 此信号在鼠标按下事件期间发出。 该信号连接到 QMainWindow 的关闭槽。
class CustomSignal(QObject):
closeApp = pyqtSignal()
使用 pyqtSignal 作为外部 CustomSignal
类的类属性创建信号。
self.c = CustomSignal()
self.c.closeApp.connect(self.close)
自定义 closeApp 信号连接到 QMainWindow 的关闭槽。
def mousePressEvent(self, event):
self.c.closeApp.emit()
当我们用鼠标指针单击窗口时,会发出 closeApp 信号。 应用程序终止。