信号槽理论总结
- 一、概述
- 二、信号槽
- 三、信号
- 四、槽函数
- 五、小例子
- 六、 信号槽的默认参数
- 七、高级使用
- 八、 在Qt 里使用第三方的信号槽
一、概述
信号和槽用于对象之间的通信。信号和槽机制是Qt的一个核心特性,也是与其他框架所提供的特性最大不同的部分。Qt的元对象系统就是信号和槽的基础。
在GUI编程中,当我们更改一个widget时,我们通常希望另一个widget也能收到通知。更一般地说,我们希望任何类型的对象都能够相互通信。例如,如果用户单击了关闭按钮,我们可能希望调用窗口的Close()函数。
其他开发工具包使用回调函数来实现这种通信。回调是一个指向函数的指针,所以如果你希望处理函数通知你一些事件,你可以将一个指向另一个函数(回调)的指针传递给处理函数。处理函数在适当的时候调用回调函数。虽然确实存在使用这种方法的成功框架,但回调可能不直观,而且在回调参数的类型方面可能存在一些不匹配的问题。
二、信号槽
在Qt中,我们有另一种回调技术:使用信号(signal)和槽(slot)。当特定事件发生时,会发出一个信号。Qt的窗口组件有很多预定义的信号,我们还可以子类化窗口组件来添加我们自己的信号。槽是响应特定信号而调用的函数。Qt的窗口组件有很多预定义的槽,但通常的做法是子类化窗口组件并添加自己的槽,这样就可以处理自定义的信号。
信号和槽机制是类型安全的:信号的签名必须与接收槽的签名匹配 也就是参数类型是匹配的。(实际上,槽的签名可能比接收到的信号短,因为它可以忽略额外的参数。)由于签名是兼容的,Qt提供了两种信号槽的语法
一种是:基于函数指针的语法时,编译器可以帮助我们检测类型不匹配,在编译时校验,推荐使用。
二种是:基于字符串的 SIGNAL 和SLOT 语法,将在运行时检测类型不匹配,在运行时校验,不推荐使用。
信号和槽是松散耦合的:一个发出信号的类既不知道也不关心哪个槽接收信号。Qt的信号和槽机制确保如果将信号连接到槽,槽将在正确的时间调用信号的参数。信号和槽可以接受任意数量的任何类型的参数。它们是完全类型安全的。
所有继承自QObject或其某个子类(如QWidget)的类都可以包含信号和槽。当对象以其他对象可能感兴趣的方式改变其状态时,就会发出信号。这就是对象通信所做的全部工作。它不知道也不关心是否有任何东西正在接收它发出的信号。这是真正的信息封装,并确保对象可以作为软件组件使用。
槽可用于接收信号,但它们也是普通的成员函数。就像对象不知道是否有任何东西接收到它的信号一样,槽也不知道是否有任何信号连接到它。这确保了可以用Qt创建真正独立的组件。
您可以将任意数量的信号连接到单个槽,并且可以根据需要将一个信号连接到任意数量的槽。甚至可以将一个信号直接连接到另一个信号。(这将在第一个信号发出时立即发出第二个信号。)
信号和槽一起构成了强大的组件编程机制。图例如下:
三、信号
当对象的内部状态以某种方式发生改变时,对象的客户端或所有者可能会感兴趣,这时对象就会发出信号。信号是公共访问函数,可以在任何地方发出,但我们建议只在定义信号及其子类的类中发出它们。
当信号发出时,与之相连的槽通常会立即执行,就像普通的函数调用一样。当发生这种情况时,信号和槽机制完全独立于任何GUI事件循环。当所有任务槽都返回后,emit语句后面的代码才会执行。使用排队连接时,情况略有不同。在这种情况下,emit关键字后面的代码将立即继续执行,而任务槽将稍后执行。
如果多个槽连接到一个信号,当信号发出时,这些槽将按照连接的顺序依次执行。
信号由moc自动生成,不能在.cpp文件中实现。它们永远不能有返回类型(即使用void)。
关于参数的注意事项:我们的经验表明,如果信号和槽不使用特殊类型,则它们的可重用性更强。 也即是信号函数传递的时候,参数越简单越好,简单就松耦合,依赖就更低。
如果QScrollBar::valueChanged()使用特殊类型,例如假设的QScrollBar::Range,它只能连接到专门为QScrollBar设计的槽。将不同的输入部件连接在一起是不可能的。
四、槽函数
当连接到槽的信号发出时,槽就被调用。槽是普通的c++函数,可以正常调用;它们唯一的特点是可以连接信号。
由于槽函数是普通的成员函数,因此直接调用时遵循普通的c++规则。
作为槽,它们可以被任何组件调用,无论其访问级别,通过信号槽连接。这意味着从任意类的实例发出的信号可能导致在不相关类的实例中调用私有槽。你还可以将slot定义为虚拟的,我们发现这在实践中非常有用。
与回调函数相比,信号和槽的速度要稍慢一些,因为它们提供了更大的灵活性,尽管在实际应用中差别不大。
一般来说,发送一个连接到某些槽的信号,比直接调用接收器(使用非虚函数调用)大约慢10倍。这是定位连接对象所需的开销,安全遍历所有连接(即检查后续的接收者在发射期间没有被销毁),以及以通用方式marshall任何参数所需的开销。虽然10个非虚函数调用听起来很多,但它比任何new或delete操作的开销要小得多。当你在后台执行需要new或delete的字符串、向量或列表操作时,信号和槽的开销只占整个函数调用开销的很小一部分。每当你在一个槽中进行系统调用时,情况也是如此;或者间接调用超过10个函数。
信号和槽机制的简单性和灵活性是值得的,你的用户甚至不会注意到这些开销。
请注意,在与基于qt的应用程序一起编译时,其他定义称为 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的程序,要使用信号或槽功能,这个类必须在声明的开头提到Q_OBJECT。它们还必须(直接或间接)派生自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)
{
if (value != m_value)
{
m_value = value;
emit valueChanged(value); //发射信号
}
}
}
//定义信号
signals:
void valueChanged(int newValue);
private:
int m_value;
};
绑定信号槽
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.b valuechanged()连接到a.setValue()时)出现无限循环。
默认情况下,你建立的每一个连接都会发出一个信号;对于重复的连接,会发出两个信号。
只要调用disconnect(),就可以断开所有这些连接。如果传递Qt::UniqueConnection类型,则仅当它不是重复连接时才会建立连接。如果已经有重复的(相同对象上的相同插槽的完全相同的信号),连接将失败,connect将返回false。
这个例子说明了对象可以一起工作,而不需要知道彼此的任何信息。要实现这一点,只需要将对象连接在一起,这可以通过一些简单的QObject::connect()函数调用或使用uic的自动连接功能来实现。
六、 信号槽的默认参数
信号和槽的签名可以包含参数,并且参数也可以有默认值。如下
void destroyed(QObject* = nullptr);
当一个QObject被删除时,它会发出这个QObject::destroyed()信号。我们希望捕获这个信号,在任何可能有对已删除的QObject的悬空引用的地方,以便我们可以清理它。合适的槽函数签名可能是:
void objectDestroyed(QObject* obj = nullptr);
要将信号连接到插槽,我们使用QObject::connect()。有几种方法可以连接信号和插槽。第一种是使用函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
将QObject::connect()与函数指针一起使用有几个优点。首先,它允许编译器检查信号的参数是否与插槽的参数兼容。如果需要,编译器也可以隐式转换参数。
你也可以连接functor或c++ 11的lambda表达式:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
在这两种情况下,我们都在connect()调用中提供this作为上下文。context对象提供了接收器应该在哪个线程中执行的信息。这很重要,因为提供上下文可以确保接收器在上下文线程中执行。
当发送者或上下文被销毁时,lambda将断开连接。你应该注意,在发出信号时,functor中使用的任何对象都是活着的。
另一种将信号连接到插槽的方法是使用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()重载时,编译器不会检查signal和slot参数。
七、高级使用
如果你需要信号发送者的信息,Qt提供了QObject::sender()函数,它返回一个指向发送信号对象的指针。
通过sender 的 objectName 就可以动态的获取每一个对象是哪个,同样的 ,可以搭配着 find
Lambda表达式是一种向槽传递自定义参数的便捷方式:
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。
其实就像这个
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
//定义槽
public Q_SLOTS:
void setValue(int value);
//定义信号
Q_SIGNALS:
void valueChanged(int newValue);
private:
int m_value;
};