Qt编程核心技术

http://saupb.blog.163.com/blog/static/47124178201102982357547/

Qt编程核心技术  

2011-01-29 20:23:57|  分类: c++&c |  标签:linux  gui  |字号 订阅

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:

表1 Qt对象模型的基本类说明
基类 基类说明
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。

图1 信号和槽连接的原理图

Qt的窗口部件有很多预定义的信号,slot是一个可以被调用处理特定信号的函数。Qt的窗口部件又很多预定义的槽,当一个特定事件发生的时候,一个信号被发射,对信号感兴趣的slot就会调用对应响应函数。

信号/槽机制在QObject类中实现,从QObject类或者它的一个子类(比如QWidget类)继承的所有类可以包含信号和槽。当对 象改变它们的状态的时候,信号被发送,对象不关心有没有其它对象接收到它所发射的信号。槽是类的正常成员函数。可以将信号和槽通过connect函数任意 相连。当一个信号被发射,它所连接的槽会被立即执行,就像一个普通函数调用一样。信号与槽连接的示意图如图3。

图3 信号和槽连接示意图 一个带有信号和槽的Qt类Foo声明如下:
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()访问的。

你可能感兴趣的:(编程,object,Class,qt,makefile,编译器)