信号和槽#

由于 Qt 的性质,QObject 需要一种通信方式,这就是为什么这种机制成为 Qt 的核心特性 的原因。

简单来说,您可以像与您家里的灯光互动一样理解 Signal 和 Slot。当你移动开关(信号)时,你会得到一个结果,这可能是你的灯泡打开/关闭(槽)。

在开发界面时,你可以通过单击按钮的效果得到一个真实的示例:'单击'将是信号,when that button is clicked 将是当按钮被点击时发生的事情,如关闭窗口,保存文档等。

注意

如果你有其他框架或工具包的经验,你可能已经看到了一个称为“回调”的概念。忽视实现细节,回调将与通知函数相关联,在程序中发生事件时传递函数指针。这种方法可能听起来很相似,但它有一些本质的区别,使其成为一个不直观的方法,例如确保回调参数的类型正确性,以及其他一些方法。

所有继承自 QObject 或其子类,如 QWidget 的类都可以包含信号和槽。 对象在特定情况下会发射信号,这些信号可能对其他对象有意义。这是对象用来通信的全部工作。它不知道或关心是否有什么东西在接受它所发射的信号。这是真正的信息封装,确保了对象可以作为软件组件使用。

槽可以用于接收信号,但它们也是正常的成员函数。就像对象不知道是否有人接收它的信号一样,槽也不知道是否有信号连接到它。这确保了可以使用 Qt 创建真正独立的组件。

您可以将任意数量的信号连接到单个槽,一个信号也可以连接到任意数量的槽。甚至可以将一个信号直接连接到另一个信号。(这将在第一个信号发出时立即发出第二个信号。)

Qt 的小部件有大量的预定义信号和槽。例如,QAbstractButton(Qt 中按钮的基类)有一个 clicked() 信号,而 QLineEdit(单行输入字段)有一个名为 clear() 的槽。因此,可以通过将一个 QToolButton 放在 QLineEdit 的右侧,并将它的 clicked() 信号连接到 clear() 槽来实现一个带有清除文本按钮的文本输入字段。这是通过信号的方法 connect() 来完成的。

button = QToolButton()
line_edit = QLineEdit()
button.clicked.connect(line_edit.clear)

connect() 返回一个 QMetaObject.Connection 对象,可以与 disconnect() 方法一起使用来断开连接。

信号还可以连接到自由函数

import sys
from PySide6.QtWidgets import QApplication, QPushButton


def function():
    print("The 'function' has been called!")

app = QApplication()
button = QPushButton("Call function")
button.clicked.connect(function)
button.show()
sys.exit(app.exec())

连接可以用代码明确指定,或者在窗口小部件表格中,可以在 Qt Widgets Designer信号-槽编辑器中设计。

信号类#

在用 Python 编写类时,信号被声明为类级别的 QtCore.Signal() 类的变量。一个基于 QWidget 的按钮可以如下发出 clicked() 信号。

from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QWidget

class Button(QWidget):

    clicked = Signal(Qt.MouseButton)

    ...

    def mousePressEvent(self, event):
        self.clicked.emit(event.button())

Signal 构造函数接收一个 Python 类型和一个 C 类型的元组或列表。

signal1 = Signal(int)  # Python types
signal2 = Signal(QUrl)  # Qt Types
signal3 = Signal(int, str, int)  # more than one type
signal4 = Signal((float,), (QDate,))  # optional types

除此之外,它还可以接收一个命名参数 name,该参数定义信号名称。如果没有传递任何内容,则新信号将具有与被分配给它的变量相同的名称。

# TODO
signal5 = Signal(int, name='rangeChanged')
# ...
rangeChanged.emit(...)

Signal 的另一个有用选项是参数名称,这对于 QML 应用程序通过名称引用发出的值非常有用。

sumResult = Signal(int, arguments=['sum'])
Connections {
    target: ...
    function onSumResult(sum) {
        // do something with 'sum'
    }

槽类#

在派生自 QObject 的类中,应使用装饰器 @QtCore.Slot() 来表示槽。同样,要定义签名,只需像在 QtCore.Signal() 类中一样传递类型。

@Slot(str)
def slot_function(self, s):
    ...

Slot() 还接受一个 name 和一个 result 关键字。result 关键字定义将返回的类型,可以是 C 或 Python 类型。与 Signal() 中的关键字具有相同的行为。如果未传递名称,则新槽将与被装饰的函数具有相同的名称。

我们建议标记所有用于信号连接的方法为 @QtCore.Slot() 装饰器。不这样做会在创建连接时因为将方法添加到 QMetaObject 而产生运行时开销。这特别重要对于已注册 QML 的 QObject 类,缺失的装饰器可能导致错误。

缺少装饰器可以通过设置日志类的激活警告来诊断 qt.pyside.libpyside;例如,通过设置环境变量

export QT_LOGGING_RULES="qt.pyside.libpyside.warning=true"

使用不同类型的信号和槽重载#

实际上可以使用相同名称的不同参数类型列表的信号和槽。这是来自 Qt 5 的遗留代码,不推荐用于新代码。在 Qt 6 中,不同类型的信号有不同的名称。

以下示例使用信号和槽的两个处理程序来展示不同的功能。

import sys
from PySide6.QtWidgets import QApplication, QPushButton
from PySide6.QtCore import QObject, Signal, Slot


class Communicate(QObject):
    # create two new signals on the fly: one will handle
    # int type, the other will handle strings
    speak = Signal((int,), (str,))

    def __init__(self, parent=None):
        super().__init__(parent)

        self.speak[int].connect(self.say_something)
        self.speak[str].connect(self.say_something)

    # define a new slot that receives a C 'int' or a 'str'
    # and has 'say_something' as its name
    @Slot(int)
    @Slot(str)
    def say_something(self, arg):
        if isinstance(arg, int):
            print("This is a number:", arg)
        elif isinstance(arg, str):
            print("This is a string:", arg)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    someone = Communicate()

    # emit 'speak' signal with different arguments.
    # we have to specify the str as int is the default
    someone.speak.emit(10)
    someone.speak[str].emit("Hello everybody!")

通过方法签名字符串指定信号和槽#

信号和槽也可以作为通过 SIGNAL() 和/或 SLOT() 函数传递的 C++ 方法签名字符串来指定

from PySide6.QtCore import SIGNAL, SLOT

button.connect(SIGNAL("clicked(Qt::MouseButton)"),
               action_handler, SLOT("action1(Qt::MouseButton)"))

通常不推荐这样做;仅在信号只能通过 QMetaObject (QAxObjectQAxWidgetQDBusInterfaceQWizardPage::registerField()) 获取的少数情况下需要。

wizard.registerField("text", line_edit, "text",
                     SIGNAL("textChanged(QString)"))

可以通过查询 QMetaMethod.methodSignature() 在检查 QMetaObject 时找到签名字符串。

mo = widget.metaObject()
for m in range(mo.methodOffset(), mo.methodCount()):
    print(mo.method(m).methodSignature())

槽应该使用 @Slot 进行装饰。