分享

PyQt6 信号,槽和事件

 禁忌石 2022-12-07 发布于浙江

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

Signals & Slots

信号是控件在发生某些事情时发出的通知。这个东西可以是任何数量的事情,从按下一个按钮,到输入框的文本更改,到窗口的文本更改。许多信号是由用户操作发起的,但这不是一条规则。

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

您还可以创建自己的自定义信号,我们将在后面讨论。

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

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

将以下应用程序大纲保存到名为app.py的文件中。

import sysfrom PyQt6.QtWidgets import QApplication, QMainWindow, QPushButtonclass MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('My App')app = QApplication(sys.argv)window = MainWindow()window.show()app.exec()

QPushButton信号

我们的简单应用程序目前有一个设置为中心控件的QMainWindow和QPushButton。让我们从将这个按钮连接到一个自定义Python方法开始。在这里,我们创建了一个名为the_button_was_clicked的简单自定义槽,它接受来自QPushButton的单击信号。

import sysfrom PyQt6.QtWidgets import QApplication, QMainWindow, QPushButtonclass 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? TrueClicked!Checked? FalseClicked!Checked? TrueClicked!Checked? FalseClicked!Checked? True

您可以将任意多个槽连接到一个信号,并且可以在槽上同时响应不同版本的信号。

存储数据

通常,将控件的当前状态存储在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)

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

您可以对任何PyQt控件使用相同的模式。如果控件没有提供发送当前状态的信号,则需要直接在处理程序中从控件检索值。例如,这里我们正在检查pressed处理程序中的checked状态。

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上保持对按钮的引用,这样我们才能在槽中访问它。

released信号在按钮释放时触发,但不发送check状态,因此我们使用.ischecked()从处理程序中的按钮获取check状态。

改变接口

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

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

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

运行它!如果你点击按钮,文本将会改变,按钮将变得不可点击。

你不局限于改变触发信号的按钮,你可以在你的slot方法中做任何你想做的事情。例如,尝试向button_was_clicked方法添加以下代码行,以更改窗口标题。

self.setWindowTitle('A new window title')

大多数控件都有自己的信号,我们正在使用的QMainWindow也不例外。在下面这个更复杂的例子中,我们将QMainWindow上的.windowTitleChanged信号连接到一个自定义的slot方法。

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

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButtonimport sysfrom random import choicewindow_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信号挂钩。

当我们点击按钮时,窗口标题将随机变化。如果新的窗口标题等于“Something went wrong”按钮将被禁用。

在这个例子中有几点需要注意。

首先,在设置窗口标题时并不总是发出windowTitleChanged信号。只有在新标题与前一个标题发生变化时,该信号才会触发。如果多次设置相同的标题,则只会在第一次触发信号。重要的是要反复检查信号触发的条件,以避免在应用中使用它们时感到惊讶。

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

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

将控件直接连接在一起

到目前为止,我们已经看到了连接控件信号到Python方法的例子。当从控件触发信号时,我们的Python方法将被调用并从该信号接收数据。但您并不总是需要使用Python函数来处理信号——您也可以将Qt控件直接连接到另一个控件。

在下面的示例中,我们向窗口添加一个QLineEdit控件和一个QLabel。在窗口的__init__中,我们将QLineEdit的.textChanged信号连接到QLabel上的.setText方法。现在,任何时候文本在QLineEdit中改变QLabel将接收该文本到它的.setText方法。

from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QVBoxLayout, QWidgetimport sysclass 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()

注意,为了将输入连接到标签,必须同时定义输入和标签。这段代码将两个控件添加到一个布局中,并在窗口上设置该布局。我们稍后将详细介绍布局,您可以先忽略它。

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

文章图片1

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

事件

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

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

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

文章图片2

例如,单击一个控件将导致QMouseEvent被发送到该控件上的.mousePressEvent事件处理程序。这个处理程序可以使用事件对象来查找关于所发生事件的信息,例如是什么触发了事件以及事件具体发生在哪里。

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

import sysfrom PyQt6.QtCore import Qtfrom PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QTextEditclass 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整数的控件相对位置

您可以在事件处理程序中使用这些方法以不同的方式响应不同的事件,或者完全忽略它们。位置方法作为QPoint对象提供全局和本地(与控件相关的)位置信息,而按键是使用Qt名称空间中的MouseButton类型报告的。

例如,下面的命令允许我们对窗口上的左、右、中单击做出不同的响应。

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支持生成这些菜单,控件有一个特定的事件用来触发它们。在下面的例子中,我们将拦截QMainWindow中的.contextMenuEvent。每当要显示上下文菜单时,就会触发此事件,并传递一个类型为QContextMenuEvent的单值event。

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

import sysfrom PyQt6.QtCore import Qtfrom PyQt6.QtGui import QActionfrom PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QMenuclass 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()

如果运行上面的代码并在窗口内右键单击,您将看到出现一个上下文菜单。您可以在菜单操作上设置正常的.触发槽(并重用为菜单和工具栏定义的操作)。

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

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

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.show() self.setContextMenuPolicy(Qt.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的行为。

Python继承转发

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

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

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

事件将继续正常运行,但您已经添加了一些不干涉行为。

布局(PyQt)转发

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

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

如果第一个控件不能处理事件,或者选择不处理,则该事件将向父控件冒泡。这种冒泡在嵌套的控件上一直持续,直到事件被处理或到达主窗口。

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

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

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

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

如果您希望控件对事件透明,那么您可以安全地忽略已经以某种方式实际响应的事件。同样,你可以选择接受你没有回应的事件,以使它们沉默。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多