精通Qt4编程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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对象树。当应用程序生成一个具有父窗口部件严格地讲应该是父QObject对象,不一定是GUI对象,在此统称父窗口部件。子窗口部件类推。的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中“父窗口部件/子窗口部件”关系是区别于面向对象编程中的对象继承关系“父对象和子对象”的。