Skip to content

10. 事件

用户与 Qt 应用程序之间的每次交互都是一个事件。事件有多种类型,每种类型代表一种不同的交互类型。Qt 使用事件对象来表示这些事件,事件对象打包了关于发生事件的信息。这些事件被传递到发生交互的控件上的特定事件处理程序。

通过定义自定义事件处理程序,您可以更改控件对这些事件的响应方式。事件处理程序与其他方法一样进行定义,但名称是根据它们处理的事件类型来指定的。

控件接收的主要事件之一是 QMouseEventQMouseEvent 事件是在控件上每次鼠标移动和按钮点击时创建的。以下事件处理程序可用于处理鼠标事件:

事件处理器被更改的事件
mouseMoveEvent鼠标移动
mousePressEvent鼠标按钮被按下
mouseReleaseEvent鼠标按钮被松开
mouseDoubleClickEvent检测到双击

例如,单击一个控件会触发一个 QMouseEvent 事件,该事件将被发送给该控件的 .mousePressEvent事件处理程序。该处理程序可以使用事件对象来查找发生的事情的相关信息,例如触发该事件的原因以及具体发生的位置。

您可以通过继承类并重写处理方法来拦截事件。您可以选择过滤、修改或忽略事件,并将它们传递给事件的正常处理程序,方法是调用父类函数并使用 super() 。这些可以添加到您的主窗口类中,如下例所示。在每种情况下,参数 e 将接收传入的事件。

Listing 84. basic/events_1.py

python
import sys
from PyQt6.QtCore import Qt
from PyQt6.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)方法在窗口上更改此行为。您还可能注意到,按下(点击)和双击事件在按下按钮时均会触发。仅在释放按钮时触发释放事件。通常,要注册用户的点击事件,您应该监听鼠标按下和释放两个事件。

在事件处理程序中,您可以访问一个事件对象。该对象包含有关事件的信息,并可根据具体发生的情况采取不同的响应方式。接下来我们将探讨鼠标事件对象。

鼠标事件

Qt 中所有鼠标事件均通过 QMouseEvent 对象进行跟踪,相关事件信息可通过以下事件方法进行读取。

方法返回的结果
.button()触发此事件的特定按钮
.buttons()所有鼠标按钮的状态(或运算标志)
`.position()控件的相对位置,以 QPoint 整数表示

您可以在事件处理程序中使用这些方法来对不同的事件做出不同的响应,或者完全忽略它们。 .position() 方法以 QPoint 对象的形式提供控件的相对位置信息,而按钮则使用 Qt 命名空间中的鼠标按钮类型进行报告。

例如,以下代码允许我们对窗口的左键、右键或中键点击做出不同的响应。

Listing 85. basic/events_2.py

python
    def mousePressEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            # 在此处理左键按下事件
            self.label.setText("mousePressEvent LEFT")
            
        elif e.button() == Qt.MouseButton.MiddleButton:
            # 在此处理中间按钮的按下操作
            self.label.setText("mousePressEvent MIDDLE")
            
        elif e.button() == Qt.MouseButton.RightButton:
            # 在此处处理右键按下事件
            self.label.setText("mousePressEvent RIGHT")
            
    def mouseReleaseEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            self.label.setText("mouseReleaseEvent LEFT")
            
        elif e.button() == Qt.MouseButton.MiddleButton:
            self.label.setText("mouseReleaseEvent MIDDLE")
            
        elif e.button() == Qt.MouseButton.RightButton:
            self.label.setText("mouseReleaseEvent RIGHT")
            
    def mouseDoubleClickEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            self.label.setText("mouseDoubleClickEvent LEFT")
            
        elif e.button() == Qt.MouseButton.MiddleButton:
            self.label.setText("mouseDoubleClickEvent MIDDLE")
            
        elif e.button() == Qt.MouseButton.RightButton:
            self.label.setText("mouseDoubleClickEvent RIGHT")

按钮标识符在 Qt 命名空间中定义,如下所示:

标识符值(二进制)代表的事件
Qt.MouseButtons.NoButton0(000)未按下按钮,或该事件与按下按钮无关。
Qt.MouseButtons.LeftButton1(001)左键被按下
Qt.MouseButtons.RightButton2(010)右键被按下
Qt.MouseButtons.MiddleButton3(100)中间的按键被按下

information

对于右手鼠标,左右按钮的位置是相反的,即按下最右边的按钮将返回 Qt.MouseButtons.LeftButton 。这意味着您无需在代码中考虑鼠标的方向。

tips

要更深入地了解这一切的工作原理,请参阅后文的 “35. 枚举和 Qt 命名空间”。

上下文菜单

上下文菜单是小型上下文相关菜单,通常在右键单击窗口时出现。Qt 支持生成这些菜单,并且控件有一个用于触发它们的特定事件。在下面的示例中,我们将拦截 QMainWindow.contextMenuEvent 。每当上下文菜单即将显示时,都会触发此事件,并传递一个类型为 QContextMenuEvent 的单一值事件。

要拦截该事件,我们只需用我们的新方法覆盖对象方法,该方法具有相同的名称。因此,在这种情况下,我们可以在 MainWindow 子类中创建一个名为 contextMenuEvent 的方法,它将接收所有此类型的事件。

Listing 86. basic/events_3.py

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QMenu,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
    def contextMenuEvent(self, e):
        context = QMenu(self)
        context.addAction(QAction("test 1", self))
        context.addAction(QAction("test 2", self))
        context.addAction(QAction("test 3", self))
        context.exec(e.globalPos())
        
app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

如果运行上述代码并在窗口内右键单击,您会看到一个上下文菜单出现。您可以像往常一样在菜单操作上设置 .triggered 槽(并重新使用为菜单和工具栏定义的操作)。

information

在将初始位置传递给 exec() 方法时,该位置必须相对于在定义时传递的父对象。在此情况下我们传递 self 作为父对象,因此可以使用全局位置。

为了完整起见,还有一种基于信号的方法来创建上下文菜单。

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.show()
        
        self.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self.customContextMenuRequested.connect(self.on_context_menu)
        
    def on_context_menu(self, pos):
        context = QMenu(self)
        context.addAction(QAction("test 1", self))
        context.addAction(QAction("test 2", self))
        context.addAction(QAction("test 3", self))
        context.exec(self.mapToGlobal(pos))

完全由您决定选择哪一个。

事件层次结构

在 pyqt6 中,每个控件都是两个不同层次结构的一部分:Python 对象层次结构和 Qt 布局层次结构。您对事件做出响应或忽略事件的方式会影响用户界面的行为。

Python 继承转发

通常情况下,您可能希望拦截一个事件,对其进行处理,但仍然触发默认事件处理行为。如果您的对象是从标准控件继承的,则它很可能默认实现了合理的行为。您可以通过调用 super() 调用父级实现来触发此行为。

information

这是 Python 的父类,而不是 pyqt6 的 .parent()

python
def mousePressEvent(self, event):
    print("Mouse pressed!")
    super(self, MainWindow).contextMenuEvent(event)

该事件将继续按正常方式运行,但您添加了部分不干扰的行为。

布局转发

当您将控件添加到应用程序时,它也会从布局中获得另一个父级。可以通过调用 .parent() 来找到控件的父级。有时,您可以手动指定这些父级,例如对于 QMenuQDialog,但通常情况下,这是自动完成的。例如,当您将控件添加到主窗口时,主窗口将成为该控件的父级。

当您为用户与用户界面的交互创建事件时,这些事件将传递到用户界面中最上面的控件。如果单击窗口中的按钮,该按钮将在窗口之前接收事件。如果第一个控件无法处理事件,或者选择不处理,则事件将向上传播到父控件,该控件将获得处理事件的机会。这种向上传播将一直持续到嵌套控件,直到事件被处理或到达主窗口。

在您自己的事件处理程序中,您可以选择通过调用 .accept() 方法将事件标记为已处理。

python
    class CustomButton(Qbutton)
        def mousePressEvent(self, e):
            e.accept()

或者,您可以通过调用事件对象的 .ignore() 方法将其标记为未处理。在这种情况下,事件将继续向上级层级传递,就像冒泡一样。

python
    class CustomButton(Qbutton)
        def mousePressEvent(self, e):
            e.ignore()

如果您希望控件对事件保持透明,则可以放心地忽略您已经以某种方式响应过的事件。同样,您也可以选择接受您未响应的事件,以让它们静默处理。

information

这可能会造成混淆,因为您可能会认为调用 .ignore() 会完全忽略该事件。但事实并非如此:您只是忽略了此控件的事件!