(一):Qt信号槽原理---元对象与moc

一:信号槽

当信号被调用时,与其关联的槽函数会被调用。调用时机与连接类型有关。

在这里插入图片描述

  • 同一个线程内的信号-槽,就相当于函数调用,和观察者模式相似,只不过信号-槽稍微有些性能损耗(这个后面细说)。
  • 跨线程的信号-槽,在信号触发时,发送者线程将槽函数的调用转化成了一次“调用事件”,放入事件循环中。接收者线程执行到下一次事件处理时,处理“调用事件”,调用相应的函数。

二:自己思考如何实现

通过映射的方式实现:通过将信号槽建立一个映射。当信号被调用的时候,通过访问映射表,调用其对应的槽函数。
如果将信号用字符串存储,将槽函数用函数指针存储,那么对应的结构可以为:

std::map<std::string, function<void()>callback> m_callbackMap;

实现链接

connect(sendr,sig,receive,slot)
{
map.insert(sig,slot);//sig为string, slot为function
}

调用

 void invok(const std::string &name)
    {
        auto it = m_callbackMap.find(name);
        //迭代器判断
        if (it != m_callbackMap.end()) {
            //迭代器有效的情况,直接调用
            it->second();
        }
    }

三:Qt实现

3.1 Q_OBJECT宏

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

函数实体代码都由 moc 工具自动生成,保存在 moc_*.cpp 里面,也就是当前类支持元对象系统的关键内部代码。

下面来分析一下:

  • static const QMetaObject staticMetaObject;

这句定义了关键的静态元对象 staticMetaObject。这个对象会保存该类的元对象系统信息。使用静态元对象,说明该类的所有实例都会共享这个静态元对象,而不需要重复占用内存。

  • virtual const QMetaObject *metaObject() const;

这个虚函数是获取当前类对象里面内部元对象的公开接口,通常情况下都会返回类的静态元对象 staticMetaObject,如果当前类的对象内部使用了动态元对象(仅 QML 程序才有),才会出现返回非静态元对象。

  • virtual void *qt_metacast(const char *);

qt_metacast 是程序运行时的对象指针转换,它可以将派生类对象的指针安全地转为基类对象指针,这是 Qt 不依赖编译器特性,自己实现的运行时类型转换。qt_metacast 参数是基类名称字符串,返回值是转换后的基类对象指针,如果转换不成功,返回 NULL。

  • QT_TR_FUNCTIONS

这个宏声明用于定义翻译的 tr() 和 trUtf8() 两个内联函数,其实本质都是调用 staticMetaObject.tr() 函数。

  • virtual int qt_metacall(QMetaObject::Call, int, void **);

    qt_metacall 是非常重要的虚函数,在信号到槽的执行过程中,qt_metacall 就是负责槽函数的调用,属性系统的读写等也是靠 qt_metacall 实现。后面专门会讲它的源码。

  • Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

    这一个私有的静态函数,Q_DECL_HIDDEN_STATIC_METACALL 是个空宏,没啥用,就是提醒程序员这是一个隐蔽的私有静态函数。前面的 qt_metacall 会调用该私有静态函数实现槽函数调用,真正调用槽函数的就是 qt_static_metacall。

  • struct QPrivateSignal {};

    QPrivateSignal 是一个私有的空结构体,对函数功能来说没啥用,就是在信号被触发时,挂在参数里提醒程序员这是一个私有信号的触发。

3.2 signal是什么

#ifndef QT_NO_SIGNALS_SLOTS_KEYWORDS
#define slots
#define signals public
#endif

# define Q_SLOTS
# define Q_SIGNALS public

上面宏定义是说,如果没有定义不能使用 Qt 信号和槽关键字的情况下,启用 slots 和 signals 关键字定义,slots 根本就是空宏,什么都没有。signals 就是 C++ 关键字 public。

class Sig_SLOT : public QDialog
{
   Q_OBJECT

public:
    Sig_SLOT(QWidget *parent = Q_NULLPTR);
	void init();
private slots:
	void slot_set_label_number();
signals:
	void label_number_change(int value);
	void number_change(int value, QString time);
private:
    Ui::Sig_SLOTClass ui;
};

既然声明的signals是一个public,那么moc如何处理signals呢?

可以看到在moc_xxx.cpp中,信号由moc函数自动生成了。

// SIGNAL 0
void Sig_SLOT::label_number_change(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void Sig_SLOT::number_change(int _t1, QString _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

可以看到信号最终将会变为一个函数

程序员不能给信号编写函数实体代码,那是因为必须由 moc 工具为信号生成实体代码,如果程序员自作聪明编一个信号函数实体,程序编译时会报错,因为重名的函数参数还一样,会报重定义错误。

_ a 的定义,_ a 里面第一个指针是 Q_NULLPTR(就是 NULL),这个指针是预留给 元对系统内部注册元方法参数类型时使用;第二个指针是参数_t1 的指针;如果有更多的参数,那么会继续将指针填充到 _a 里面。_a 是用于传递从信号到槽函数的参数的。

为什么信号的参数可以比槽函数的参数多?因为槽函数接收到 _a 指针数组时,只需要取出自己需要的前面几个参数就够了,槽函数不管多余的参数。信号里的参数不能比槽函数里的少,那样槽函数访问指针数组时会越界,造成内存访问错误。

QMetaObject::activate 函数是负责联络接收方槽函数的,它根据源头对象指针 this、源头的元对象指针 &staticMetaObject、信号序号 2、信号参数数组 _a 去找寻需要激活的槽函数,最终会调用每个关联到该信号的槽函数

3.3 Moc的静态数据

3.3.1 qt_meta_stringdata_Sig_SLOT_t 静态字符串数据

结论:qt_meta_stringdata_XXX_t 中存储了类名、信号、信号参数、槽函数、槽函数参数,属性名 等字符串变量。分为两部分存储:QByteArraData数组及stringdata0。其QByteArrayData存入了访问stringdata0子字符串的指针变量及长度。通过QByteArrayData::data()函数可以方便的访问qt_meta_stringdata_Sig_SLOT_t 实例中的字符串。基于这些字符串,Qt 程序才能在运行时自行查询类的名称、根据元方法名称字符串调用元方法、根据属性名称查询设置属性值等。

解析:

struct qt_meta_stringdata_Sig_SLOT_t {
    QByteArrayData data[8];
    char stringdata0[85];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_Sig_SLOT_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_Sig_SLOT_t qt_meta_stringdata_Sig_SLOT = {
    {
QT_MOC_LITERAL(0, 0, 8), // "Sig_SLOT"
QT_MOC_LITERAL(1, 9, 19), // "label_number_change"
QT_MOC_LITERAL(2, 29, 0), // ""
QT_MOC_LITERAL(3, 30, 10), // "textNumber"
QT_MOC_LITERAL(4, 41, 13), // "number_change"
QT_MOC_LITERAL(5, 55, 2), // "va"
QT_MOC_LITERAL(6, 58, 4), // "time"
QT_MOC_LITERAL(7, 63, 21) // "slot_set_label_number"

    },
    "Sig_SLOT\0label_number_change\0\0textNumber\0"
    "number_change\0va\0time\0slot_set_label_number"
};
#undef QT_MOC_LITERAL
  1. QT_BEGIN_MOC_NAMESPACE 宏和文件末尾的 QT_END_MOC_NAMESPACE 都是空宏,仅用于提示程序员,这两个宏中间是 MOC 里的代码。然后 struct 关键字声明了结构体类型 qt_meta_stringdata_Sig_SLOT_t ,里面有两个成员,data 是 8 个 QByteArrayData 数组,stringdata 是一个长度85 的字节数组。这里仅仅是结构体声明(相当于类的声明),没有定义结构体实例。qt_meta_stringdata_Sig_SLOT_t 名字里 Sig_SLOT是类名,不同的类使用的结构体名称不一样。qt_meta_stringdata_Sig_SLOT对字符串结构体进行了实例化,并使用初始化列表初始话该结构体。

  • QT_MOC_LITERAL改宏定义是构造一个QByteArrayData:初始化offset,size成员
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_Sig_SLOT_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
  1. QT_MOC_LITERAL 宏接收 3 个参数,idx 是 stringdata 里面字符串的序号,ofs 是 stringdata 里面字符串的起始偏移,len 是指 stringdata 里面字符串的长度。举例来说:
  • 第 0 号字符串 “Sig_SLOT\0” (类名),在字符串数组里偏移是 0,长度是 8;
  • 第 1 号字符串 “label_number_change\0”(信号名称),在字符串数组里偏移是 9,长度是 19;
  • 第 2 号字符串 “\0”(占位字符串,空的),在字符串数组里偏移是 23,长度是 0;
  • 第 3 号字符串 “value\0”(信号里的参数名),在字符串数组里偏移是 30, 长度是 5;
  • 以此类推

QT_MOC_LITERAL 宏的意义就是根据三元组 (idx, ofs, len) 计算并填充 QByteArrayData 实例里面 size 和 offset 两个成员变量(其他成员用固定值),然后通过 QByteArrayData::data() 函数就能获取 stringdata 里序号为 idx 的字符串起始位置指针。 :
size代表字符串长度,offset表示通过QByteArray实例访问stringdata0中子字符串首地址的偏移量(data函数)。

offset 计算方式比较特别:

  qptrdiff(offsetof(qt_meta_stringdata_Sig_SLOT_t, stringdata0) + ofs - idx * sizeof(QByteArrayData))

外层的 qptrdiff 就是把里面的数据长度变得与系统平台指针长度一样,对于 32 位系统把里面数据变成 32 位长度 qint32,对于 64 位系统把里面的数据变成 64 位长度 qint64。本人测试用的 32 位系统,所以不用管外层的 qptrdiff() ,当 qint32 类型数值就行了。

offsetof(qt_meta_stringdata_Sig_SLOT_t, stringdata0)是获取stringdata0在整个结构体中的偏移量。

减去idx * sizeof(QByteArrayData))的原因:QByateArray访问字符串的机制导致。我们可以看到,QArrayData内部本身并不存储字符串,他只是有五个成员,这里只会用到size和offset。

struct Q_CORE_EXPORT QArrayData
{
    QtPrivate::RefCount ref;
    int size;
    uint alloc : 31;
    uint capacityReserved : 1;

    qptrdiff offset; // in bytes from beginning of header

    void *data()
    {
        Q_ASSERT(size == 0
                || offset < 0 || size_t(offset) >= sizeof(QArrayData));
        return reinterpret_cast<char *>(this) + offset;
    }

    const void *data() const
    {
        Q_ASSERT(size == 0
                || offset < 0 || size_t(offset) >= sizeof(QArrayData));
        return reinterpret_cast<const char *>(this) + offset;
    }
    //后面无关的都忽略掉
};

因此,如果想通过QArrayData访问qt_meta_string_xxx_t中的元对象槽函数及信息,那么必须将string_data0中每个字符串对应的size和offset填入QArrayData中。由于QArrayData中data是this+offset。因此每个QArrayData的this地址会相对qt_meta_stringdata_Sig_SLOT的地址增加一个sizeof(QArrayData)个单位。 要想使用QArrayData的data函数正确访问到对应的字符串,那么我们必须对offset进行修正,由于this指针的地址每次递增sizeof(QArrayData)大小,因此我们必须要将offset-sizeof(QArrayData)*index,这样才能访问stringdata0中相应的字符串。

注:16 = sizeof(QByteArrayData)

  • 对于 0 号的 QByteArrayData [0]实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 0*16;想要正确的访问“Sig_SLOT\0”,那么QByteArrayData成员中的offset就必须减去0*index。
  • 对于 1 号的 QByteArrayData [1]实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 1*16;所以对于第0个QByteArrayData,想要正确的访问“label_number_change\0”,那么QByteArrayData成员中的offset就必须减去1*16。
  • 对于 2 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 2*16;
  • 对于 3 号的 QByteArrayData 实例,它的 this 指向 qt_meta_stringdata_Widget_t 起始 + 3*16;

我们可以通过一下代码将结果打印出来

   for (int i=0; i<8; i++)
	{
		printf("%s\n", qt_meta_stringdata_Sig_SLOT.data[i].data());
	}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yy2987oB-1604499482958)(C:\Users\ChengKeKe\AppData\Roaming\Typora\typora-user-images\image-20201102133222695.png)]

3.3.2 qt_meta_data_Sig_SLOT数组

static const uint qt_meta_data_Sig_SLOT[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       3,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    1,   29,    2, 0x06 /* Public */,
       4,    2,   32,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       7,    0,   37,    2, 0x08 /* Private */,

 // signals: parameters
    QMetaType::Void, QMetaType::Int,    3,
    QMetaType::Void, QMetaType::Int, QMetaType::QString,    5,    6,

 // slots: parameters
    QMetaType::Void,

       0        // eod 代表定义结束
};

1. content为目录:它的个数是固定14个。

其定义为:

//QMetaObjectPrivate 结构体就是对 qt_meta_data_Sig_SLOT 等数组(Widget是类名)的统一封装,结构体还声明了一大堆函数,就是用于处理该数组的数据条目。qt_meta_data_Sig_SLOT 数组的目录部分就是为了描述后面不定长度的数据条目,如 信号、槽、属性条目等。

struct QMetaObjectPrivate
{
    enum { OutputRevision = 7 }; // Used by moc, qmetaobjectbuilder and qdbus

    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData; //since revision 2
    int flags; //since revision 3
    int signalCount; //since revision 4
    // revision 5 introduces changes in normalized signatures, no new members
    // revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
    // revision 7 is Qt 5   
    //后面的一大堆函数省略
};
  • revision 为 7,这是 moc_.cpp 文件格式的修订版本号,之前 Qt 4.8. 用的是修订版 6,而目前 Qt 5 用的是修订版 7。

  • className 总是为 0,这是前面 qt_meta_data_Sig_SLOT.data[0].data() 指向的字符串。

  • classInfoCount 计数为 0,这是 Q_CLASSINFO附加信息对应的计数,因为我们这个综合示例没有加额外信息声明,所以是 0。

  • classInfoData 为 0,因为附加信息计数为零,在本数组 qt_meta_data_Sig_SLOT里面没有 classinfo 条目。

  • methodCount 计数为 3,元方法计数,总共有 2 个信号,1个 set 槽函数。

  • methodData 为 14,表示元方法的数据条目从本数组 qt_meta_data_Sig_SLOT[14] 开始。

  • propertyCount 计数为 0,有 0 个属性声明。

  • propertyData 为 0,表明没有属性数据。

  • enumeratorCount 计数为 0 ,因为我们没有用 Q_ENUMS(…) 声明枚举类型。

  • enumeratorData 为 0 ,没有枚举类型的数据条目。

  • constructorCount 计数为 0,因为我们没有用 Q_INVOKABLE 声明元构造函数。

  • constructorData 为 0,没有元构造函数的数据条目。

  • flags 为 0,因为我们没有用 Q_FLAGS(…) 声明标志位。

  • signalCount 计数为 3,元方法的数据条目以信号函数打头,信号函数需要 moc 工具生成代码,所以需要信号计数,其他元方法的函数实体是由程序员编写的,不用额外计数。

**2. 信号和槽的索引表示:signals及slots: **

 // signals: name, argc, parameters, tag, flags
       1,    1,   29,    2, 0x06 /* Public */,
       4,    2,   32,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       7,    0,   37,    2, 0x08 /* Private */,
  • name 为 1,对应前面 qt_meta_stringdata_Sig_SLOT.data[1].data() 指向的字符串,即 “label_number_change”。
  • argc 为 1,代表 1 个参数。
  • parameters 为 29,它的参数类型位于本数组 qt_meta_data_Sig_SLOT[29] 开始的位置。
  • tag 为 2,没有元方法标签描述,指向空串,即 qt_meta_stringdata_Sig_SLOT.data[2].data() 指向的字符串。
  • flags 为 0x06,是指 MethodFlags::AccessPublic | MethodFlags::MethodSignal,公有信号,其实信号都是公有的。 MethodSlot = 0x08,表示共有槽函数。

**3. 参数:parameters: **

 // signals: parameters
    QMetaType::Void, QMetaType::Int,    3,
    QMetaType::Void, QMetaType::Int, QMetaType::QString,    5,    6,

 // slots: parameters
    QMetaType::Void,
  • Void 是返回值类型,Int参数类型,3 代表 qt_meta_stringdata_Sig_SLOT.data[3].data() 指向的字符串,就是参数的名称 “textNumber”。
  • QMetaType 类是 Qt 对自己知道的所有数据类型做的辅助类,包含了各种参数类型的枚举,可用于元对象系统的参数类型识别。
  1. 属性

    本实例没有,主要一点就行,flags由以下特性或运算得出

    enum PropertyFlags {
        Invalid = 0x00000000,
        Readable = 0x00000001,
        Writable = 0x00000002,
        Resettable = 0x00000004,
        EnumOrFlag = 0x00000008,
        StdCppSet = 0x00000100,
    //     Override = 0x00000200,
        Constant = 0x00000400,
        Final = 0x00000800,
        Designable = 0x00001000,
        ResolveDesignable = 0x00002000,
        Scriptable = 0x00004000,
        ResolveScriptable = 0x00008000,
        Stored = 0x00010000,
        ResolveStored = 0x00020000,
        Editable = 0x00040000,
        ResolveEditable = 0x00080000,
        User = 0x00100000,
        ResolveUser = 0x00200000,
        Notify = 0x00400000,
        Revisioned = 0x00800000
    };
    

3.3.3 Sig_SLOT::staticMetaObject 类的静态元对象

Q_OBJECT宏内部声明了一个静态元对象:

    static const QMetaObject staticMetaObject; 

在moc_xxx.cpp中对此变量进行初始化,如下:

QT_INIT_METAOBJECT const QMetaObject Sig_SLOT::staticMetaObject = {
    { &QDialog::staticMetaObject, qt_meta_stringdata_Sig_SLOT.data,
      qt_meta_data_Sig_SLOT,  qt_static_metacall, nullptr, nullptr}
};

普通的 Qt 窗体程序都会使用这个静态元对象,它就是元对象系统的核心数据。QMetaObject 就是封装和处理元对象系统数据的核心类,它的内部有一个关键的私有数据块 d,与上面大括号里的赋值一一对应,它的声明位于

struct Q_CORE_EXPORT QMetaObject
{

// 忽略前面关系不 大的
    struct { // private data
        const QMetaObject *superdata;
        const QByteArrayData *stringdata;
        const uint *data;
        typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
        StaticMetacallFunction static_metacall;
        const QMetaObject * const *relatedMetaObjects;
        void *extradata; //reserved for future use
    } d;
};

里面 typedef 一句是声明函数指针类型,实际函数指针变量是下面一行的 static_metacall。typedef只是一个类型声明,不算struct成员变量。实际函数指针变量是下面一行的 static_metacall。

  • superdata 是基类元对象指针,赋值为 &QDialog::staticMetaObject。
  • stringdata 是 QByteArrayData 数组指针,赋值为前面介绍的 qt_meta_stringdata_Sig_SLOT.data,用于获取元对象系统静态字符串。
  • data 是元对象系统索引数值的数据块,赋值为前面介绍的 qt_meta_data_Sig_SLOT。
  • static_metacall 是私有静态函数指针,赋值为 qt_static_metacall,因为用到这个函数指针,所以静态元对象赋值代码放在该函数之后。
  • relatedMetaObjects 是相关元对象指针,这里没有,赋值为空指针 Q_NULLPTR。
  • extradata 是保留做将来用途,这里也没有,赋值为空指针 Q_NULLPTR。

元对象系统使用的静态数据就是这些,两个全局数据块 qt_meta_stringdata_Sig_SLOT、qt_meta_data_Sig_SLOT以及类自己的静态元对象 QDialog::staticMetaObject,设置这些数据都是在做准备工作,最终都是要靠函数来运转的

3.4 Mocz中的函数

Q_OBJECT 中有三个函数,在moc阶段也生成对应的实现代码在moc_Sig_SLOT.cpp中

  virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \

private: \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
  1. 首先来看metaObject( )
const QMetaObject *Sig_SLOT::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

对于普通的 Qt 图形界面程序,QObject::d_ptr->metaObject 总是为 NULL,只有 QML 界面程序才会使用动态元对象。所以例子中的 Sig_SLOT::metaObject() 函数不会返回动态元对象,在不使用 QML 的情况下,Widget::metaObject() 函数总是返回我们上面小节介绍的静态元对象指针 &staticMetaObject 。metaObject() 是虚函数,如果我们在程序运行时获得了一个 QObject* 指针 pObj,不知道它原本是什么派生类的,那么就可以执行:pObj->metaObject()->className();

  1. qt_metacast
void *Sig_SLOT::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_meta_stringdata_Sig_SLOT.stringdata0))
        return static_cast<void*>(this);
    return QDialog::qt_metacast(_clname);
}

qt_meta_stringdata_Widget.stringdata 里面其实有很多个以 ‘\0’ 分隔的字符串,打头的是类名,由于 strcmp 以 ‘\0’ 作为结束标志,所以 strcmp 只能看到打头的类名字符串,而看不到后面的一大堆东西,因此能直接将 _clname 与 qt_meta_stringdata_Widget.stringdata 做比较。如果 _clname 与当前类名不一样,那么就继续调用基类的 QWidget::qt_metacast(_clname),这个过程一直迭代到根上的基类 QObject::qt_metacast(_clname) 为止,如果 _clname 不在类的继承树上,那么返回值就是 NULL。

qt_metacast() 函数的作用就是能在运行时根据字符串名,将当前对象转为相应的基类对象指针,如果转换不成功就是 NULL。这是 Qt 自己的运行时类型转换,而不用要求编译器的特性。


  1. **qt_metacall(QMetaObject::Call _c, int _id, void **_a)

!!!该函数非常重要!!!

int Sig_SLOT::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QDialog::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 3)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 3;
    }
    return _id;
}

无论是属性的 get/set,还是从信号到槽函数的调用过程等,都可能用到这个函数。

函数参数:函数头有三个参数,_c 是元调用方式,而 _id 和 _a 的意义根据用途有区别:

  • 对于元方法调用,_id 是元方法的绝对序号, _a 与信号函数里封装的参数指针数组 _a 是对应的;
  • 对于属性操作,_id 是属性的绝对序号,_a 是属性操作需要的参数指针数组。解释一下绝对序号和相对序号:基类和当前类都有一大堆元方法,这些所有的元方法都有它的绝对序号和相对序号,绝对序号是从顶层基类 QObject 开始计数,相对序号从当前类开始计数。属性的序号也是有绝对序号和相对序号,计数原理也是类似的。
  • QMetaObject::Call 是元对象系统内部使用的元调用类型枚举

enum Call {

        InvokeMetaMethod,
        ReadProperty,
        WriteProperty,
        ResetProperty,
        QueryPropertyDesignable,
        QueryPropertyScriptable,
        QueryPropertyStored,
        QueryPropertyEditable,
        QueryPropertyUser,
        CreateInstance,
        IndexOfMethod,
        RegisterPropertyMetaType,
        RegisterMethodArgumentMetaType
    };
  • InvokeMetaMethod 代表元方法调用,比如信号、槽函数、其他 invokable 元方法。
  • 然后从 ReadProperty 到 QueryPropertyUser 一堆枚举,是属性操作相关的,因为属性声明的东西多,它的调用方式枚举也很多。
  • CreateInstance 是用元构造函数生成新实例的调用方式。
  • IndexOfMethod 是 Qt5 新增的调用方式,在新式语法 connect 函数内部,需要先确认 connect 函数里源头的函数指针是不是真的信号函数指针,再进行关联,后面 qt_static_metacall() 私有静态函数会根据这个调用方式,查询匹配的信号函数的相对序号
  • RegisterPropertyMetaType 是注册属性类型的调用方式,与 qt_meta_data_Widget 数组属性条目的 QMetaType:😗 对应。
  • RegisterMethodArgumentMetaType 是注册元方法参数类型的调用方式,与 qt_meta_data_Sig_SLOTS 数组元方法参数类型条目的 QMetaType:😗 对应。

函数体:

_id = QDialog::qt_metacall(_c, _id, _a);

调用基类的qt_metacall函数,这一句代码同时完成了两个任务:第一,如果参数里的绝对序号 _id 是属于基类的,那么基类会调用相应的元方法或进行属性操作(递归,直到id<0为止);第二,如果参数里的绝对序号 _id 是当前类 Widget 自己声明的元方法或属性,那么绝对序号 _id 经过基类处理做减法,那么基类 QWidget::qt_metacall 返回的新 _id 就是我们当前类的元方法或属性的相对 _id ,这样就能根据当前的元方法或属性进行操作了。

  if (_id < 0)
        return _id;

当相对序号 _id < 0 时,说明以前的绝对序号由基类处理完了,我们这一层类就不需要干活,直接返回就可以了。_id < 0 是说明活都干完了。

接下来就是元方法调用的代码:

   if (_c == QMetaObject::InvokeMetaMethod) 
   { 
       if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 3)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 3;
    }

3 为该类的3个元方法:2个信号,1个槽。

QMetaObject::InvokeMetaMethod 表明为元对象调用。id<3表明本级类的元方法,然后调用qt_static_metacall 静态函数来处理。

QMetaObject::RegisterMethodArgumentMetaType,是注册元方法参数类型

  1. **qt_static_metacall **

看 qt_static_metacall() 函数里面 if - else if 的结构,可以知道它是两种用途,

  • 第一种就是上面 qt_metacall() 调用IndexOfMethod元方法函数时,会通过 qt_static_metacall() 来实现;

  • 另一种用途是以 QMetaObject::IndexOfMethod 方式调用。用于查询信号的相对序号。

    注意qt_static_metacall() 负责查询本级类的信号函数的相对序号,而 Qt 文档中另一个函数是查询元方法绝对序号的:

    int QMetaObject::indexOfMethod(const char * method) const,首字母小写,这是一个普通函数

    • 公开函数 QMetaObject::indexOfMethod()是基于字符串的比较,与函数指针没关系,用于所有元方法的绝对序号查询。
    • Sig_SLOT::qt_static_metacall() 里面 QMetaObject::IndexOfMethod 相关代码,是在新式语法 connect 函数调用里,判断 connect 函数参数源头的函数指针是不是真的信号函数指针,基于函数指针匹配,得到信号函数的相对序号。
void Sig_SLOT::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Sig_SLOT *_t = static_cast<Sig_SLOT *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->label_number_change((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->number_change((*reinterpret_cast< int(*)>(_a[1])),(*reinterpret_cast< QString(*)>(_a[2]))); break;
        case 2: _t->slot_set_label_number(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            typedef void (Sig_SLOT::*_t)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Sig_SLOT::label_number_change)) {
                *result = 0;
                return;
            }
        }
        {
            typedef void (Sig_SLOT::*_t)(int , QString );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Sig_SLOT::number_change)) {
                *result = 1;
                return;
            }
        }
    }
}


1. 先看QMetaObject::InvokeMetaMethod元方法的调用

void Sig_SLOT::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
	if (_c == QMetaObject::InvokeMetaMethod) {
        Sig_SLOT *_t = static_cast<Sig_SLOT *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->label_number_change((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->number_change((*reinterpret_cast< int(*)>(_a[1])),(*reinterpret_cast< QString(*)>(_a[2]))); break;
        case 2: _t->slot_set_label_number(); break;
        default: ;
        }
    }

第一个是对象指针 _o,因为静态函数没有对象的 this 指针,需要手动传递;
第二个是元调用的方式,QMetaObject::InvokeMetaMethod 是元方法调用,QMetaObject::IndexOfMethod 是元方法(信号)查询;
第三个是元方法的相对序号,调用元方法需要这个参数,而查询信号不用这个参数;
第四个是参数指针数组,调用元方法时,_a 是参数指针数组,而查询信号相对序号时,_a[0]记录相对序号数值。

qt_static_metacall(this, _c, _id, _a); 手动传递了 this 指针,_c 是调用方式,_id 是元方法的相对序号,_a 是参数指针数组。

  • qt_static_metacall() 先把参数里的 _o 指针(原来是 this 指针)转换为原本的 Sig_SLOT* 类型,现在对象指针叫 _t ,

  • 然后根据相对序号_id,调用对应的槽函数及信号,参数包存在指针数组_a中,_a[0]为空,所以从a[1]开始取参数,Moc工具已经生成了应该取几个参数的代码,因此不必考虑过多。

问题:为什么此处还要调用信号?

​ ----》因为Qt不仅支持信号槽的连接,还支持信号与信号的连接。为了支持这两种情况,因此将所有的信号槽的进行编号,在此处按照相对索引进行调用。


2. IndexOfMethod 部分代码比较简单:函数指针类型对比,如果和信号是一种类型,则返回该信号的序号。填入result。

if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            typedef void (Sig_SLOT::*_t)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Sig_SLOT::label_number_change)) {
                *result = 0;
                return;
            }
        }
        {
            typedef void (Sig_SLOT::*_t)(int , QString );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Sig_SLOT::number_change)) {
                *result = 1;
                return;
            }
        }

qt_static_metacall() 里面关于元方法相对序号查询的代码,实际仅与信号函数有关,没有槽函数什么事。这与上面代码的用途有关,在新式语法的 connect 函数里,比如:

connect(ui->pushButton, &QPushButton::clicked, this, &Widget::FoodIsComing);

这句函数调用里面,怎么知道 clicked 一定是信号,而不是其他成员函数呢?所以新式语法里面必须先检查 &QPushButton::clicked 是不是信号函数指针,然后再做关联。新式语法的 connect 函数调用,源头的函数指针必须是信号函数指针,因此需要通过源头的 qt_static_metacall() 来查询源头的函数指针是不是信号函数指针,根据反馈的 _a[0] 指向的整型变量数值来确认。
新式语法对于接收端的函数指针其实没什么要求,可以是信号、槽、普通成员函数,甚至是 Lambda 函数,所以接收端函数不需要判断是不是槽。之前没说新式语法接收端的函数指针可以是普通成员函数、Lambda 函数,是不想把问题搞复杂。

总结一下之前的所学:

  1. Q_OBJECT宏会由moc工具生成一份mac_xxx.cpp,其中定义了信号函数,信号函数中会调用active函数
  2. moc.cpp还会生成一个meta_call函数和qt_static_metacall函数。meta_call函数会调用qt_static_metacall函数。
  3. qt_static_metacall对于receive函数进行调用,对信号序号进行检测
  4. 定义了一个静态对象 QMetaObject staticMetaObject对象,里面包存了该类信号槽的字符串qt_meta_stringdata_xxx_t,Qt_meta_data_xxx_t,及静态函数qt_static_metacall指针。

到此,一个元对象定义结束了,可以使用Qt的信号槽机制了,信号触发会发送active数据,接收时调用qt_static_metacall函数。

这句函数调用里面,怎么知道 clicked 一定是信号,而不是其他成员函数呢?所以新式语法里面必须先检查 &QPushButton::clicked 是不是信号函数指针,然后再做关联。新式语法的 connect 函数调用,源头的函数指针必须是信号函数指针,因此需要通过源头的 qt_static_metacall() 来查询源头的函数指针是不是信号函数指针,根据反馈的 _a[0] 指向的整型变量数值来确认。

新式语法对于接收端的函数指针其实没什么要求,可以是信号、槽、普通成员函数,甚至是 Lambda 函数,所以接收端函数不需要判断是不是槽。之前没说新式语法接收端的函数指针可以是普通成员函数、Lambda 函数,是不想把问题搞复杂。

总结一下之前的所学:

  1. Q_OBJECT宏会由moc工具生成一份mac_xxx.cpp,其中定义了信号函数,信号函数中会调用active函数
  2. moc.cpp还会生成一个meta_call函数和qt_static_metacall函数。meta_call函数会调用qt_static_metacall函数。
  3. qt_static_metacall对于receive函数进行调用,对信号序号进行检测
  4. 定义了一个静态对象 QMetaObject staticMetaObject对象,里面包存了该类信号槽的字符串qt_meta_stringdata_xxx_t,Qt_meta_data_xxx_t,及静态函数qt_static_metacall指针。

到此,一个元对象定义结束了,可以使用Qt的信号槽机制了,信号触发会发送active数据,接收时调用qt_static_metacall函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值