QT 中的元对象系统(一):元对象和元数据

news/2025/2/26 15:14:16

目录

1.为什么需要元系统

2.元数据

3.模拟元对象系统

3.1.元对象声明

3.2.对C++扩展

3.3初始化元对象

3.4.使用元对象

4.QT的元系统

4.1.元对象系统基于QObject类、Q_OBJECT宏、元对象编译器MOC实现

4.2.元对象系统的功能

4.3.Q_PROPERTY()的使用

4.4.Q_INVOKABLE使用


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++ 作为静态语言,想要获取这些编译期有关的信息,我们只能选择在编译时或者编译前来做这件事,直觉告诉我们,我们要做编译器之前来做这件事,有两个显而易见的原因

  1. 不要妄图修改编译器,成本巨大且危险
  2. 直接修改编译器显示不是用户能接受的方式

当然可以手动编写这个文件,把类的信息一个个提炼出来,但是那样太不程序员了,我们需要写一段程序,在编译器之前来做这个事情(你可以把它当成一段生成代码的脚本),我们可以这样做:

  1. 在我们写的类里面加上一个标记,来表示该类使用了元对象,需要处理并正确初始化 MetaObejct,我们这里假设就用 DEBUG_OBJ 来表示
  2. 运行我们的程序,如果在某个文件里面发现了标记,解析这个文件,获取他的类型信息(ClassInfo),方法信息(ClassMethod),继承信息等
  3. 脚本生成了一个 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 是跨进城、跨线程对象之间通信的重要利器。


    http://www.niftyadmin.cn/n/5868874.html

    相关文章

    ARM 可执行程序的生成过程

    一&#xff1a;ARM 可执行程序的生成过程 1. 课程内容介绍 汇编语言&#xff1a;汇编语言是与计算机硬件直接交互的低级语言&#xff0c;使用助记符表示机器指令。学习汇编语言有助于理解计算机的工作原理和优化程序性能。调试程序&#xff1a;调试是软件开发中不可或缺的一部…

    20250225使用Timeshift备份Ubuntu20.04系统

    sudo apt-get install timeshif 20250225使用Timeshift备份Ubuntu20.04系统 2025/2/25 20:41 缘起&#xff1a;以前是用ghost来备份win2000/xp&#xff0c;以及WIN7系统。后来WIN10用ghost优势不再了&#xff01; 貌似是symantec不再开发/升级/维护了。Symatec Ghost。 偶然发现…

    5分钟使用Docker部署Paint Board快速打造专属在线画板应用

    文章目录 前言1.关于Paint Board2.本地部署paint-board3.使用Paint Board4.cpolar内网穿透工具安装5.创建远程连接公网地址6.固定Paint Board公网地址 &#x1f4a1; 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住…

    【Git学习笔记】Git常用命令

    Git常用命令 1、仓库2、配置3、增加/删除文件4、代码提交5、分支6、标签7、查看信息8、远程同步9、撤销10、其他 1、仓库 # 在当前目录新建一个Git代码库 $ git init# 新建一个目录&#xff0c;将其初始化为Git代码库 $ git init [project-name]# 下载一个项目和它的整个代码历…

    DeepSeek-R1技术全解析:如何以十分之一成本实现OpenAI级性能?

    一、现象级爆火背后的技术逻辑 2025年1月20日&#xff0c;中国AI公司深度求索&#xff08;DeepSeek&#xff09;发布新一代大模型R1&#xff0c;其性能直接对标OpenAI的o1版本&#xff0c;但训练成本仅为后者的1/20&#xff08;600万美元 vs. 1.2亿美元&#xff09;&#xff0…

    【CSP/信奥赛通关课(六):信奥赛STL专题】

    CSP/信奥赛通关课&#xff08;六&#xff09;&#xff1a;信奥赛STL专题 课程简介&#xff1a; 讲解信奥赛C中的STL核心组件&#xff1a;容器、迭代器、算法等&#xff0c;分析重点案例&#xff0c;让学生在实践的过程中熟练掌握信奥赛C相关的STL重要知识点。 课程教学目标&…

    SpringBatch简单处理多表批量动态更新

    项目需要处理一堆表&#xff0c;这些表数据量不是很大都有经纬度信息&#xff0c;但是这些表的数据没有流域信息&#xff0c;需要按经纬度信息计算所属流域信息。比较简单的项目&#xff0c;按DeepSeek提示思索完成开发&#xff0c;AI真好用。 阿里AI个人版本IDEA安装 IDEA中使…

    MongoDB 面试题目

    一、基础概念 MongoDB 的特点是什么&#xff1f; MongoDB是一种NoSQL数据库&#xff0c;具有以下特点&#xff1a; 文档存储模型 MongoDB 使用 BSON&#xff08;Binary JSON&#xff09; 格式存储数据&#xff0c;数据以文档的形式组织&#xff0c;类似于JSON对象。文档可以包…