Signal and Slots 用于对象之间通信。 它是 Qt 的核心特性之一, 并且也是Qt 与其它框架差别最大的部分。
概述
在GUI编程中, 如果我们改变了一个控件, 我们可能想其它控件知道; 换言之, 我们希望任何类型的 Object 能够彼此通信。
一些Tookits使用回调函数来实现通信,如果你想在一个函数中通告一些事件, 你需要在这个函数中使用一个函数指针。
但是这种做法有两个缺陷:
- 他们不是类型安全的, 我们在编译期无法获知这个指针是否正确。
- 这个回调函数把调用者和被调用者仅仅结合在了一起。
在Qt中, 我们有回调函数的替代办法, 使用Signal and Slots。 一个Signal 会在某个事件发生时被发射, 而slot 是一个用于响应特定signal的函数。 Qt 有很多预定义的信号 、槽, 但是我们可以通过定义 Object 的子类来加入自己的信号、槽。
- 类型安全: signal 和 slot 的签名(名称、参数)必须一致; 不过事实上,slot的参数是取signal的前n个(n表示signal的参数个数), 因为slot可以忽略信号附带的一些值。 因为这种签名机制兼容于 C++, 因此编译器可以发现类型错误。
- 发射信号的类不关注是谁接收了这个信号。
当一个Object发生变化、并且它希望这种变化为外界所知道时, 它会发出信号;但它不会理会最终是谁处理了这个信号。 这就是信息封装, 它使得 Qt Object 能够像软件组件那样被使用。
一个信号可以连接多个槽; 多个信号可以连接单个槽; 信号和信号也可以直接连接。
编译
C++ 预处理器修改、删除signal slot和 emit 关键字, 为 C++编译器提供了符合 标准 C++语法的代码。
在包含signal slot的类定义中运行moc, 一个代码源文件会产生。 它能够使用工程中的其他 object 文件进行编译和链接, 也能被其它Object文件如此使用。如果使用qmake, 这个规则会在qmake 产生的makefile中自动添加。
Signal
信号能够在任何地方发射, 但是最好在定义它的类及子类里使用。
当一个信号发出后, slot函数会立即执行, 跟一个函数调用表现相同; 此时, 这个signal-slot 是独立于event system的。 但是如果在connect时使用 Queued Connection, emit之后的代码会继续执行。 而slot会在稍后的事件循环中被调用。
如果一个信号连接多个槽, 那么这些槽函数会按照它们被连接的顺序依次执行。
如果在信号槽中使用了特殊类型, 则该信号或者槽很难与其它类型通信。 比如 QScrollBar::valueChanged() 使用了 QScrollBar::Range 作为参数, 则它只可能与QScrollBar的槽函数连接。
在一些第三方库中也会有signal /slots 关键字, 这可能会导致编译器发生 warning 或者 error。 可以使用 #undef 去掉某个 signal / slots 的定义。
Slots
除了能够在其连接的signal发射时被调用, slot 是正常的 C++成员函数。 因此, 它们在被调用时, 跟一般成员函数表现一致。 而作为 slot, 无论它的访问级别(protected, private, public), 它们能够被任意Object 调用。 也就是说, 一个任意Object的信号能够引发某一类的private slots。
同时, virtual 类型的slot在实际中也很有用。
和callback 相比, Signal and Slots 提供的灵活性也会稍微有些慢,当然这个在实际应用中是无关紧要的。 一般来说, 在没有虚函数调用的前提下, 从发出一个信号, 到槽函数调用,会比直接调用槽函数慢 10倍。 这些开销其中包括 定位连接的类, 安全的遍历所有连接(i.e. checking that subsequent receivers have not been destroyed during the emission) , 按照通用的模式准备参数。 尽管10个函数调用才及的上一个 signal-slot, 它还是比 new 和 delete 的开销要小得多。 整体开来, 它在所有的函数调用导致的开销中所占比重非常小。系统调用也导致比 signal-slot大的多的开销, 它甚至有可能直接导致多余10个的函数调用。
在一台 i586-500 机子上, 你可以对一个 receiver 发出 2,000,000 个信号, 或者对两个receiver 发出 1,200,000 个信号。
Meta Object In Signal/Slots
Meta Object Compiler 遍历声明类的文件, 并生成初始化 Meta-Object 的文件。 MetaObject 包含了所有的信号槽, 以及指向他们的指针。
MetaObject 中包含了C++ 类的一些额外的信息。比如,
类名;
调用 inherits 查询某个对象的类别所需信息;
使用 qobject_cast 所需信息;
Q_OBJECT 宏会在预处理器中被展开, 并生成几个函数, 这些函数会在moc中生成函数体。 如果发生编译错误“undefined reference to vtable for ClassName
”, 你可能忘记运行moc, 或者在链接时没有加上 moc 的输出文件。
Moc 会忽略其它的成员函数(只处理 signal slot)。
对于定义了默认参数值的signal, slot中若有这个参数, 则应该有相同的定义, 或者忽略该参数, 例如 对于QObject::destroyed(QObject * = 0)。
应该是:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
高阶用法
如果你需要发送者的信息,你可以调用 QObject::sender(), 获取指向sender 的指针。
QSignalMapper用于”有多个signal 同时连接一个槽, 而这个槽需要对不同的signal 做不同处理”的情况。
例如, 你有3个pushButton用于选择需要打开的文件: "Tax File", "Accounts File", or "Report File",
为了正确的打开文件, 可以使用QSignalMapper::setMapping把click信号映射到QSignalMapper类, 然后把click信号连接到 QSignalMapper 的map槽函数。
signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
connect(taxFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(accountFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(reportFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
然后再把 mapped 信号连接到处理文件的readFile 槽函数上。
connect(signalMapper, SIGNAL(mapped(QString)),
this, SLOT(readFile(QString)));
以下的代码也会运行, 但是由于 Qt参数归一化操作(signature normalization), 会稍微慢一些。
connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));
使用 3rd signal/slot
可以在Qt中使用3rd的 signal/slot机制, 甚至可以在同一个工程中使用。但需要把下面一行代码加入到pro文件中:
CONFIG += no_keywords
它告诉Qt不适用 moc的关键字signals/slots/emit, 因为这些名字会在3rd中出现, 比如boost。 在使用signal/slot的地方, 可以简单的使用 Qt宏 Q_SIGNALS (or Q_SIGNAL), Q_SLOTS (or Q_SLOT), and Q_EMIT 替代。
Comments:
- signature normalization: 对于 QObject::connect的 Queued Connection, Qt 会把参数信息保存起来, 以在event system中查询, 这时会调用 参数的拷贝构造函数以及其它信息。 因此参数必须是能够被 Qt‘s meta-object system 识别的类, 如果希望使用自定义类, 应该在connect之前调用qRegisterMetaType(), 把自定义类添加到 Qt‘s meta-object system。
参考 stackoverflow
What benefits does QT get with normalized signature
Qt MOC: When default and copy constructor are used?