信号槽
信号和槽机制是 Qt 的核心机制之一,要掌握 Qt 编程就需要对信号和槽有所了解。信号和槽是一种高级接口,它们被应用于对象之间的通信,它们是 Qt 的核心特性,也是 Qt不同于其它同类工具包的重要地方之一。
在 GUI 工具包中,窗口小部件(widget)通常有一个回调函数用于响应它们触发的动作,这个回调函数通常是一个指向某个函数的指针。在 Qt 中用信号和槽取代了上述机制。
信号与槽的关联
槽和普通的 C++成员函数几乎是一样的:可以是虚函数;可以被重载;可以是共有的、保护的或是私有的,并且也可以被其它 C++成员函数直接调用;还有,它们的参数可以是任意类型。唯一不同的是:槽还可以和信号连接在一起,在这种情况下,每当发射这个信号的时候,就会自动调用这个槽。
信号槽连接的特点:
-
信号与槽并不限制连接的数量,即一个信号可以连接多个槽,一个槽也可以连接多个不同的信号。
-
一个信号可以与另外一个信号相连接,当发射第一个信号时,也会发射第二个信号。
-
连接可以被移除,例如:
disconnect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));
在大多数情况下,我们不需要手动移除连接,因为当删除对象时,Qt 会自动移除和这个对象相关的所有连接。
-
要把信号成功连接到槽(或者连接到另外一个信号),它们的参数必须具有相同的顺序和相同的类型。有一种情况例外,如果信号的参数比它所连接的槽的参数多,也可以进行连接,但多余的参数将会被简单的忽略掉。
connect(ftp, SIGNAL(rawCommandReply(int, const Qstring &)), this, SLOT(checkErrorCode(int)));
有几种连接信号和槽的方法:
-
使用成员函数指针(推荐)
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
优点:允许编译器检查信号是否与槽的参数兼容;编译器可以隐式转换参数
-
使用仿函数或lambda表达式作为slot
connect(sender, &QObject::destoryed, this, [=](){this-> m_objects.remove(sender);});
使用仿函数,第三个this指针也是需要的,否则当this已经释放掉时,函数只知道this指针中指向的对象地址,会发生内存错误
-
使用SIGNAL和SOLT宏
如果参数具有默认值,传递给SIGNAL()宏的签名的参数不得少于传递给SLOT()宏的签名的参数。使用这种方法发生拼写错误时,会比较难排查。
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(QObject*)));
注意事项
信号与槽机制是比较灵活的,但有些局限性我们必须了解,避免产生一些错误。下面就介绍一下这方面的情况。
-
信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586- 133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。
-
信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样信号。
-
如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被激活的顺序将是随机的,并且我们不能指定该顺序。
-
宏定义不能用在 signal 和 slot 的参数中。
-
构造函数不能用在 signals 或者 slots 声明区域内。
-
函数指针不能作为信号或槽的参数。
-
信号与槽不能有缺省参数。
-
信号与槽也不能携带模板类参数。
信号的实现
在类中只用定义信号,而不用实现信号,是因为在 moc_mywidget.cpp
已经实现好了。
// SIGNAL 0
void MyWidget::signal_test(int _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
我们可以看到它实际上时调用了 QMetaObject::activate
函数,可以发现这个函数并没有传递函数的名称或者函数指针,那么它是怎么区分是发送了哪个 signal 呢?分析一下就可以知道,前两个参数是类和元对象的指针,最后一个参数是信号所带的参数,所以只可能是通过第三个参数区分信号,它的表示的意思就是这个类中的第几个signal被发送出来了。
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
int signal_index = local_signal_index + QMetaObjectPrivate::signalOffset(m);
if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
doActivate<true>(sender, signal_index, argv);
else
doActivate<false>(sender, signal_index, argv);
}
当执行流程进入到 QMetaObject::activate
函数中后,会先从 connectionLists
这个变量中取出与这个signal相对应的 connection list
,它根据的就是刚才所传入进来的 signal index
。这个 connection list
中保存了所有和这个 signal 相链接的 slot 的信息,每一对 connection (即:signal 和 slot 的连接)是这个 list 中的一项。
在每个一具体的链接记录中,还保存了这个链接的类型,是自动链接类型,还是队列链接类型,或者是阻塞链接类型,不同的类型处理方法还不一样的。如果是异步的连接,会将其加入到信号队列中进行处理,如果是直接连接,就会在这个函数中获取连接的 QSlotObjectBase
对象,最后,通过以下调用最终执行目标对象的槽函数。
template<typename Func, typename Args, typename R> class QSlotObject : public QSlotObjectBase
{
typedef QtPrivate::FunctionPointer<Func> FuncType;
Func function;
static void impl(int which, QSlotObjectBase *this_, QObject *r, void **a, bool *ret)
{
switch (which) {
case Destroy:
delete static_cast<QSlotObject*>(this_);
break;
case Call:
-> FuncType::template call<Args, R>(static_cast<QSlotObject*>(this_)->function, static_cast<typename FuncType::Object *>(r), a);
break;
case Compare:
*ret = *reinterpret_cast<Func *>(a) == static_cast<QSlotObject*>(this_)->function;
break;
case NumOperations: ;
}
}
public:
explicit QSlotObject(Func f) : QSlotObjectBase(&impl), function(f) {}
};
static void call(Function f, Obj *o, void **arg) {
FunctorCall<typename Indexes<ArgumentCount>::Value, SignalArgs, R, Function>::call(f, o, arg);
}
struct FunctorCall<IndexesList<II...>, List<SignalArgs...>, R, SlotRet (Obj::*)(SlotArgs...)> {
static void call(SlotRet (Obj::*f)(SlotArgs...), Obj *o, void **arg) {
(o->*f)((*reinterpret_cast<typename RemoveRef<SignalArgs>::Type *>(arg[II+1]))...), ApplyReturnValue<R>(arg[0]);
}
};
connection在构造时,使用了大量的模板,这是因为信号和槽的参数和返回值并不确定,通过模板可以在编译时生成需要使用的函数指针的返回值和参数,而不需要使用者自己编写
需要注意的是,不同的情况下,触发信号后的处理逻辑并不相同,这里只指出了其中的一种情形(在同一线程中,一个信号触发另一个对象的槽函数的情形)