信号与槽¶
由于Qt的特性,QObject
需要一种通信方式,这就是为什么这个机制成为Qt的核心特性。
简单来说,你可以用与家中灯光互动的方式来理解信号与槽。当你移动电灯开关(信号)时,你会得到一个结果,可能是你的灯泡被打开/关闭(槽)。
在开发界面时,您可以通过点击按钮的效果获得一个真实的例子:'点击'将是信号,而槽将是点击该按钮时发生的事情,比如关闭窗口、保存文档等。
注意
如果您有其他框架或工具包的经验,您可能听说过一个叫做“回调”的概念。抛开实现细节不谈,回调将与通知函数相关,传递一个函数指针,以防由于程序中发生的事件而需要它。这种方法可能听起来相似,但存在一些本质上的差异,使其成为一种不直观的方法,比如确保回调参数的类型正确性,以及其他一些差异。
所有继承自QObject
或其子类(如QWidget
)的类都可以包含信号和槽。信号由对象发出,当它们以可能对其他对象感兴趣的方式改变状态时。这是对象进行通信的全部内容。它不知道也不关心是否有任何东西在接收它发出的信号。这是真正的信息封装,确保对象可以作为软件组件使用。
插槽可以用于接收信号,但它们也是普通的成员函数。就像一个对象不知道是否有任何东西接收它的信号一样,一个插槽也不知道是否有任何信号连接到它。这确保了可以使用Qt创建真正独立的组件。
你可以将任意数量的信号连接到一个单一的槽,一个信号也可以连接到任意数量的槽。甚至可以将一个信号直接连接到另一个信号。(每当第一个信号发出时,这将立即发出第二个信号。)
Qt的小部件有许多预定义的信号和槽。例如,
QAbstractButton
(Qt中按钮的基类)
有一个clicked()
信号,而QLineEdit
(单行输入字段)有一个名为clear()
的槽。
因此,可以通过在QLineEdit
的右侧放置一个QToolButton
并将其clicked()
信号连接到槽
clear()
来实现一个带有清除文本按钮的文本输入字段。这是通过信号的connect()
方法来完成的:
button = QToolButton()
line_edit = QLineEdit()
button.clicked.connect(line_edit.clear)
connect()
返回一个
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的信号-槽编辑器中设计。
connect()
函数接受一个可选的 ConnectionType
参数,该参数指定了与线程和事件循环相关的行为。
信号类¶
在Python中编写类时,信号被声明为类级别的变量,属于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派生的类中的槽应该由装饰器@Slot
指示。同样,要定义一个签名,只需传递类似于Signal
类的类型。
@Slot(str)
def slot_function(self, s):
...
Slot()
也接受一个 name
和一个 result
关键字。
result
关键字定义了将返回的类型,可以是 C 或 Python 类型。name
关键字的行为与 Signal()
中的相同。如果没有传递名称,则新插槽将具有与被装饰函数相同的名称。
我们建议使用@Slot
装饰器标记所有由信号连接使用的方法。如果不这样做,会导致在创建连接时将方法添加到QMetaObject
中,从而产生运行时开销。这对于使用QML注册的QObject
类尤其重要,缺少装饰器可能会引入错误。
可以通过设置日志类别 qt.pyside.libpyside
的警告来诊断缺失的装饰器;例如通过设置环境变量:
export QT_LOGGING_RULES="qt.pyside.libpyside.warning=true"
使用不同类型重载信号和槽¶
实际上可以使用具有不同参数类型列表的同名信号和槽。这是从Qt 5遗留下来的,不建议在新代码中使用。在Qt 6中,信号对于不同类型具有不同的名称。
以下示例使用两个处理程序来展示Signal和Slot的不同功能。
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
(QAxObject
、QAxWidget
、QDBusInterface
或 QWizardPage::registerField()
)访问:
wizard.registerField("text", line_edit, "text",
SIGNAL("textChanged(QString)"))
签名字符串可以通过在自省QMetaObject
时查询QMetaMethod.methodSignature()
找到:
mo = widget.metaObject()
for m in range(mo.methodOffset(), mo.methodCount()):
print(mo.method(m).methodSignature())
插槽应使用@Slot
进行装饰。