信号与槽

介绍

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

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

信号与槽

在 Qt 中,我们有另一种方法来替代回调技术:我们使用信号与槽。当发生特定事件时,会发出一个信号。Qt 的小部件有许多预定义的信号,但我们总是可以子类化小部件以向它们添加自己的信号。槽是响应特定信号而被调用的函数。Qt 的小部件有许多预定义的槽,但通常是子类化小部件并添加自己的槽,以便您可以处理您感兴趣的信号。

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

所有继承自 QObject 或其子类(例如,QWidget)的类都可以包含信号与槽。当对象改变状态的方式可能对其他对象具有兴趣时,它们会发出信号。这是对象进行通信所做的一切。它不知道或关心是否有任何正在接收它发出的信号。这是真正的信息封装,并确保对象可以作为软件组件使用。

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

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

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

信号

当对象的状态以某种可能对其客户端或所有者感兴趣的方式发生变化时,对象会发出信号。信号是公开访问的函数,可以从任何地方发出,但我们建议只从定义信号及其子类的类中发出它们。

当发射信号时,连接到它的槽通常会立即执行,就像正常的函数调用一样。当发生这种情况时,信号和槽机制完全独立于任何GUI事件循环。在所有槽返回后,才会执行紧随其后 emit 语句的代码。当使用 排队连接 时,情况略有不同;在这种情况下,随后的代码将立即继续执行,槽将在稍后执行。

如果多个槽连接到一个信号,当信号发出时,槽将依次执行,它们的执行顺序与它们连接的顺序一致。

信号是由 moc 自动生成的,不应在 .cpp 文件中实现。

关于参数的一点说明:我们的经验表明,如果信号和槽不使用特殊类型,它们将更易于重用。如果 QScrollBar::valueChanged() 使用类似于假设的 QScrollBar::Range 的特殊类型,它就只能连接到为 QScrollBar 定制的槽。将不同的输入小部件连接在一起将是不可能的。

当连接到它的信号发出时,会调用槽。槽是常规的 C++ 函数,可以正常调用;它们唯一特殊的功能是信号可以连接到它们。

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

您还可以定义虚拟槽,这在实际应用中非常有用。

与回调相比,由于信号和槽提供了更多的灵活性,它们会稍微慢一些,尽管在实际应用中这种差异并不显著。通常,发出连接到某些槽的信号比直接调用接收者(使用非虚函数调用)慢约十倍。这是定位连接对象、安全迭代所有连接(即检查在发出时后续接收者是否已被销毁)、以及以通用方式串行化任何参数所需的开销。虽然十个非虚函数调用可能听起来很多,但它们比任何newdelete操作都少得多。当你进行字符串、向量或列表操作时,幕后需要newdelete,信号和槽的开销仅占完整函数调用成本的一小部分。当你在槽中做系统调用或者在间接调用超过十个函数时,也是如此。信号和槽机制的简单性和灵活性足以补偿这种开销,而用户甚至可能注意不到。

请注意,定义了名为signalsslots的变量的其他库,在编译与基于Qt的应用程序一起时可能会发出编译器警告和错误。要解决这个问题,请使用#undef撤销该编译器符号。

一个小例子

可能的一个最小C++类声明如下

class Counter
{
public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }
    void setValue(int value);

private:
    int m_value;
};

一个小QObject-基于类的示例可能如下

#include <QObject>

class Counter : public 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() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

基于QObject的版本具有相同的状态,并提供了公共方法来访问状态,但它还支持使用信号和槽进行组件编程。此类可以通过发出信号valueChanged()来通知外部世界其状态已更改,并且它有一个槽,其他对象可以向其发送信号。

所有包含信号或槽的类都必须在其声明顶部提及Q_OBJECT。它们还必须(直接或间接地)从QObject派生。

槽由应用程序程序员实现。以下是对Counter::setValue()槽的可能的实现

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

emit行中,对象从将新值作为参数发出信号valueChanged()

在下面的代码片段中,我们创建了两个Counter对象,并使用QObject::connect()连接第一个对象的价值变了信号到第二个对象的setValue()

    Counter a, b;
    QObject::connect(&a, &Counter::valueChanged,
                     &b, &Counter::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发出相同的信号,但由于没有将槽连接到b的valueChanged()信号,该信号将被忽略。

请注意,setValue()函数仅当value不等于m_value时设置值和发出信号。这可以防止循环连接中的无限循环(例如,如果b.valueChanged()连接到a.setValue())。

默认情况下,每建立一个连接,都会发出一个信号;重复连接会发出两个信号。你可以使用单个disconnect()()调用断开所有这些连接。如果你传递了Qt::UniqueConnection 类型,那么只有在它不是重复的情况下才会建立连接。如果已经存在重复的(相同的信号到相同对象上的相同槽),连接将失败,且connect()将返回false

此示例说明对象可以在不了解彼此信息的情况下协同工作。为此,只需将这些对象连接起来,这可以通过一些简单的QObject::connect()()函数调用来实现,或者通过uic自动连接功能来实现。

实际示例

以下是一个简单的小部件类的头文件示例,其中没有成员函数。目的是展示你如何在自己的应用中利用信号和槽。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber通过QFrameQWidget继承自QObject,它具有大部分信号-槽知识。它与内置的QLCDNumber小部件颇为类似。

Q_OBJECT宏被预处理器展开,用于声明由moc实现的几个成员函数;如果你收到类似“对LcdNumber的vtable未定义引用”的编译器错误,你很可能忘记了运行moc或包括链接命令中的moc输出。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

在类构造函数和public成员之后,我们声明类的signals。当LcdNumber被请求显示不可能的值时,它会发出一个overflow()信号。

如果你不关心溢出,或者你知道不会发生溢出,你可以忽略overflow()信号,即不将其连接到任何槽。

相反,如果你想当数字溢出时调用两个不同的错误函数,只需将信号连接到两个不同的槽。Qt将按连接顺序调用它们。

public slots:
    void display(int num);
    void display(double num);
    void display(const QString &str);
    void setHexMode();
    void setDecMode();
    void setOctMode();
    void setBinMode();
    void setSmallDecimalPoint(bool point);
};

#endif

槽是一个接收函数,用于获取其他小部件状态变化的信息。LcdNumber使用它,如上面的代码所示,以设置显示的数字。由于display()是程序与类接口的一部分,因此槽是公开的。

几个示例程序将valueChanged()信号连接到display()槽,因此LCD数字连续显示滚动条的值。

请注意,display()是重载的;Qt将根据连接的信号选择合适的版本。与回调相比,你需要找到五个不同的名称,并且你自己跟踪类型。

带有默认参数的信号和槽

信号和槽的签名可以包含参数,并且参数可以有默认值。以QObject::destroyed()为例。

void destroyed(QObject* = nullptr);

当一个QObject被删除时,它会发出这个QObject::destroyed()信号。我们想要捕捉这个信号,无论我们在哪里可能有一个悬空引用的QObject,以便我们可以清理它。合适的槽签名可能是

void objectDestroyed(QObject* obj = nullptr);

要连接信号到槽,我们使用QObject::connect().连接信号和槽有几种方法。第一种是使用函数指针

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

使用函数指针与QObject::connect()有几个优点。首先,它允许编译器检查信号参数是否与槽参数兼容。如果需要,编译器还可以隐式转换参数。

您还可以将信号连接到函数对象或C++11的lambda表达式

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

在这两种情况下,我们在connect()调用的上下文中提供this。上下文对象提供有关接收器应在哪个线程中执行的信息。这是很重要的,因为提供上下文可以确保接收器在上下文线程中执行。

当发送者或上下文被销毁时,lambda将不再连接。您应确保在函数对象内部使用的任何对象在信号被发射时仍然有效。

将信号连接到槽的另一种方法是使用QObject::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对象,而信号不会发送这个对象。这种连接将报告运行时错误。

请注意,在使用这种QObject::connect()重载时,编译器不会检查信号和槽参数。

高级信号和槽使用

对于可能需要有关信号发送者信息的情况,Qt提供了QObject::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宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT替换Qt moc关键字的全部使用。

基于Qt库的信号和槽

基于Qt库的公共API应使用关键字Q_SIGNALS和Q_SLOTS而不是signals和slots。否则,在定义了QT_NO_KEYWORDS的项目中使用此类库会很困难。

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

此定义排除了信号和槽,但不会影响库实现中是否可以使用其他Qt特定关键字。

另请参阅QLCDNumberQObject::connect(),元对象系统Qt属性系统

© 2024 Qt公司有限公司。本文档中包含的贡献文档的所有权属于各自的所有者。此处提供的文档是根据自由软件基金会发布的GNU自由文档许可证版本1.3的条款许可的。Qt及其相应的标示是芬兰和/或其他国家的Qt公司及其所有公司的商标。所有其他商标均为各自所有者的财产。