元对象系统的概念
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(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操作符在堆上进行创建,不过一定要指定其父部件,这样就不用使用delete操作符来销毁该对象了。
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中绘制控件时,遍历该控件的属性, 将这些属性显示在属性窗口中,便于修改;
在信号/槽对话框编辑时,遍历该控件的属性,提取信号/槽函数,放入对应的编辑区域里;