信号槽

​ 信号和槽机制是 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*)));
    

注意事项

​ 信号与槽机制是比较灵活的,但有些局限性我们必须了解,避免产生一些错误。下面就介绍一下这方面的情况。

  1. 信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586- 133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。

  2. 信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样信号。

  3. 如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被激活的顺序将是随机的,并且我们不能指定该顺序。

  4. 宏定义不能用在 signal 和 slot 的参数中。

  5. 构造函数不能用在 signals 或者 slots 声明区域内。

  6. 函数指针不能作为信号或槽的参数。

  7. 信号与槽不能有缺省参数。

  8. 信号与槽也不能携带模板类参数。

信号的实现

​ 在类中只用定义信号,而不用实现信号,是因为在 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在构造时,使用了大量的模板,这是因为信号和槽的参数和返回值并不确定,通过模板可以在编译时生成需要使用的函数指针的返回值和参数,而不需要使用者自己编写

需要注意的是,不同的情况下,触发信号后的处理逻辑并不相同,这里只指出了其中的一种情形(在同一线程中,一个信号触发另一个对象的槽函数的情形)