>Signal-Slot的作用是对象间的通信; Signals-Slots机制是Qt的核心特性, 也可能是Qt和其他大多数框架提供的特性不同的部分;
介绍
>GUI编程中, 当我们改变了一个widget,经常希望另一个widget能被通知到; 通常我们希望各种对象间能互相通信. Example: 用户点击了CLOSE按钮, 我们会想要让window的close()函数被调用;
>老一点的toolkit包使用callback机制实现通信. callback是指向函数的指针, 如果你希望一个processing function能在一些事件上通知你, 需要传递一个函数指针到那个processing function; processing function会在何时的时候调用callback; Callbacks有两个基本瑕疵: 第一, 不是类型安全的type-safe. 我们永远不能确定processing function会用正确的参数来调用callback; 第二, callback和processing function有很强的耦合, processing function必须知道要调用哪一个callback;
Signals and Slots
>Qt使用Signals-Slots代替callback技术; signal在一个特定事件发生时被发出; Qt的widgets有很多预定义的signals, 我们可以自定义subclass来添加自己的signals; slot是一个函数, 接收到对应的signal时会被调用; 同样, Qt有预定义的slots, 我们也可以自定义slots来处理相关的signals;
>signals-slots机制是type-safe的: signal的原型signature必须和接收信号的slot的signature一致;(slot的signature会短一些, 因为它可以忽略多余的参数) signature和编译器兼容, 所以编译器可以进行类型匹配; signal和slot是松耦合的: 一个类发出一个signal, 它不会去关心哪个slot接受到; 对于相关联的signal和slot, Qt的signal-slot机制保证了slot会在合适的时间接收到signal的参数并被调用; Signal-slot可以传递任意个数和类型的参数; 完全type safe;
>所有继承于QObject或Object的subclass的类都可以包含signal和slot; Signal在对象改变状态时被发送, 对这个事件感兴趣的对象可以处理这个Signal; 我们不知道也不关心是否有对象接收到这个发送的信号; 这是真正的信息封装, 保证对象被当作软件的组件来使用;
>Slots可以接收Signals, 他们也可以被当作普通的成员函数; 和Signal一样, Slot也不用知道它是否和Signal连接起来了. 这样保证了Qt能创建真正独立的组件;
>你可以将任意多的signals连接到一个slot, 也可以将一个signal连接到任意多的slots; 甚至可以将一个signal连接到另一个signal(当第一个signal被发出时第二个signal会立即被发出)
Small Example
>基于QObject的类能发出signal告诉外界它的状态改变了, valueChanged(), 同时有一个slot可以接收其他对象的signal; 所有包含signal-slot的类必须在类声明的开始声明Q_OBJECT宏, 并且(直接或间接)继承自QObject类;
class Counter : public QObject { Q_OBJECT public: Counter(QObject *parent = 0) { m_value = 0; } int value() const { return m_value; } public slots: void setValue(int value); signals: void valueChanged(int newValue); private: int m_value; };
//Slot由程序员实现 void Counter::setValue(int value) { if (value != m_value) { m_value = value; //发出signal,传递新的参数值 emit valueChanged(value); } }>下面的代码段创建2个Counter对象, 把第一个对象的valueChanged() signal关联到第二个对象的setValue() slot;
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), 会发送一个signal valueChanged(12), 对象b会在slot setVaule()接收到, 并调用这个slot; 对象b同样会发出signal valueChanged(), 但是没有slot和对象b的valueChanged()连接, signal会被忽略;
Note setValue()只有在value != m_value的时候才会发出signal. 这样可以防止signal-slot环形关联导致无限循环的情况; (e.g. b.vauleChanged() 和 a.setVaule()互相关联)
>默认情况下, 你创建一个信号关联就应该有一个signal发出; 重复的关联会有两个或以上的signal发出; 你可以调用一个disconnect()打断这些关联; 使用Qt::UniqueConnect类型参数, 这样只有在不重复的情况下, 信号关联才会建立; 如果已经有了一个重复的关联(同样的object上的完全一样的signal和完全一样的slot), connect()会失败并且返回false;
>这个例子说明对象间不需要知道对方的任何信息, 照样可以互相通信; 为了实现通信机制我们只需要将对象互相关联, 调用简单的QObject::connect(), 或者使用uic的自动关联特性(名字关联 QMetaObject::connectSlotsByName(this));
构建Example
>C++预处理会改变或者移除signal-slot和emit关键字, 这样编译器可以按照标准C++来处理代码;
>对于包含signal-slot的类定义, 进行moc会产生一个C++源文件, 这个文件需要和其他的object文件一同编译和链接; 如果你使用qmake, makefile规则中自动调用moc的部分会加到你工程中的makefile;
Signals
>Signals会在对象的内部状态改变的时候被发出, 状态的改变可能被对象的client或owner所注意; Signals是public的函数, 可以在任何地方被发出, 但是我们推荐只在定义signal的类或者子类中发signal;
>当一个signal被发出, 相关联的slot一般会立即被执行, 就像普通函数的调用; 这个情况下, signal-slot机制完全独立于任GUI的事件循环event loop; 在emit代码段后面的代码会在所有的slots都返回了以后被执行; 当使用queued connections的时候情况稍有不同, queued情况下在emit关键字后面的代码还会继续立即执行, slots则会在稍后执行;
>如果多个slots关联到了一个signal上, 这些slots会一个接一个地被执行, 先后次序是按照它们被connected的次序来排列;
>Signals会被moc自动生成, 并且不能在cpp文件中实现. Signal不可以有返回值.(e.g. 使用void)
Note: 关于参数, 如果signal-slot不用特殊类型的参数, 他们可以更多地被重用; e.g. 如果QScrollBar::valueChanged()试图使用一个特别类型, 假设是QScrollBar::Ranger, 那么它只能被关联到特别为QScrollBar设计的slots上了, 想要关联到其他的input widget上基本不可能;
Slots
>Slot会在关联的signal被发送的时候被调用; Slot是普通的C++函数, 可以被正常调用; 它们的特别之处只是可以和signal关联起来;
>直接调用Slot时它就是普通的函数, 遵循一般的C++规则. 不过, 作为Slot, 忽略权限级别的话, 它可以被任何组件通过signal-slot关联来激发. 这表示随意一个类的实例发送了一个signal, 都可能激发其他不相关的类的实例的私有slot.
>Slot也可以定义成virtual的, 在实际使用中很有用;
>虽然在实际的应用程序中区别很小, 但是和callback相比, signal-slot会稍微慢一些, 因为它提供了更多的灵活性. 不考虑虚函数调用时, 普遍来说, 发出一个和一些slots相关联的signal, 大约比直接调用接收函数receivers慢10倍. 主要的消耗是在: 安全地遍历所有的connections, 锁定关联的对象(检查后续的receivers没有在发送的过程中被销毁), 然后按照通用的方式安置参数; 如果有10个非虚函数被调用, 听起来好像很多, 实际的损耗比任何new或delete操作要小很多. e.g. 当你在操作一个string, vector或list的时候, 如果需要new或delete, signal-slot的消耗只占了整个函数花费的效率很小的一个比例; 如果你调用系统函数或者间接调用10个函数, 情况是类似的; 简单和灵活是signal-slot机制值得那一点小小消耗的理由;
Note 如果有其他库定义的变量调用了signals和slots, 可能会在同时编译Qt-based应用的时候导致编译器的warnning和error. 解决的办法是 #undef掉这些offending预编译符号;
Meta-Object信息
>meta-object compiler(moc)会在一个C++文件中解析类的声明并且生成C++代码, 初始化meta-object. meta-object包含了所有signal和slot成员的名字, 还有指向这些函数的指针;
>meta-object还包含其他信息: 比如对象的类名. 你可以检查对象是否继承自某个特定的类; example:
if (widget->inherits("QAbstractButton")) { QAbstractButton *button = static_cast<QAbstractButton *>(widget); button->toggle(); }
>meta-object信息也可以被qobject_cast<T>()使用, 和QObject::inherits()类似, 但是更加安全less error-prone;
if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget)) button->toggle();
实际例子
>LcdNumber继承自QObject, 通过QFrame和QWidget具备signal-slot机制; 某种程度上和内建的QLcdNumber类似;
>Q_OBJECT宏会被预编译展开来去声明多个被moc实现的成员函数; 如果你在编译时遇到"undefined reference to vtable for LcdNumber"的错误, 你可能忘了先要运行moc命令, 把moc命令输出的moc文件Include进来.
//略过moc不关心的一些析构函数和成员函数 class LcdNumber : public QFrame { Q_OBJECT public: LcdNumber(QWidget *parent = 0); 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); };
>如果你继承自QWidget, 基本上肯定需要在构造函数中加上parent参数, 把它传递给基类的;
>当LcdNumber被要求显示一些非法的值时, 会发送overflow() signal;
>如果你不关心溢出, 或者知道不可能发生溢出, 你可以忽略overflow() signal; 可以不把它关联到任何slot上;
>相反如果你想要在溢出时调用两个不同的错误处理函数, 简单地关联到两个不同的slots就行; Qt会按照关联的次序调用两个函数;
>Slot是被用来获得其他widget状态改变的信息的接收函数; 在示例代码中, LcdNumber使用它去设置显示的数字; 因为display()是类的一个接口, 所以这个slot设置为public;
>多个示例程序关联QScrollBar的valueChanged() signal到了dispaly() slot, 因此LCD数字会不停得在scrollbar上显示;
Note display()被重载overload了; 当你把一个signal和一个slot关联起来, Qt会选择适合的版本; 如果是使用callback, 你将不得不自己来找出5个不同的函数名字并且控制不同的类型;
具有默认参数的Signals和Slots
>signal和slot的原型可能包含了参数, 参数可能有默认值. 考虑QObject::destroyed():
void destroyed(QObject* = 0);
>当一个QObject被删除, 它会发出QObject::destoryed() signal. 我们想要捕获这个signal, 不论在哪我们可能有一个dangling reference指向被删除的QObject, 这样我们可以清除它; 一个合适的slot原型可能是:
void objectDestroyed(QObject* obj = 0);
>有多种方式使用QObject::connect()来关联signal-slot, 第一种是以函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
>使用函数指针有很多优点. 首先, 允许编译器检查signal的参数是否和slot的参数兼容; 需要的话参数也能被编译器隐式地转换;
>你也可以使用C++11 lamdas表达式:
connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });
Note 如果你的编译器不支持C++ 11可变参数模板 variadic templates, 这个语法只能在signal和slot具有小于或等于6个参数的情况下有效;
>还有一个方法是使用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()));
//这个无法工作,因为slot预期的是接收一个参数QObject,这个signal不会发出参数, connection会报错; connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
Note 当使用宏的QObject::connect()重载, 编译器不会检查signal-slot的参数;
更多Signal-Slot的使用
>如果你想得到发送signal的sender的信息, Qt提供了QObject::sender()函数, 返回一个指向发送signal的对象的指针;
>当遇到很多signals关联到一个相同的slot的情况, 并且这个slot需要对每个signal作出不同的处理时, 可以用QSignalMapper类;
>假设你有三个push buttons, 用来决定打开哪种文件: Tax File, Accounts File, Report File.
>为了打开正确的文件, 使用QSignalMapper::setMapping()把所有的clicked() signals和QSignalMapper对象map起来; 然后把文件的QPushButton::clicked() signal和QSignalMapper::map() slot关联起来;
signalMapper = new QSignalMapper(this); signalMapper->setMapping(taxFileButton, QString("taxfile.txt")); signalMapper->setMapping(accountFileButton, QString("accountsfile.txt")); signalMapper->setMapping(reportFileButton, QString("reportfile.txt")); connect(taxFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map); connect(accountFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map); connect(reportFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map);
connect(signalMapper, SIGNAL(mapped(QString)),this, SLOT(readFile(QString)));
>Qt可以使用第三方3rd Party signal-slot机制. 你甚至可以在一个项目里同时使用两种机制. 需要做的就是把下面一行代码加到qmake项目文件(.pro)中.
Qt使用第三方Signals-Slots
CONFIG += no_keywords
>这行代码告诉Qt不要去定义moc关键字signals, slots, emit, 因为这些名字会被第三方库使用. e.g, Boost. 在定义了no_keywords标签的情况下继续使用Qt, 把源代码中Qt关键字简单地替换成相应的Qt宏: Q_SIGNALS(or Q_SIGNAL), Q_SLOTS(Q_SLOT)和Q_EMIT;
在Qt5中处理signals-slots的重载
>新的Qt5的语法为了解释和关联正确的重载函数会进行显式地转换. ClassA定义了两个重载函数作为signal;
class ClassA : public QObject { Q_OBJECT ... signals: void mySignal(double d); void mySignal(QString s); ... };>假设有个ClassB的实例b, 它的slot有一个QString参数, 那么将ClassA的实例a上重载的第二个signal与其关联的正确方式是:
connect(&a, static_cast<void (ClassA::*)(QString)>(&ClassA::mySignal), &b, &ClassB::mySlot);>如果有多个重载的slot, 用同样的方式来connect;
End
<Refer to> http://qt-project.org/doc/qt-5.0/qtcore/signalsandslots.html#signals-and-slots