到目前为止,我们已经创建了一个窗口,并在其中添加了一个简单的按钮部件,但这个按钮并没有做任何事情。这根本不是很有用 -- 当你创建GUI应用程序时,你通常希望它们能做一些事情我们需要的是一种方法,将按下按钮的动作与发生的事情联系起来。在Qt中,这是由信号和槽或事件提供的。
信号和槽
信号是小部件在事情发生时发出的通知。这个东西可以是任何数量的东西,从按下一个按钮,到一个输入框的文本变化,到窗口的文本变化。许多信号是由用户行动发起的,但这并不是一个规则。
除了通知正在发生的事情,信号还可以发送数据以提供关于所发生事情的额外背景。
你还可以创建你自己的自定义信号,我们将在后面探讨。
槽是Qt对信号接收者的称呼。在Python中,你的应用程序中的任何函数(或方法)都可以作为槽使用--只需将信号连接到它。如果信号发送了数据,那么接收函数也会收到这些数据。许多Qt部件也有自己的内置槽,这意味着你可以直接将Qt部件挂在一起。
让我们来看看Qt信号的基本原理,以及你如何使用它们来钩住widget,使之在你的应用程序中发生作用。
把下面的应用程序大纲保存到一个名为app.py
.
python
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class 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
的clicked
信号。
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!"的文字。
python
Clicked!
Clicked!
Clicked!
Clicked!
复制代码
接收数据
这是个好的开始!我们已经听说,信号也可以发送数据,以提供更多关于刚刚发生的事情的信息。.clicked
信号也不例外,它也为按钮提供一个检查(或切换)的状态。对于普通的按钮来说,这总是False
,所以我们的第一个槽忽略了这个数据。然而,我们可以让我们的按钮变成可检查的,看看效果如何。
在下面的例子中,我们添加了第二个槽,输出检查状态。
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)
复制代码
运行它!如果你按下这个按钮,你会看到它被高亮显示为选中。再按一次就可以释放它。在控制台中寻找检查的状态。
python
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
复制代码
你可以随心所欲地将多个槽连接到一个信号上,并且可以在你的槽上同时响应不同版本的信号。
储存数据
通常情况下,将一个部件的当前状态存储在一个Python变量中是很有用的。这允许你像其他Python变量一样处理这些值,而不需要访问原始的widget。你可以把这些值作为单独的变量来存储,或者如果你愿意的话,使用一个字典。在下一个例子中,我们把我们的按钮的检查值存储在一个叫做button_is_checked
的变量中,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
),然后使用默认值来设置widget的初始状态。当widget的状态发生变化时,我们接收信号并更新变量以匹配。
你可以对任何PyQt小组件使用这个相同的模式。如果一个widget不提供发送当前状态的信号,你将需要在你的处理程序中直接从widget中获取该值。例如,在这里我们要在一个按下的处理程序中检查检查状态。
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")
复制代码
同样,因为我们需要能够在我们的the_button_was_clicked
方法中访问button
,所以我们在self
上保留了对它的引用。按钮的文本是通过传递一个str
到.setText()
来改变的。要禁用一个按钮,可以用False
来调用.setEnabled()
。
*运行它!*如果你点击按钮,文本将发生变化,并且按钮将变得不可点击。
你并不局限于改变触发信号的按钮,你可以在你的槽方法中做任何你想做的事情。例如,尝试在the_button_was_clicked
方法中加入以下一行,以同时改变窗口的标题。
python
self.setWindowTitle("A new window title")
复制代码
大多数小部件都有自己的信号 -- 我们为窗口使用的QMainWindow
也不例外。在下面这个更复杂的例子中,我们将QMainWindow
上的.windowTitleChanged
信号连接到一个自定义的槽方法。
.windowTitleChanged
在下面的例子中,我们把QMainWindow
上的信号连接到一个方法槽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为你的应用程序提供的widget,以及它们提供的信号。
直接将widget连接在一起
到目前为止,我们已经看到了将部件信号连接到 Python 方法的例子。当一个信号从widget被触发时,我们的Python方法被调用并接收来自信号的数据。但你并不总是需要使用Python函数来处理信号 -- 你也可以直接将Qt widget连接到彼此之间。
在下面的例子中,我们在窗口中添加了一个QLineEdit
widget和一个QLabel
。在窗口的\\__init__
中,我们将我们的行编辑.textChanged
信号连接到QLabel
上的.setText
方法。现在,任何时候QLineEdit
中的文本变化,QLabel
将接收该文本到它的.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小组件都有可用的槽,你可以把任何发出它所接受的相同类型的信号连接到这些槽上。 小组件文档中的 "公共槽 "下列出了每个小组件的槽。例如,见doc.qt.io/qt-5/qlabel…
事件
用户与Qt应用程序的每一次交互都是一个事件。有许多类型的事件,每一种都代表不同类型的交互。Qt使用事件对象来表示这些事件,这些对象封装了关于所发生事件的信息。这些事件被传递给发生交互的部件上的特定事件处理程序。
通过定义自定义或扩展的事件处理程序,你可以改变你的小组件对这些事件的响应方式。事件处理程序就像其他方法一样被定义,但其名称是针对其处理的事件类型的。
小组件接收的主要事件之一是QMouseEvent
。QMouseEvent事件是为小组件上的每一次鼠标移动和按钮点击而创建的。
事件处理程序 | 事件类型 移动 |
---|---|
mouseMoveEvent | 鼠标移动 |
mousePressEvent | 鼠标按钮被按下 |
mouseReleaseEvent | 鼠标按钮释放 |
mouseDoubleClickEvent | 检测到双击 |
例如,点击一个小组件将导致一个QMouseEvent
,发送到该小组件的.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 flags)。 |
.position() | 小工具的相对位置,作为一个QPoint 的整数 |
你可以在一个事件处理程序中使用这些方法来对不同的事件做出不同的反应,或者完全忽略它们。位置方法以QPoint
对象的形式提供全局和局部(widget-relative)位置信息,而按钮则使用来自Qt
命名空间的鼠标按钮类型报告。
例如,下面允许我们对窗口上的左键、右键或中键做出不同的反应。
python
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 ) | 中间的按钮被按下了。 |
在右手的鼠标上,左右按钮的位置是相反的,也就是说,按下最右边的按钮将返回Qt.LeftButton
。这意味着你不需要在你的代码中考虑鼠标的方向。
上下文菜单
上下文菜单是小型的上下文敏感菜单,通常在右击窗口时出现。Qt支持生成这些菜单,部件有一个特定的事件用来触发它们。在下面的例子中,我们将拦截.contextMenuEvent
aQMainWindow
。这个事件在上下文菜单即将显示时被触发,并被传递一个类型为QContextMenuEvent
的单一值event
。
为了拦截该事件,我们只需用同名的新方法覆盖对象方法。所以在这种情况下,我们可以在我们的MainWindow
子类上创建一个方法,名称为contextMenuEvent
,它将接收所有这种类型的事件。
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
槽(并重新使用为菜单和工具条定义的动作)。
当把初始位置传给exec
函数时,这必须是相对于定义时传入的父对象而言的。在本例中,我们将self
作为父对象,因此我们可以使用全局位置。
为了完整起见,实际上有一种基于信号的方法来创建上下文菜单。
python
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中,每个widget都是两个不同层次的一部分:Python对象层次和Qt布局层次。你如何响应或忽略事件会影响你的用户界面的行为方式。
Python 继承转发
通常你可能想拦截一个事件,对它做一些事情,但仍然会触发默认的事件处理行为。如果你的对象是从一个标准的部件继承而来,它很可能在默认情况下有合理的行为实现。你可以通过使用super()
调用到父级实现来触发它。
这是Python的父类,而不是PyQt的.parent()
。
python
def mousePressEvent(self, event):
print("Mouse pressed!")
super(self, MainWindow).contextMenuEvent(event)
复制代码
该事件将继续保持正常的行为,但你已经添加了一些非干扰性的行为。
布局转发
当你在你的应用程序中添加一个widget时,它也会从布局中获得另一个父节点。一个widget的父级可以通过调用.parent()
。有时你会手动指定这些父对象,比如QMenu
或QDialog
,通常是自动的。例如,当你把一个widget添加到你的主窗口时,主窗口将成为widget的父窗口。
当为用户与用户界面的交互创建事件时,这些事件会被传递给用户界面中最上层的部件。所以,如果你在一个窗口上有一个按钮,并且点击了这个按钮,这个按钮将首先收到事件。
如果第一个部件不能处理该事件,或者选择不处理,该事件就会冒泡到父部件上,父部件将被轮到。这种冒泡的方式一直持续到嵌套的部件,直到事件被处理或者到达主窗口。
在你自己的事件处理程序中,你可以选择将一个事件标记为已处理,调用.accept()
--
python
class CustomButton(QPushButton)
def mousePressEvent(self, e):
e.accept()
复制代码
或者,你也可以通过在事件对象上调用.ignore()
,将其标记为未处理。在这种情况下,该事件将继续在层次结构中冒泡。
class CustomButton(QPushButton)
def event(self, e):
e.ignore()
复制代码
如果你想让你的小组件看起来对事件透明,你可以安全地忽略那些你已经以某种方式实际回应的事件。同样地,你可以选择接受你没有回应的事件,以便让它们安静下来。