警告
本节包含从C++自动翻译到Python的代码片段,可能包含错误。
信号与槽¶
Qt的信号和槽机制概述。信号和槽用于对象之间的通信。信号和槽机制是Qt的核心特性,可能也是与其他框架提供的特性最不同的部分。信号和槽的实现依赖于Qt的元对象系统。
介绍¶
在图形用户界面编程中,当我们更改一个小部件时,我们通常希望另一个小部件得到通知。更一般地说,我们希望任何类型的对象都能够相互通信。例如,如果用户点击关闭按钮,我们可能希望调用窗口的close()函数。
其他工具包使用回调来实现这种通信。回调是指向函数的指针,因此如果你希望一个处理函数通知你某个事件,你可以将一个指向另一个函数(回调)的指针传递给处理函数。处理函数在适当的时候调用回调。虽然使用这种方法成功的框架确实存在,但回调可能不直观,并且在确保回调参数的类型正确性方面可能会遇到问题。
信号与槽¶
在Qt中,我们有一个替代回调技术的方案:我们使用信号和槽。当特定事件发生时,信号会被发出。Qt的小部件有许多预定义的信号,但我们总是可以通过子类化小部件来添加自己的信号。槽是一个函数,它在响应特定信号时被调用。Qt的小部件有许多预定义的槽,但通常的做法是子类化小部件并添加自己的槽,以便处理你感兴趣的信号。
信号和槽机制是类型安全的:信号的签名必须与接收槽的签名匹配。(实际上,槽的签名可以比它接收的信号短,因为它可以忽略额外的参数。)由于签名是兼容的,编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。信号和槽是松散耦合的:发出信号的类既不知道也不关心哪些槽接收信号。Qt的信号和槽机制确保如果你将一个信号连接到一个槽,槽将在正确的时间以信号的参数被调用。信号和槽可以接受任意数量的任意类型的参数。它们是完全类型安全的。
所有继承自QObject或其子类(例如,QWidget)的类都可以包含信号和槽。当对象以可能对其他对象感兴趣的方式改变其状态时,信号由对象发出。这是对象进行通信的全部内容。它不知道也不关心是否有任何东西在接收它发出的信号。这是真正的信息封装,确保对象可以用作软件组件。
槽可以用于接收信号,但它们也是普通的成员函数。就像一个对象不知道是否有任何东西接收它的信号一样,一个槽也不知道是否有任何信号连接到它。这确保了可以使用Qt创建真正独立的组件。
你可以将任意数量的信号连接到一个槽,一个信号也可以连接到任意数量的槽。甚至可以将一个信号直接连接到另一个信号。(每当第一个信号发出时,这将立即发出第二个信号。)
信号和槽共同构成了一个强大的组件编程机制。
信号¶
当对象的内部状态以某种可能对对象的客户端或所有者感兴趣的方式发生变化时,对象会发出信号。信号是公共访问函数,可以从任何地方发出,但我们建议仅从定义信号的类及其子类中发出它们。
当信号被发出时,连接到它的槽通常会立即执行,就像普通的函数调用一样。当这种情况发生时,信号和槽机制完全独立于任何GUI事件循环。一旦所有槽返回,emit语句后面的代码就会执行。当使用queued connections时,情况略有不同;在这种情况下,emit关键字后面的代码会立即继续执行,而槽将在稍后执行。
如果多个槽连接到一个信号,当信号发出时,这些槽将按照它们被连接的顺序依次执行。
信号由moc自动生成,不得在.cpp文件中实现。
关于参数的说明:我们的经验表明,如果信号和槽不使用特殊类型,它们更具可重用性。如果QScrollBar::valueChanged()使用特殊类型,例如假设的QScrollBar::Range,它只能连接到专门为QScrollBar设计的槽。将不同的输入小部件连接在一起将是不可能的。
插槽¶
当一个连接到它的信号被发出时,槽被调用。槽是普通的C++函数,可以正常调用;它们唯一的特殊之处是信号可以连接到它们。
由于槽是普通的成员函数,当直接调用时,它们遵循普通的C++规则。然而,作为槽,它们可以通过信号-槽连接被任何组件调用,无论其访问级别如何。这意味着从任意类的实例发出的信号可以导致在不相关的类的实例中调用私有槽。
你也可以将插槽定义为虚拟的,这在实际应用中我们发现非常有用。
与回调相比,信号和槽由于提供了更高的灵活性,因此稍微慢一些,尽管在实际应用中的差异微不足道。一般来说,发出连接到某些槽的信号,大约比直接调用接收者慢十倍,使用非虚函数调用。这是定位连接对象、安全地遍历所有连接(即检查在信号发射期间后续接收者是否未被销毁)以及以通用方式编组任何参数所需的开销。虽然十次非虚函数调用听起来很多,但它比任何new或delete操作的开销要小得多。一旦你执行了在幕后需要new或delete的字符串、向量或列表操作,信号和槽的开销只占整个函数调用成本的很小一部分。每当你在槽中进行系统调用或间接调用超过十个函数时,情况也是如此。信号和槽机制的简单性和灵活性完全值得这些开销,你的用户甚至不会注意到。
请注意,其他定义了名为signals或slots变量的库在与基于Qt的应用程序一起编译时可能会导致编译器警告和错误。要解决此问题,请使用#undef取消定义有问题的预处理器符号。
一个小例子¶
一个最小的C++类声明可能如下:
class Counter(): # public Counter() { m_value = 0; } int value() { return m_value; } def setValue(value): # private m_value = int()
一个基于QObject的小类可能如下所示:
from PySide6.QtCore import QObject class Counter(QObject): Q_OBJECT # Note. The Q_OBJECT macro starts a private section. # To declare public members, use the 'public:' access modifier. # public Counter() { m_value = 0; } int value() { return m_value; } # public slots def setValue(value): # signals def valueChanged(newValue): # private m_value = int()
基于QObject的版本具有相同的内部状态,并提供公共方法来访问状态,但此外它还支持使用信号和槽进行组件编程。这个类可以通过发出信号valueChanged()来告诉外界它的状态已经改变,并且它有一个槽,其他对象可以向其发送信号。
所有包含信号或槽的类必须在声明的顶部提及Q_OBJECT。它们还必须(直接或间接)继承自QObject。
插槽由应用程序程序员实现。以下是Counter::setValue()插槽的一个可能实现:
def setValue(self, value): if value != m_value: m_value = value valueChanged.emit(value)
emit 行从对象发出信号 valueChanged(),并将新值作为参数。
在以下代码片段中,我们创建了两个Counter对象,并使用connect()将第一个对象的valueChanged()信号连接到第二个对象的setValue()槽:
a, = Counter() a.valueChanged.connect( b.setValue) a.setValue(12) # a.value() == 12, b.value() == 12 b.setValue(48) # a.value() == 12, b.value() == 48
调用 a.setValue(12) 会使 a 发出一个 valueChanged(12) 信号,b 将在其 setValue() 槽中接收到该信号,即调用 b.setValue(12)。然后 b 发出相同的 valueChanged() 信号,但由于没有槽连接到 b 的 valueChanged() 信号,该信号被忽略。
请注意,setValue() 函数仅在 value != m_value 时设置值并发出信号。这可以防止在循环连接的情况下出现无限循环(例如,如果 b.valueChanged() 连接到 a.setValue())。
默认情况下,对于你建立的每一个连接,都会发出一个信号;对于重复的连接,会发出两个信号。你可以通过一次disconnect()调用来断开所有这些连接。如果你传递UniqueConnection type,则只有在连接不是重复的情况下才会建立连接。如果已经存在重复的连接(完全相同的信号连接到相同对象的完全相同槽),连接将失败,并且connect将返回false。
这个例子说明了对象可以在不需要了解彼此任何信息的情况下协同工作。为了实现这一点,对象只需要被连接在一起,这可以通过一些简单的connect()函数调用,或者使用uic的自动连接功能来实现。
一个真实的例子¶
以下是一个没有成员函数的简单小部件类的头文件示例。目的是展示如何在自己的应用程序中利用信号和槽。
#ifndef LCDNUMBER_H #define LCDNUMBER_H from PySide6.QtWidgets import QFrame class LcdNumber(QFrame): Q_OBJECT
LcdNumber 继承自 QObject,通过 QFrame 和 QWidget,它拥有大部分信号-槽的知识。它与内置的 QLCDNumber 小部件有些相似。
Q_OBJECT 宏由预处理器扩展,以声明由 moc 实现的几个成员函数;如果你遇到类似“未定义对 LcdNumber 的 vtable 引用”的编译器错误,你可能忘记了运行 moc 或在链接命令中包含 moc 输出。
# public LcdNumber(QWidget parent = None) # signals def overflow():
在类构造函数和public成员之后,我们声明了类signals。LcdNumber类在被要求显示一个不可能的值时,会发出一个信号overflow()。
如果你不关心溢出,或者你知道溢出不会发生,你可以忽略overflow()信号,即不要将其连接到任何槽。
另一方面,如果你想在数字溢出时调用两个不同的错误函数,只需将信号连接到两个不同的槽。Qt 将按连接顺序调用这两个槽。
# public slots def display(num): def display(num): def display(str): def setHexMode(): def setDecMode(): def setOctMode(): def setBinMode(): def setSmallDecimalPoint(point): #endif
槽是一个接收函数,用于获取其他小部件状态变化的信息。LcdNumber 使用它,如上代码所示,来设置显示的数字。由于 display() 是类与程序其余部分接口的一部分,因此槽是公开的。
几个示例程序将QScrollBar的valueChanged()信号连接到display()槽,因此LCD数字持续显示滚动条的值。
请注意,display() 是重载的;当你将一个信号连接到槽时,Qt 会选择适当的版本。使用回调时,你必须找到五个不同的名称并自己跟踪类型。
带有默认参数的信号与槽¶
信号和槽的签名可能包含参数,并且这些参数可以有默认值。考虑 destroyed() :
void destroyed(QObject* = nullptr);
当一个QObject被删除时,它会发出这个destroyed()信号。我们希望捕获这个信号,无论我们在哪里可能有对已删除的QObject的悬空引用,以便我们可以清理它。一个合适的槽函数签名可能是:
void objectDestroyed(QObject* obj = nullptr);
要将信号连接到槽,我们使用connect()。有几种方法可以连接信号和槽。第一种是使用函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
使用connect()与函数指针有几个优点。首先,它允许编译器检查信号的参数是否与槽的参数兼容。如果需要,编译器还可以隐式转换参数。
你也可以连接到仿函数或C++11 lambda表达式:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
在这两种情况下,我们在调用connect()时提供this作为上下文。上下文对象提供了关于接收器应在哪个线程中执行的信息。这很重要,因为提供上下文确保了接收器在上下文线程中执行。
当发送者或上下文被销毁时,lambda将被断开连接。你应确保在信号发出时,函子内使用的任何对象仍然存活。
将信号连接到槽的另一种方法是使用connect()以及SIGNAL和SLOT宏。关于是否在SIGNAL()和SLOT()宏中包含参数的规则是,如果参数有默认值,传递给SIGNAL()宏的签名不能比传递给SLOT()宏的签名参数少。
所有这些都可以工作:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*))); connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed())); connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
但这个不会起作用:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
…因为插槽将期望一个QObject,而信号不会发送。此连接将报告运行时错误。
请注意,当使用此connect()重载时,信号和槽的参数不会被编译器检查。
高级信号和插槽使用¶
对于可能需要信号发送者信息的情况,Qt提供了sender()函数,该函数返回发送信号的对象的指针。
Lambda表达式是一种将自定义参数传递给槽的便捷方式:
connect(action, &QAction::triggered, engine, [=]() { engine->processAction(action->text()); });
使用Qt与第三方信号和插槽¶
可以使用Qt与第三方信号/槽机制。你甚至可以在同一个项目中同时使用这两种机制。为此,请将以下内容写入你的CMake项目文件中:
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)
在qmake项目(.pro)文件中,你需要编写:
CONFIG += no_keywords
它告诉Qt不要定义moc关键字signals、slots和emit,因为这些名称将被第三方库使用,例如Boost。然后,要继续使用带有no_keywords标志的Qt信号和槽,只需将源代码中所有使用的Qt moc关键字替换为相应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT。
基于Qt库的信号和槽¶
基于Qt的库的公共API应使用关键字Q_SIGNALS和Q_SLOTS,而不是signals和slots。否则,在定义了QT_NO_KEYWORDS的项目中很难使用这样的库。
为了强制执行此限制,库创建者可以在构建库时设置预处理器定义 QT_NO_SIGNALS_SLOTS_KEYWORDS。
此定义排除了信号和槽,而不影响其他Qt特定关键字是否可以在库实现中使用。