Qt元对象系统

元对象系统的概念

Qt中的元对象系统(Meta-Object System)提供了对象间通信的信号和槽机制、运行时类型信息和动态属性系统。元对象系统是基于以下3个条件的:

  • 该类必须继承自QObject类;
  • 必须在类的私有声明区声明Q_OBJECT宏(在类定义时,如果没有指定public或者private,则默认为private);
  • 元对象编译器Meta-Object Compiler(moc),为QObject的子类实现元对象特性提供必要的代码。

其中,moc工具读取一个C+ +源文件,如果它发现一个或者多个类的声明中包含有Q_OBJECT宏,便会另外创建一个C+ +源文件(就是在项目目录中的debug目录 下看到的以moc开头的C+ +源文件),其中包含了为每一个类生成的元对象代码。 这些产生的源文件或者被包含进类的源文件中,或者和类的实现同时进行编译和链接。

元对象系统主要是为了实现信号和槽机制才被引入的,不过除了信号和槽机制以外,元对象系统还提供了其他一些特性:

  • QObjeCt::metaObject()函数可以返回一个类的元对象,它是QMetaObject类的对象;
  • QMetaObject::className()可以在运行时以字符串形式返回类名,而不需要C+ +编辑器原生的运行时类型信息(RTTI)的支持;
  • QObject:: “inherits()函数返回一个对象是否是QObject继承树上一个类的实例的信息;
  • QObject: :tr()和QObject: :trUtf8()迸行字符串翻译来实现国际化;
  • QObject::setProperty()和QObject::property()通过名字来动态设置或者获取对象属性;
  • QMetaObject: :newlnstance()构造该类的一个新实例。

除了这些特性,还可以使用qobject_cast()函数来对QObject类进行动态类型转换,这个函数的功能类似于标准C+ +中的dynamic_cast()函数,但它不再需要RTTI的支持。这个函数尝试将它的参数转换为尖括号中的类型的指针,如果是正确的类型,则返回一个非零的指针,如果类型不兼容则返回0。例如:

QObject *obj = new MyWidget;
QWidget *widget = qobject_cast<QWidget *>(obj);

信号和槽机制是Qt的核心内容,而信号和槽机制必须依赖于元对象系统,所以它是Qt中很关键的内容。这里只是说明了它的一些应用,关于它的具体实现机制,这里不再讲述。关于元对象系统的具体描述,可以在Qt中查看The Meta Object System关键字。

1 QObject类


  QObject类是所有使用元对象系统类的基类;

元对象系统的特性是通过QObject的一些函数实现的,主要包括以下几个部分:

1、元对象(meta object):

每个QObject子类的实例都是一个元对象(静态变量staticMetaObject),函数metaObject()可以返回它的指针。获取一个对象的元对象有两种方式,示意代码如下:

        QPushButton *btn=new QPushButton();
        const QMetaObject *metaPtr=btn->metaObject();        //获取元对象指针
        const QMetaObject metaObj=btn->staticMetaObject;        //获取元对象

2、类型信息:QObject的inherits()函数可以判断继承关系。

3、动态翻译:函数tr()返回一个字符串的翻译版本。

4、对象树:表示对象间从属关系的树状结构。QObject提供了parent()、children()、findChildren()等函数。对象树中的某个对象被删除时,它的子对象也将被删除。

5、信号与槽:对象间的通信机制。

6、属性系统:可以使用宏Q_PROPERTY定义属性,QObject的setProperty()会设置属性的值或定义动态属性;property()函数会返回属性的值。

Q_OBJECT()宏
定义在每一个类的私有数据段,用来启用元对象功能,比如动态属性、信号和槽。

在一个QObject类或者其派生类中,如果没有声明Q_OBJECT宏,那么类的metaobject对象不会被生成,类实例调用metaObject()返回的就是其父类的metaobject对象,导致的后果是从类的实例获得的元数据其实是父类的数据。因此类所定义和声明的信号和槽都不能使用,所以,任何从QObject继承出来的类,无论是否定义声明了信号、槽和属性,都应该声明Q_OBJECT 宏。(如果 A 继承了 QObject 并且定义了 Q_OBJECT,B 继承了 A 但没有定义 Q_OBJECT,C 继承了 B,则 C 的 QMetaObject::className() 函数将返回 A,而不是本身的名字。)
 

2 QMetaObject类


元对象是QMetaObject类型的实例。元对象存储了类的实例所属类的各种元数据,包括类信息元数据、方法元数据、属性元数据等。所以,元对象实质上是对类的描述;

QMetaObject类实际上是通过一些接口函数来获取所属类的各种元数据,包括类信息元数据、方法元数据、属性元数据等。

    QObject* object = new QPushButton;
    
    //QObject::metaObject()返回类关联的元对象。
    qDebug() << object->metaObject()->className();   //返回"QPushButton"
    
    QPushButton* pushbtn = qobject_cast<QPushButton*>(object);
    qDebug() << pushbtn->metaObject()->className();   //返回"QPushButton"
    
    //inherits()函数返回继承关系
    qDebug() << pushbtn->inherits("QObject");   //返回 true。pushbtn是继承自QObject
    qDebug() << pushbtn->inherits("QTimer");   //返回 false
    
    //superClass()函数返回父类的元对象
    qDebug() << object->metaObject()->superClass()->className();   //返回"QAbstractButton"

    object->setProperty("name", "tingtaishou");
    QString name = object->property("name").toString();

    const QMetaObject* meta = object->metaObject();
    int index = meta->indexOfProperty("name");
    QMetaProperty prop = meta->property(index);
    bool res = prop.isWritable();
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

    Q_CLASSINFO("author","tingtaishou")
    Q_CLASSINFO("version","1.0.0")

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QMetaProperty>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->metaObject()->classInfo(0).name();
    this->metaObject()->classInfo(0).value();
    this->metaObject()->classInfo(1).name();
    this->metaObject()->classInfo(1).value();
}

Widget::~Widget()
{
    delete ui;
}


 

3 运行时类型信息(RTTI) 


(1)函数QMetaObject::className():该函数运行时返回类名称的字符串 

(2)函数QObjetc::inhetits()。可以判断一个对象是不是继承自某个类的实例。顶层的父类是QObject ;

(3)函数QMetaObject::superClass()。用来返回该元对象所描述类的父类的元对象,通过父类的元对象可以获取父类的一些元数据;

(4)函数qobject_cast(): 对于Object及其子类对象,可以使用函数qobject_cast()进行动态类型转换,此处可以理解为c++中的强制类型转换;如下:
 

QObject *btn=new QPushButton();      //创建QPushButton对象,但是使用QObject指针
const QMetaObject *meta=btn->metaObject();
QString str1=QString(meta->clasName());    //str1="QPushButton"
 
QPushButton *btnPush=qobject_cast<QPushButton*>(btn);    //转换成功,将Object类型转换成QPushButton类型
const QMetaObject *meta2=btnPush->metaObject();
QString str2=QString(meta2->clasName());    //str1="QPushButton"
#此时的转换是成功的,因为btn就是QPushButton对象指针,但是,如果将btn转换成QCheckBox对象指针就会失败,因为QChechBox不是QPushButton的父类;

4 属性系统

在QObject的子类中,使用宏Q_PROPERTY定义属性;格式如下:

Q_PROPERTY(type name
           (READ getFunction [WRITE setFunction] |
            MEMBER memberName [(READ getFunction | WRITE setFunction)])
           [RESET resetFunction]
           [NOTIFY notifySignal]
           [REVISION int]
           [DESIGNABLE bool]
           [SCRIPTABLE bool]
           [STORED bool]
           [USER bool]
           [CONSTANT]
           [FINAL])

MEMBER:指定一个成员变量与属性关联,使之成为可读可写的属性,指定后无需再设置READ和WRITE。

RESET:是可选的,用于将属性设置为上下文指定的默认值

Q_PROPERTY这个宏,简单用法如下:

Q_PROPERTY(type name READ getFunction WRITE setFunction)

Q_PROPERTY(参数类型 参数名称 READ 获得属性值函数 WRITE 设置属性值函数)

比如Q_PROPERTY(bool bIsDoubi READ getDoubi WRITE setDoubi),属性类型是bool类型,bIsDoubi是属性名称。除此之外还需要写两个函数,第一个是设置属性的函数void setDoui(bool),第二个是获得属性的函数bool getDoubi()。

QMetaObject类的一些函数可以提供元对象所描述类的属性元数据,属性元数据用QMetaProperty类描述,它有各种函数可反应属性的一些特性,例如下面的一段代码:
 

const QMetaObject *meta=ui->spinBoy->metaObject();    //获取一个SpinBox的元对象
int index=meta->indexOfProperty("value");             //获取属性value的序号
OMetaProperty prop=meta->property(index);             //获取属性value的元数据
bool res=prop.isWritable();                 //属性是否可写,值为true
res=prop.isDesignable();                    //属性是否可设计,值为true
res=prop.hasNotifySignal();                 //属性是否有反映属性值变化的信号,值为true

5 信号与槽

qt使用信号与槽机制实现对象之间的通信,它隐藏了复杂的底层实现;是Qt的核心特性;

connect函数不同的参数形式

connect(sender,SIGNAL(signal()),receiver,SLOT(slot()));

如下:

connect(ui->radioButton_red, SIGNAL(clicked()),this, SLOT(do_fontcolor()));
connect(ui->radioButton_blue, &QRadioButton::clicked,this, &do_fontcolor);

当槽函数是overload型时,需要使用模板函数qOverload()来明确参数类型;

//如果在窗口类中包含下列两个槽函数
void do_click(bool checked);
void do_click();
 
connect(ui->checkBox,&QcheckBox::clicked,this,qOverload<bool>(&Widget::do_click));
connect(ui->checkBox,&QcheckBox::clicked,this,qOverload<>(&Widget::do_click));

注意:遇到重载的槽函数一定要使用qOverload()模板函数来明确参数类型; 

对于overload型信号,只要槽函数不是overload型,就可以使用传递函数指针的connect()来进行信号与槽的关联;Qt会根据槽函数的参数自动确定使用哪个信号;

QObject::connect(pushButton_ok, &QPushButton::clicked, Dialog, qOverload<>(&QDialog::accept));
QObject::connect(pushButton_close, &QPushButton::clicked, Dialog, qOverload<>(&QDialog::close));

 6对象树

Qt中使用对象树(object tree)来组织和管理所有的QObject类及其子类的对象。当创建一个QObject时,如果使用了其他的对象作为其父对象(parent),那么这个 QObject就会被添加到父对象的children()列表中,这样当父对象被销毁时,这个QObject也会被销毁。实践表明,这个机制非常适合于管理GUI对象。例如,一个 QShortcut(键盘快捷键)对象是相应窗口的一个子对象,所以当用户关闭了这个窗口 时,这个快捷键也可以被销毁。

QWidget作为能够在屏幕上显示的所有部件的基类,扩展了对象间的父子关系。 一个子对象一般也就是一个子部件,因为它们要显示在父部件的区域之中。例如,当关闭一个消息对话框(message box)后要销毁它时,消息对话框中的按钮和标签也会被销毁,这也正是我们所希望的,因为按钮和标签是消息对话框的子部件。当然,也可以自己来销毁一个子对象。关于这一部分内容,可以在帮助索引中査看Object Trees & Ownership关键字。

元对象应用场景:

1、当一个窗口中有很多同类型的对象时,我们不需要将每个对象都写一个信号与槽,只需将所有对象都连接到同一个槽函数中,并在改槽函数中获取发送信号的对象(sender()函数),进而获取每个对象在创建时设置的不同的属性(ui->label_boy->setProperty("sex","boy"))从而区分不同的对象


对象树的示例程序

新建Qt Gui应用,项目名称为myOwnership,基类选择QWidget,然后类名保持Widget不变。完成后向项目中添加新文件,模板选择C+ +类,类名为MyButton,基类为QPushButton,类型信息选择“继 承自QWidget”。添加完文件后将mybuuon. h文件修改如下:

#ifndef MYBUTTON_H
#define MYBUTTON_H

#include <QPushButton>
#include <QDebug>

class MyButton : public QPushButton
{
    Q_OBJECT
public:
    explicit MyButton(QWidget *parent = nullptr);
    ~MyButton();
};

#endif // MYBUTTON_H

这里主要是添加了析构函数的声明。然后到mybutton. cpp文件中,修改如下:

#include "mybutton.h"

MyButton::MyButton(QWidget *parent) :
    QPushButton(parent)
{
}

MyButton::~MyButton()
{
    qDebug() << "delete button";
}

这里添加了析构函数的定义,这样当 MyButton 的对象被销毁时,就会输出相应的信息。

下面到widget.cpp文件中,修改如下:

#include "widget.h"
#include "ui_widget.h"
#include "mybutton.h"
#include <QDebug>
#include <QHBoxLayout>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //创建按钮部件,指定widget为父部件
    MyButton *button = new MyButton(this);
    button->setText(tr("button"));
}

Widget::~Widget()
{
    delete ui;
    qDebug() << "delete widget";
}

当Widget窗口被销毁时,将输出信息。下面运行程序,然后关闭窗口,在Qt Creator的应用程序输出栏中的输出信息为:

delete widget
delete button

可以看到,当关闭窗口后,因为该窗口是顶层窗口,所以应用程序要销毁该窗口部件(如果不是顶层窗口,那么关闭时只是隐藏,不会被销毁),而当窗口部件销毁时会自动销毁其子部件。这也就是为什么在Qt中经常只看到new操作而看不到delete操作 的原因。

再来看一下main.cpp文件,其中Widget对象是建立在栈上的:

Widget w;
w.show();

这样对于对象w,在关闭程序时会自动销毁。而对于Widget中的部件,如果是在堆上创建(使用new操作符),那么只要指定Widget为其父窗口就可以了,也不需要进行delete操作。当对象w销毁时会自动销毁它的所有子部件,这些都是Qt的对象树所完成的。

所以,对于规范的Qt程序,我们要在main()函数中将主窗口部件创建在栈上,例如“Widget w;”,而不要在堆上进行创建(使用new操作符)。对于其他窗口部件,可以使用new操作符在堆上进行创建,不过一定要指定其父部件,这样就不用使用de­lete操作符来销毁该对象了。

QT的反射机制

相对于Java天生的这一特性,C++并不具备;但进入到Qt的领域,这一切都边得简单自如了。来理解反射机制的作用。

反射机制:是指在运行时,能获取任意一个类对象的所有类型信息、属性、成员函数等信息的一种机制。

作用:编写足够通用的代码来处理所有具有自我描述能力的类。

使用条件:

原理:

从Qt的元对象系统可知,除了提供信号/槽机制的特性之外,它还提供了以下特性:

■ QObject::metaObject()

返回关联的元对象

■ QObject::className()

在运行时状态下返回类名

■ QObject::inherits()

判断类的继承关系

■ QObject::tr()

QObject::trUtf8()

提供国际化,翻译字符串

■ QObject::setProperty()

QObject::property()

通过名称来动态设置和获取属性

■ QObject::newInstance()

创建实例

其中QObject::className()、QObject::setProperty()和QObject::property()比较吸引眼球,这正是反射机制中的基本功能。建立在Qt的元对象系统的基础上,只要拥有元对象(QMetaObject)的类就支持反射!如何拥有元对象?方法很简单:只需继承于QObject或者它的子类(多重继承的话,QObject类应该放在第一个),在头文件的结构声明中写上Q_OBJECT即可!

元对象信息

通过QObject::metaObject()方法, 所有继承于QObject的类可以 返回元对象系统为其生成的metaObject对象。看看QMetaObject提供的一些重要信息:

 QMetaClassInfo

通过宏Q_CLASSINFO的支持,提供类的附加信息

■ QMetaEnum

Qt特色的枚举对象,支持key和 value之间的互转

■ QMetaMethod

提供类成员函数的元数据

■ QMetaProperty

提供类成员属性的元数据

根据QMetaObject提供的数据对象,完全可以编写通用的代码来支持反射特性。

例子:

声明一个类,继承于QObject

classReflectionObject : publicQObject

{

      Q_OBJECT

      Q_PROPERTY(intId READ Id WRITE setId)

      Q_PROPERTY(QString Name READ Name WRITEsetName)

      Q_PROPERTY(QString Address READ Address WRITEsetAddress)

      Q_PROPERTY(PriorityType Level READ Priority WRITEsetPriority)

 
      Q_ENUMS(PriorityType)


public:

      enumPriorityType { High, Low, VeryHigh,VeryLow };

 
      Q_INVOKABLEint Id() {returnm_Id; }

      Q_INVOKABLEQString Name() { returnm_Name; }

      Q_INVOKABLEQString Address() { returnm_Address; }

      Q_INVOKABLEPriorityType Priority() const {returnm_Priority; }

 

      Q_INVOKABLEvoid setId(constint& id) {m_Id = id; }

      Q_INVOKABLEvoid setName(constQString& name) {m_Name = name; }

      Q_INVOKABLEvoid setAddress(constQString& address) {m_Address = address; }

      Q_INVOKABLEvoid setPriority(PriorityType priority) {m_Priority = priority; }

 

private:

      int         m_Id;

      QString  m_Name;

      QString  m_Address;

 

      PriorityTypem_Priority;

};

为了能检测到类成员函数,得在函数前加上一个宏Q_INVOKABLE, 这意味着该函数在元对象系统编译该类时注册该函数,则在运行过程中能被元对象调用。

经过上述声明,在运行时即可做些操作:

/*遍历该类的成员: */

ReflectionObjecttheObject;

constQMetaObject*theMetaObject =theObject.metaObject();

 

intmetathodIndex;

intmetathodCount = theMetaObject->methodCount();

for(metathodIndex = 0; metathodIndex < metathodCount; ++metathodIndex)

{

      QMetaMethodoneMethod =theMetaObject->method(metathodIndex);

      qDebug() <<"typeName: " <<oneMethod.typeName();

      qDebug() <<"signature: " <<oneMethod.signature();

      qDebug() <<"methodType: " <<oneMethod.methodType();;

      qDebug() <<"parameterNames: " <<oneMethod.parameterNames() <<"\n";

}

/*遍历该类的属性: */

intpropertyIndex;

intpropertyCount = theMetaObject->propertyCount();

for(propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex)

{

      QMetaPropertyoneProperty =theMetaObject->property(propertyIndex);

      qDebug() <<"name: " << oneProperty.name();

      qDebug() <<"type: " <<oneProperty.type() <<"\n";

}

/*遍历该类的枚举集合*/

intenumeratorIndex;

intenumeratorCount = theMetaObject->enumeratorCount();

for(enumeratorIndex = 0; enumeratorIndex < enumeratorCount; ++enumeratorIndex)

{

      QMetaEnumoneEnumerator =theMetaObject->enumerator(enumeratorIndex);

 

      intenumIndex;

      intenumCount = oneEnumerator.keyCount();

      for(enumIndex = 0;enumIndex < enumCount; ++enumIndex)

      {

             qDebug() <<oneEnumerator.key(enumIndex) <<" ==> " <<oneEnumerator.value(enumIndex);

      }

}

在这里我们看到QMetaEnum存在key、value配对出现,这必然可以互转,而QMetaEnum确实提供了方式:valueToKey()、keyToValue()。

通过这个测试能将ReflectionObject这个类的方法和属性完全遍历出来,完成自身的检查,也即反射。

反射在Qt应用程序中的用途

在Qt的元对象系统支持下,赋予了C++并不直接拥有的此特性。这样加大了开发应用程序的自由度,尤其在软件工程中强调高内聚低耦合的状态下。

具体的用例中, 可以通过Qt Designer这个工具来描述(尽管暂不清楚内部是否利用此方法来实现J):

对于其中的控件,放入到Designer中时,Designer事先并不知道(其实也不需要知道)放入插件的类型、方法和属性。将插件导入时,它可以检测该控件的类型、方法和属性;

这样在Designer中绘制控件时,遍历该控件的属性, 将这些属性显示在属性窗口中,便于修改;

在信号/槽对话框编辑时,遍历该控件的属性,提取信号/槽函数,放入对应的编辑区域里;

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高亚奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值