[关闭]
@Andream 2017-07-17T18:25:28.000000Z 字数 3351 阅读 1122

【Qt学习笔记】1.1.5 信号/槽机制

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++类如下

  1. class Counter
  2. {
  3. public:
  4. Counter() { m_value = 0; }
  5. int value() const { return m_value; }
  6. void setValue(int value);
  7. private:
  8. int m_value;
  9. };

把它改写为QObject如下

  1. #include <QObject>
  2. class Counter : public QObject
  3. {
  4. Q_OBJECT
  5. public:
  6. Counter() { m_value = 0; }
  7. int value() const { return m_value; }
  8. public slots:
  9. void setValue(int value);
  10. signals:
  11. void valueChanged(int newValue);
  12. private:
  13. int m_value;
  14. };

另外,在发送信号的地方添加信号发送代码:

  1. void Counter::setValue(int value)
  2. {
  3. if (value != m_value) {
  4. m_value = value;
  5. emit valueChanged(value);
  6. }
  7. }

之后,你可以在其他地方将信号和槽连接起来:

  1. Counter a, b;
  2. QObject::connect(&a, &Counter::valueChanged,
  3. &b, &Counter::setValue);
  4. a.setValue(12); // a.value() == 12, b.value() == 12
  5. b.setValue(48); // a.value() == 12, b.value() == 48

真丶例子

再来一个阉割了的真例子:

  1. #ifndef LCDNUMBER_H
  2. #define LCDNUMBER_H
  3. #include <QFrame>
  4. class LcdNumber : public QFrame
  5. {
  6. Q_OBJECT

定义溢出信号:

  1. signals:
  2. void overflow();

定义槽(响应别的对象的信号,即状态变化):

  1. public slots:
  2. void display(int num);
  3. void display(double num);
  4. void display(const QString &str);
  5. void setHexMode();
  6. void setDecMode();
  7. void setOctMode();
  8. void setBinMode();
  9. void setSmallDecimalPoint(bool point);
  10. };
  11. #endif

带默认参数的信号 & 槽

考虑销毁对象信号:

  1. void destroyed(QObject* = 0);

销毁响应函数:

  1. void objectDestroyed(QObject* obj = 0);

三种连接方式

指针

  1. connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

好处是可以让编译器帮我们做类型检测和隐式类型转换。

lambda

  1. connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });

  1. connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
  2. connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
  3. connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

这种写法要求SIGNAL参数个数 ≥ SLOT参数个数(多余的参数被忽略)

  1. //像这样就不行
  2. connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

高级用法

有的情况下,我们希望获取信号发送者的信息。Qt提供了对应函数:QObject::sender()返回信号发送者的指针。

屏蔽关键字

其他的第三方库可能会使用signals或者slots作为变量名称,这会导致编译器警告。用#undef 可以消除警告。

还有的第三方库提供了信号/槽机制(如Boost),你可以用第三方的替换官方的信号/槽机制。
.pro里加入下面这句:

  1. CONFIG += no_keywords

你甚至可以两者同时使用,但官方的就不能用singals, slots, emit关键字了,要替换为Q_SIGNALS, Q_SLOTS, Q_EMIT这些宏。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注