@Andream
2017-07-17T18:25:28.000000Z
字数 3351
阅读 1117
Qt
本文为Qt官方文档译文,查看原文
信号/槽机制,实现了QObject对象间的通信。这是Qt相比于其他框架的一大特色,它是基于Qt的元对象系统实现的(meta-object system)。
在GUI编程中,当我们改变一个小部件的时候,我们经常想要通知另一个小部件也做出相应改变。 更普遍的说,我们想要的任何类型的对象能够相互通信。 例如,如果用户点击 关闭 按钮时,我们可能希望窗口的 close() 函数被调用。
其他框架实现使用回调函数
来实现这种通信。 回调函数是一个函数指针,所以如果你希望一个对象A通知对象B一些事件,对象A就要保留一个对象B的回调函数指针
,当A发生某个事件时,A就能调用B的函数。这样就实现了对象间的通信。(监听者模式)
确实有很多框架使用这种方法实现了对象间通信,但是这有点反直觉,有时候还要纠结回调函数的参数是不是匹配。就很麻烦嘛,Qt信号槽大法就是好嘛!...
在Qt中,我们用信号/槽
机制取代监听者模式
。当一个特定事件发生时,一个信号
(signal)被发送
(emit)到绑定的槽(slot)。Qt的Widget预先定义了一些信号和槽,我们也可以定义自己的信号和槽。
信号和槽的签名必须匹配才能将信号发送到槽。而且信号和槽是解耦的,你在设计信号的时候不用关心谁会接收信号。只要你将信号和槽绑定在一起,Qt就能保证信号能实时发送到槽。
所有继承自QObject的类都能添加信号和槽。当对象执行某个其他对象可能感兴趣的事件时,就发出一个信号,而不用管谁来接收。这样,每个对象都能作为一个单独的软件构件来设计。
槽用来接收信号,但槽本身只是一个普通函数。就像信号发送者不知道谁来接收信号,槽也不知道谁会发信号给它。这确保了Qt组件之间的独立性。这是真封装啊!
一个信号可以连接多个槽,一个槽也可以连接多个信号。甚至一个信号可以连接到另一个信号。
就这样,信号、槽一同构成了宇宙超级无敌巨无霸强大的组件编程机制。
当对象内部状态发生某些可能其他对象会感兴趣的变化时,就可以发出一个信号。信号的访问范围是public的,也就是说可以在对象外部发出该对象的信号。不过我们建议最好只在定义信号的类及其子类内部发送信号。
当信号发送的时候,与之连接的槽将会立即响应,就像调用普通函数一样。如果一个信号连接了多个槽,槽会按照连接的顺序依次执行,执行完毕后再返回信号发送的地方继续往后执行。(单线程顺序执行)
信号只有定义,没有具体实现,而且返回值类型只能是void。
设计信号的时候,有个经验:尽量不要定义特殊类型的参数。因为信号和槽必须要类型匹配才能通信,如果参数过于复杂,使用信号就不方便了。比如:QScrollBar::valueChanged(int),如果定义成QScrollBar::valueChanged(QScrollBar::Range),就会限制信号的使用范围。
槽用于接收信号,但它实际上只是一个普通函数。唯一一点就是它和信号建立了连接。
槽可以是private:信号无视访问权限,就像中国电信,可以覆盖到山区,可以覆盖到你的卧室。
槽可以是virtual:这在设计的时候也常常会用到。
和回调函数
相比,信号和槽机制非常灵活,但它牺牲了运行速度(虽然实际上影响不大)。一般来说,信号发送到槽的时间大约是直接调用回调函数的十倍。这听起来有点恐怖,但实际上系统底层执行的其他操作,这点损耗微不足道,用户都感觉不到的!所以用这点时间来换取灵活性是大大的赚啊!
一个简单的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;
};
另外,在发送信号的地方添加信号发送代码:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(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
再来一个阉割了的真例子:
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
定义溢出信号:
signals:
void overflow();
定义槽(响应别的对象的信号,即状态变化):
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
考虑销毁对象信号:
void destroyed(QObject* = 0);
销毁响应函数:
void objectDestroyed(QObject* obj = 0);
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
好处是可以让编译器帮我们做类型检测和隐式类型转换。
connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
这种写法要求SIGNAL参数个数 ≥ SLOT参数个数(多余的参数被忽略)
//像这样就不行
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
有的情况下,我们希望获取信号发送者的信息。Qt提供了对应函数:QObject::sender()返回信号发送者的指针。
其他的第三方库可能会使用signals或者slots作为变量名称,这会导致编译器警告。用#undef 可以消除警告。
还有的第三方库提供了信号/槽机制(如Boost),你可以用第三方的替换官方的信号/槽机制。
.pro里加入下面这句:
CONFIG += no_keywords
你甚至可以两者同时使用,但官方的就不能用singals, slots, emit关键字了,要替换为Q_SIGNALS, Q_SLOTS, Q_EMIT这些宏。