Qt 元对象系统最主要的功能是实现信号和槽机制,当然也有其他功能,就是支持属性系统。有些高级语言通过编译器的 __property 或者 [property] 等关键字实现属性系统,用于提供对成员变量的访问权限,Qt 则通过自己的元对象系统支持属性访问,Qt 是基于标准 C++ 的,不需要底层编译器支持属性,Qt 本身提供了通用的跨平台的属性系统。关于属性系统可以在 Qt 助手索引里面输入“The Property System”,找到相应的主题文档。Qt 类库大量使用属性,通常开发基于 Qt 的类库时,也会用到属性系统。下面我们介绍简化版的属性系统,只列举了属性系统里面几个基本的条目。
属性系统简介
为了保持类的封装特性,通常成员变量需要保持私有状态,而为了与其他对象协作,就需要提供相应的 get/set 函数。如果成员变量的数值发生了变化,通常也需要提供通知(NOTIFY)信息告知相关对象,Qt 里的通知一般都是使用信号触发。set 函数可以作为槽函数,方便接收相关对象的信号以实现自动调整,比如上一节标签控件的 setText 槽函数。set 函数会导致成员变量数值变化,为了通知相关对象,set 函数里通常会 emit 该成员变量发生变化的信号。
属性系统也是通过元对象系统实现的,它也是需要直接或间接从 QObject 类继承,并在类的声明里需要 Q_OBJECT 宏。下面介绍简化版的属性声明,第一类是不指明与属性相关的私有成员变量,这时必须至少提供该属性的读函数:
Q_PROPERTY(
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 通知信号是必须的,一般建议把成员变量、读函数、写函数、通知信号都明确标出来,这样方便程序员阅读和使用。
普通属性示例
使用 Q_PROPERTY()宏声明的其实都是声明好的普通属性,在程序编译时就已经规定好了。下面示范一个简单的例子。
新建一个Qt Widget Application项目,基类选择widget,创建后在widget.h中添加属性的声明代码,读写函数声明,信号声明和成员变量。
#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_PROPERTY(QString nickName READ nickName WRITE setNickName NOTIFY nickNameChanged)
//直接标出成员变量的形式
Q_PROPERTY(int count MEMBER m_count READ count WRITE setCount NOTIFY countChanged)
//标出成员变量,可以省略读写函数
Q_PROPERTY(double value MEMBER m_value NOTIFY valueChanged)
//nickName读函数声明
const QString& nickName();
//count读函数
int count();
//value属性声明时,指出了成员变量并省略了读写函数,它的读写代码由moc工具自动生成
//value数值被修改时,valueChanged信号会自动触发
//自动生成的读写代码只有简单的读写功能,不能做数值有效性检查
signals:
//三个属性数值变化时发信号
void nickNameChanged(const QString& strNewName);
void countChanged(int nNewCount);
void valueChanged(double dblNewValue);
public slots:
//写函数通常可以作为槽函数,方便与其他信号关联,自动调整数值
//nickName写函数声明
void setNickName(const QString& strNewName);
//count写函数声明
void setCount(int nNewCount);
//value写代码由 moc 自动生成,没有写函数
private:
Ui::Widget *ui;
//三个私有变量,对应是三个属性
QString m_nickName;
int m_count;
double m_value;
};
#endif // WIDGET_H
上面代码声明了三个属性,首先是 nickName:
- 没有直接指出其成员变量,但其实是有的;
- 读函数为 nickName(),返回值为属性里声明的 QString 类型(引用也算的);
- 写函数为 setNickName(),写函数接收 QString 类型参数,并返回 void;
- 当属性值变化时,发送 nickNameChanged 信号,信号参数里会带有新的数值。
属性声明里的读写函数都需要程序员自己编写,信号自己声明,并在写函数里触发。写函数 setNickName() 的声明放到槽函数声明部分了,这样方便关联其他信号,实现自动调整属性值。虽然属性声明里没说有成员变量,但是需要自己定义一个私有变量 m_nickName 保存属性的数值。
第二个属性是 count:
- 直接指出它对应的成员变量是 m_count;
- 读函数是 count(),返回 int 类型;
- 写函数是 setCount(),接收一个 int 参数,并返回 void;
- 当属性值变化时,发送 countChanged 信号,信号参数里会带有新的数值;
count 属性的工作原理 nickName 属性差不多,仅仅是在声明里明确说了对应的成员变量为 m_count。
第三个属性是 value ,这是属性声明的极简形式,读写函数都省略了,属性读写的代码由 moc 工具自动生成。因此这种极简形式的属性,它只需要在头文件里放三行声明代码,其他所有代码都不需要:
Q_PROPERTY(double value MEMBER m_value NOTIFY valueChanged)
signals:
void valueChanged(double dblNewValue);
private:
double m_value;
除了属性声明本身的一句,就只需要声明属性值变化是的信号和私有成员变量。
moc 工具自动生成的读写代码仅仅是代码片段(4.5 节专门讲这些代码),没有函数的,自然没有相关的槽函数,所以如果希望把写函数作为槽函数来用,就得用类 似 count 属性的声明,并自己编读写函数的实体代码。
头文件代码就如上编辑,下面添加 widget.cpp 中必须的函数实体代码:
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
//读函数定义
//读nickName数值
const QString& Widget::nickName()
{
return m_nickName;
}
//读count数值
int Widget::count()
{
return m_count;
}
//写函数,在数值发生变化是才发信号
void Widget::setNickName(const QString &strNewName)
{
if(strNewName == m_nickName)
{
//数值没变化,直接返回
return;
}
//修改数值并触发信号
m_nickName = strNewName;
emit nickNameChanged(strNewName);
}
void Widget::setCount(int nNewCount)
{
if(nNewCount == m_count)
{
//数值没变化,直接返回
return;
}
//修改数值并触发信号
m_count = nNewCount;
emit countChanged(nNewCount);
}
在 widget.cpp 只需要为 nickName 和 count 两个属性添加读写函数的代码。读函数 nickName() 和 count() 非常简 单,就是返回相应的私有成员变量数值。
对于写函数 setNickName()里面代码,首先判断传入的参数值是否与旧的值相同,如果是一样的就不用修改,直接返回。如果新旧数值不同,那就设置成员变量为新的值,并发送数 值发生变化的信号 nickNameChanged,并把新值作为参数放到信号里面。set 函数里其实可以顺便做一下数值的有效性检查,上面代码里省略了有效性 检查。
对于写函数 setCount 是类似的过程,新旧数值一样就不变,不一样就修改成员变量并发信号。
窗口类 Widget 的代码就是上面那么多。如何使用这些属性呢?我们可以编辑 main.cpp ,尝试修改和读取这些属性:
#include "widget.h"
#include <QApplication>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
//属性读写
//通过写函数、读函数
w.setNickName( "Wid" );
qDebug()<<w.nickName();
w.setCount(100);
qDebug()<<w.count();
//通过 setProperty() 函数 和 property() 函数
w.setProperty("value", 2.3456);
qDebug()<<fixed<<w.property("value").toDouble();
//显示窗体
w.show();
return a.exec();
}
属性的读写既可以使用各个属性自己的读写函数,如 setNickName()、nickName()、setCount()、count(),也可以使用属性通用的函数:setProperty() 写属性,property() 读属性,都是通过属性的名称来寻找特定属性实现读写。
注意由于上面的 value 属性没有声明读写函数,所以对它的读写只能靠 setProperty() 和 property() 函数,这对通用的属性读写函数的声明如下:
- bool QObject::setProperty(const char * name, const QVariant & value)
- QVariant QObject::property(const char * name) const
setProperty() 第一个参数是普通字符串,就是属性的名称,第二个参数是属性的数值,QVariant 是 Qt 定义的通用变量类型,标准 C++ 的类型和 Qt 自己的数值类型都可以自动转为 QVariant 类的对象。
property() 函数以变量名的字符串作为输入参数,返回一个通用的 QVariant 对象,将 QVariant 对象转为标准类型或者 Qt 数值类型,可以用对应的 to**** 函数,toDouble 是转为双精度浮点数,toInt 是转为整型数, toString 是转为 QString 字符串,还有其他转换函数,详细的请查阅 QVariant 帮助文档。
最后可以运行一下查看结果。