4.4 属性系统
Qt 元对象系统最主要的功能是实现信号和槽机制,当然也有其他功能,就是支持属性系统。有些高级语言通过编译器的 __property 或者 [property] 等关键字实现属性系统,用于提供对成员变量的访问权限,Qt 则通过自己的元对象系统支持属性访问,Qt 是基于标准 C++ 的,不需要底层编译器支持属性,Qt 本身提供了通用的跨平台的属性系统。关于属性系统可以在 Qt 助手索引里面输入"The Property System",找到相应的主题文档。Qt 类库大量使用属性,通常开发基于 Qt 的类库时,也会用到属性系统。下面我们介绍简化版的属性系统,只列举了属性系统里面几个基本的条目。
4.4.1 属性系统简介
为了保持类的封装特性,通常成员变量需要保持私有状态,而为了与其他对象协作,就需要提供相应的 get/set 函数。如果成员变量的数值发生了变化,通常也需要提供通知(NOTIFY)信息告知相关对象,Qt 里的通知一般都是使用信号触发。set 函数可以作为槽函数,方便接收相关对象的信号以实现自动调整,比如上一节标签控件的 setText 槽函数。set 函数会导致成员变量数值变化,为了通知相关对象,set 函数里通常会 emit 该成员变量发生变化的信号。
属性系统也是通过元对象系统实现的,它也是需要直接或间接从 QObject 类继承,并在类的声明里需要 Q_OBJECT 宏。下面介绍简化版的属性声明,第一类是不指明与属性相关的私有成员变量,这时必须至少提供该属性的读函数:
Q_PROPERTY(type name
READ getFunction
WRITE setFunction
RESET resetFunction
NOTIFY notifySignal\] ) Q_PROPERTY()宏就是属性的声明: * **type 是指属性的类型,可以是 C++ 标准类型、类名、结构体、枚举等,name 就是属性的名字。** * **READ 标出该属性的读函数 getFunction,Qt 属性的读函数通常省略 get 三个字母。** * **WRITE 标出该属性的写函数 setFunction,中括号表示可选,写函数不是必须的。** * **RESET 标出该属性的重置函数 resetFunction,重置函数将属性设为某个默认值,中括号表示可选,重置函数不是必须的。** * **NOTIFY 标出该属性变化时发出的通知信号 notifySignal,中括号表示可选,这个信号不是必须的。** 这仅仅列举了属性声明里简单的几行,复杂需要查阅 Qt 帮助文档。对于属性,注意 name 仅仅是一个用于标识属性的名字,它不是实际存在的成员变量,属性系统不会自动生成成员变量,它就是虚无的名字代号(不同属性的名字不能相同)。对于属性用到的数值会存在一 个真正的私有成员变量里面, 私有成员变量、读函数、写函数、信号等需要另外编写这些声明,对于函数还需要编写实体代码。 上面是不明确指出私有成员变量的情形,也可以明确指出使用了哪个成员变量,这时候属性声明为: Q_PROPERTY(type name **MEMBER memberName** \[READ getFunction
WRITE setFunction
RESET resetFunction
NOTIFY notifySignal\] )
这里的 MEMBER 标出属性使用的成员变量 memberName,其他的行与上面的声明类似。
在明确标出属性使用的成员变量的情况下,属性的读写函数可以省略不写,Qt 的 moc 工具会自动为成员变量生成读写代码;而重置函数、信号等需要自己声明,并编写必须的代码;如果声明了属性值变化的通知信号,那么 moc 工具生成的写属性代码会自动触发该通知信号。
如果希望自己的编写的类库支持 QML,那么 NOTIFY 通知信号是必须的,一般建议把成员变量、读函数、写函数、通知信号都明确标出来,这样方便程序员阅读和使用。
4.4.2 普通属性示例
使用 Q_PROPERTY()宏声明的其实都是声明好的普通属性,在程序编译时就已经规定好了,本小节示范普通属性的例子,下一小节示范在运行时动态添加属性。本 小节示范为窗口类添加自定义的属性,然后从 main 函数里面设置和打印属性的值。
打开 QtCreator ,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 normalpros,创建路径 D:\\QtProjects\\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开头文件 widget.h,添加属性的声明代码、读写函数声明、信号声明和成员变量:
```cpp
#ifndef WIDGET_H
#define WIDGET_H
#include QMetaClassInfo QMetaObject::classInfo(int index) const 对于基本信息,可以用 QMetaObject::className() 函数获取类名。如果要判断继承树上是否有某个基类,使用 QObject::inherits() 函数判断。这两个函数声明如下: const char * QMetaObject::className() const bool QObject::inherits(const char * className) const 下面示范类的附加信息例子,并顺路打印类的基本信息,判断基类名。 重新打开 QtCreator ,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写: ①项目名称 classinfo,创建路径 D:\QtProjects\ch04,点击下一步; ②套件选择里面选择全部套件,点击下一步; ③基类选择 QWidget,点击下一步; ④项目管理不修改,点击完成。 建好项目之后,编辑 widget.h 头文件,添加附加信息: 头文件里添加了三行,也就是三对"名称-值",都是普通字符串。附加信息只需要添加到声明里,不需要其他代码,所以 widget.cpp 文件保持原样,不修改。 下面我们在 main.cpp 里面访问窗体的附加信息和基本信息: 文件开头添加了 <QDebug> 和 <QMetaClassInfo> 两个头文件,QMetaClassInfo 就是存储附加信息项的类,它有两个公开函数:name() 获取附加信息项的名称,value() 获取附加信息项的值。 在 main 函数里,定义好窗体对象 w,之后,先通过 metaObject() 函数得到一个 QMetaObject 常量指针 pMO; 通过 pMO->classInfoCount() 获知附加信息项的个数; 然后在 for 循环里枚举 pMO->classInfo(i) 信息项,打印信息项的名称和值。 接下来是一些基本信息的获取,类的名字也是通过元对象指针获取 pMO->className(); 窗体对象名可以直接用 w.objectName() 获取; 判断窗体是否从某个基类派生,使用 inherits 函数,这个函数接收一个字符串,如果是对象的直接或间接基类名称就返回 true,否则返回 false。 这个示例运行结果如下图所示(这里窗体的类名和对象名正好是一样的,其他情况可能不一样):
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
//类的附加信息
Q_CLASSINFO("Version", "1.0.0")
Q_CLASSINFO("Author", "Winland")
Q_CLASSINFO("Site", "https://lug.ustc.edu.cn/sites/qtguide/")
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include <QApplication>
#include <QDebug>
#include <QMetaClassInfo>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
//获取类的附加信息
const QMetaObject *pMO = w.metaObject();
//附加信息个数
int nInfoCount = pMO->classInfoCount();
//打印所有的附加信息
for(int i=0; i<nInfoCount; i++)
{
QMetaClassInfo info = pMO->classInfo(i);
qDebug()<<info.name()<<"\t"<<info.value();
}
//基本信息
qDebug()<<"Class Name: "<<pMO->className();
qDebug()<<"Object Name: "<<w.objectName();
//判断是否为基类
qDebug()<<w.inherits("QWidget");
qDebug()<<w.inherits("nothing");
//显示窗口
w.show();
return a.exec();
}