IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> QT核心-元对象的其他妙用 -> 正文阅读

[C++知识库]QT核心-元对象的其他妙用

首先,这个功能不是我原创的,它是发表于QT内部的一个大佬,原文链接:Dynamic Signals and Slots。他的这篇文章指出,在常规的开发中,信号和槽都是在编译期指定的,没办法在运行时增删。注意:这里的运行时增删不是指connect连接或disconnect取消连接信号槽,而是增加一个函数,把这个函数当作信号或者槽来使用。
原文的代码看着我实在头疼,感觉跟写着玩儿似的,而且到现在为止我也没有用到动态增删信号槽的功能,所以这里不解析原文的代码了,而是介绍一下我看了原文代码之后对它所使用功能的理解和感悟所实现的其他功能。

正文

注意:这里指的是用SIGNAL和SLOT宏包裹的字符串形式连接的信号槽,而不是直接取函数地址的形式。前者依赖的元对象,后者依赖的C++模板元。

信号槽实现原理

我们首先要搞清楚发送一个信号后是如何调用到槽函数的,我前面的文章也指出了,信号槽作为一个高级功能它依赖了三个核心功能: 生存线程、事件循环和元对象(取函数地址的形式依赖的C++模板元)。生存线程和事件循环是用到当connect函数的第五个参数为Qt::QueuedConnection和Qt::BlockingQueuedConnection的时候的(为Qt::AutoConnection的时候也可能用到,参考我前面的文章),而元对象就是实现通过字符串调用到槽函数的功能。
我们可以创建一个Test.h文件,定义一个Test类继承至QObject并添加Q_OBJECT宏,然后添加一个信号和一个槽函数并编译:

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = nullptr);

signals:
    void testSignal();

public slots:
    void testSlot() {}
};

#endif // TEST_H

我们首先点开Q_OBJECT宏,可以找到里面的一个函数virtual int qt_metacall(QMetaObject::Call, int, void **);

#define Q_OBJECT \
public: \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

这个函数就是通过元对象操作本类(Test)的实例对象的入口,比如QObject::setProperty/QObject::property的写入-读取数据,或者连接信号槽之后发送信号调用到槽函数(当然还有一些QMeta*的类能做更多的操作)。这个函数我们不会去实现,它由QT的moc编译器检测到Test.h中使用了Q_OBJECT宏之后自动帮我们实现的。比如上面的文件编译后我们就能在build文件夹里面找到一个名为moc_Test.cpp文件,里面就有这个函数的实现。而且这个函数还是一个虚函数,moc_Test.cpp里面的实现其实就是重写了父类的同名函数。打开这个文件就能找到这个函数的实现:

int Test::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 2)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 2;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 2)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 2;
    }
    return _id;
}
  1. 这个函数的第一行是调用父类的同名函数,这个步骤是必须的,因为这个函数里面判断id是从0开始,而传入这个函数的id加上了所有父类的元函数数量,所有的元函数数量可以通过QMetaObject::methodCount函数获取,而当前类的起始id可以通过QMetaObject::methodOffset函数获取。这一步就是减去父类的元函数,可以类比于这个类里面的_id -= 2;
  2. 接下来调用槽函数时_c就等于QMetaObject::InvokeMetaMethod,所以接下来就是进入到qt_static_metacall函数。其实通过元对象调用被Q_INVOKABLE标记的函数和读写属性都是进入这个静态函数。
void Test::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Test *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->testSignal(); break;
        case 1: _t->testSlot(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (Test::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Test::testSignal)) {
                *result = 0;
                return;
            }
        }
    }
    Q_UNUSED(_a);
}
  1. 这个函数里就只是一个switch-case语句,每一个元函数都有一个唯一id标识,这个id在moc编译时确定。比如这里调用槽函数的id就等于1(注意这个id不是真的等于1,而是上面说的减去了父类元函数数量之后的值,由QMetaMethod::methodIndex指定)。
  2. 现在我们知道了槽函数是通过id来调用的,但是这个id是怎么得来的呢?我们进入QObject::connect函数的内部去查看他的源码(注意我所使用的版本为5.15.2,可能每个版本不太一样,但大同小异)。
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)
{
    if (sender == nullptr || receiver == nullptr || signal == nullptr || method == nullptr) {
        qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
                 sender ? sender->metaObject()->className() : "(nullptr)",
                 (signal && *signal) ? signal+1 : "(nullptr)",
                 receiver ? receiver->metaObject()->className() : "(nullptr)",
                 (method && *method) ? method+1 : "(nullptr)");
        return QMetaObject::Connection(nullptr);
    }
    QByteArray tmp_signal_name;

    if (!check_signal_macro(sender, signal, "connect", "bind"))
        return QMetaObject::Connection(nullptr);
    const QMetaObject *smeta = sender->metaObject();
    const char *signal_arg = signal;
    ++signal; //skip code
    QArgumentTypeArray signalTypes;
    Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7);
    QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
    int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
            &smeta, signalName, signalTypes.size(), signalTypes.constData());
    if (signal_index < 0) {
        // check for normalized signatures
        tmp_signal_name = QMetaObject::normalizedSignature(signal - 1);
        signal = tmp_signal_name.constData() + 1;

        signalTypes.clear();
        signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
        smeta = sender->metaObject();
        signal_index = QMetaObjectPrivate::indexOfSignalRelative(
                &smeta, signalName, signalTypes.size(), signalTypes.constData());
    }
    if (signal_index < 0) {
        err_method_notfound(sender, signal_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(nullptr);
    }
    signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
    signal_index += QMetaObjectPrivate::signalOffset(smeta);

    QByteArray tmp_method_name;
    int membcode = extract_code(method);

    if (!check_method_code(membcode, receiver, method, "connect"))
        return QMetaObject::Connection(nullptr);
    const char *method_arg = method;
    ++method; // skip code

    QArgumentTypeArray methodTypes;
    QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
    const QMetaObject *rmeta = receiver->metaObject();
    int method_index_relative = -1;
    Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7);
    switch (membcode) {
    case QSLOT_CODE:
        method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    case QSIGNAL_CODE:
        method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    }
    if (method_index_relative < 0) {
        // check for normalized methods
        tmp_method_name = QMetaObject::normalizedSignature(method);
        method = tmp_method_name.constData();

        methodTypes.clear();
        methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
        // rmeta may have been modified above
        rmeta = receiver->metaObject();
        switch (membcode) {
        case QSLOT_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        case QSIGNAL_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        }
    }

    if (method_index_relative < 0) {
        err_method_notfound(receiver, method_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(nullptr);
    }

    if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(),
                                              methodTypes.size(), methodTypes.constData())) {
        qWarning("QObject::connect: Incompatible sender/receiver arguments"
                 "\n        %s::%s --> %s::%s",
                 sender->metaObject()->className(), signal,
                 receiver->metaObject()->className(), method);
        return QMetaObject::Connection(nullptr);
    }

    int *types = nullptr;
    if ((type == Qt::QueuedConnection)
            && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
        return QMetaObject::Connection(nullptr);
    }

#ifndef QT_NO_DEBUG
    QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
    QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset());
    check_and_warn_compat(smeta, smethod, rmeta, rmethod);
#endif
    QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
        sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
    return handle;
}
  1. 这个函数的源码很庞大,但大多数都是校验,我们只需要看最后一行QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));,前面会根据我们传入的信号和槽函数的名字来获取signal_indexmethod_index_relative。当然这个函数里面是使用QMetaObjectPrivate来获取的,这个类我们用不了(当然通过特殊办法也可以用,但不建议),我们在外部可以使用QMetaObjectQMetaMethod来获取。然后再使用QMetaObjectPrivate::connect函数将信号的id和槽函数的id连接起来,我们这里就是将id为0的信号和id为1的槽函数连接起来,当id为0的信号发送之后,QT内部就会去查id为1的槽函数,然后再传给Test::qt_metacall函数就可以调用到testSlot函数啦。
  2. 上面说了QMetaObjectPrivate::connect函数我们是没办法使用的,但是QT在QMetaObject里面开放了一个connect函数QMetaObject::Connection QMetaObject::connect(const QObject *sender, int signal_index, const QObject *receiver, int method_index, int type = 0, int *types = nullptr),我们进到这个函数内部可以看到它也是调用了QMetaObjectPrivate::connect
QMetaObject::Connection QMetaObject::connect(const QObject *sender, int signal_index,
                                          const QObject *receiver, int method_index, int type, int *types)
{
    const QMetaObject *smeta = sender->metaObject();
    signal_index = methodIndexToSignalIndex(&smeta, signal_index);
    return Connection(QMetaObjectPrivate::connect(sender, signal_index, smeta,
                                       receiver, method_index,
                                       nullptr, //FIXME, we could speed this connection up by computing the relative index
                                       type, types));
}
  1. 最后再看一个问题,就是signal_index指代的id是从什么地方发出的。再次查看moc_Test.cpp文件,可以找到moc为我们自动实现的信号
// SIGNAL 0
void Test::testSignal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

所以所谓的发送信号就是调用QMetaObject::activate函数并将信号的id传入进去,比如这个函数传入的就是固定的0。
8. 现在我们就知道了从信号到槽函数的整个调用过程,总结一下:

  • 去掉槽函数所在类里面的Q_OBJECT宏,但注意还是需要继承至QObject
  • 槽函数所在类手动重写virtual int qt_metacall(QMetaObject::Call, int, void **);函数
  • 使用QMetaObject::connect连接两个唯一id
  • 调用QMetaObject::activate函数并传入连接时的信号id
  • 槽函数手动重写的qt_metacall函数里面判断id为连接时的槽id并作相应的处理

下面改造一下Test类:

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
public:
    explicit Test(QObject *parent = nullptr);

    int qt_metacall(QMetaObject::Call _c, int _id, void **_a) override;
};

#endif // TEST_H
#include "Test.h"

#include <QDebug>

Test::Test(QObject *parent)
    : QObject{parent}
{
    QMetaObject::connect(this, metaObject()->methodCount(), this, metaObject()->methodCount() + 1);
    QMetaObject::activate(this, metaObject(), metaObject()->methodCount(), nullptr);
}

int Test::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id == 1) { // 注意连接槽函数时id有加一
            qDebug() << "槽函数被调用了";
        }
        _id -= 2;
    }
    return _id;
}
#include "MainWindow.h"

#include <QApplication>

#include "Test.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Test t;
    MainWindow w;
    w.show();
    return a.exec();
}

运行上面的代码就能看到输出槽函数被调用了;

剩下的单开一章吧

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 11:50:13  更:2022-04-04 11:55:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:05:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码