1 Qt概述
Qt是一个跨平台的C++图形用户界面库,由挪威TrollTech公司出品,目前包括Qt/X11, 基于Framebuffer的Qt Embedded,快速开发工具Qt Designer,国际化工具Qt Linguist 等,Qt支持Unix系统及Linux,还支持WinNT/Win2k,Win95/98平台。Qt的良好封装机制使得Qt的模块化程度非常高,可重用性 较好,对于用户开发来说是非常方便的。Qt API和开发工具对所有支持平台都是一致的,从而可以进行独立于平台的程序开发和配置。它使得跨平台软件编程直观、简易和方便。Qt 提供了一种称为signals/slots 的安全类型来替代 callback回调函数,这使得各个控件之间的协同工作变得十分简单。
Qt在Linux下有GPL版,可方便用户的学习及开发。如果用户使用 C++,对库的稳定性,健壮性要求比较高,并且希望跨平台开发的话,那么使用Qt是较好的选择,Qt还支持 2D/3D图形渲染、OpenGL、XML等。
Qt Script for Applications (QSA)是Trolltech的跨平台脚本工具箱。Qt为静态的Qt/C++程序提供了一个脚本界面,可以定制和扩展程序。
Qtopia是为基于Linux的PDA,智能电话和其他移动设备设计的一个全面的,可以用户化的应用程序平台和用户界面。
Qt/Embedded是面向嵌入式系统的Qt版本,是Qt的嵌入式Linux窗口,是完整的自包含C++ GUI和基于Linux的嵌入式平台开发工具。Qt/Embedded API可用于多种开发项目。许多基于Qt的X Window程序可以非常方便地移植到嵌入式版本,适用于高端PDA等产品。Qt/Embedded内部对于字符集的处理采用了UNICODE编码标准。
Qt是基于面向对象的C++语言,Qt提供了signal和slot的对象通信机制,具有可查询和可设计的属性以及强大的事件和事件过滤 器,同时,还具有字符国际化,即支持根据上下文进行国际化的字符串翻译。许多Qt的特性是基于QObject的继承,通过标准C++技术实现的。
2 Qt对象模型
标准的C++对象模型提供了非常有效的对运行时参数的支持,但C++对象模型的静态特性在某些问题上缺乏灵活性。图形用户界面编程需要运行的高效和高层次的灵活性。Qt提供了C++的高速及Qt对象模型的灵活性。Qt增加了这些特性到C++中:
● 非常有效的对象通信:信号与槽。
● 可查询和可设计的对象属性。
● 有效的事件及事件过滤器。
● 为国际化提供了上下文式的字符串翻译。
● 定时器使得在一个事件驱动的GUI中整合多个任务成为可能。
● 层次化并可查询的对象树以对象继承关系这样自然的方式组织对象。
● 保护指针QGuardedPtr在引用对象销毁时自动设置到0,不象正常的C++指针在它们的对象销毁时,C++指针变成危险的指针。
Qt的这些特征基于QObject的继承性应用在标准C++技术上。另外,象对象通信机制和动态属性系统需要Qt自已的元对象编译器(moc Meta Object Compiler)提供的元对象系统。
元对象系统是一个C++扩展,这个扩展使得Qt更适合于真正的组件GUI编程。
组成Qt对象模型的基本类说明如表1:
基类 | 基类说明 |
QGuardedPtr | 它是个模板类,提供了对QObject对象指针的保护。 |
QMetaObject | 关于Qt对象的元信息。 |
QMetaProperty | 存储有关属性的元数据。 |
QObject | 所有Qt对象的基类。 |
QObjectCleanupHandler | 监视多个QObject的生命周期。 |
QVariant | 扮作大多数通用Qt数据类型的联合。 |
表1中QGuardedPtr类和QObjectCleanupHandler的使用方法说明如下:
一个保护的指针QGuardedPtr<X>,除了它在引用对象被销毁时能自动被设置到0外,其它在使用时就象一个正常的 C++指针X*一样。而不象正常的C++指针,在对象销毁时就变成了不确定的指针。其中X必须是QObject的一个子类。保护的指针在你需要存一个别人 拥有的QObject指针时很有用,这个指针所指对象在你还在持有它的引用时,别人可能删除它。你可以安全地测试这个指针的有效性。
保护的指针QGuardedPtr<X>使用的方法如下面的例子:QGuardedPtr<QLabel> label = new QLabel( 0, "label" ); label->setText( "I like guarded pointers" ); delete (QLabel*) label; // 模拟别人销毁了label if ( label) label->show(); else qDebug("The label has been destroyed");
程序将输出"The label has been destroyed",而不是引用了一个无效的地址。 如果你需要知道别人拥有的多个QObject何时已被删除,QObjectCleanupHandler将是很有用的,如:分配在一个共享库里的应用程序 卸载共享库时必须知道共享库的所有对象必须销毁。一个使用QObjectCleanupHandler来监视对象销毁的例子如下:
class FactoryComponent : public FactoryInterface, public QLibraryInterface { public: ... QObject *createObject(); bool init(); void cleanup(); bool canUnload() const; private: QObjectCleanupHandler objects; }; // 分配一个新的对象并把它加入到cleanup handler中 QObject *FactoryComponent::createObject() { return objects.add( new QObject() ); } // QLibraryInterface接口应用 bool FactoryComponent::init() { return TRUE; } void FactoryComponent::cleanup() { } //当所有QObject对象被销毁时,卸载库才是安全的。 bool FactoryComponent::canUnload() const { return objects.isEmpty(); }
元对象系统不能对信号与槽使用模板。一个简单的理由是:由于各个编译器的不充分,Qt不能在多个平台的应用程序中完全应用模板。即使今天,许多使用很广的C++编译器应用到先进模板时有问题。例如:你不能安全依赖于部分模板实例。
即使C++编译器对模板有优秀的支持,我们也不能抛弃元对象编译器使用的基于字符串访问。原因如下:
Qt信号与槽的语法是直观的、易用易读,在类定义中声明了信号确保了信号在保护成员函数中被保护。
Qt的元编译器moc (Meta Object Compiler)产生能被任何标准C++编译器访问的附加C++代码。moc读取C++代码文件,如果它发现类声明中含有"Q_OBJECT"宏,它将 给这些类产生另外的C++代码,其中装有元对象代码。这些被moc产生有C++源代码必须被编译连接到这个类(或它能被#include进这个类的源文件 里)。moc通常不被手动调用,而是被编译系统自动调用,这样它不需要编程员做另外的工作。
由于给信号与槽增加了moc,我们能加其它有用的东西到Qt,这是模板类不能做的。如:使用tr()函数翻译,先进的属性系统和扩展的运行 类型信息。属性系统可用于象Qt Designer这样的通用用户界面设计工具。带有moc预处理器的C++基本上提供了面向对象的C的灵活性或类似Java的运行环境,并保持了C++的 执行效率和扩展性。
2.1 信号(signal)和槽(slot)
signal和slot用于对象间的通讯。信号/槽机制是Qt的一个重要特征。 在图形用户界面编程中,常需要将一个窗口部件的变化通知给另一个窗口部件,或者说希望对象间进行通讯。一般的图形用户界面编程中采用回调函数进行对象间通 信,这样回调和处理函数捆绑在一起,没有signal和slot机制的简便和灵活。信号和槽连接的有原理图如图1。
Qt的窗口部件有很多预定义的信号,slot是一个可以被调用处理特定信号的函数。Qt的窗口部件又很多预定义的槽,当一个特定事件发生的时候,一个信号被发射,对信号感兴趣的slot就会调用对应响应函数。
信号/槽机制在QObject类中实现,从QObject类或者它的一个子类(比如QWidget类)继承的所有类可以包含信号和槽。当对 象改变它们的状态的时候,信号被发送,对象不关心有没有其它对象接收到它所发射的信号。槽是类的正常成员函数。可以将信号和槽通过connect函数任意 相连。当一个信号被发射,它所连接的槽会被立即执行,就像一个普通函数调用一样。信号与槽连接的示意图如图3。
class Foo : public QObject { Q_OBJECT //包含信号和/或者槽的类必须声明Q_OBJECT public: Foo(); int value() const { return val; } public slots: void setValue( int ); //槽的声明 signals: void valueChanged( int ); //信号声明 private: int val; }; void Foo::setValue( int v ) { if ( v != val ) { val = v; emit valueChanged(v); //发送信号 } }
下面把两个对象连接在一起:
Foo a, b;connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));//信号与槽连接 b.setValue( 11 ); // a == undefined b == 11 a.setValue( 79 ); // a == 79 b == 79
b.value();
调用a.setValue(79)会使a发射一个valueChanged() 信号,b将会在它的setValue()槽中接收这个信号,也就是b.setValue(79) 被调用。接下来b会发射同样的valueChanged()信号,但是因为没有槽被连接到b的valueChanged()信号,所以没有发生任何事(信 号消失了)。
signals、slots和emit不是C++的标准关键字,需要预处理程序改变或者移去了signals、slots和emit 这些关键字,然后再使用标准的C++编译器。对包含有信号和槽的类定义运行MOC(Meta Object Compiler)。生成一个可以和其它对象文件编译和连接成引用程序的C++源文件。
槽是普通成员函数,它和普通成员函数一样分为public、protected和private三类,public slots表示声明的是任何信号都可以相连的槽。protected slots表示这个类的槽和它的子类的信号才能连接。private slots表示这个类本身的信号可以连接这个类的槽。
元对象编译器(moc)解析一个C++文件中的类声明并且生成初始化元对象的C++代码。元对象包括所有信号和槽函数的名称,还有这些函数的指针。
2.2 元对象系统
Qt中的元对象系统是用来处理对象间通讯的信号/槽机制、运行时的类型信息和动态属性系统。它基于QObject类、类声明中的私有段中的Q_OBJECT宏和元对象编译器(moc)。
moc读取C++源文件,如果它发现类的声明中含有Q_OBJECT宏,它就会给含有Q_OBJECT宏的类生成另一个含有元对象代码的C++源文件。这个生成的源文件可以被类的源文件包含(#include)并和这个类的实现一起编译连接。
除了提供对象间通信的信号和槽机制之外(这是使用元对象最主要的原因),QObject中的元对象代码还实现其它特征:
className()函数在运行的时候以字符串返回类的名称,不需要C++编译器中的本地运行类型信息的支持。
inherits()函数返回本对象一个在QObject继承树中一个特定类的实例。
tr()和trUtf8() 两个函数是用于国际化中的字符串翻译。
setProperty()和property()函数用来通过名称动态设置和获得对象属性。
metaObject()函数返回这个类所关联的元对象。
在类的定义中声明了Q_OBJECT宏,这个类才能使用元对象系统相关的牲。建议QObject 的所有子类使用Q_OBJECT宏,而不管它们是否实际使用了信号、槽和属性。
元对象编译器读取一个C++源文件。如果它发现其中的一个或多个类的声明中含有Q_OBJECT宏,它就会给这个使用Q_OBJECT宏的类生成另外一个包含元对象代码的C++源文件。
如果你是用qmake来生成你的Makefile文件,当需要的时候,编译规则中会包含调用元对象编译器,所以你不需要直接使用元对象编译器。
元对象编译器生成的输出文件必须被编译和连接,就像你的程序中的其它的C++代码一样,这种操作是用下述两种方式之一解决的:
方法1:类的声明放在一个头文件(.h文件)中。
如果在文件myclass.h中发现类的声明,元对象编译器的输出文件将会被放在一个叫moc_myclass.cpp的文件中。这个文件 象普通文件一样被编译,输出对象文件的结果是moc_myclass.o(在Unix下)或者moc_myclass.obj(在Windows下)。这 个对象接着会被包含到一个对象文件列表中,它们将在程序的最后连接阶段被连接在一起。
方法2:类的声明放在一个实现文件(.cpp文件)中。
如果在文件myclass.cpp中发现类的声明,元对象编译器的输出文件将会被放在一个叫myclass.moc的文件中。这个文件需要被实现文件包含(#include),也就是说myclass.cpp需要包含下面这行放在所有的代码之后:
#include "myclass.moc"
这样,元对象编译器生成的代码将会和myclass.cpp中普通的类定义一起被编译和连接。
方法1是常规的方法。方法2用在你想让实现文件自包含,或者Q_OBJECT类是内部实现的并且在头文件中不可见的这些情况下使用。
建议使用自由makefile生成工具qmake来生成你的Makefile。这个工具可以识别方法一和方法二风格的源文件,并建立一个可以做所有必要的元对象编译操作的Makefile。
如果你想自己建立你的Makefile,根据上面的方法1和方法2所提的声明Q_OBJECT宏不同方法,下面说明如何在makefile中包含元对象编译操作:
对于在头文件中声明了Q_OBJECT宏的类,如果你只使用GNU的make,在makefile如下添加元对象编译规则:moc_%.cpp: %.h moc $< -o $@
你还可以按下面的格式写单个文件的规则:
moc_NAME.cpp: NAME.h moc $< -o $@
你必须把moc_NAME.cpp添加到你的SOURCES变量中并且把moc_NAME.o(Linux操作系统下)或者moc_NAME.obj(windows操作系统下)添加到你的OBJECTS变量中。 对于在实现文件(.cpp文件)中声明Q_OBJECT的类,你可使用下面这样的makefile规则:
NAME.o: NAME.moc NAME.moc: NAME.cpp moc -i $< -o $@
这将会保证make程序会在编译NAME.cpp之前运行元对象编译器。然后你可以把下面这行放在NAME.cpp的末尾,这样在这个文件中的所有的类声明都知道这个元对象。
#include "NAME.moc"
元对象编译中常出现的错误是:
YourClass::className() is undefined
或者
YourClass lacks a vtbl
出现这种错误的绝大多数情况是你忘记了编译或者#include元对象编译器产生的C++代码,或者(在前面的情况下)没有在连接命令中包含那个对象文件。
2.3 元对象编译器限制
元对象编译器并不展开#include或者#define,它简单地忽略它所遇到的所有预处理程序。元对象编译器无法处理所有的C++语法。主要的问题是类模板不能含有信号和槽。下面是一个错误的例子:class SomeTemplate<int> : public QFrame { Q_OBJECT ... signals: void bugInMocDetected( int ); };
元对象编译器的限制说明如下:
(1)多重继承把QObject的子类作为第一个父类
如果你使用了多重继承,元对象编译器认为第一个继承类是QObject的子类。这是因为元对象编译器并不展开#include或#define,它无法发现基类中哪个是QObject。例如:class SomeClass : public QObject, public OtherClass { ... };
(2)函数指针不能作为信号或槽的参数 下面是一个不合法的语法的例子:
class SomeClass : public QObject { Q_OBJECT ... public slots: // 不合法的 void apply( void (*apply)(List *, void *), char * ); };
可以象下面这样纠正这个错误:
typedef void (*ApplyFunctionType)( List *, void * );class SomeClass : public QObject {
Q_OBJECT ...public slots:
void apply( ApplyFunctionType, char * );
};
(3)不能把友声明friend放在信号或者槽的声明部分 通常情况下,友声明friend不能放在信号或者槽的声明部分。把它们替换到private、protect或public部分中。这里是一个不合法的例子:
class SomeClass : public QObject { Q_OBJECT ... signals: friend class ClassTemplate<char>; // 错的 };
(4)信号和槽不能被升级 把继承的成员函数升级为公有状态这一个C++特征对信号和槽并不适用。下面是一个不合法的例子:
class Whatever : public QButtonGroup { ... public slots: void QButtonGroup::buttonPressed; // 错的,槽是保护的。 ... };
(5)宏不能用在信号和槽的参数中 因为元对象编译器不能展开#define,在信号和槽中类型宏作为一个参数是不能工作的。下面是一个不合法的例子:
#ifdef ultrix #define SIGNEDNESS(a) unsigned a #else #define SIGNEDNESS(a) a #endif class Whatever : public QObject { ... signals: void someSignal( SIGNEDNESS(int) ); ... };
(6)嵌套类不能放在信号部分或者槽部分,也不能含有信号和槽 下面是一个例子:
class A { Q_OBJECT public: class B { public slots: // 错的 void b(); ... }; signals: class B { // 错的 void b(); ... }: };
(7)构造函数不能用于信号或槽的声明部分 下面是一个错误的例子:
class SomeClass : public QObject { Q_OBJECT public slots: SomeClass( QObject *parent, const char *name ) : QObject( parent, name ) { } // 错的 ... };
(8)属性的声明应该public声明部分之前 如果在public之后声明属性,元对象编译器将不能找到函数或解析这个类型。下面是一个错误的例子:
class SomeClass : public QObject { Q_OBJECT public: ... Q_PROPERTY( Priority priority READ priority WRITE setPriority ) // 错的 Q_ENUMS( Priority ) // 错的 enum Priority { High, Low, VeryHigh, VeryLow }; void setPriority( Priority ); Priority priority() const; ... };
这个例子纠正错误后列出如下:
class SomeClass : public QObject {Q_OBJECT Q_PROPERTY( Priority priority READ priority WRITE setPriority ) Q_ENUMS( Priority )public:
... enum Priority { High, Low, VeryHigh, VeryLow }; void setPriority( Priority ); Priority priority() const; ...
};
2.4 属性
Qt的属性也基于元对象系统,在类声明中用宏Q_PROPERTY来声明。属性只能在继承于QObject的子类中声明。宏Q_OVERRIDE用来覆盖一些子类中由继承得到的属性。属性也是一个类的成员。
元对象系统中设置属性和得到属性的成员函数列出如下:
QObject::setProperty() 可以让你控制类中那些在编译时不可用的属性。
QMetaObject::propertyNames() 返回所有可用属性的名称。
QMetaObject::property() 返回一个指定属性的属性数据:一个QMetaProperty对象。
下面两个设置函数是等效的:
// QButton *b和QObject *o指向同一个按钮时 b->setDown( TRUE ); o->setProperty( "down", TRUE );
示例:使用元对象系统的属性
下面的类MyClass用来设置和得到优先级,但还不能使用元对象系统的属性。类MyClass列出如下:
class MyClass : public QObject { Q_OBJECT public: MyClass( QObject * parent=0, const char * name=0 ); ~MyClass(); enum Priority { High, Low, VeryHigh, VeryLow }; void setPriority( Priority ); Priority priority() const; };
为了使用元对象的属性系统,必须用宏Q_PROPERTY来声明属性。宏Q_PROPERTY语法如下:
Q_PROPERTY( type name READ getFunction [WRITE setFunction] [RESET resetFunction] [DESIGNABLE bool] [SCRIPTABLE bool] [STORED bool] )
宏Q_PROPERTY说明如下:
type name是属性的类型及名字,它可以是一个QVariant支持的类型或者在类中已经定义的枚举类型。枚举类型必须使用Q_ENUMS宏来进行注册。
READ getFunction表示用于读取属性的函数是getFunction。
WRITE setFunction表示用于写(或设置)属性的函数是setFunction。
RESET resetFunction表示用函数resetFunction设置属性到缺省状态(这个缺省状态可能和初始状态不同)。这个函数必须返回void并且不带有参数。
DESIGNABLE bool声明这个属性是否适合被一个图形用户界面设计工具修改。bool缺省为TRUE,说明这个属性可写,否则,FALSE说明不能被图形用户界面设计工具修改。
SCRIPTABLE bool声明这个属性是否适合被一个脚本引擎访问。bool缺省为TRUE,说明可以被访问。
STORED bool声明这个属性的值是否必须作为一个存储的对象状态而被记住。STORED只对可写的属性有意义。缺省是TRUE。
这样,修改成使用了宏Q_PROPERTY的MyClass类列出如下:
class MyClass : public QObject { Q_OBJECT Q_PROPERTY( Priority priority READ priority WRITE setPriority ) Q_ENUMS( Priority ) public: MyClass( QObject * parent=0, const char * name=0 ); ~MyClass(); enum Priority { High, Low, VeryHigh, VeryLow }; void setPriority( Priority ); Priority priority() const; };
当枚举数据可以被同时被读或写时,必须使用Q_SETS来注册这个枚举类型。
宏Q_CLASSINFO可以用来把名称/值这样一套的属性添加到一个类的元对象中,例如:
Q_CLASSINFO( "Version", "3.0.0" )
和其它元数据一样,类信息在运行时是可以通过元对象QMetaObject::classInfo()访问的。