3.5 Qt对象模型
标准C++对象模型(C++ object model)非常高效地支持对象范式(object paradigm)。然而,在某些方面却表现不足,比如信号的传递、事件的传递和处理等。GUI编程既要求运行时的高效性,又需要有更好的灵活性。Qt GUI编程语言结合了Qt对象模型(Qt Object Model)的灵活性,以及标准C++运行时的高效性。
为了更好地满足GUI图形用户界面编程,Qt的对象模型在标准C++的基础上新增了一些特性:
● 元对象系统(Meta-Object System),提供
对象通信机制:信号和槽;
动态的对象转换(dynamic cast)。
● 可查询和可设计的对象属性(object properties);
● 层次结构、可查询的对象树(object trees);
● 安全的指针(QPointer),在对象销毁时,该指针自动设置为NULL值,这有别于C++的指针;
● 强大的事件和事件过滤器;
● 支持国际化的文本转换机制;
● 定时器机制,使得多个任务(tasks)可以集成在一个事件驱动的(event-driven)GUI中。
Qt对象模型中的大多数特性都是通过标准C++技术实施的。其他的特性,像对象通信机制、动态属性系统(dynamic property system)等需要Qt自身提供的元对象编译器(Meta-Object Compiler, moc)的支持。
本小节主要介绍元对象系统、对象属性系统和对象树,其他内容将在相关章节中陆续阐述。
3.5.1 元对象系统
Qt元对象系统提供了对象间的通信机制(信号和槽)、运行时类型信息和动态属性系统的支持,是标准C++的一个扩展,它使得Qt更好地实现GUI图形用户界面编程。Qt的元对象系统不支持C++模板,尽管模板扩展了标准C++的功能,但是元对象系统提供了模板无法提供的一些特性。
Qt的元对象系统基于三个事实:
● 基类QObject,任何想使用元对象系统功能的类必须继承自QObject;
● Q_OBJECT宏,Q_OBJECT宏必须出现在类的私有声明区,以启动元对象的特性;
● 元对象编译器(Meta-Object Compiler, moc),为QObject子类实现元对象特性提供必要的代码实现。
下面以CFindFileForm类的头文件findfileform.h为例,看一下Qt moc工具的工作过程:
在编译应用程序的时候,由make工具调用moc工具进行处理;
moc工具读取头文件findfileform.h,看是否包含Q_OBJECT宏,如果包含则进行下一步处理,否则moc放弃对findfileform.h的处理;
moc根据findfileform.h生成另一个C++源文件(默认的情况下名字命名为moc_findfileform. cpp),该源文件包含了元对象代码的实现;
接着,C++编译器处理moc_findfileform.cpp文件,生成中间文件moc_findfileform.o;
最后,连接器将moc_findfileform.o与其他应用程序的中间文件连接起来,生成可执行应用程序文件。
元对象系统除了支持信号/槽机制和对象的动态转换(这两个内容已经在前面的章节中讲到,不再赘述)外,还提供一些其他的特性,包括:
● QObject::metaObject(),返回一个类的元对象;
● QMetaObject::className(),在运行时以字符串的形式返回类名,但不需要C++编译器的RTTI的支持(RTTI会降低C++应用程序的运行效率);
● QObject::inherits(),判断一个对象是否是某个指定类的子类实例;
● QObject::tr()和QObject::trUtf8(),进行国际化的字符串翻译,等等。
下面,看一下使用元对象特性的一个例子。
// chapter03/metaobj/src/metaobj.cpp. #include <QDebug> #include <QtGui> #include <QtCore> int main(int argc, char *argv[]) { QApplication app(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForName("gb18030")); QObject* obj = new QLabel; const QMetaObject* mo = obj->metaObject(); qDebug() << QObject::tr("类名:%1").arg(mo->className()); qDebug() << QObject::tr("是否继承自QWidget:%1") .arg(obj->inherits("QWidget") ? QObject::tr("是"):QObject::tr("否") ); return 0; }
程序运行的结果为:
"类名:QLabel" "是否继承自QWidget:是"
3.5.2 属性系统
Qt的属性系统建立在Qt元对象系统的基础上,可以工作在Qt支持的任何平台,并且能够使用任何标准的C++编译器。
在类的定义文件中,通过使用Q_PROPERTY()宏声明一个属性,而且只有继承自QObject的子类才可以使用Qt的属性系统。一个属性类似于类的数据成员,不过同数据成员相比,属性具有下列一些特征:
● 必须有一个读(read)函数;
● 一个可选的写(write)函数(只读属性没有写函数);
● 一个可选的重置(reset)函数,将属性恢复到一个默认值,该函数必须返回void并且没有任何参数;
● 一个可选的“DESIGNABLE”特性,表明该属性是否在GUI构造器(例如,Qt设计器)中可用的,可写属性的默认值为true,只读属性的默认值为false;
● 一个可选的“SCRIPTABLE”特性,表明脚本引擎(scripting engine)是否可以访问该属性,可写属性的默认值为true,只读属性默认值为false;
● 一个可选的“STORED”特性,表明该属性是否持久的,即当存储一个对象的状态时,是否保存该属性的值,该特性仅仅对可写属性有意义,默认值为true。
读、写和重置函数可以像一般的成员函数一样,既可以是虚的,也可以从父类继承。不同的是,多继承情况下,读、写和重置函数必须继承自第一个父类(即类的定义中,继承类列表中最左边的类)。
下面,看一个使用对象属性的例子(KDevelop工程名:property)。首先看一下类的定义文件weapon.h。
// chapter03/property/src/weapon.h. #include <QObject> #ifndef _WEAPON_H_ #define _WEAPON_H_ typedef enum { nil, ready, fired, exceptional }Status; class CWeapon : public QObject { Q_OBJECT Q_PROPERTY(Status status READ getStatus WRITE setStatus) Q_ENUMS(Status) public: CWeapon(QObject *parent = 0); void setStatus(Status s); Status getStatus() const; private: Status status; }; #endif
头文件中定义了一个Status枚举类型,表示武器的状态:无弹、就绪、开火和异常。该枚举类型之所以定义在类CWeapon的外面,是为了方便在CWeapon作用域之外使用它(否则,getStatus()函数的返回类型必须是CWeapon::Status)。
宏Q_PROPERTY()向元对象系统注册一个属性,它的语法如下,
Q_PROPERTY(Type name READ getFunction [WRITE setFunction] [RESET resetFunction] [DESIGNABLE bool] [SCRIPTABLE bool] [STORED bool])
其中,类型Type必须是QVariant支持的类型或者是同属性所在类一起声明的枚举类型。而且,枚举类型必须通过Q_ENUM()宏在属性系统进行注册。
另一个类似于Q_ENUMS()的宏是Q_FLAGS()。与Q_ENUMS()不同的是,Q_FLAGS()除了向属性系统注册枚举类型外,它还会将这个枚举标识为一组“标记(flags)”,这些枚举值可以进行位操作。
宏Q_PROPERTY()只是向元对象系统注册一个属性,因此还需要对属性及其操作进行声明。此外,Q_PROPERTY()宏中的属性名字name可以和私有成员的名字不同,而操作名字必须相同。例如,
Q_PROPERTY(Status status READ getStatus WRITE setStatus)
而私有区的成员可以声明为:
private: Status m_Status; Status m_St;
不过,读写函数的实现必须做出相应的修改,对属性的读、写等操作必须针对m_Status(如果m_Status是属性status对应的成员的话)。而读、写函数名字必须是getStatus和setStatus。对类CWeapon的外部而言,只有名字status是可见的(比如在Qt设计器或者在脚本当中,可以直接对CWeapon的status属性进行修改。具体详见第19章“Qt插件”和第20章“脚本——QtScript”的相关内容)。
下面是类CWeapon的实现文件weapon.cpp的内容。
// chapter03/property/src/weapon.cpp. #include "weapon.h"
CWeapon::CWeapon(QObject * parent) : QObject(parent) { }
构造函数完成对象的初始化。
Status CWeapon::getStatus() const { return status; }
属性的读函数获取属性的值。
void CWeapon::setStatus(Status s) { status = s; }
属性的写函数设置属性的值。
除了使用Q_PROPERTY()宏定义一个属性外,还可以通过QObject::setProperty()函数在运行时添加一个动态属性。相应的,QObject::property()可以获取动态属性的值。
下面,看一下对象动态属性的例子。
// chapter03/property/src/property.cpp. #include <QtCore> #include "weapon.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); CWeapon weapon; weapon.setStatus(fired); qDebug() << "status " << weapon.getStatus(); weapon.setProperty("own", 1); qDebug() << "own" << weapon.property("own").toInt(); return 0; }
上面的程序将会给CWeapon对象weapon添加一个名字为“own”的动态属性,并且输出该动态属性的值。
编译运行应用程序,输出结果为:
status 2 own 1
3.5.3 对象树
Qt提供了一种机制,能够自动、有效地组织和管理继承自QObject的Qt对象(包括继承自QObject类的自定义类),这种机制就是Qt对象树。当应用程序生成一个具有父窗口部件的Qt对象的时候(通过在构造函数中的QObject* parent参数指定),这个新的对象将会被添加到父窗口部件的孩子列表中(通过QObject::children()能够获取对象的子窗口部件),而当对象的父窗口部件被销毁的时候,Qt的对象树机制能够保证也销毁它所有的子窗口部件。
Qt的对象树机制对于图形用户界面编程是非常有用的,例如,一个QShortcut对象被设置为一个窗口部件的孩子,当用户关闭并销毁该窗口部件时,这个QShortcut对象也就被销毁了,这极大地减少了程序员的工作,能够将主要精力放到系统的业务上,提高了编程效率,也增强了系统的稳健性。
C++编程一个重要的方面是防止内存泄露,方法是通过new操作创建的对象必须通过delete操作进行销毁。尽管程序员已经在这方面做得很小心,但好像做得并不出色。而Qt的对象树机制可以减轻内存泄露带来的压力。在进行Qt GUI编程的时候,可以选择Qt对象的销毁方式:
● 销毁Qt对象树中的顶层Qt对象,由对象树机制保证子窗口部件的销毁;
● 在代码中显示销毁(delete)子窗口部件。
对于显式销毁子窗口部件对象,要保证先销毁子窗口部件之后,再销毁它的父窗口部件。否则的话,将会导致语义未定义。这是因为销毁一个不存在的窗口部件造成的(因为先销毁父窗口部件的话,子窗口部件也被销毁了,当再显式销毁子窗口部件的时候,相当于销毁一个不存在的对象,语义是未定义的)。
下面,看一下利用对象树销毁窗口部件的例子。
// chapter03/objtree/exam1. #include <QtGui> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); QDialog* dlg = new QDialog(0); qDebug() << "dlg(1) = " << dlg;
创建一个对话框堆窗口对象(父设置为0),输出该对话框对象。输出结果为:
dlg(1) = QDialog(0x8bca500)
注意,括号中的地址会因环境的差异而会不同。
QTableWidget* tbl = new QTableWidget(dlg); qDebug() << "tbl(1) = " << tbl;
创建一个QTableWidget堆对象,父设置为上面创建的对话框窗口,然后输出该表格窗口部件。输出结果为
tbl(1) = QTableWidget(0x8bcd080) dlg->exec();
执行对话框(在出现的对话框中执行关闭操作)。
qDebug() << "dlg(2) = " << dlg; qDebug() << "tbl(2) = " << tbl;
再次输出对话框窗口和表格窗口部件,输出结果分别与dlg(1)和tbl(1)的结果相同,说明关闭一个窗口,只会隐藏该窗口,而不会销毁。
dlg->exec(); delete dlg;
再次执行对话框,然后销毁对话框窗口dlg。
Q_ASSERT(dlg != NULL); Q_ASSERT(tbl != NULL);
尽管窗口部件被销毁了,但对象指针没有被赋值为0,这是由编译器决定的。注意,C++标准约定,允许销毁一个值为NULL的指针。
qDebug() << "after delete parent"; delete tbl;
销毁表格子窗口部件tbl,结果会出现段错误。说明对象树在起作用,已经将表格子窗口部件销毁掉了。输出结果为
after delete parent
段错误
return 0; }
现在,改变一下销毁对象的顺序:先销毁子窗口部件,再销毁父窗口部件。
// chapter03/objtree/exam2. #include <QtGui> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); QDialog* dlg = new QDialog(0); qDebug() << "dlg(1) = " << dlg; QTableWidget* tbl = new QTableWidget(dlg); qDebug() << "tbl(1) = " << tbl; dlg->exec(); qDebug() << "dlg(2) = " << dlg; qDebug() << "tbl(2) = " << tbl; delete tbl; dlg->exec();
销毁表格子窗口部件tbl,再次执行对话框。这时,呈现的对话框没有表格了。说明表格子窗口部件已经被销毁掉,而父窗口仍然可以运行。
Q_ASSERT(dlg != NULL); Q_ASSERT(tbl != NULL); qDebug() << "after delete child"; delete dlg;
销毁父窗口dlg,应用程序运行正常。
return 0; }
也可以隐含设置Qt GUI对象间的父子关系,该方法不需要在构造函数中设置参数<QWidget*parent>。
// chapter03/objtree/exam3. #include <QtGui> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); QDialog* dlg = new QDialog(0); QTableWidget* tbl = new QTableWidget(dlg); QHBoxLayout* layout = new QHBoxLayout; layout->addWidget(tbl); dlg->setLayout(layout); QList<QObject*> list = dlg->children(); QDebug() << "dialog's children: "; for(int i=0; i<list.size(); ++i) qDebug() << list.at(i);
函数QObject::children()获取Qt对象的子窗口部件。
输出结果为:
dialog's children: QTableWidget(0x83a8f50) QHBoxLayout(0x84705f0) list = layout->children(); qDebug() << "layout's children: "; for(int i=0; i<list.size(); ++i) qDebug() << list.at(i);
输出QHBoxLayout对象的子窗口部件,输出结果为:
layout's children:
即QHBoxLayout对象layout的子窗口部件为空,说明通过QHBoxLayout::addWidget()添加的窗口部件并不是由QHBoxLayout的对象树进行组织管理。
delete tbl; list = dlg->children(); qDebug() << "afterdelete tablewidget, dialog's children: "; for(int i=0; i<list.size(); ++i) qDebug() << list.at(i);
销毁表格子窗口部件后,输出对话框的子窗口部件。输出结果为:
afterdelete tablewidget, dialog's children: QHBoxLayout(0x84705f0) return 0; }
注意,Qt GUI中“父窗口部件/子窗口部件”关系是区别于面向对象编程中的对象继承关系“父对象和子对象”的。