QT元对象系统
- 01、元对象系统
- 1.1、 元对象运行原则
- 1.2、 Q_OBJECT宏
- 1.3、 Qt Creator启动元对象系统
- 1.4、 命令行启动元对象(不常用)
- 02、反射机制
- 2.1、 Qt实现反射机制
- 2.2、 反射机制获取类中成员函数的信息
- 2.1.1、 QMetaMethon类
- 2.1.2、QMetaObject类
- 2.3、 反射机制获取类相关的信息
01、元对象系统
元对象系统是Qt对原有的C++进行的一些扩展,主要是为了实现信号和槽机制而引入的,信号和槽机制是Qt的核心特性。
Qt的元对象系统提供的功能有:
1、对象间通信的信号和槽机制
2、运行时类型信息和动态属性系统等
要使用元对象系统的功能,需要满足三个前置条件。
- 该类必须继承自QObject类。
- 必须在类申明的私有区域添加
Q_OBJECT
宏,该宏用于启动元对象特性,然后就可以使用动态特性、信号和槽等功能。 - 元对象编译器(moc)为每个QObject的子类提供实现了元对象特性所必须得代码。
1.1、 元对象运行原则
- 因为元对象系统是对C++的扩展,因此使用传统的编译器是不能直接编译启用了元对象系统的Qt程序的,对此在编译Qt程序之前,需要把扩展的语法去掉,该功能就是moc要做的事情。
- moc的全称是:Meta-Object Compiler(元对象编译器),它是一个工具(类似于qmake),该工具读取并分析C++源文件,若发现一个或多个包含了
Q_OBJECT
宏的类的申明,则会生成另外一个包含了Q_OBJECT
宏实现代码的C++源文件(该源文件通常名称为:moc_*.cpp),这个新的源文件要么被#include包含到类 的源文件中,要么被编译键解到类的实现中(通常使用这种方法)。注意:新文件不会“替换”掉旧的文件,而是同源文件一起编译。
其他概念 :
1、元对象代码:指的是moc工具生成的源文件的代码,其中包含有Q_OBJECT
宏的实现代码
2、moc工具的路径一般在安装路径下,如:C:\app\Qt5.8.0MinGw\5.8\mingw53_32\bin
1.2、 Q_OBJECT宏
先看一张图片,只要包含了Q_OBJECT
宏,然后F2,就能进入。
可见,Q_OBJECT
宏为申明的类之中增加了一些成员,而且红框标注的是虚函数成员(注意这些虚函数没有定义),按照C++语法,虚函数必须定义或者被申明为纯虚函数,moc工具的工作之一就是生成以上成员的定义,并且还会生成一些其他必要的代码。
1.3、 Qt Creator启动元对象系统
由于moc工具是通过Qt Creator来使用的,因此必须保证moc能发现并处理项目中包含有
Q_OBJECT
宏的类,所以,需要遵守以下规则:
1、从QObject派生的含有Q_OBJECT
宏的类的定义必须在头文件中;
2、确保pro
文件中,是否列举了项目中的所有源文件(SOURCES变量)和头文件(HEADERS变量);
3、应在头文件中使用逻辑指令如(#ifndef、#define、#endif
)防止头文件重复包含多次。
4、QObject类应是基类列表中的第一个类。
由以上规则可见,使用 Qt Creator 编写代码时,类应定义在头文件中,成员函数
的定义应位于源文件中(这样可避免头文件被包含多次产生的重定义错误),虽然
这样编写程序比较麻烦,但这是一种良好的代码组织方式。
注意:如果定义了QObject类的派生类,并进行了构建(构建未添加O_OBJECT
,而是在构建完成之后才添加),则此时必须再执行一次qmake命令,否则moc不能生成代码。
这里贴上书中的原话:
运行以上程序,可在 debug 目录下找到一个 moc_m.cpp 的源文件,该源文件就是使用 moc
工具生成的,该源文件中的代码就是元对象代码,读者可查看其代码。若在该目录没有
moc_m.cpp 文件,说明 moc 工具未能正常启动,这时需在 Qt Creator 中执行 qmake 命令,
再构建程序。
1.4、 命令行启动元对象(不常用)
- 在命令行需使用 moc 工具,并在源文件中包含 moc 工具生成的 cpp 文件。
- 此时包含 Q_OBJECT 的类不需要位于头文件中,假设位于 m.cpp 文件内,内容为:
#include<QObject>
class A : public QObject
{
Q_OBJECT
public:
A(){}
};
int main(int argc, char *argv[]) {
A ma; return 0;
}
- 打开 Qt 5.8 for Desktop (MinGW 5.3.0 32 bit)命令行工具,输入如下命令:
moc d:\qt\m.cpp -o d:\qt\mm.cpp
以上命令会根据 m.cpp 生成 mm.cpp 文件,mm.cpp 文件中的代码就是元对象代码,此处
m.cpp 和 mm.cpp 都位于 d:\qt 文件夹下。
- 然后再次打开 m.cpp,在其中使用#include 把 mm.cpp 包含进去,如下:
#include <QObject>
//#include "mm.cpp" // 不能把mm.cpp包含在类A的定义之前
class A : public QObject {
Q_OBJECT
public:
A(){}
};
#include "mm.cpp" // 必须把mm.cpp包含在类A的定义后面,因为mm.cpp源文件中有对A的成员定义,此时必须见到类A的完整定义。
int main(int argc, char* argv[]) {
A ma;
return 0;
}
- 然后再使用 qmake 生成项目文件和 makefile 文件,再使用 mingw32-make 命令即可。
命令行这种方式,一般来说QT开发者是很少用到,因为Qt Creator已经帮我们做了这件事,这里只是看见书上有讲,记录一下。
02、反射机制
reflection模式(反射模式/反射机制):是指在运行时,能获取任意一个类对象的所有类型信息、属性、成员函数等信息的一种机制。
元对象系统与反射机制:
1、元对象系统提供的功能之一是为 QObject 派生类对象提供运行时的类型信息及数据成
员的当前值等信息,也就是说,在运行阶段,程序可以获取 QObject 派生类对象所属
类的名称、父类名称、该对象的成员函数、枚举类型、数据成员等信息,其实这就是
反射机制。
2、因为 Qt 的元对象系统必须从 QObject 继承,又从反射机制的主要作用可看到,Qt 的
元对象系统主要是为程序提供了 QObject 类对象及其派生类对象的信息,也就是说不
是从 QObject 派生的类对象,则无法使用 Qt 的元对象系统来获取这些信息。
元对象的具体解释:是指用于描述另一个对象结构的对象。
具象化到程序上如下:
class B
{
//TODO....
};
class A : public B
{
B mb;
};
假设mb是用来描述类A创建的对象的,则mb就是元对象。
2.1、 Qt实现反射机制
- Qt使用了一系列的类来实现反射机制,这些类对对象的各个反面进行了描述,其中
QMetaObject
类描述了QObject及其派生类对象的所有元信息,该类是Qt元对象系统的核心类,通过该类的成员函数可以获取QObject及其派生类对象的所有元信息,因此可以说QMetaObject
类的对象是Qt中的元对象。 注意:要调用QMetaObject
类中的成员函数需要使用QMetaObject
类型的对象。 - 对对象的成员进行描述:一个对象包含数据成员、函数成员、构造函数、枚举成员等,在Qt中,这些成员分别使用了不同的类对其进行描述。比如函数成员使用类
QMetaMethod
、属性使用QMetaProperty
进行描述等。最后使用QMetaObject类对整个类对象进行描述,比如要获取成员函数的变量名:
QMetaMethod qm = metaObject->method(1); // 获取索引为1的成员函数
qDebug()<< qm.name() << "\n"; // 输出该成员函数的名称
使用Qt反射机制的前置条件:
- 需要继承自QObject类,并需要在类之中加入Q_OBJECT宏。
- 注册成员函数:若希望普通成员函数能够被反射,需要在函数声明之前加入
QObject::Q_INVOKABLE
宏。 - 注册成员变量:若希望成员变量能够被反射,需要使用
Q_PROPERTY
宏。
其实关于
QObject::Q_INVOKABLE
、Q_PROPERTY
这两个宏,在Qt5中的QQuick中就有很深切的体会,QML要使用C++这边的属性、方法都是需要注明这个宏的。
Qt 反射机制实现原理简述:
Q_OBJECT
宏展开之后有一个虚拟成员函数meteObject()
,该函数会返回一个指向QMetaObject
类型的指针,其原型为:
virtual const QMetaObject* metaObject() const;
因为启动了元对象系统的类都包含了Q_OBJECT
宏,所以这些类都含有metaObject()
虚拟成员函数,通过该函数返回的指针调用QMetaObject
类中的成员函数,便可以查询到QObject及其派生类对象的各种信息。
- Qt的moc会完成以下工作:
1、为Q_OBJECT
宏展开后所声明的成员函数的生成实现代码;
2、识别Qt中特殊的关键字及宏,比如识别出Q_PROPRTY
、Q_INVOKABLE
、slot
、signals
等。
2.2、 反射机制获取类中成员函数的信息
2.1.1、 QMetaMethon类
作用:用于描述对象的成员函数,可使用该类的成员函数获取对象成员函数的信息。
列举一些常用的成员:
// 此枚举用于描述函数的类型,即:普通成员函数、信号、槽、构造函数
enum MethodType {Method, Signal, Slot, Constructor}
// 此枚举主要用于描述函数的访问级别(私有的、受保护的、公有的)
enum Access {Private,Protected,Public}
// 返回函数的签名(qt5.0)
QByteArray methodSignature() const;
// 返回函数的类型(信号、槽、成员函数、构造函数)
MethodType methodType() const;
// 返回函数的名称(qt5.0)
QByteArray name() const;
// 返回函数的参数数量(qt5.0)
int parameterCount() const;
// 返回函数参数名称的列表
QList<QByteArray> parameterNames() const;
// 返回指定索引处的参数类型,返回值是使用QMetaType注册的类型,若类型未注册,则返回值为QMetaType::UnknownType。
int parameterType(int index) const;
// 返回函数参数类型的列表
QList<QByteArray> parameterTypes() const;
// 返回函数的返回类型。返回值是使用QMetaType注册的类型,若类型未注册,则返回值为QMetaType::UnknownType。
int returnType() const;
// 返回函数的返回类型的名称
const char* typeName() const;
// 返回函数的访问级别(私有的、受保护的、公有的)
Access access() const;
2.1.2、QMetaObject类
作用:用于提供关于类的元对象信息。
列举一些常用的成员:
/* 返回名为 f 的函数的索引号,否则返回-1。此处应输入正确的函数签名,比如函数形
式为 void f(int i,int j);则正确的形式为 xx.indexOfMethod("f(int,int"); 以下形式都不是
正确的形式,"f(int i, int j)"、"void f(int, int)"、 "f"、"void f"等。*/
int indexOfMethod(const char* f) const;
// 返回信号 s 的索引号,否则返回-1,若指定的函数存在,但不是信号,仍返回-1。
int indexOfSignal(const char * s) const;
// 返回构造函数 c 的索引号,否则返回-1
int indexOfConstructor(const char *c) const;
// 返回构造函数的数量。
int constructorCount() const ;
// 返回指定索引 i 处的构造函数的元数据。
QMetaMethod constructor(int i)const;
// 返回函数的数量,包括基类中的函数、信号、槽和普通成员函数。
int methodCount() const;
// 返回父类中的所有函数的总和,也就是说返回的值是该类中的第一个成员函数的索引位置。
int methodOffset() const;
// 返回指定索引 i 处的函数的元数据。
QMetaMethod method(int i) const;
下面是书上的一个示例,仅供参考
#include "m.h"
#include <QMetaMethod>
#include <QByteArray>
#include <iostream>
using namespace std;
int main(){ A ma; B mb; //创建两个对象
const QMetaObject *pa=ma. metaObject ();
const QMetaObject *pb=mb. metaObject ();
//以下为通过 QMetaObject 的成员函数获取的信息。
int j=pa->methodCount(); /*返回对象 ma 中的成员函数数量,包括从父类 QObject 继承而来的 5 个
成员函数及本对象中的 2 个成员函数(注意,不包括 g1)、1 个信号,所以
总数为 8。*/
cout<<j<<endl; //输出 8
int i=pa->indexOfMethod("g(int,float)"); //获取对象 ma 中的成员函数 g 的索引号。
cout<<i<<endl; //输出 7
i=pa->constructorCount(); //获取对象 ma 所属类中的构造函数的数量。
cout<<i<<endl; //输出 2
i=pb->constructorCount(); /*获取对象 mb 所属类 B 中的构造函数的数量,因类 B 无构造函数,所以
返回值为 0,此处也可看到,构造函数数量不包含父类的构造函数*/
cout<<i<<endl; //输出 0。
i=pa->indexOfConstructor("A(int)"); //获取对象 ma 所属类中的构造函数 A(int)的索引号。
cout<<i<<endl; //输出 1。
i=pa->indexOfSignal("gb3()"); //获取对象 ma 的信号 gb3()的索引号。
cout<<i<<endl; //输出 5。
i=pa->indexOfSignal("f()"); /*获取对象 ma 的信号 f()的索引号。因为成员函数 f 存在,但不是信
号,所以返回值为-1。*/
cout<<i<<endl; //输出-1。
i=pb->methodOffset(); /*获取父类的成员函数数量,包括父类A及QObject中的成员函数,总共为8。
*/
cout<<i<<endl; //输出 8,此处相当于是对象 mb 自身成员函数开始处的索引号。
//以下为通过 QMetaMethon 的成员函数获取的信息。
//获取对象 ma 的成员函数 g 的元数据。
QMetaMethod m=pa->method(pa->indexOfMethod("g(int,float)"));
QByteArray s= m.name(); //获取成员函数 g 的函数名。
cout<<s.data()<<endl; //输出 g
s=m.methodSignature(); //获取函数 g 的签名
cout<<s.data()<<endl; //输出 g(int,float)
i=m.methodType(); /*获取函数 g 的类型,此处返回的是 QMetaMethod::MethodType 中定义的枚举值,
其中 Method=0,表示类型为成员函数*/
cout<<i<<endl; //输出 0(表示成员函数)。
//以下信息与函数的返回类型有关
s=m.typeName(); //获取函数 g 的返回值的类型名
cout<<s.data()<<endl; //输出 void
i=m.returnType(); /*获取函数 g 返回值的类型,此处的类型是 QMetaType 中定义的枚举值,其中枚举
值 QMetaType::void=43*/
cout<<i<<endl; //输出 43
//以下信息与函数的参数有关
i=m.parameterType(1); /*获取函数 g 中索引号为 1 的参数类型,此处的类型是 QMetaType 中定义的
枚举值,其中枚举值 QMetaType::float=38*/
cout<<i<<endl; //输出 38
QList<QByteArray> q=m.parameterNames(); //获取函数 g 的参数名列表
cout<<q[0].data()<<q[1].data()<<endl; //输出 ij
q=m.parameterTypes(); //获取函数 g 的参数类型列表。
cout<<q[0].data()<<q[1].data()<<endl; //输出 intfloat
return 0; }
2.3、 反射机制获取类相关的信息
- QMetaObject类中获取与类相关的信息的成员函数有:
/*获取类的名称,注意,若某个 QObject 的子类未启动元对象系统(即未使用 Q_OBJECT
宏),则该函数将获取与该类最接近的启动了元对象系统的父类的名称,而不再返回
该类的名称,因此建议所有的 QObject 子类都使用 Q_OBJECT 宏。
*/
const char* className() const;
// 返回父类的元对象,若没有这样的对象则返回0
const QMetaObject* superClass() const;
// 若该类继承自mo描述的类型,则返回true,否则返回false。类被认为继承自身。
bool inherits(const QMetaObject* mo) const; // (Qt5.7)
- QObject类中获取与类相关的信息的成员函数有:
// 若该类是className指定的类的子类则返回true,否则返回false。类被认为继承自身
bool inherits(const char* className) const;
下面是一个示例:
头文件:
#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include<QObject>
class A:public QObject{ Q_OBJECT};
class B:public A{ Q_OBJECT};
class C:public QObject{Q_OBJECT};
class D:public C{};
#endif // M_H
源文件:
#include "m.h"
#include <QMetaMethod>
#include <iostream>
using namespace std;
int main(){ A ma; B mb; C mc; D md;
const QMetaObject *pa=ma. metaObject ();
const QMetaObject *pb=mb. metaObject ();
cout<<pa->className()<<endl; //输出类名 A
//使用 QMetaObject::inherits()函数判断继承关系。
cout<<pa->inherits(pa)<<endl; //输出 1,类被认为是自身的子类
cout<<pa->inherits(pb)<<endl; //输出 0,由 pb 所描述的类 B 不是类 A 的子类。
cout<<pb->inherits(pa)<<endl; //输出 1,由 pa 所描述的类 A 是类 B 的子类。
//使用 QObject::inherits()函数判断继承关系。
cout<<ma.inherits("B")<<endl; //输出 0,类 A 不是类 B 的子类。
cout<<ma.inherits("A")<<endl; //输出 1,类被认为是自身的子类
cout<<md.inherits("D")<<endl; //输出 0,因为类 D 未启动元对象系统。
cout<<md.inherits("C")<<endl; /*输出 1,虽然类 D 未启动元对象系统,但类 C 已启动,此种情形下
能正确判断继承关系。*/
cout<<md. metaObject ()->className()<<endl; /*输出 C,此处未输出 D,因为类 D 未启动元对象系统,
与类 D 最接近的启动了元对象系统的父类是 C,因此返回 C。*/
return 0; }
- qobject_cast函数,使用语法如下:
DestType* qobject_cast<DestType*>(QObject* p);
1、该函数类似于 C++中的 dynamic_cast,但执行速度比 dynamic_cast 更快,且不需要
C++的 RTTI 的支持,但 qobject_cast 仅适用于 QObject 及其派生类。
2、主要作用是把源类型 QObject 转换为尖括号中的目标类型 DesType(或者子类型),并
返回指向目标类型的指针,若转换失败,则返回 0。或者说源类型 QObject 属于目标
类型 DestType(或其子类型),则返回指向目标类型的指针,否则返回 0。
3、使用 qobject_cast 的条件:目标类型 DestType 必须继承(直接或间接)自 QObject,并
使用 Q_OBJECT 宏。
一个示例:
头文件:
#ifndef M_H //要使用元对象系统,需在头文件中定义类。
#define M_H
#include<QObject>
#include <iostream>
using namespace std;
class A:public QObject{ Q_OBJECT
public:void fa(){cout<<"FA"<<endl;} };
class B:public A{ Q_OBJECT
public:void fb(){cout<<"FB"<<endl;} };
class C:public QObject{Q_OBJECT
public:void fc(){cout<<"FC"<<endl;} };
class D:public C{ public: void fd(){cout<<"FD"<<endl;} };
#endif // M_H
源文件:
#include "m.h"
#include <QMetaMethod>
#include <iostream>
using namespace std;
//qobject_cast 的简单应用(类型判断)
void g(QObject *p){
if(qobject_cast<A*>(p)) //若 p 是类 A 及其派生类类型
{cout<<"GA"<<endl;}
if(qobject_cast<B*>(p))//若 p 是类 B 及其派生类类型
{cout<<"GB"<<endl;}
else //若 p 不是类 B 及其派生类类型
cout<<"XX"<<endl; }
int main(){ A *pa=new A; B *pb=new B; C *pc=new C; D *pd=new D;
qobject_cast<B*>(pa)->fb(); //输出 FB,转换成功后可调用子类中的函数。
//qobject_cast<D*>(pc); //错误,因为类 D 未使用 Q_OBJECT 宏。
g(pa); //输出 GA、XX。因为 pa 不是 B 及其派生类类型所以会输出 XX。
g(pb); //输出 GA、GB。因为 pb 是 A 的派生类类型,所以首先输出 GA,然后输出 GB。
g(pc); //输出 XX,因为 pc 即不是 A 也不是 B 及其派生类的类型,所以输出 XX。
g(pd); //输出 XX,原因同上。
return 0; }
未完待续!!!