信号和槽(Signals and Slots)
(原创)本文翻译自Qt 5.10.1 参考文档,(翻译的本意是为了更好学习,如有错误请批评指正)
信号和槽用于对象之间的通信。信号和槽机制是Qt的核心特性,可能是与其他框架提供的特性最大不同的部分。信号和槽是由QT的元对象系统实现的。
引言
在GUI编程中,当我们更改一个小部件时,我们通常希望通知另一个小部件。更一般地,我们希望任何类型的对象都能相互通信。例如,如果用户单击“关闭”按钮,我们可能希望调用窗口的CLOSE()函数。
其他工具包使用回调实现这种通信。回调是函数的指针,因此,如果希望处理函数通知您某个事件,则将指向另一个函数(回调)的指针传递给处理函数。然后,处理函数在适当的时候调用回调。虽然使用这种方法的成功框架确实存在,但回调可能是不直观的,并且在确保回调参数的类型正确性方面可能会遇到问题。
信号和槽(Signals and Slots)
在Qt中,我们有一个替代回调技术的方法:我们使用信号和槽。在特定事件发生时发出信号。Qt的部件有许多预定义的信号,但是我们总是可以通过将部件子类化来添加我们自己的信号到它们中去。槽是响应特定信号而被调用的函数。Qt的部件有许多预定义的槽,但是通常的做法是子类化部件并添加您自己的插槽,这样您就可以处理你感兴趣的信号了。
信号和槽机制是类型安全的:信号的签名必须与接收槽的签名相匹配。(事实上,槽的签名可能比它所接收信号的签名短,因为它可以忽略额外的参数。)由于签名是兼容的,编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配。基于字符串的信号和槽语法将在运行时检测类型不匹配。信号和槽是松散耦合的:发出信号的类,不知道也不关心哪个槽接收该信号。如果您将一个信号连接到一个槽,Qt的信号和槽机制能确保在适当的时候用信号的参数调用该槽。信号和插槽可以接受任意数量任何类型的参数。它们绝对是类型安全的。
所有从QObject或其子类(例如QWidget)继承的类都可以包含信号和槽。当对象以其他对象可能感兴趣的方式改变其状态时,它们就会发出信号。这就是通信时对象所做的全部。它不知道也不关心是否有谁正在接收它发出的信号。这就是真正的信息封装,并确保对象可以用作软件组件。
槽可用来接收信号,但它们也是正常的成员函数。就像一个对象不知道是否有谁接收它的信号一样,一个槽也不知道是否有什么信号连接到它。这确保了可以使用Qt创建真正独立的组件。
您可以将任意多的信号连接到单个槽,也可根据需要将一个信号连接到任意多个插槽。甚至可以将一个信号直接连接到另一个信号。(每当发出第一个信号时,将立即发出第二个信号。)
信号和槽一起构成了强大的组件编程机制。
信号(Signals)
当对象的内部状态以某种对象的客户或所有者感兴趣的方式发生改变时,该对象发出的信号。信号是公共访问函数,可以从任何地方发出,但我们建议只从定义信号的类及其子类中发出。
当信号发出时,连接到它的槽通常会立即执行,就像一般的函数调用一样。这种情况下,信号和插槽机制完全独立于任何GUI事件循环。一旦所有槽返回,就会执行发出信号语句后面的代码。在使用队列连接时,情况略有不同;在这种情况下,Emit关键字后面的代码将立即执行,而槽将在稍后执行。
如果多个槽连接到一个信号,则当信号被发射时,槽将按照它们连接的顺序一个接一个地执行。
信号由MOC自动生成,不能在.cpp文件中实现。它们永远不能有返回类型(即使用void)。
关于参数的说明:我们的经验表明,如果信号和插槽不使用特殊类型,它们就方便重用。试想QScrollBar::lue eChanged()使用一种特别类型,如假定QScrollBar::Rnge,则只能将其连接到专门为QScrollBar设计的插槽。将不同的输入部件连接在一起是不可能的。
槽(Slots)
当槽连接的信号被发射时,槽被调用。槽是标准的C++函数,可以正常调用;它们唯一特别的是信号可以连接到它们。
由于槽是标准成员函数,因此它们在直接调用时遵循标准的C++规则。但是,作为槽时,任何组件都可以通过信号-槽连接调用它们,而不管其访问级别如何。这意味着从任意类的实例中发出的信号可以导致一个不相关类实例中的私有槽被调用。
你还可以将槽定义为virtual,这在实践中非常有用。
因为它们提供了更大的灵活性,与回调相比信号和槽稍慢一点,尽管如此在实际应用程序中的差别不大。一般来说,发出一个连接到某些槽的信号,比直接呼叫接收器的非虚拟函数调用慢大约十倍。这是定位连接对象、安全地迭代所有连接(即检查随后的接收器在发射过程中未被销毁)和以一般方式序列化所有参数所需的开销。虽然10次非虚拟函数调用听起来可能很大,但它的开销比任何new或delete操作都要小得多。一旦在执行字符串、向量或列表操作时需要new或delete的场景下,信号和槽开销只占整个函数调用成本的一小部分。当你在槽中执行系统调用时,或者间接调用超过10个函数时,情况也是如此。信号和槽机制的简单性和灵活性是非常超值的,你的用户甚至根本不会注意。
请注意,定义名为signals或slots的变量的其他库在与基于Qt的应用程序一起编译时可能会导致编译器警告和错误。为了解决这个问题,#undef冲突的预处理器符号。
小例子
可读的短小的C++类声明:
1 class Counter 2 { 3 public: 4 Counter() { m_value = 0; } 5 6 int value() const { return m_value; } 7 void setValue(int value); 8 9 private: 10 int m_value; 11 };
基于QObject的类:
1 #include2 class Counter : public QObject 3 { 4 Q_OBJECT 5 public: 6 Counter() { m_value = 0; } 7 8 int value() const { return m_value; } 9 10 public slots: 11 void setValue(int value); 12 13 signals: 14 void valueChanged(int newValue); 15 16 private: 17 int m_value; 18 };
基于QObject的版本具有相同的内部状态,并提供访问状态的公共方法,但除此之外,它还支持使用信号和槽进行组件编程。这个类可以通过发出信号valueChanged()来告诉外部世界它的状态已经改变了,并且它有一个其他对象可以发送信号到达的槽。
所有包含信号或插槽的类都必须在声明的顶部提到Q_Object。它们还必须(直接或间接)从QObject派生出来。
槽由应用程序员实现。下面是Counter::setValue() 槽的一个可能实现:
1 void Counter::setValue(int value) 2 { 3 if (value != m_value) { 4 m_value = value; 5 emit valueChanged(value); 6 } 7 }
发出行从对象发出信号value eChanged(),以新值作为参数。
在下面的代码片段中,我们创建了两个计数器对象,并使用QObject::connect()将第一个对象的value eChanged()信号连接到第二个对象的setValue()槽:
1 Counter a, b; 2 QObject::connect(&a, &Counter::valueChanged, 3 &b, &Counter::setValue); 4 5 a.setValue(12); // a.value() == 12, b.value() == 12 6 b.setValue(48); // a.value() == 12, b.value() == 48
调用a.setValue(12)使a发出一个value eChanged(12)信号,b将在其setValue()槽中接收该信号,即调用b.setValue(12)。然后b发出相同的value eChanged()信号,但是由于没有连接到b的value eChanged()信号的槽,因此忽略了该信号。
注意,setValue()函数设置值,只有当value!=m_value时才发出信号。这防止了循环连接情况下的无限循环(例如,如果b.value eChanged()连接到a.setValue())。
默认情况下,对于所建立的每个连接,都会发出一个信号;对于重复的连接,会发出两个信号。您可以通过一个disconnect()调用来断开所有这些连接。如果您传递了Qt::UniqueConnection类型,则只有当它不是重复时才会进行连接。如果已经存在重复(相同的信号指向同一对象上的完全相同的槽),则连接将失败,连接将返回false。
这个例子说明对象可以一起工作,而不需要知道关于彼此的任何信息。为了实现这一点,对象只需要连接在一起,而这可以通过一些简单的QObject::connect()函数调用来实现,或者使用UIC的自动连接功能来实现。
一个具体的例子
下面是一个部件的简单注释示例。
#ifndef LCDNUMBER_H #define LCDNUMBER_H #includeclass LcdNumber : public QFrame { Q_OBJECT
LcdNumber通过QFrame和QWidget继承了拥有大部分信号-槽知识的QObject。它有点类似于内置的QLCDNumber小部件。
预处理器对Q_Object宏进行扩展,以声明由MOC实现的几个成员函数;如果得到编译器错误“undefined reference to vtable for LcdNumber”,则可能忘记运行moc或在未在链接命令中包含moc输出。
public: LcdNumber(QWidget *parent = 0);
它显然与moc无关,但是如果继承QWidget,您几乎肯定希望在构造函数中包含父参数,并将其传递给基类的构造函数。
这里省略了一些析构函数和成员函数;moc忽略成员函数。
signals: void overflow();
当要求LcdNumber显示一个不可能的值时,LcdNumber会发出一个信号。
如果不关心溢出,或者知道溢出不会发生,你可以忽略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()是类接口的一部分是与程序其余部分一起的,所以槽是public。
几个示例程序将QScrollBar的valueChanged()信号连接到display()槽,因此LCDnumber连续显示滚动条的值。
注意,display()已经重载;当您将信号连接到槽时,Qt将选择适当的版本。使用回调,您必须找到五个不同的名称,并自己跟踪类型。
本例中省略了一些不相关的成员函数。
信号和槽的签名可能包含参数,参数可以具有默认值。思考QObject::destroyed():
void destroyed(QObject* = 0);
当一个QObject被删除时,它会发出qObject::destroyed()信号。我们想要捕捉到这个信号,在任何地方我们可能有一个被删除的QObject的悬空引用,所以我们可以清理它。合适的槽签名可能是:
void objectDestroyed(QObject* obj = 0);
要将信号连接到槽,我们使用QObject::connect()。有几种连接信号和插槽的方法。第一种方法是使用函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
使用QObject::connect()和函数指针有几个优点。首先,它允许编译器检查信号的参数是否与槽的参数兼容。如果需要,编译器也可以隐式转换参数。
您还可以连接到仿函数或C++11的lambdas:
connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });
将信号连接到槽的另一种方法是使用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
它告诉QT不要定义moc关键字signals、slots和emit,因为这些名称将被第三方库使用,例如Boost。然后,要继续使用带有NO_关键字标志的Qt信号和槽,只需将源代码中Qt moc关键字的所有用法替换为相应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT。
还请参阅Meta-Object System 和Qt's Property System。