警告

本节包含从C++自动翻译到Python的片段,可能存在错误。

信号与槽#

Qt中信号和槽的跨对象通信机制的概述。信号和槽用于对象之间的通信。信号和槽机制是Qt的核心特性,可能是与其他框架提供的功能差异最大的部分。信号和槽是通过Qt的元对象系统实现的。

简介#

在GUI编程中,当我们更改一个小部件时,我们经常希望通知另一个小部件。更普遍地说,我们希望任何类型的对象都能够互相通信。例如,如果用户点击了关闭按钮,我们可能希望调用窗口的close()函数。

其他工具包通过回调实现这种通信。回调是指向函数的指针,因此如果您想要一个处理函数通知您某个事件,您会将另一个函数(回调)的指针传给处理函数。然后,处理函数在适当的时候调用回调。虽然存在使用此方法成功的框架,但是回调可能不可直观,并可能在保证回调参数的类型正确性方面存在问题。

信号与槽#

在Qt中,我们有回调技术的替代方案:我们使用信号和槽。特定事件发生时,会发出信号。Qt的控件有许多预定义的信号,但我们可以始终通过子类化控件为它们添加自己的信号。槽是对特定信号做出响应的函数。Qt的控件有许多预定义的槽,但将控件子类化并添加自己的槽,以便您处理感兴趣的工具包的习惯做法很常见。

../_images/abstract-connections.png

信号和槽机制是类型安全的:信号的签名必须与接收槽的签名匹配。(实际上,槽的签名可以比接收的信号短,因为它可以忽略额外的参数。)由于签名是兼容的,编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。信号和槽可以接受任何数量和类型的参数。它们是完全类型安全的。

所有继承自 QObject 或其子类(例如 QWidget)的类都可以包含信号和槽。当对象状态变更而可能对其他对象感兴趣时,会发出信号。这是对象进行通信的全部内容。它不知道也不关心是否有任何东西正在接收它发出的信号。这是真正的信息封装,并确保对象可以作为软件组件使用。

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

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

信号和槽一起构成了一个强大的组件编程机制。

信号#

当对象内部状态以某种方式改变,可能会让对象或其拥有的客户端感兴趣时,会发出信号。信号是公共访问函数,可以来自任何地方发出,但我们建议只从定义信号及其子类的类中发出。

当发出信号时,连接到该信号的所有槽将像普通函数调用一样立即执行。在这种情况下,信号和槽机制完全独立于任何GUI事件循环。在所有槽返回后,将执行 emit 语句之后的代码。当使用 queued connections 时,情况略有不同;在这种情况下,跟随 emit 关键字的代码将立即继续执行,而槽将在稍后执行。

如果有多个槽连接到同一个信号,当信号发出时,槽将按连接的顺序一个接一个地执行。

信号由 moc 自动生成,必须在 .cpp 文件中实现。

关于参数的说明:我们的经验表明,如果不使用特殊类型,信号和槽的可重用性会更高。如果 QScrollBar::valueChanged() 使用诸如假定的 QScrollBar::Range 之类的特殊类型,则只能将其连接到专门为 QScrollBar 设计的槽。将不同的输入小部件连接在一起将是无法实现的。

#

当连接到槽的信号发出时,会调用该槽。槽是普通的 C++ 函数,可以像通常一样调用;它们唯一的特殊特性是可以将信号连接到它们。

由于槽是普通的成员函数,在直接调用时遵循正常的 C++ 规则。但是,作为槽,通过信号-槽连接,无论其访问级别如何,任何组件都可以调用它们。这意味着从任意类的实例发出的信号可以导致在无关类的实例中调用私有槽。

您还可以将槽定义为虚拟的,这在实践中发现非常有用。

与回调函数相比,信号和槽由于提供了更高的灵活性,所以略慢一些,但在实际应用中的差异微乎其微。一般来说,触发连接到一些槽的信号,大约比直接调用接收器慢十倍,这是由于非虚拟函数调用造成的。这是找到连接对象、安全地迭代所有连接(即检查在触发过程中后续接收器没有被销毁),以及以通用方式编组任何参数所必须的开销。虽然十次非虚拟函数调用听起来很多,但它比任何newdelete操作的开销要小得多。例如,只要你在后台操作的串行或向量列表操作需要newdelete,信号和槽的开销就只占整个函数调用成本的一小部分。无论何时你在槽中执行系统调用;或者间接调用超过十个函数,情况都是如此。信号和槽机制简单灵活,足以抵消开销,而用户甚至不会注意到。

注意,其他定义称为signalsslots的变量的库,当与Qt应用程序一起编译时可能会产生编译警告和错误。要解决这个问题,取消定义引起麻烦的预处理器符号。

一个小例子#

一个最小的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
# 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() 信号,但由于没有槽与 bvalueChanged() 信号连接,因此该信号被忽略。

请注意,只有当值 value != m_value 时,setValue() 函数才会设置值并发出信号。这防止了在循环连接情况下(例如,如果 b.valueChanged() 连接到 a.setValue())发生无限循环。

默认情况下,对于每个你创建的连接,都会发出一个信号;重复连接会发出两个信号。你可以通过单个 disconnect() 调用来断开所有这些连接。如果你传递 UniqueConnection类型的值,则只有在它不是重复连接时才会创建连接。如果已经存在重复(同一对象上的完全相同的信号到同一槽),则连接将失败,并且连接将返回 false

此示例说明了对象可以协同工作而不需要彼此之间的任何信息。为了实现这一点,只需要将对象连接起来,这可以通过一些简单的 connect() 函数调用或uic的自动连接功能来实现。

真实示例#

以下是一个不带成员函数的简单小部件类的头文件示例。目的是展示你如何在你的应用程序中使用信号和槽。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

from PySide6.QtWidgets import QFrame

class LcdNumber(QFrame):
Q_OBJECT

LcdNumber 通过 QFrame 和 QWidget 继承了 QObject,它拥有了大部分的信号-槽知识。这有点类似于内置的 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 lambdas

connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });

在这两种情况下,我们在连接()调用中提供 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 关键字 signalsslotsemit,因为这些名称将被第三方库(例如,Boost)使用。然后,要继续使用带 no_keywords 标志的 Qt 信号和槽,只需将你的源代码中所有 Qt moc 关键字的用法替换为相应的 Qt 宏 Q_SIGNALS(或 Q_SIGNAL)、Q_SLOTS(或 Q_SLOT) 和 Q_EMIT

基于 Qt 的库中的信号和槽#

基于 Qt 的库的公共 API 应使用关键字 Q_SIGNALSQ_SLOTS,而不是 signalsslots。否则,在定义了 QT_NO_KEYWORDS 的大型项目中使用这样的库将变得很困难。

为了强制执行此限制,库创建者可能在构建库时设置预处理器定义 QT_NO_SIGNALS_SLOTS_KEYWORDS

本定义排除了信号和槽,但不影响在其他库实现中使用 Qt 特定关键词的能力。