信号&槽
信号和槽被用于对象之间的通信。信号和槽机制是 Qt 的一个核心特性,也许是与其他框架提供的功能最不同的部分。信号和槽是通过 Qt 的元对象系统实现的。
简介
在 GUI 编程中,当我们改变一个小部件时,通常希望另一个小部件能够收到通知。更一般地说,我们希望任何类型的对象都能够相互通信。例如,如果用户点击一个关闭按钮,我们可能希望窗口的 close() 函数被调用。
其他工具包使用回调来实现这种通信。回调是指向函数的指针,因此如果您希望处理函数通知您某个事件,您将一个指向另一个函数(回调)的指针传递给处理函数。处理函数在适当的时候调用回调。虽然使用此方法的成功框架确实存在,但回调可能不直观,并且在确保回调参数的类型正确性方面可能会遇到问题。
信号和槽
在 Qt 中,我们有一种替代回调技术的方法:我们使用信号和槽。当特定事件发生时,会发射一个信号。Qt 的小部件有许多预定义的信号,但我们始终可以对小部件进行子类化,以向它们添加我们自己的信号。槽是响应特定信号调用的函数。Qt 的小部件有许多预定义的槽,但通常的做法是对小部件进行子类化并添加自己的槽,以便您可以处理您感兴趣的信号。
信号和槽机制是类型安全的:信号的签名必须与接收槽的签名匹配。(实际上,槽的签名可以比接收的信号短,因为它可以忽略额外的参数。)由于签名是兼容的,编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配。基于字符串的 SIGNAL 和 SLOT 语法将在运行时检测到类型不匹配。信号和槽是松散耦合的:发射信号的类既不知道也不关心哪些槽接收信号。Qt 的信号和槽机制确保如果将信号连接到槽,槽将在正确的时间使用信号的参数调用。信号和槽可以接受任意数量的任意类型的参数。它们是完全类型安全的。
所有继承自 QObject 或其子类的类都可以包含信号和槽。当对象以可能对其他对象感兴趣的方式更改其状态时,它们会发出信号。这是对象进行通信的全部内容。它不知道也不关心是否有任何东西接收它发出的信号。这是真正的信息封装,并确保对象可以作为软件组件使用。
槽可用于接收信号,但它们也是普通的成员函数。正如对象不知道是否有任何东西接收其信号一样,槽也不知道是否有任何信号连接到它。这确保可以使用 Qt 创建真正独立的组件。
您可以将任意数量的信号连接到单个槽,并且一个信号可以连接到尽可能多的槽。甚至可以将一个信号直接连接到另一个信号。(这将在第一个信号发出时立即发出第二个信号。)
综合起来,信号和槽组成了一个强大的组件编程机制。
信号
当对象的内部状态发生变化时,该对象可能会发出信号。信号是公共访问函数,可以从任何地方发出,但我们建议只从定义信号及其子类的类中发出。
当信号被发出时,通常与其连接的槽会立即执行,就像普通的函数调用一样。在这种情况下,信号和槽机制完全独立于任何 GUI 事件循环。在所有槽返回后,emit
语句后的代码才会执行。当使用排队连接时情况稍有不同;在这种情况下,emit
关键字后面的代码将立即继续执行,而槽将稍后执行。
如果有多个槽连接到一个信号,当信号被发出时,这些槽将按连接的顺序依次执行。
信号是由 moc 自动生成的,不能在 .cpp
文件中实现。它们永远不能具有返回类型(即使用 void
)。
关于参数的说明:我们的经验表明,如果信号和槽不使用特殊类型,则它们更具可重用性。如果 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
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);
}
}
在以下代码片段中,我们创建了两个 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
继承自 QObject,通过 QFrame 和 QWidget 获得大部分的信号槽知识。它与内置的 QLCDNumber 小部件有些相似。
Q_OBJECT 宏由预处理器展开,用于声明由 moc 实现的几个成员函数;如果您在编译器出现类似于 “undefined reference to vtable for LcdNumber
” 的错误,您可能忘记了运行 moc 或在链接命令中包含 moc 的输出。
public:
LcdNumber(QWidget *parent = nullptr);
signals:
void overflow();
在类构造函数和公共成员之后,我们声明类的信号。当 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()
是类与程序的其余部分之间的接口的一部分,所以槽是公共的。
许多示例程序将 QScrollBar 的 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 与第三方信号/槽机制。甚至可以在同一项目中同时使用两种机制。只需在您的 qmake 项目 (.pro) 文件中添加以下行。
CONFIG += no_keywords
传递给槽的一种方便方式:
connect(action, &QAction::triggered, engine,
[=]() { engine->processAction(action->text()); });
使用 Qt 与第三方信号和槽
可以使用 Qt 与第三方信号/槽机制。甚至可以在同一项目中同时使用两种机制。只需在您的 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。