本页内容

信号与槽

简介

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

其他工具包通过回调来实现这种通信。回调是一个函数指针,因此,如果你希望某个处理函数在发生某些事件时通知你,就需要将另一个函数(即回调)的指针传递给该处理函数。 随后,处理函数会在适当的时候调用该回调。虽然确实存在采用此方法且运行良好的框架,但回调机制可能不够直观,且在确保回调参数的类型正确性方面可能存在问题。

信号与槽

在 Qt 中,我们提供了一种替代回调技术的方法:使用信号与槽。当特定事件发生时,会发出一个信号。Qt Widgets 具有许多预定义的信号,但我们始终可以通过继承 Widgets 来为其添加自定义信号。 槽(slot)是一种在响应特定信号时被调用的函数。Qt Widgets 拥有许多预定义的槽,但通常的做法是继承控件并添加自定义槽,以便处理您感兴趣的信号。

对象之间的关联

信号与槽机制是类型安全的:信号的签名必须与接收它的槽的签名相匹配。(实际上,槽的签名可能比它接收的信号更短,因为它可以忽略多余的参数。)由于签名兼容,当使用基于函数指针的语法时,编译器可以帮助我们检测类型不匹配的情况。 基于字符串的 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() 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() 将第一个对象的valueChanged() 信号连接到第二个对象的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 也会发出相同的valueChanged() 信号,但由于没有槽连接到b 的valueChanged() 信号,因此该信号将被忽略。

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

默认情况下,每次建立连接时都会发出一个信号;若存在重复连接,则会发出两个信号。您可以通过一次disconnect() 调用断开所有这些连接。 若将Qt::UniqueConnection type 作为参数传递,则仅当该连接不属于重复连接时才会建立。如果已经存在重复连接(即完全相同的信号连接到同一对象上的完全相同的插槽),则连接将失败,connect 函数将返回false 。

此示例说明了对象无需了解彼此的任何信息即可协同工作。要实现这一点,只需将对象相互连接即可,这可以通过一些简单的QObject::connect()函数调用,或利用uic的 自动连接功能来实现。

一个实际示例

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

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber 该类通过QFrame 和QWidget 继承了QObject ,后者包含了大部分关于信号-槽的知识。它与内置的QLCDNumber 控件有些相似。

Q_OBJECT 宏会被预处理器展开,以声明若干由moc 实现的成员函数;如果您遇到类似“LcdNumber 的 vtable 引用未定义”的编译器错误,很可能是您忘记运行 moc或在链接命令中包含 moc 的输出结果。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

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

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

另一方面,如果你希望在数字溢出时调用两个不同的错误处理函数,只需将该信号连接到两个不同的槽即可。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

槽(slot)是一种接收函数,用于获取其他控件状态变化的信息。如上文代码所示,LcdNumber 通过它来设置显示的数字。由于 `display() ` 是该类与程序其余部分交互的接口的一部分,因此该槽是公共的。

几个示例程序将QScrollBar 的valueChanged()信号连接到display() 槽,从而使LCD上的数字持续显示滚动条的值。

请注意,display() 已被重载。Qt 的信号与槽为您提供了强类型连接。与传统的回调相比,它们在编译时更安全。使用回调时,您需要使用不同的函数名并手动跟踪类型。但使用重载函数时,您需要指定要使用的版本。下一节将向您展示具体方法。

连接重载的信号和槽

当信号或槽被重载(即存在多个具有不同参数的版本)时,您需要使用函数指针语法显式指定要连接的版本。您可以使用qOverload() 或static_cast 来消除歧义:

// Connect to the QString overload of QCompleter::activated(const QString &)
connect(completer, qOverload<const QString &>(&QCompleter::activated),
        this, &MyClass::onActivatedText);

// Or select QLCDNumber::display(int) when connecting from QSlider::valueChanged(int)
connect(slider, &QSlider::valueChanged,
        lcd, qOverload<int>(&QLCDNumber::display));

// Using static_cast (more verbose):
connect(completer, static_cast<void(QCompleter::*)(const QString &)>(&QCompleter::activated),
        this, &MyClass::onActivatedText);

// Or using a lambda to call the correct overload:
connect(slider, &QSlider::valueChanged,
        this, [lcd](int value) { lcd->display(value); });

自动连接管理

Qt XML 会自动管理QObject 派生类型之间的连接生命周期。当发送方或接收方对象被销毁时,连接会自动解除,从而防止对已删除对象的调用。这既适用于函数指针语法,也适用于基于字符串的 SIGNAL/SLOT 语法。

对于 lambda 连接,请提供一个上下文对象(通常为this ),以确保在上下文被销毁时断开 lambda 的连接:

connect(button, &QPushButton::clicked, this, [this]{ handleClick(); });

虽然 Qt 会防止信号传递给已被完全销毁的对象,但在对象销毁过程中,当派生类的析构函数完成后但尚未到达~QObject 之前,信号仍可能被传递。这一限制特别适用于使用函数指针语法建立的连接。当基类的析构函数发出信号,而已销毁的派生类曾与该信号建立连接时,可能会发生这种情况。 如果这对您的类可能造成问题,请考虑在析构函数中显式断开此类信号。

带有默认参数的信号与槽

信号和槽的签名可能包含参数,且这些参数可以具有默认值。以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 XML 提供了 `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)所使用。 然后,若要继续使用 Qt 信号和槽并启用 `no_keywords ` 标志,只需将源代码中所有 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 特有的关键字。

另请参阅 QLCDNumber 、QObject::connect()、元对象系统以及Qt 的属性系统。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.