Qt工作笔记-Qt元对象系统解析【2合1】

博文转载地址:

https://blog.csdn.net/spwper/article/details/51332187


说Qt信号与槽是一个很好机制,不如说Qt的元对象系统很强大。这也是大家讲Qt就必须将信号与槽,讲信号与槽就要讲Qt的元对象系统。当然初学者知道怎么用就OK啦,当然随着你写的代码越多,接触的平台越多的时候,你就会好奇Qt是如何把两个(多个)任意不相关(必须都继承与QObject)的对象联系在一起的。我们围绕一些问题来认识一下Qt元对象系统:

  • 什么是Qt元对象系统,它包含了哪些内容,它在我们的程序中做了什么?
  • moc工具是什么,Q_OBJECT宏是什么?
  • 元对象系统如何工作在Qml、C++、Javvascrip的混合编程的?
  • 信号与槽机制、Qt事件机制、MFC消息机制三者的区别是什么?
  • 信号与槽在单线程与多线程中是如何工作的?
  • 如何在信号与槽机制中,传递自定义类型参数? 
    什么是Qt元对象系统 
    元对象系统是一个基于标准C++的扩展,为Qt提供了1、信号与槽机制2、实时类型信息3、动态属性系统。 
    这个元对象主要基于三个东西:

  • Object类。大家都知道QObject类是Qt的核心类,很多Qt类都是由它继承而来,那它具体到底是什么东西呢?我们一起去看下Object的Detail Description我们一起来趴一下它的主要特性: 
    (1)它是对象模型的核心,信号与槽是基于对象模型的(两个对象的连接),而它是对象模型的核心。体现在我们常用的QObject::connect()函数上,我们后面会分析这个conncet()源码,趴一下它是怎么工作的。 
    (2)对象的组织方式以树形结构的。这也就是Qt框架那章Core模块的一个特性“树形对象模型”。我们常用相关的函数体现在QObject::setParent()、QObject::findChild()、QObject::findChilren()这几个函数上。这种树形结构保持了众多对象之间的严密的父子、逻辑关系。 
    (3)每一个对象都有一个独立的名字,并且可以查出该对象的继承关系。这些对象不同的名字是我们使用findChild()函数的关键,也是我们在Qml、C++混合编程时的关键。这里QObject有这个属性,并不是它自己实现的,是QMetaobject帮助实现的,QMetaObject是设置这些属性规则,并建立对象们之间的关系的关键。(每个人都有自己名字手机QQ,但是你们怎么相互联系呢,它就是帮助建立通讯录) 
    (4)对象在销毁时会发出一个信号。这里没什么好多说的。 
    (5)添加安装事件过滤器。让对象接受或者不接受某些事件以及事件的处理。我们常用到的有mouseEvent()、timeEvent(),在某些没有继承Object类中是不能使用这些函数的,如QGraphicsItem以及它派生出来的其他图元类。后面我们会讲事件与信号槽的区别。 
    前面(1)(2)(3)是Object与元对象系统紧密联系的属性,其他属性大家可以去看看帮助文档扒一扒,小白英文太烂。到这里你只要明白为什么元对象非要和QObject相关就行啦。

  • Q_OBJECT。简单理解就是一些宏定义代码,就是你们自己定义的一些类、类的信号、槽函数、(Qml混合编程的属性、自己注册的Qt数据类型等)这么多属性,怎么保存到通讯录里面呢?就是通过这个宏定义的函数,帮助你们实现的,后面会结合moc文件讲解这个函数是做什么的。
  • moc(Meta-Object-Compiler)元对象编译器,从概念上和其他编译器一样来理解就好了。signals、slots关键字并不是标准C++里面的东西,代码最后要交给C++编译器,那么就需要把这部分转化成C++编译器认识的东西,这个工作就是moc来完成了。这里需要注意的是,moc过程是发生在预编译之前的,简单说就是moc之后每一个包含Q_OBJECT宏头文件,都会根据该头文件里面的signals、slots、Q_MENU l来生成以moc_XXXX(自定义类名)的.cpp文件,我们常用IDE的构建生成的.o文件,就是最终的目标文件(包含moc生成的cpp)。这个中间生成用qamke生成Makefile可以清楚的看到编译文件的连接情况。后面讲Qt工程的时候会讲解Makefile。这里大家想要理解moc更多的使用规则,帮助里面输入moc查看帮助文档,这里推荐也个中文翻译版(小白英语实在太烂)http://www.kuqin.com/qtdocument/moc.html 
    其实大家都知道这三个基本原则,小白在这里也碰到一个疑问,在《零基础学Qt4编程》里面说moc在生成cpp文件的同时,也会生成头文件XXXX.moc.h格式,很显然小白是没有这个文件的,在帮助里看到的也是read a C++ source file。C++编译器在预编译处理过程中就是处理头文件,如果Qt没有转换头文件,C++编译器怎么认识signals、slots这样的关键字呢?在此不知道有没有熟悉编译原理里的大牛给小白普及一下。 
    元对象系统除了提供信号与槽(communication between objects ,the main reason for introducing the system。主要特性)。我们常用的国际化QObject::tr()、qsTr()还有常用的QObject::setProperty()、QObject::property()。

    到这里我们大致的理解开始我们提出问题的前三个,每个object都有自己的name,这是我们混合编程的前提,至于它是怎么查找的我们下一节来扒。


第二篇:

博文地址:https://blog.csdn.net/spwper/article/details/51351793


我们来看一个信号与槽的小例子 
头文件:

#ifndef MYCLASS
#define MYCLASS
#include <QObject>
class myClass : public QObject
{
    Q_OBJECT
public:
    explicit myClass(QObject *parent = 0 );
     ~myClass();
    void triggerOne();
    void triggerTwo();
signals:
    void signalOne(QString);     //自定义信号one
    void signalTwo(int);         //自定义信号two
private slots:                   //私有槽函数只能与自己关联
protected slots:                 //在共有保护派生子类可连接
public slots:                    //共有继承派生类可连接
    QString slotOne(QString msg);
    int slotTwo(int a);

};

#endif // MYCLASS
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

实现文件:

#include <QApplication>
#include <QObject>
#include "myclass.h"
#include <iostream>
using std::cout;
using std::endl;
myClass::myClass(QObject *parent):
    QObject(parent)
{
    //Qt4里面信号与槽的连接方式
//    connect(this , SIGNAL(signalOne(QString)) , this ,SLOT(slotOne(QString) , Qt::AutoConnection);
//    connect(this , SIGNAL(signalTwo(int)) , this ,SLOT(slotTwo(int) , Qt::AutoConnection);

    //Qt5的连接方式,推荐使用。大家可以去看下这两者的区别
    connect(this , &myClass::signalOne , this , &myClass::slotOne , Qt::AutoConnection);
    connect(this , &myClass::signalTwo , this , &myClass::slotTwo ,  Qt::AutoConnection);

    triggerOne();
    triggerTwo();
}
myClass::~myClass()
{

}
void myClass::triggerOne()
{
    emit signalOne("SignalOne");
}
void myClass::triggerTwo()
{
    emit signalTwo(88);
}
QString myClass::slotOne(QString msg)
{
    cout<<"this is slot one::"+msg.toStdString()<<endl;
    return msg;
}
int myClass::slotTwo(int a)
{
    cout<<"this is slot two::"<<a<<endl;
    return a;

}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    myClass test;
    return a.exec();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

在写这个小例子过程中,小白脑残的本来想写在一个cpp文件中。最后发现不行。查资料才知道,moc工具识别Q_OBJECT宏定义是在头文件中去识别的,些就是说在cpp文件里面包含这个宏是没有用的。详细看此贴http://blog.chinaunix.net/uid-20801802-id-1839159.html 
帮助里面的“read a C++ source file”也只是形象的说法。

下面我们来慢慢分析Q_OBJEC是怎么实现moc_myclass.cpp文件的。 
1、我们看一下Q_OBJECT在Qt库文件(5.5版本)的定义。

#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    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_WARNING_POP \
    QT_TR_FUNCTIONS \
private: \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    struct QPrivateSignal {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

moc的第一工作肯定是统计分析开发者写的这个类有哪些信号槽以及各自的参数情况: 
这里是moc生成moc_mycalss.cpp文件信号槽数量数据代码:

struct qt_meta_stringdata_myClass_t {
    QByteArrayData data[8];
    char stringdata0[51];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_myClass_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_myClass_t qt_meta_stringdata_myClass = {
    {
QT_MOC_LITERAL(0, 0, 7), // "myClass"
QT_MOC_LITERAL(1, 8, 9), // "signalOne"
QT_MOC_LITERAL(2, 18, 0), // ""
QT_MOC_LITERAL(3, 19, 9), // "signalTwo"
QT_MOC_LITERAL(4, 29, 7), // "slotOne"
QT_MOC_LITERAL(5, 37, 3), // "msg"
QT_MOC_LITERAL(6, 41, 7), // "slotTwo"
QT_MOC_LITERAL(7, 49, 1) // "a"

    },
    "myClass\0signalOne\0\0signalTwo\0slotOne\0"
    "msg\0slotTwo\0a"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_myClass[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfocount,classinfodata
       4,   14, // methodscount,methoddata
       0,    0, // propertiescount,propertiesdata
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    1,   34,    2, 0x06 /* Public */,
       3,    1,   37,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       4,    1,   40,    2, 0x0a /* Public */,
       6,    1,   43,    2, 0x0a /* Public */,

 // signals: parameters
    QMetaType::Void, QMetaType::QString,    2,
    QMetaType::Void, QMetaType::Int,    2,

 // slots: parameters
    QMetaType::QString, QMetaType::QString,    5,
    QMetaType::Int, QMetaType::Int,    7,

       0        // eod
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

这里前面的数字注释都说的很清楚,基本数据的统计。在signals和slots里面的:1、3、4、6表示自己在字符串:”myClass\0signalOne\0\0signalTwo\0slotOne\0” 
“msg\0slotTwo\0a”中从第几个“\0”开始表示自己的信息。这里从偏移量开始signal的第一个”\0”后面表示信号名称,第二个”\0”表示返回值(同学要问了,信号函数实现都没有哪来的返回值,对于异步调用返回值永远是默认构造函数出来的,因为调用的函数都没执行完,只能默认一个返回值,对于同步调用,则是最后连接的那个函数的执行的返回值,如果中间有不匹配的情况,Qt会帮你解决,默认了),同理后面的slots更好理解了。 
(1)static const QMetaObject staticMetaObject; 
其对应的moc文件部分(set):

const QMetaObject myClass::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_myClass.data,
      qt_meta_data_myClass,  qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};
//staticMetaObject静态值是由Q_OBJECT引出的,这里只对其自定义部分的参数进行赋值保存,QMetaObject结构体远比这个复杂,只是其他部分与开发者自定义内容无关,moc编译器会自行处理,所以这里生成的moc_myclass.cpp不做处理:
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
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

看别人的博客是没有: 
typedef void (StaticMetacallFunction)(QObject , QMetaObject::Call, int, void **); 
StaticMetacallFunction static_metacall; 
const QMetaObject * const *relatedMetaObjects; 
这三项的,不知道别人使用的是哪个版本。讲道理的话这里,在多类继承的情况下,是有记录关联类信息的,但看代码也只有一个。但是在多类继承的时候,想要拥有QObject的属性时候,具有QObject属性的父类必须放在第一位。大牛说moc规定多继承的情况下,moc会假设第一个继承的类为QObject, 并且必须要保证在多继承中,只有唯一一个类是继承自QObject的。这样看上去,多余一个QObject继承的,第二个QObject根本没办法识别出来。这里小白只知道要放在第一个,不清楚moc的具体工作流程。 
get部分:

const QMetaObject *myClass::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
  • 1
  • 2
  • 3
  • 4

这里的d_ptr是一个Qt里面的模板类指针,并不是QObject的静态成员,这里我在帮助文档里也没找到dynamicMetaObject()成员函数,猜测是一个安全处理,返回staticMetaObject。 
(2)virtual void qt_metacast(const char );

void *myClass::qt_metacast(const char *_clname)
{
    if (!_clname) return Q_NULLPTR;
    if (!strcmp(_clname, qt_meta_stringdata_myClass.stringdata0))
        return static_cast<void*>(const_cast< myClass*>(this));
    return QObject::qt_metacast(_clname);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当传入的字符串数据是当前这个类的话,就将this指针转换成void指针传给外界: 这个操作相当危险。

如果不是当前类的话,就调用父类的qt_metacast继续查询。

在这里,我的父类是QObject,所以自然就调用QObject::qt_metacase了。 
(3)virtual int qt_metacall(QMetaObject::Call, int, void );**

void myClass::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        myClass *_t = static_cast<myClass *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->signalOne((*reinterpret_cast< QString(*)>(_a[1]))); break;
        case 1: _t->signalTwo((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 2: { QString _r = _t->slotOne((*reinterpret_cast< QString(*)>(_a[1])));
            if (_a[0]) *reinterpret_cast< QString*>(_a[0]) = _r; }  break;
        case 3: { int _r = _t->slotTwo((*reinterpret_cast< int(*)>(_a[1])));
            if (_a[0]) *reinterpret_cast< int*>(_a[0]) = _r; }  break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        void **func = reinterpret_cast<void **>(_a[1]);
        {
            typedef void (myClass::*_t)(QString );
            if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&myClass::signalOne)) {
                *result = 0;
            }
        }
        {
            typedef void (myClass::*_t)(int );
            if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&myClass::signalTwo)) {
                *result = 1;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

看名字也知道这里该类所有信号处理函数,并不是我们常用的slots。当我们emit一个信号时,也是调用该函数再调用真正的信号函数,当其他关联信号过来是,才是调用真正的槽函数。

真正的信号函数:

// SIGNAL 0
void myClass::signalOne(QString _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void myClass::signalTwo(int _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

我在帮助里并没有找到activate函数,看大牛们的说的就是:当执行流程进入activate中,会先从connectionLists中取出这个signal相对应的connection list,而这个总的连接表是由connect()函数完成的,下章我们会讲到。看上面两个函数的第三个参数,代表的就是这个类的第几个信号的索引,类的信号槽索引是一起编排的。

很多地方小白也是半知半解,不知道讲清楚没有。到这里moc就是把Qt的信号与槽的那部分相关代码转化为C++代码。如果有大牛,求教育啊。。


猜你喜欢

转载自blog.csdn.net/qq78442761/article/details/80653129