【使用 PyQt6-第02章】 创建信号、槽和事件

一、说明

   我们已经创建了一个窗口并向其中添加了一个简单的按钮小部件,但该按钮不执行任何操作。这根本不是很有用——当您创建 GUI 应用程序时,您通常希望它们做一些事情!我们需要的是一种将按下按钮的动作与某些事情发生联系起来的方法。在 Qt 中,这是由信号和槽或事件提供的。

二、信号和槽

信号是小部件在发生某些事情时发出的通知。该事物可以是任意数量的事物,从按下按钮,到输入框的文本发生变化,再到窗口的文本发生变化。许多信号是由用户操作发起的,但这不是规则。

除了通知正在发生的事情之外,信号还可以发送数据以提供有关所发生事情的附加上下文。

您还可以创建自己的自定义信号,我们稍后将对此进行探讨。

插槽是 Qt 用于信号接收器的名称。在 Python 中,应用程序中的任何函数(或方法)都可以用作槽——只需将信号连接到它即可。如果信号发送数据,那么接收函数也将接收该数据。许多 Qt 小部件也有自己的内置插槽,这意味着您可以直接将 Qt 小部件连接在一起。

让我们看一下 Qt 信号的基础知识,以及如何使用它们来连接小部件以使应用程序中的事情发生。

将以下应用程序大纲保存到名为app.py.

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("My App")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

三、QPushButton信号

我们的简单应用程序目前有一个QMainWindow集合QPushButton作为中央小部件。让我们首先将此按钮连接到自定义 Python 方法。在这里,我们创建一个名为的简单自定义插槽the_button_was_clicked,它接受clicked来自QPushButton.

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        button.clicked.connect(self.the_button_was_clicked)

        # Set the central widget of the Window.
        self.setCentralWidget(button)

    def the_button_was_clicked(self):
        print("Clicked!")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

运行!如果单击该按钮,您将看到文本“Clicked!”在控制台上。

Clicked!
Clicked!
Clicked!
Clicked!

四、接收数据

这是一个好的开始!我们已经听说信号还可以发送数据以提供有关刚刚发生的情况的更多信息。该.clicked信号也不例外,还为按钮提供了选中(或切换)状态。对于普通按钮,这始终是False,因此我们的第一个插槽忽略了此数据。但是,我们可以使按钮可检查并查看效果。

在下面的示例中,我们添加第二个槽来输出checkstate。

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        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)

运行!如果您按下该按钮,您会看到它突出显示为已选中。再按一次即可释放。在控制台中查找检查状态。

Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True

您可以根据需要将任意数量的插槽连接到信号,并且可以在插槽上同时响应不同版本的信号。

五、存储数据

通常,将小部件的当前状态存储在 Python 变量中很有用。这允许您像任何其他 Python 变量一样使用这些值,而无需访问原始小部件。您可以将这些值存储为单独的变量,也可以根据需要使用字典。在下一个示例中,我们将按钮的检查button_is_checked值存储在名为on的变量中self。

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)

首先,我们设置变量的默认值(为True),然后使用默认值设置小部件的初始状态。当小部件状态发生变化时,我们收到信号并更新变量以匹配。

您可以将相同的模式与任何 PyQt 小部件一起使用。如果小部件不提供发送当前状态的信号,您将需要直接在处理程序中从小部件检索值。例如,这里我们正在检查按下处理程序中的检查状态。

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.button_is_checked = True

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.setCheckable(True)
        self.button.released.connect(self.the_button_was_released)
        self.button.setChecked(self.button_is_checked)

        self.setCentralWidget(self.button)

    def the_button_was_released(self):
        self.button_is_checked = self.button.isChecked()

        print(self.button_is_checked)

我们需要保留对按钮的引用,self以便我们可以在插槽中访问它。

当按钮被释放时,释放信号会触发,但不会发送检查状态,因此我们使用.isChecked()处理程序中的按钮获取检查状态。

六、改变接口

到目前为止,我们已经了解了如何接受信号并将输出打印到控制台。但是当我们点击按钮时让界面发生一些事情怎么样?让我们更新槽方法来修改按钮,更改文本并禁用按钮,使其不再可单击。我们现在还将关闭可检查状态。

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.clicked.connect(self.the_button_was_clicked)

        self.setCentralWidget(self.button)

    def the_button_was_clicked(self):
        self.button.setText("You already clicked me.")
        self.button.setEnabled(False)

        # Also change the window title.
        self.setWindowTitle("My Oneshot App")

同样,因为我们需要能够button在我们的the_button_was_clicked方法中访问 ,所以我们在 上保留对它的引用self。通过传递strto来更改按钮的文本.setText()。要禁用按钮,.setEnabled()请使用False.

运行!如果单击该按钮,文本将发生变化并且该按钮将变得不可单击。

您不仅限于更改触发信号的按钮,您可以在插槽方法中执行任何您想要的操作。例如,尝试将以下行添加到the_button_was_clicked方法中以同时更改窗口标题。

self.setWindowTitle("A new window title")

大多数小部件都有自己的信号——QMainWindow我们用于窗口的信号也不例外。在下面更复杂的示例中,我们将.windowTitleChanged上的信号连接QMainWindow到自定义槽方法。

在下面的示例中,我们将.windowTitleChanged上的信号连接QMainWindow到方法 slot the_window_title_changed。该插槽还接收新的窗口标题。

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton

import sys
from random import choice

window_titles = [
    'My App',
    'My App',
    'Still My App',
    'Still My App',
    'What on earth',
    'What on earth',
    'This is surprising',
    'This is surprising',
    'Something went wrong'
]


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.n_times_clicked = 0

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.clicked.connect(self.the_button_was_clicked)

        self.windowTitleChanged.connect(self.the_window_title_changed)

        # Set the central widget of the Window.
        self.setCentralWidget(self.button)

    def the_button_was_clicked(self):
        print("Clicked.")
        new_window_title = choice(window_titles)
        print("Setting title:  %s" % new_window_title)
        self.setWindowTitle(new_window_title)

    def the_window_title_changed(self, window_title):
        print("Window title changed: %s" % window_title)

        if window_title == 'Something went wrong':
            self.button.setDisabled(True)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

首先,我们设置一个窗口标题列表——我们将使用 Python 的内置random.choice().我们将自定义槽方法挂接the_window_title_changed到窗口的.windowTitleChanged信号上。

当我们单击按钮时,窗口标题将随机更改。如果新窗口标题等于“出现问题”,则该按钮将被禁用。

运行!重复单击该按钮,直到标题更改为“出现问题”并且该按钮将被禁用。

在此示例中,有几件事需要注意。

首先,设置窗口标题时windowTitleChanged并不总是发出信号。仅当新标题较前一标题发生更改时才会触发该信号。如果多次设置相同的标题,则只会在第一次时触发信号。仔细检查信号触发的条件非常重要,以避免在应用程序中使用它们时感到惊讶。

其次,请注意我们如何使用信号将事物链接在一起。发生一件事——按下按钮——可以依次触发其他多件事发生。这些后续影响不需要知道是什么造成的,而只是简单规则的结果。这种效果与其触发器的解耦是构建 GUI 应用程序时需要理解的关键概念之一。

在本节中,我们介绍了信号和槽。我们演示了一些简单的信号以及如何使用它们在应用程序中传递数据和状态。接下来我们将了解 Qt 提供的用于您的应用程序的小部件以及它们提供的信号。

七、直接将小部件连接在一起

到目前为止,我们已经看到了将小部件信号连接到 Python 方法的示例。当小部件发出信号时,我们的 Python 方法将被调用并接收来自信号的数据。但您并不总是需要使用 Python 函数来处理信号——您还可以将 Qt 小部件直接相互连接。

在下面的示例中,我们向窗口添加一个QLineEdit小部件和 a 。QLabel在\__init__for 窗口中,我们将行编辑信号.textChanged连接到.现在,每当文本发生变化时,都会将该文本接收到它的方法中。.setTextQLabelQLineEditQLabel.setText

from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QVBoxLayout, QWidget

import sys


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        self.label = QLabel()

        self.input = QLineEdit()
        self.input.textChanged.connect(self.label.setText)

        layout = QVBoxLayout()
        layout.addWidget(self.input)
        layout.addWidget(self.label)

        container = QWidget()
        container.setLayout(layout)

        # Set the central widget of the Window.
        self.setCentralWidget(container)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

请注意,为了将输入连接到标签,必须定义输入和标签。此代码将两个小部件添加到布局中,并将其设置在窗口上。稍后我们将详细介绍布局,您现在可以忽略它。

运行!在上面的框中输入一些文本,您会看到它立即出现在标签上。

输入中键入的任何文本都会立即显示在标签上
输入中键入的任何文本都会立即显示在标签上。

大多数 Qt 小部件都有可用的插槽,您可以将发出与其接受的类型相同的任何信号连接到该插槽。小部件文档在“公共插槽”下列出了每个小部件的插槽。例如,请参阅 https://doc.qt.io/qt-5/qlabel.html#public-slots[QLabel]。

八、活动

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

通过定义自定义或扩展事件处理程序,您可以改变小部件响应这些事件的方式。事件处理程序的定义与任何其他方法一样,但名称特定于它们处理的事件类型。

小部件接收的主要事件之一是QMouseEvent. QMouseEvent 事件是为小部件上的每次鼠标移动和按钮单击创建的。以下事件处理程序可用于处理鼠标事件——

事件处理程序 事件类型已移动
mouseMoveEvent 鼠标移动了
mousePressEvent 鼠标按钮被按下
mouseReleaseEvent 释放鼠标按钮
mouseDoubleClickEvent 检测到双击
例如,单击某个小部件将导致QMouseEvent将 a 发送到.mousePressEvent该小部件上的事件处理程序。该处理程序可以使用事件对象来查找有关所发生事件的信息,例如触发事件的原因以及事件发生的具体位置。

您可以通过子类化并覆盖类上的处理程序方法来拦截事件。您可以选择过滤、修改或忽略事件,通过使用 调用父类函数将它们传递给事件的普通处理程序super()。这些可以添加到您的主窗口类中,如下所示。在每种情况下e都会收到传入的事件。

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() 所有鼠标按钮的状态(OR’ed 标志)
.position() 小部件相对位置(QPoint 整数)
您可以在事件处理程序中使用这些方法来以不同的方式响应不同的事件,或者完全忽略它们。位置方法以对象的形式提供全局和本地(小部件相对)位置信息QPoint,而按钮则使用命名空间中的鼠标按钮类型来报告Qt。

例如,以下内容允许我们对窗口上的左键、右键或中键单击做出不同的响应。

def mousePressEvent(self, e):
    if e.button() == Qt.MouseButton.LeftButton:
        # handle the left-button press in here
        self.label.setText("mousePressEvent LEFT")

    elif e.button() == Qt.MouseButton.MiddleButton:
        # handle the middle-button press in here.
        self.label.setText("mousePressEvent MIDDLE")

    elif e.button() == Qt.MouseButton.RightButton:
        # handle the right-button press in here.
        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.MouseButton.NoButton 0 ( 000) 没有按下按钮,或者事件与按钮按下无关。
Qt.MouseButton.LeftButton 1 ( 001) 左键被按下
Qt.MouseButton.RightButton 2 ( 010) 右侧按钮被按下。
Qt.MouseButton.MiddleButton 4 ( 100) 中间的按钮被按下。
在惯用左手的鼠标上,左右按钮的位置是相反的,即按下最右边的按钮将返回Qt.LeftButton。这意味着您不需要在代码中考虑鼠标方向。

十、上下文菜单

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

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

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照常在菜单操作上设置插槽(并重复使用为菜单和工具栏定义的操作)。

将初始位置传递给exec函数时,这必须相对于定义时传入的父级。在本例中,我们self作为父级传递,因此我们可以使用全局位置。

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

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))

这完全取决于您的选择。

十一、事件层次结构

在 PyQt 中,每个小部件都是两个不同层次结构的一部分:Python 对象层次结构和 Qt 布局层次结构。您响应或忽略事件的方式可能会影响您的 UI 行为。

11.1 Python继承转发

通常,您可能希望拦截事件,对其执行某些操作,但仍然触发默认的事件处理行为。如果您的对象是从标准小部件继承的,则它可能会默认实现合理的行为。您可以通过使用调用父实现来触发此操作super()。

这是 Python 父类,而不是 PyQt .parent()。

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

该事件将继续正常运行,但您添加了一些非干扰行为。

11.2 Python布局转发

当您向应用程序添加一个小部件时,它还会从布局中获取另一个父部件。可以通过调用 来找到小部件的父级.parent()。有时您手动指定这些父项,例如 forQMenu或QDialog,通常它是自动的。例如,当您将一个小部件添加到主窗口时,主窗口将成为该小部件的父窗口。

当为用户与 UI 交互创建事件时,这些事件将传递到UI 中最上面的小部件。因此,如果窗口上有一个按钮,然后单击该按钮,该按钮将首先接收该事件。

如果第一个小部件无法处理该事件,或者选择不处理该事件,则该事件将向上冒泡到父小部件,该父小部件将被轮流处理。这种冒泡一直持续到嵌套小部件,直到事件被处理或到达主窗口。

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

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

或者,您可以通过调用事件对象将其标记为未处理.ignore()。在这种情况下,事件将继续在层次结构中向上冒泡。

class CustomButton(QPushButton)
    def event(self, e):
        e.ignore()

如果您希望您的小部件对事件显示为透明,您可以安全地忽略您实际上以某种方式响应的事件。同样,您可以选择接受您未响应的事件以使它们保持沉默。

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无水先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值