目录
4.1.元对象系统基于QObject类、Q_OBJECT宏、元对象编译器MOC实现
1.为什么需要元系统
Qt 作为跨平台的GUI框架,在实际项目中应用广泛,在日常的使用中,随手使用的一些机制(如著名的信号槽机制),属性(如Property系统),以及重载各种事件函数来完成定制化;还有qml中直接访问QObject的Property。
在Qt项目中,可以直接通过类名创建对象:
class MyClass : public QObject {
Q_OBJECT
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:
MyClass(QObject *parent = nullptr) : QObject(parent) {}
void myMethod() { qDebug() << "Hello, world!"; }
};
//根据类名创建对象
const QMetaObject *metaObject = MyClass::staticMetaObject();
QObject *myObject = metaObject->newInstance(Q_ARG(QObject*, nullptr));
可以函数名直接访问类的方法:
QMetaObject::invokeMethod(myObject, "myMethod");
运行时增加属性,在运行时根据当前的上下文为一个对象增加或者删除属性,并且要做到在其他地方使用的时候无感——就像这个属性原来就声明在类中一样:
MyClass obj;
obj.setProperty("age", 10); //固定属性,事先声明定义好的
obj.setProperty("name", 110); //动态属性
等等,有了元系统,使得在掌握很少类内部信息都能完成类数据的获取和方法的调用。
2.元数据
元数据是描述数据的数据,它提供了关于数据对象的附加信息。在Qt中,元数据通常用于描述类的属性、方法、信号和槽等信息。试想一下,我们会怎么描述一个类 MyClass:
class MyClass : public Object
{
public:
MyClass();
~MyClass();
enum Type
{
//...
};
public:
virtual void fool() override;
void bar();
//...
};
- 这个类的类名为MyClass
- 继承了一个基类 Object
- 有一个无参的构造函数和一个析构函数
- 实现了继承来的一个虚方法
- 自己有一个名为bar的public方法
- 内定义了一个枚举类型
- ...
上述描述内容就是元数据,用来描述我们声明的一个class,如果我们把以上数据封装为一个类,我们简单的认为这个类就是元对象。
3.模拟元对象系统
Qt 的元对象系统发展这么久,完善是真的完善,代码多也是真的多!在迷失于复杂繁琐的源代码中之前,不妨先来设计一个简单的元对象系统来帮助我们理解思想。
3.1.元对象声明
联系前面的元数据的说明,朴素的想法是我们可以用另一个对象来描述这些信息,即元对象,在运行时通过这个对象来获取相关的具体类型等。
根据我们的需要,元对象应该具有以下信息
- 类型名
- 继承的父类信息
- 成员函数的信息
- 内部定义的枚举变量可能也是需要的
- ...
看起来像是这样
class MetaObject
{
public:
// 其他成员函数
// ...
private:
// 简单起见,直接用对象了
ClassInfo m_info;
ClassInfo* m_superClass;
ClassMethod m_methods;
ClassEnums m_enums;
};
3.2.对C++扩展
为了使我们能在软件系统中有效的管理,我们需要对MyClass做一些拓展,现在MyClass看上去像这样:
// MyClass.h
class MyClass : public Object
{
// ... 和之前一样
// 重写一个来自Object的虚方法
virtual const MetaObject *metaObject() const override;
static const MetaObject staticMetaObject; // 一个静态成员
};
现在,只要这个数据能够正确初始化,如果我们需要,我们就可以借助多态的特性,通过接口来获得这个类的相关信息了。
3.3初始化元对象
那么问题来了,怎么初始化这个变量呢,C++ 作为静态语言,想要获取这些编译期有关的信息,我们只能选择在编译时或者编译前来做这件事,直觉告诉我们,我们要做编译器之前来做这件事,有两个显而易见的原因
- 不要妄图修改编译器,成本巨大且危险
- 直接修改编译器显示不是用户能接受的方式
当然可以手动编写这个文件,把类的信息一个个提炼出来,但是那样太不程序员了,我们需要写一段程序,在编译器之前来做这个事情(你可以把它当成一段生成代码的脚本),我们可以这样做:
- 在我们写的类里面加上一个标记,来表示该类使用了元对象,需要处理并正确初始化 MetaObejct,我们这里假设就用 DEBUG_OBJ 来表示
- 运行我们的程序,如果在某个文件里面发现了标记,解析这个文件,获取他的类型信息(ClassInfo),方法信息(ClassMethod),继承信息等
- 脚本生成了一个 moc_MyClass.cpp 文件,用上述信息初始化 MetaObject,类似于下面这样:
// 由脚本生成的文件
// moc_MyClass.cpp
#include "MyClass.h"
// 这里是脚本解析原来头文件生成的数据
// 解析了类的名称,成员,继承关系等等
// ...
const MetaObject MyClass::staticMetaObject = {
// 用解析来的数据来初始化元对象内容
};
const MetaObject *MyClass::metaObject() const
{
return &staticMetaObject;
}
然后把这个文件也为做源文件一起编译就行了。
3.4.使用元对象
现在再回头来看前面的问题
1)现在直接通过虚函数多态性质拿到 MetaObject,再拿到元数据,比较两个类名是不是一致即可,如果我们采用静态的字符串数组来存类名,甚至我们不需要比较字符串是否一致,只需要比较字符串指针是否相同就可以了。
2)现在直接绑定两个对象的方法字符串即可,我们可以在 MetaObject 提供两各方法
- 检查这两个字符串是否是类的方法(ClassMethod中有没有这个字符串以及参数检查),以判断绑定是否能成功
- 一个统一的调用形式,内部根据字符串来调用相关方法
3)现在你可添加属性,实际添加到元数据中,而存取就像你调用get,set方法一样自然
大功告成,至此,一个简单的元对象系统就设计好了!
4.QT的元系统
4.1.元对象系统基于QObject类、Q_OBJECT宏、元对象编译器MOC实现
1) QObject 类
作为每一个需要利用元对象系统的类的基类。
2) Q_OBJECT宏
定义在每一个类的私有数据段,用来启用元对象功能,比如动态属性、信号和槽。
在一个QObject类或者其派生类中,如果没有声明Q_OBJECT宏,那么类的metaobject对象不会被生成,类实例调用metaObject()返回的就是其父类的metaobject对象,导致的后果是从类的实例获得的元数据其实都是父类的数据。因此类所定义和声明的信号和槽都不能使用,所以,任何从QObject继承出来的类,无论是否定义声明了信号、槽和属性,都应该声明Q_OBJECT 宏。
3) 元对象编译器MOC (Meta Object Complier)
MOC分析C++源文件,如果发现在一个头文件(header file)中包含Q_OBJECT 宏定义,会动态的生成一个moc_xxxx命名的C++源文件,源文件包含Q_OBJECT的实现代码,会被编译、链接到类的二进制代码中,作为类的完整的一部分。
4.2.元对象系统的功能
qt元对象系统主要提供了三个能力:
- 对象间通信(信号槽机制)
- 运行时信息(类似反射机制)
- 动态的属性系统
除了这些功能外,还提供了如下功能:
QObject::metaObject()
返回与该类相关联的元对象。QMetaObject::className()
在运行时以字符串形式返回类名,而无需通过 C++ 编译器提供本地运行时类型信息(RTTI)支持。QObject::inherits()
函数返回一个对象是否是在 QObject 继承树内继承了指定类的实例。QObject::tr()
和QObject::trUtf8()
用于国际化的字符串翻译。QObject::setProperty()
和QObject::property()
动态地通过名称设置和获取属性。QMetaObject::newInstance()
构造该类的新实例。- 使用qobject_cast()方法在QObject类之间提供动态转换,qobject_cast()方法的功能类似于标准C++的dynamic_cast(),但qobject_cast()不需要RTTI的支持
4.3.Q_PROPERTY()的使用
#define Q_PROPERTY(text)
Q_PROPERTY定义在/src/corelib/kernel/Qobjectdefs.h文件中,用于被MOC处理。
Q_PROPERTY(type name
READ getFunction
[WRITE setFunction]
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[CONSTANT]
[FINAL])
Type:属性的类型
Name:属性的名称
READ getFunction:属性的访问函数
WRITE setFunction:属性的设置函数
RESET resetFunction:属性的复位函数
NOTIFY notifySignal:属性发生变化的地方发射的notifySignal信号
REVISION int:属性的版本,属性暴露到QML中
DESIGNABLE bool:属性在GUI设计器中是否可见,默认为true
SCRIPTABLE bool:属性是否可以被脚本引擎访问,默认为true
STORED bool:
USER bool:
CONSTANT:标识属性的值是常量,值为常量的属性没有WRITE、NOTIFY
FINAL:标识属性不会被派生类覆写
注意:NOTIFY notifySignal声明了属性发生变化时发射notifySignal信号,但并没有实现,因此程序员需要在属性发生变化的地方发射notifySignal信号。
Object.h:
#ifndef OBJECT_H
#define OBJECT_H
#include <QObject>
#include <QString>
#include <QDebug>
class Object : public QObject
{
Q_OBJECT
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
Q_PROPERTY(int score READ score WRITE setScore NOTIFY scoreChanged)
Q_CLASSINFO("Author", "Scorpio")
Q_CLASSINFO("Version", "1.0")
Q_ENUMS(Level)
protected:
QString m_name;
QString m_level;
int m_age;
int m_score;
public:
enum Level
{
Basic,
Middle,
Advanced
};
public:
explicit Object(QString name, QObject *parent = 0):QObject(parent)
{
m_name = name;
setObjectName(m_name);
connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int)));
connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int)));
}
int age()const
{
return m_age;
}
void setAge(const int& age)
{
m_age = age;
emit ageChanged(m_age);
}
int score()const
{
return m_score;
}
void setScore(const int& score)
{
m_score = score;
emit scoreChanged(m_score);
}
signals:
void ageChanged(int age);
void scoreChanged(int score);
public slots:
void onAgeChanged(int age)
{
qDebug() << "age changed:" << age;
}
void onScoreChanged(int score)
{
qDebug() << "score changed:" << score;
}
};
#endif // OBJECT_H
Main.cpp:
#include <QCoreApplication>
#include "Object.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Object ob("object");
//设置属性age
ob.setProperty("age", QVariant(30));
qDebug() << "age: " << ob.age();
qDebug() << "property age: " << ob.property("age").toInt();
//设置属性score
ob.setProperty("score", QVariant(90));
qDebug() << "score: " << ob.score();
qDebug() << "property score: " << ob.property("score").toInt();
//内省intropection,运行时查询对象信息
qDebug() << "object name: " << ob.objectName();
qDebug() << "class name: " << ob.metaObject()->className();
qDebug() << "isWidgetType: " << ob.isWidgetType();
qDebug() << "inherit: " << ob.inherits("QObject");
return a.exec();
}
4.4.Q_INVOKABLE使用
#define Q_INVOKABLE
Q_INVOKABLE定义在/src/corelib/kernel/Qobjectdefs.h文件中,用于被MOC识别。
Q_INVOKABLE宏用于定义一个成员函数可以被元对象系统调用,Q_INVOKABLE宏必须写在函数的返回类型之前。如下:
Q_INVOKABLE void invokableMethod();
invokableMethod()函数使用了Q_INVOKABLE宏声明,invokableMethod()函数会被注册到元对象系统中,可以使用 QMetaObject::invokeMethod()调用。
Q_INVOKABLE与QMetaObject::invokeMethod均由元对象系统唤起,在Qt C++/QML混合编程、跨线程编程、Qt Service Framework以及 Qt/ HTML5混合编程以及里广泛使用。
1) 在跨线程编程中的使用
如何调用驻足在其他线程里的QObject方法呢?Qt提供了一种非常友好而且干净的解决方案:向事件队列post一个事件,事件的处理将以调用所感兴趣的方法为主(需要线程有一个正在运行的事件循环)。而触发机制的实现是由MOC提供的内省方法实现的。因此,只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。如果不想通过跨线程的信号、槽这一方法来实现调用驻足在其他线程里的QObject方法。另一选择就是将方法声明为Q_INVOKABLE,并且在另一线程中用invokeMethod唤起。
2) Qt Service Framework
Qt服务框架是Qt Mobility 1.0.2版本推出的,一个服务(service)是一个独立的组件提供给客户端(client)定义好的操作。客户端可以通过服务的名称,版本号和服务的对象提供的接口来查×××。 查找到服务后,框架启动服务并返回一个指针。
服务通过插件(plug-ins)来实现。为了避免客户端依赖某个具体的库,服务必须继承自QObject,保证QMetaObject 系统可以用来提供动态发现和唤醒服务的能力。要使QmetaObject机制充分的工作,服务必须满足,其所有的方法都是通过 signal、slot、property或invokable method和Q_INVOKEBLE来实现。
QServiceManager manager;
QObject *storage ;
storage = manager.loadInterface("com.nokia.qt.examples.FileStorage");
if(storage)
QMetaObject::invokeMethod(storage, "deleteFile", Q_ARG(QString, "/tmp/readme.txt"));
上述代码通过service的元对象提供的invokeMethod方法,调用文件存储对象的deleteFile() 方法。客户端不需要知道对象的类型,因此也没有链接到具体的service库。 当然在服务端的deleteFile方法,一定要被标记为Q_INVOKEBLE,才能够被元对象系统识别。
Qt服务框架的一个亮点是它支持跨进程通信,服务可以接受远程进程。在服务管理器上注册后,进程通过signal、slot、invokable method和property来通信,就像本地对象一样。服务可以设定为在客户端间共享,或针对一个客户端。 在Qt服务框架推出之前,信号、槽以及invokable method仅支持跨线程。 下图是跨进程的服务/客户段通信示意图。invokable method和Q_INVOKEBLE 是跨进城、跨线程对象之间通信的重要利器。