求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
要资料
 
追随技术信仰

随时听讲座
每天看新闻
 
 
Qt 编程指南
第1章 Qt 开发环境
1.1 Qt 介绍
1.2 Qt 下载
1.3 Qt 在 Windows 下安装
1.4 Qt 在 Linux 下安装
1.5 认识开发工具
1.6 常见的名词术语
第2章 从Hello World开始
2.1 Hello World
2.2 Hello Qt
2.3 Hello Designer
2.4 Hello Creator
2.5 Qt程序调试
2.6 Qt帮助文档
第3章 字符串和字符编码
3.1 字符编码方式
3.2 Qt 程序字符编码
3.3 使用 QString
3.4 使用 QByteArray
第4章 信号和槽函数
4.1 元对象系统
4.2 使用原有的信号和槽
4.3 自定义信号和槽
4.4 系统属性
4.5 扩展阅读:ui_*.h代码
4.6 扩展阅读:moc_*.cpp代码
第5章 简单控件的使用
5.1 按钮类的控件
5.2 单行编辑控件
5.3 丰富文本编辑控件
 

 
目录
属性系统
11 次浏览
1次  

Qt 元对象系统最主要的功能是实现信号和槽机制,当然也有其他功能,就是支持属性系统。有些高级语言通过编译器的 __property 或者 [property] 等关键字实现属性系统,用于提供对成员变量的访问权限,Qt 则通过自己的元对象系统支持属性访问,Qt 是基于标准 C++ 的,不需要底层编译器支持属性,Qt 本身提供了通用的跨平台的属性系统。关于属性系统可以在 Qt 助手索引里面输入“The Property System”,找到相应的主题文档。Qt 类库大量使用属性,通常开发基于 Qt 的类库时,也会用到属性系统。下面我们介绍简化版的属性系统,只列举了属性系统里面几个基本的条目。

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 通知信号是必须的,一般建议把成员变量、读函数、写函数、通知信号都明确标出来,这样方便程序员阅读和使用。

2. 普通属性示例

使用 Q_PROPERTY()宏声明的其实都是声明好的普通属性,在程序编译时就已经规定好了,本小节示范普通属性的例子,下一小节示范在运行时动态添加属性。本 小节示范为窗口类添加自定义的属性,然后从 main 函数里面设置和打印属性的值。

打开 QtCreator ,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:

①项目名称 normalpros,创建路径 D:\QtProjects\ch04,点击下一步;

②套件选择里面选择全部套件,点击下一步;

③基类选择 QWidget,点击下一步;

④项目管理不修改,点击完成。

建好项目之后,打开头文件 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 帮助文档。

​代码先添加这些,我们运行这个示例,它执行的结果会打印到输出面板,如下图所示:

run1

这个例子展示属性的声明和访问,但是还有一个问题,我们在 set*** 函数里修改了属性值,应该发了值改变的信号,我们没有接收这些信号,还不知道它们真发信号还是假发信号,得验验货。

我们为这个例子添加一个 C++ 类,名字为 ShowChanges,基类为 QObject:

run1

编辑 showchanges.h,为 valueChanged 信号添加对应的槽函数 RecvValue 声明:

#ifndef SHOWCHANGES_H
#define SHOWCHANGES_H

#include <QObject>
class ShowChanges : public QObject
{
Q_OBJECT
public:
explicit ShowChanges(QObject *parent = 0);
~ShowChanges();
signals:
public slots:
//槽函数,接收 value 变化信号
void RecvValue(double v);
};
#endif // SHOWCHANGES_H

编辑 showchanges.cpp,为槽函数添加实体代码:

#include "showchanges.h"
#include <QDebug>

ShowChanges::ShowChanges(QObject *parent) : QObject(parent)
{
}
ShowChanges::~ShowChanges()
{
}
//接收并打印 value 变化后的新值
void ShowChanges::RecvValue(double v)
{
qDebug()<<"RecvValue: "<<fixed<<v;
}

这个仅仅是类的代码,接收信号必须要定义一个对象,然后才能关联源头信号和接收端槽函数,下面修改 main.cpp 代码,添加接收端对象和关联函数代码:

#include "widget.h"
#include <QApplication>
#include <QDebug>
#include "showchanges.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w; //源头对象
//接收端对象
ShowChanges s;
//关联
QObject::connect(&w, SIGNAL(valueChanged(double)), &s, SLOT(RecvValue(double)));
//属性读写
//通过写函数、读函数
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();
}

value 属性值被修改成了 2.3456,这个值一被修改,就会触发 valueChanged 信号,槽函数 RecvValue 被自动调用,并且会接收到新数值并打印出来。运行这个修改后的示例,得到的结果如下:

run1

可以看到,槽函数打印的信息会在 qDebug()<<fixed<<w.property("value").toDouble(); 这行调用之前打印出来。无论使用通用的

setProperty() 函数,还是属性自己独有的 set 函数,变化信号都会正确地发出来。

上面只接收了 valueChanged 信号,还有 nickNameChanged 信号和 countChanged 信号没接收,这个两个信号对应的槽函数和关联函数代码作为练习,请读者自己动手完成。下面小节示范动态属性的使用。

3. 动态属性示例

不仅在源代码里面添加属性声明,程序运行时也可以通过 setProperty() 函数为当前类对象添加动态属性,获取动态属性的值也是用 property() 函数。获取全部的动态属性名称列表可以通过 ​dynamicPropertyNames 函数:

QList QObject::​dynamicPropertyNames() const

​dynamicPropertyNames 返回当前所有动态属性名称字符串的列表。

如果尝试用 property() 函数获取不存在的属性名的数值,那么会得到一个特殊的 QVariant 对象,这个对象处于不可用状态,这时 QVariant 对象的 isValid() 函数返回 false。下面示范动态属性的例子。

重新打开 QtCreator ,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:

①项目名称 dynamicpros,创建路径 D:\QtProjects\ch04,点击下一步;

②套件选择里面选择全部套件,点击下一步;

③基类选择 QWidget,点击下一步;

④项目管理不修改,点击完成。

建好项目之后,Widget 类的代码不需要修改,动态属性不需要额外声明,直接在 main 函数里面使用就行了,我们编辑 main.cpp:

#include "widget.h"
#include <QApplication>
#include <QDebug>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
//如果动态属性不存在,QVariant 对象的 isValid() 函数返回 false,变量不可用
qDebug()<<w.property("myValue").isValid();
//添加属性 myValue
w.setProperty("myValue", 2.3456);
//获取并打印
qDebug()<<w.property("myValue").toDouble();
//添加属性 myName
w.setProperty("myName", "Wid");
//获取并打印
qDebug()<<w.property("myName").toString();
//获取所有动态属性名称,打印名称列表
qDebug()<<w.dynamicPropertyNames();
w.show();
return a.exec();
}

在 main 函数里,我们先尝试获取一个不存在的属性 myValue,property() 函数返回 QVariant 对象,由于属性不存在,所以 QVariant 对象 isValid() 函数会返回 false,表明不可用状态,也就意味着该属性是不存在的。

然后我们向窗体对象 w 添加了动态属性 myValue,并用 property() 函数获取了该属性的 QVariant 对象,然后转成双精度浮点数打印。接着我们向窗体对象 w 添加了动态属性 myName,类似地用 property() 函数获取属性的 QVariant 对象,然后转为字符串打印。

最后我们用窗体对象 w 的 dynamicPropertyNames() 函数获取了所有的动态属性名称列表,并打印了动态属性名称列表。

例子修改的代码只有上面那些,例子运行的效果如下图所示:

run1

通过在头文件声明普通的属性,可以为属性标明相关的成员变量、读函数、写函数、变化通知信号等等,而动态属性就没有这些特性,它其实就是以成对的属性名、属性值的 形式存在,可以通过 setProperty() 和 property() 读写,仅此而已,所以动态属性应用得比较少。

属性系统不仅可以声明普通属性和在运行时添加动态属性,还有一个衍生功能,就是为类添加一些附加信息,运行时可以查询这些附加信息,方便程序员或用户查询相关信 息,下面学习为类添加附加信息的例子。

4. 类的附加信息

Qt 程序运行时可以查询当前对象的名称,我们之前 4.2.3 节用 QObject::objectName() 函数获取过对象名称。运行时还可以查询类的信息,主要包括两种类信息,一种是基本的类信息,Qt 类会自动生成这些信息,包括当前类名、继承树上的基类名称等;另一类是程序员手动添加的类附加信息,通过 Q_CLASSINFO() 宏,在类的声明里添加类附加信息,比如:

Q_CLASSINFO("Version", "1.0.0")

附加信息也是成对的“名称-值”,不过注意 名称和值 都是普通字符串。如果希望在运行时查询类的附加信息,可以先用 QObject::metaObject() 获取当前对象包含的元对象数据,然后使用 QMetaObject::​classInfo 函数查询某个序号的附加信息:

const QMetaObject * QObject::​metaObject() const [virtual]
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 头文件,添加附加信息:

#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

头文件里添加了三行,也就是三对“名称-值”,都是普通字符串。附加信息只需要添加到声明里,不需要其他代码,所以 widget.cpp 文件保持原样,不修改。

下面我们在 main.cpp 里面访问窗体的附加信息和基本信息:

#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();
}

文件开头添加了 <QDebug> 和 <QMetaClassInfo> 两个头文件,QMetaClassInfo 就是存储附加信息项的类,它有两个公开函数:name() 获取附加信息项的名称,value() 获取附加信息项的值。

在 main 函数里,定义好窗体对象 w,之后,先通过 metaObject() 函数得到一个 QMetaObject 常量指针 pMO;

通过 pMO->classInfoCount() 获知附加信息项的个数;

然后在 for 循环里枚举 pMO->classInfo(i) 信息项,打印信息项的名称和值。

接下来是一些基本信息的获取,类的名字也是通过元对象指针获取 pMO->className();

窗体对象名可以直接用 w.objectName() 获取;

判断窗体是否从某个基类派生,使用 inherits 函数,这个函数接收一个字符串,如果是对象的直接或间接基类名称就返回 true,否则返回 false。

这个示例运行结果如下图所示(这里窗体的类名和对象名正好是一样的,其他情况可能不一样):

run1
本节大部分内容是上面这些,后面留两个练习,希望读者动手实践一下,并复习一下 C++ 类继承的多态性。下两节我们把本章最复杂的问题尝试讲解一下,就是由 Qt 工具自动生成的代码 ui_*.h 和 moc_*.cpp 等。

tip 练习

① 4.4.2 小节的示例留了两个信号 nickNameChanged 和 countChanged ,没有接收它们的槽函数,请仿造 RecvValue 槽函数的写法,为这两个信号编写对应的槽函数,并实现关联,测试一下效果。

② 修改 4.4.4 例子的代码,将原来的这句:

const QMetaObject *pMO = w.metaObject();

替换为新的两句:

QObject *pObj = &w;

const QMetaObject *pMO = pObj->metaObject();

然后运行修改后的例子,查看结果有没有变化。注意 pObj 是 QObject 类对象指针,由于 metaObject() 具有多态性,它实际调用的是派生类 Widget 对象的 metaObject() 函数。


您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码: 验证码,看不清楚?请点击刷新验证码 必填



11 次浏览
1次