Qt编程核心技术

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,除了它在引用对象被销毁时能自动被设置到0外,其它在使用时就象一个正常的C++指针X*一样。而不象正常的C++指针,在对象销毁时就变成了不确定的指针。其中X必须是QObject的一个子类。保护的指针在你需要存一个别人拥有的QObject指针时很有用,这个指针所指对象在你还在持有它的引用时,别人可能删除它。你可以安全地测试这个指针的有效性。

保护的指针QGuardedPtr使用的方法如下面的例子:
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()访问的。

3 QObject类

QObject类从Qt类继承,是所有Qt对象的基类,Qt类含有全局需要的各种枚举类型、类型的定义 。通常情况下,用户不需要关心这个类,因为它是QObject及很少几个类的基类。例如:它定义了枚举类型ButtonState说明了鼠标按钮的状态,它定义了枚举类型WidgetState说明了窗口部件状态等。

3.1 对象树

QObject类是所有Qt对象的基类,是Qt对象模型的核心。 通过QObject对象可组织成对象树,QObject类提供了对对象树进行访问需要的各种成员函数。当你创建一个对象时,这个对象的父对象自动调用函数insertChild()将这个对象插入到父对象的孩子对象链表中,并调用函数children()可得到孩子对象链表。调用函数objectTrees()可得到对象树根的所有对象的链表。调用函数queryList可查询得到符合查询条件的对象的链表。

对象链表是通过QObjectList 类实现的,QObject对象树是一个静态的QObjectList类对象object_trees,object_trees链表中存有所有的对象指针,通过object_trees链表可查询到所有的对象。QObject对象树的层次图如图11。从图中可见,QObject对象是分层管理的,顶层链表链接的是无父对象的对象,一般是顶层窗口,第二层链表是无父对象的对象的孩子链表,依此类推。object_trees对象树与文件系统的文件组织方式类似。这种树型分层结构加快了对象的查找速度。

 

图11 QObject对象树的层次图

QObject对象链表类QObjectList从类QPtrList继承,类QObjectList的定义列出如下:

static QObjectList* object_trees = 0; //整个对象链表
class Q_EXPORT QObjectList : public QPtrList<QObject>
{
public:
    QObjectList() : QPtrList<QObject>() {}
    QObjectList( const QObjectList &list ) : QPtrList<QObject>(list) {}
   ~QObjectList() { clear(); }
    QObjectList &operator=(const QObjectList &list)
	{ return (QObjectList&)QPtrList<QObject>::operator=(list); }
};

QObject类构造函数将本对象加到对象树中,QObject类构造函数列出如下:

QObject::QObject( QObject *parent, const char *name )
    :
    isSignal( FALSE ),			// 不是一个信号对象
    isWidget( FALSE ), 			//不是一个窗口部件对象
    pendTimer( FALSE ),			//还没有定时器
    blockSig( FALSE ),      			// 不阻塞信号
    wasDeleted( FALSE ),       			//监控2次以上删除的标识
    isTree( FALSE ), 			// 不是树,即不在树顶层链表中
    objname( name ? qstrdup(name) : 0 ),          //设置对象名
    parentObj( 0 ),				// 不是父对象,它被函数 insertChild()设置
    childObjects( 0 ), 			//不是孩子对象
    connections( 0 ),				//还没有连接
    senderObjects( 0 ),        			//还没有信号连接
    eventFilters( 0 ), 				//还没安装过滤器
    postedEvents( 0 ), 			//还没有事件传递
    d( 0 )
{
    if ( !metaObj )				// 创建对象字典
	(void) staticMetaObject();
 
    if ( parent ) {//如果父对象存在 
    //插这个对象对父对象的孩子对象链表中,如果孩子对象不存在,创建孩子对象链表
	parent->insertChild( this );  
    } else {//父对象不存在,插入到对象树顶层链表中
	insert_tree( this ); 
	isTree = TRUE;
    }
}

下面是一个查询对象的例子。主要使用了QObject类的函数queryList。这个例子查询得到所有的QButton类对象的链表,如果对象obj是链表中的QButton类对象,将按钮设置为失效状态。样例代码如下:

QObjectList *l = topLevelWidget()->queryList( "QButton" ); //查询得到QButton类所有对象
    QObjectListIt it( *l );
    QObject *obj;
 
    while ( (obj = it.current()) != 0 ) { // 遍历所有的QButton类对象
        // 对于每个找到的对象
        ++it;
        ((QButton*)obj)->setEnabled( FALSE );
    }
    delete l; // 删除链表l,并没删除对象obj

3.2 事件处理过程

在Qt里,一个事件是继承自QEvent的对象。事件通过调用QObject::event()被发送到继承自 QObject 的对象。事件发送表明一个事件已经产生,用 QEvent表达这个事件,且QObject 需要做出回应。多数事件针对 QWidget和他的子类的,此外还有些不和图形相关的重要事件,比如,套接字激活,它是QSocketNotifier使用的事件。

一些事件来自窗口系统,如QMouseEvent,一些来自其他地方,如QTimerEvent,还有一些来自应用程序。Qt一视同仁,你可以象Qt自己的事件循环所作的方式一样地发送事件。

大多数事件类型有特定的类,常用的有QResizeEvent、QPaintEvent、QMouseEvent、QKeyEvent和QCloseEvent。有很多别的事件类。每个类派生自QEvent且添加事件特定的函数;例如,QResizeEvent。在QResizeEvent中,就被加入了QResizeEvent::size()和QResizeEvent::oldSize()。

有些类支持多种事件类型。QMouseEvent支持鼠标移动、按压、粘滞按压、拖拽、点击、右按压等。

因为程序需要以多变和复杂的方式作出返回,因此,Qt的事件分发机制就是灵活的。一个事件被发送的通常方法是通过调用一个虚拟函数。例如:QPaintEvent通过调用QWidget::paintEvent()被分发。

(1) 创建用户事件

创建一个自定义类型的事件,你需要定义一个事件号,其值必须大于QEvent::User。为了传递有关你的自定义事件的特性。可能自定义的事件需要从QCustomEvent类继承。

示例1:用户事件类的编写

下面列出了一个用户事件类TEST_Event的示例,编写用户事件类的方法是先定义一个事件号,再实现用户事件类,应用程序将把用户事件类与Qt的事件类一样进行处理。

用户事件类TEST_Event的头文件test_event.h列出如下:

#include 
#include 
 
#define REFRESHUI_EVENT QEvent::User+2
class TEST_Event: public QCustomEvent
{
public:
    TEST_Event();
};

用户事件类TEST_Event的实现文件test_event.cpp列出如下:

#include 
TEST_Event::TEST_Event():QCustomEvent(REFRESHUI_EVENT )
{
}

(2)事件发送

许多应用程序都要创建和发送他们自己的事件。这需要创建一个相应的事件类的对象,然后调用QApplication::sendEvent()或者QApplication::postEvent()发送事件。对用户事件来说,还需要用户有事件对应的操作函数。对于Qt事件来说,Qt中已实现了事件对应的操作函数。

示例2:使用用户事件类

在test_engine.cpp文件中,函数refreshUI发送事件到主窗口,要求刷新主窗口界面。在main_window.cpp文件中,主窗口类MainWindow_UI重载了customEvent函数,实现了事件需求的操作:刷新主窗口界面。

在test_engine.cpp文件中函数refreshUI实现代码列出如下:

void Engine_View::refreshUI()
{
  // pmainWindowGui是应用程序主窗口类MainWindow_UI的实例
    qApp->postEvent( pmainWindowGui, new TEST_Event() ); //发送事件
qApp->wakeUpGuiThread(); //唤醒Gui线程
}

在main_window.cpp文件中函数customEvent的实现代码列出如下:

void MainWindow_UI::customEvent( QCustomEvent * e)
{
 
    if ( e->type() == REFRESHUI_EVENT  )
    {
	进行主窗口刷新操作
    }
}

在应用程序的基类QApplication中有事件处理函数的实现,其中,事件的发送函数说明如下:

bool QApplication::sendEvent ( QObject * receiver, QEvent * event ) [static]

sendEvent() 立即发送事件给接收对象receiver,当sendEvent()返回时,(事件过滤器和)对象已经处理过事件了。对于很多事件类,可以通过调用isAccepted()函数知道该事件能否被处理者所接受或者拒绝。

void QApplication::postEvent ( QObject * receiver, QEvent * event ) [static]

postEvent()投寄事件到一个队列,以便能延迟分发。在下次Qt的主事件循环运行时,它分发全部事件,还可进行一些优化。例如,若有数个resize事件,它们就被压缩成一个。对于paint事件同样如此:QWidget::update()调用了 postEvent(),postEvent()投寄事件可以避免屏幕因多次重画闪烁,同时还加快了运行速度。

postEvent()在对象初始化期间常常被使用,因为在对象完成初始化后,投送的消息会被很快派发。

bool QApplication::notify ( QObject * receiver, QEvent * e ) [virtual]

发送事件到接收对象者receiver,返回值是从receiver的事件处理函数中返回的值。对于某一类型的事件(如:鼠标和键事件)来说,如果接收者对事件不感兴趣(如:返回FALSE),事件将被传播到receiver的父类,如果父类不感兴趣,就一直向上级传递,直到顶层的object类。

(3)事件的处理

有5种不同的处理事件的方法,列出如下:

(1)重载函数QApplication::notify(),这可提供有效的事件控制,但仅能在派生于QApplication的类中重实现这个函数。

(2)在qApp(是QApplication的全局实例)中实现事件过滤,这样的一个事件过滤器能为所有的widget处理所有的事件,而且,可以有超过一个全局应用程序的事件过滤器。如:鼠标事件的全局事件过滤器设置了鼠标跟踪使能,则鼠标移动事件对于所有widget有效。

(3)重载QObject::event()(在QWidget类中),在任何widget特定的事件过滤器之前,QObject::event()能看到所有事件。QObject::event()函数声明列出如下:

bool QObject::event ( QEvent * e ) [virtual]

这个虚函数接收给一个对象的事件,如果事件被识别并被处理,将返回TRUE。这个函数能被用来重实现一个对象的行为。

(4)在对象上安装事件过滤器。

(5)重载Qt基类事件处理函数

当用户发现Qt基类的事件处理函数不能满足用户的需要时,可以在用户类中重载这些函数,对于特定的Qt事件,可以重载特定的事件函数,如:重载paintEvent(), mousePressEvent()等等函数。如果想对多个Qt事件处理函数进行修改,可以重载QObject::event()来实现。

示例1:重载QObject::event()

下面是重载QObject::event()函数的例子,它进行特定的tab键处理,还处理用户事件。

bool MyClass:event( QEvent * e ) 
{
   if ( e->type() == QEvent::KeyPress )
   {
       QKeyEvent * ke = (QKeyEvent*) e;
       if ( ke->key() == Key_Tab )
       {
          // 这里是特定的tab处理
          k->accept();
          return TRUE;
       }
    }
    else if ( e->type() >= QEvent::User ) 
    {
          QCustomEvent * c = (QCustomEvent*) e;
          // 这里是自定义事件处理
          return TRUE;
      }
      QWidget::event( e );
}

3.3 事件运行机制

当应用程序的main函数中调用qApp->exec()时,应用程序进入Qt的主事件循环,Qt的主事件循环从事件队列中取出本窗口及系统事件,把它们翻译成QEvents,并使用函数QApplication::notify发送翻译的事件给相应的对象QObjects。同时,还处理控制台tty的信号、QWSserver服务器的事件,将来自socket的消息转化成事件进行分发。这些事件的处理工作在函数QEventLoop::processEvents(flags)中完成。

Qt的主事件循环函数QApplication::exec()的调用层次图如图3,从函数的调用层次图可看出事件的分发处理流程,这里没有给出源代码分析,读者可参考Qt 3.4源代码。

通常事件来自于窗口系统(用spontaneous()函数检查时返回TRUE),也可能来自使用Application::sendEvent()和QApplication::postEvent()手动发送的事件(用spontaneous()函数检查时返回FALSE)。

QObjects通过调用它们的QObject::event()接收事件,这个函数也可被子类重载来处理事件,最典型的重载是QWidget::event()。

 

图3 函数QApplication::exec()的调用层次图

3.4 事件过滤器

一个事件过滤器是一个能接收所有发送到这个对象上的事件的对象。这个过滤器能停止或转发到这个对象上的事件。事件过滤器通过eventFilter()函数来接收事件,如果这个事件应该被过滤(如:停止事件等),eventFilter()函数返回TRUE, 否则,返回FALSE。如果多个事件过滤器被安装在一个对象上,最后安装的过滤器将被激活。

QObject类还提供了事件过滤器的安装,QObject类与事件过滤相关的几个成员函数说明如下:

bool QObject::event ( QEvent * e ) [virtual] 接收到一个对象的事件,如果事件被识别并处理时,返回TRUE。这个函数能被重载来定制一个对象的行为。

bool QObject::eventFilter ( QObject * watched, QEvent * e ) [virtual] 如果一个对象上已安装了事件过滤器,eventFilter函数将被用来过滤事件。在这个函数的重载中,如果你想过滤事件e(如:让它停止不再被处理),就返回TRUE,否则返回FALSE。

void QObject::installEventFilter ( const QObject * filterObj ) 在对象filterObj上安装事件过滤器。

下面是一个事件过滤器的使用样例,MyMainWindow类在本对象上安装了事件过滤器,事件过滤处理函数eventFilter被重载用来在textEdit对象上处理KeyPress事件。没处理的事件被传递到基类的eventFilter()函数中,因为基类也可能因为内部事件处理的原因已重载了eventFilter()函数。如果你在这个函数中删除了接收者对象,确信返回TRUE,否则,Qt将向前传递事件到删除的对象中,程序将崩溃。

class MyMainWindow : public QMainWindow
    {
    public:
        MyMainWindow( QWidget *parent = 0, const char *name = 0 );
 
    protected:
        bool eventFilter( QObject *obj, QEvent *ev );
 
    private:
        QTextEdit *textEdit;
    };
 
    MyMainWindow::MyMainWindow( QWidget *parent, const char *name )
        : QMainWindow( parent, name )
    {
        textEdit = new QTextEdit( this );
        setCentralWidget( textEdit );
        textEdit->installEventFilter( this ); //在本对象上安装事件过滤器
    }
 
    bool MyMainWindow::eventFilter( QObject *obj, QEvent *ev )
    {
        if ( obj == textEdit ) { //过滤的对象
            if ( e->type() == QEvent::KeyPress ) {//过滤的事件
                qDebug( "Ate key press %d", k->key() );
                return TRUE; //已对事件处理,必须返回TRUE,这样,系统不会对这个事件做第二次处理了
            } else {
                return FALSE;
            }
        } else {
            // 传递事件到父类
            return QMainWindow::eventFilter( obj, ev );
        }
    }

3.5 定时器

使用定时器有2种方法,一种是使用QTimer类,另一种是使用QObject类的定时器。定时器的精确度依赖于操作系统和硬件,大多数平台支持20ms的精确度。

(1)QObject类的定时器

Qobject是所有Qt对象的基类,它提供了一个基本的定时器。通过QObject::startTimer(),你可以把一个以毫秒为单位的时间间隔作为参数来开始定时器。这个函数返回一个唯一的整数的定时器的标识符。这个定时器现在就会在每一个时间间隔"触发",直到你明确地使用这个定时器的标识符来调用QObject::killTimer()结束。

应用程序在main函数中通过调用QApplication::exec()来开始进行事件循环。当定时器触发时,应用程序会发送一个QTimerEvent,在事件循环中,处理器按照事件队列顺序来处理定时器事件,当处理器正忙于其它事件处理时,定时器就不能马上触发。

QObject类还提供定时器的功能,与定时器相关的成员函数有startTimer()、timerEvent()、killTimer()和killTimers()。QObject基类中的startTimer和timerEvent原型及说明如下:

int QObject::startTimer ( int interval )

开始一个定时器并返回定时器ID,如果不能开始一个定时器将返回0。定时器开始后,每隔 interval毫秒间隔将触发一次超时事件,直到killTimer()或killTimers()被调用来删除定时器。如果interval为0,那么定时器事件每次发生时没有窗口系统事件处理。

void QObject::timerEvent ( QTimerEvent * ) [virtual protected]

虚拟函数timerEvent被重载来实现用户的超时事件处理函数。如果有多个定时器在运行,QTimerEvent::timerId()被用来查找是哪个定时器被激活。

当定时器事件发生时,虚函数timerEvent()随着QTimerEvent事件参数类一起被调用。重载这个函数可以获得定时器事件。

定时器的用法样例如下:

class MyObject : public QObject
    {
	Q_OBJECT
    public:
	MyObject( QObject *parent = 0, const char *name = 0 );
 
    protected:
	void timerEvent( QTimerEvent * );
    };
 
    MyObject::MyObject( QObject *parent, const char *name )
	: QObject( parent, name )
    {
	startTimer( 50 );    // 50ms定时器
	startTimer( 1000 );  // 1s定时器
	startTimer( 60000 ); // 1分钟定时器
    }
 
void MyObject::timerEvent( QTimerEvent *e ) //重载timerEvent函数
{
    qDebug( "timer event, id %d", e->timerId() );
}
 
void MyObject::stopTimer()
{
    killTimer( showDateTimer );
    showDateTimer = -1;
}

(2)定时器类QTimer

定时器类QTimer提供当定时器触发的时候发射一个信号的定时器,QTimer提供只触发一次的超时事件。通常的使用方法如下:

QTimer * testtimer = new QTimer( this ); //创建定时器
connect(testtimer, SIGNAL(timeout()),
         this, SLOT(updateCaption()) );//将定时器超时信号与槽(功能函数)连接起来
testtimer ->start( 1000 );  //开始运行定时器,定时时间间隔为1000ms

testtimer定时器被作为这个窗口部件的子类,这样当这个窗口部件被删除时,定时器也会被删除。

QTimer还提供了一个简单的只有一次定时的函数singleShot。例如:一个定时器在100ms后触发处理函数animateTimeout,并且只触发一次。代码如下:

QTimer::singleShot( 100, this, SLOT(animateTimeout(� );

3.5 连接函数connect

QObject类还提供了信号与槽的连接函数connect()和断开连接函数disconnect()。这两个函数的用法,在信号与槽一节中详细说明。

3.6 字符串翻译函数

QObject类还提供了字符串的翻译函数tr()和trUtf8()。这两个函数说明如下:

QString QObject::tr ( const char * sourceText, const char * comment ) const

返回字符串sourceText的翻译,如果没有合适的翻译版本就返回原字符串sourceText。参数comment是翻译的上下文,是对字符串的补充说明,如:说明属于哪个类,可用来辅助标识字符串,这样可起到相同字符串要求不同的翻译。

QString QObject::trUtf8 ( const char * sourceText, const char * comment ) const

返回字符串sourceText的翻译,如果没有合适的翻译版本返回字符串QString::fromUtf8(sourceText)。其它与函数tr()相同。

4 Qt国际化

软件的国际化是指软件支持多国语言,能适应不同用户的语言、输入方法、字符编码甚至表达习惯等。软件的国际化使软件能被多个国家的人使用。

4.1 软件中字符串国际化方法

软件可以按下面的几个方法实现国际化:

(1)对用于Gui界面的字符串使用QString

QString内部使用了Unicode编码,Unicode编码包括了世界上的每种语言的字符编码。在Qt的基类中,有关字符串的函数大多数使用了QString作为参数。这样减小了char*到QString的转换的时间开销。

另外,还有QCString和QChar,它们的用法类似于传统C中的const char*和char。通过使用QString、QCString和QChar类,你不会注意到你在使用Unicode。

(2)对所有文字形式的文本使用tr()

当程序需要显示字符串到用户时,要确保它被QApplication::translate()函数处理过。其实做到这一点只需要使用QObject::tr()。例如,假设LoginWidget是QWidget的一个子类:

LoginWidget::LoginWidget()
{
    QLabel *label = new QLabel( tr("Password:"), this );
    ...
}

这样就解决了大部分你可能要写的用户可见的字符串。如果这些被引用的字符串不是在QObject子类的成员函数中,可以使用一个适当的类的tr()函数,或者直接使用QApplication::translate()函数,使用这两个函数的例子如下:

void some_global_function( LoginWidget *logwid )
{
    QLabel *label = new QLabel( LoginWidget::tr("Password:"), logwid );
}
 
void same_global_function( LoginWidget *logwid )
{
    QLabel *label = new QLabel(
                qApp->translate("LoginWidget", "Password:"),
                logwid );
}

lupdate工具会自动给每个源文本提供一个上下文。这个上下文是装有tr()调用的类的类名,这足够用来处理相同字符串需要不同的翻译的大多数情况。但有时候,需要更多的信息来惟一标识源文本。

示例:用上下文来惟一标识字符串

一个装有两个按钮对话框,每个按钮上有"Enabled"字符串,但翻译需要有差异,这时,需要用上下文来惟一标识字符串。这两个按钮字符翻译的上下文,分别是"Color frame"和"Hue frame"。这个例子列出如下:

rbc = new QRadioButton( tr( "Enabled", "Color frame" ), this);
rbh = new QRadioButton( tr( "Enabled", "Hue frame" ), this);

(3)翻译函数外的文本

如果你需要翻译函数外的文本,有两个宏可以使用:QT_TR_NOOP()和QT_TRANSLATE_NOOP()。它们仅仅给文本作出标签,以便于被lupdate工具提取。使用QT_TR_NOOP()的例子如下:

QString FriendlyConversation::greeting( int greet_type )
{
    static const char* greeting_strings[] = {
            QT_TR_NOOP( "Hello" ),
            QT_TR_NOOP( "Goodbye" )
    };
    return tr( greeting_strings[greet_type] );
}

使用QT_TRANSLATE_NOOP()的例子如下:

static const char* greeting_strings[] = {
    QT_TRANSLATE_NOOP( "FriendlyConversation", "Hello" ),
    QT_TRANSLATE_NOOP( "FriendlyConversation", "Goodbye" )
};
 
QString FriendlyConversation::greeting( int greet_type )
{
    return tr( greeting_strings[greet_type] );
}
 
QString global_greeting( int greet_type )
{
    return qApp->translate( "FriendlyConversation",
                                greeting_strings[greet_type] );
}

如果你使用了宏定义QT_NO_CAST_ASCII编译你的软件,就会关闭了从const char*到QString的自动转换,你很可能会得到错误的字符串。

如果你的源码语言使用Latin-1之外的字符集,你会发现QObject::trUtf8()比QObject::tr()更好用,因为tr()依赖于QApplication::defaultCodec(),这使它比QObject::trUtf8()更脆弱。

对于加速键值(Accelerator value),例如Ctrl+Q或者Alt+F,有时需要翻译者重载它,这时可使用使用QKeySequence()。如果你的应用给"Quit"直接编码(hardcode)为CTRL+Key_Q,翻译者就不能重载它了。正确的用法如下:

QPopupMenu *file = new QPopupMenu( this );
file->insertItem( tr("&Quit"), this, SLOT(quit()),
                      QKeySequence(tr("Ctrl+Q", "File|Quit")) );

(4)对字符串参数使用QString::arg()

对于国际化的文本,在字符串中使用类似printf()风格的插入参数不是好的选择,因为有时候有必要在翻译时改变参数的顺序。QString::arg()函数为参数替换提供了一种简单的途径,下面是使用QString::arg()的例子:

void FileCopier::showProgress( int done, int total,
                                   const QString& current_file )
{
    label.setText( tr("%1 of %2 files copied./nCopying: %3")
                        .arg(done)
                        .arg(total)
                        .arg(current_file) );
}

4.2 创建译本

在程序代码中使用tr()之后,你就可以开始制作程序中用户可见的文本的译本了。Qt提供了Qt语言学家(Qt Linguist)工具、lupdate和lrelease来进行翻译处理。其中, Qt Linguist是图形界面工具,用户可以使用它,将字符串对应的翻译填入.ts文件中。

在你运行lupdate之前,你应该准备一个项目文件(.pro文件)。下面是一个项目文件名为myproject.pro的例子:

HEADERS         = funnydialog.h /
                  wackywidget.h
SOURCES         = funnydialog.cpp /
                  main.cpp /
                  wackywidget.cpp
FORMS           = fancybox.ui
TRANSLATIONS    = superapp_dk.ts /
                  superapp_fi.ts /
                  superapp_no.ts /
                  superapp_se.ts

当你运行lupdate或者lrelease时,你必须以命令行参数给出项目文件的名称。例如:lupdate myproject.pro。在本例中,支持四种语言:Danish、Finnish、Norwegian和Swedish。如果你使用qmake(或者tmake),你一般不需要给lupdate的附加项目文件;只要你加上TRANSLATIONS条目,你的qmake项目文件就会正常工作。

Qt应用的翻译过程分为三步:

第1步:运行lupdate,即使用命令:lupate project-file –ts ts-files。提取Qt应用的C++源代码中的可翻译文本,会产生一个翻译信息文件(.ts文件)。lupdate能识别出tr()结构和上面描述的QT_*_NOOP宏。lupdate读一个.pro项目文件,并产生或更新列出在项目文件中的.ts翻译源文件。

第2步:使用Qt语言学家(Qt Linguist)翻译工具软件打开.ts文件,然后,人工翻译.ts文件中源文本的字符串,将字符串的译文使用Qt Linguist工具加到.ts文件中。.ts文件是XML格式,你也可以手工编辑它们。

第3步:运行lrelease,即使用命令lrelease ts-files –qm qm-file。从.ts文件中得到只适用于最后使用的轻量级的信息文件(.qm文件)。你可以把.ts文件看成"源文件",把.qm文件看成"目标文件"。翻译者编辑的是.ts文件,可是你的应用的用户只需要.qm文件。这两种文件都是平台和地区(locale)无关的。你将对应用地每个发表版本重复这几步。lupdate工具会尽力重用以前的发表版本的译文。

在应用中,你必须使用QTranslator::load()来装载对应用户语言的译文文件,再使用QApplication::installTranslator()来安装它们。

如果你一直使用以前的Qt工具(findtr、msg2qm和mergetr),可以使用qm2ts来转换你以前的.qm文件。

虽然这些工具提供了生成.qm文件的方便途径,可任何能编写.qm文件的系统也都够用。你可以制做一个应用,以利用QTranslator::insert()把译文加入到QTranslator中,接着再利用QTranslator::save()写出一个.qm文件。用这种办法可以从任何你选择的源文件中产生译文。

Qt本身包含有大约400个也需要翻译为目标语言的字符串。在$QTDIR/translations下,你会找到French和German的译文文件,也可以作为翻译为其他语言的模板。

示例3:装载译文文件

下面的例子说明了如何装载译文文件,在这个例子,应用程序会根据不同的本地语言来安装相应的译文文件。

int main( int argc, char **argv )
{
    QApplication app( argc, argv );
 
    /*装载Qt翻译文件*/
QTranslator qt( 0 );  //装载没有父类的QTranslator对象
    //根据本地语言选择译文文件(.qm文件)
    qt.load( QString( "qt_" ) + QTextCodec::locale(), "." ); 
    app.installTranslator( &qt );
 
    //给应用程序字符串装载翻译文件
    QTranslator myapp( 0 );
    myapp.load( QString( "myapp_" ) + QTextCodec::locale(), "." );
    app.installTranslator( &myapp );
    ...
    return app.exec();
}

4.3 编码支持

当应用启动时,机器的地区(locale)决定了处理8-bit文本数据方法(如字体选择、文本显示、8-bit文本I/O和字符输入所用的8-bit编码)。

应用程序中有时会需要不同于缺省的本地8-bit编码。QTextCodec类和QTextStream类中函数,会支持很多对于用户数据的输入输出编码。例如,一个Cyrillic KOI8-R locale(俄罗斯的事实标准locale)的应用可能需要以ISO 8859-5编码输出Cyrillic。这样的代码可能会是:

QString string = ...; // 一些Unicode编码的文本
 
QTextCodec* codec = QTextCodec::codecForName( "ISO 8859-5" ); //使用指定的编码
QCString encoded_string = codec->fromUnicode( string ); //得到指定编码的字符串

对于把Unicode转换为本地8-bit编码,有一个快捷办法:QString的local8Bit()方法返回的就是这样的8-bit数据。另一个有用的快捷办法是utf8()方法,它以8-bit的UTF-8编码返回文本,UTF-8编码的好处是:如果Unicode完全是US-ASCII的话,它可以完全保留Unicode信息,而看起来又是一般的US-ASCII。

Qt提供完整的Unicode支持,包括输入法、字体、剪贴板、拖放和文件名。 文件I/O缺省为Latin-1,在QTextStream中带有Unicode选项。Unicode(UTF16或者UTF8)是包含了所有语言的字符编码方法,使用Unicode编码的软件,可以最大地提高了各国用户之间的文本兼容性。然而,有时在同一语言的群体内,本地编码标准也许更为合适。这个编码就是由QTextCodec::codecForLocale()所返回的编码方法。你还可以使用QTextCodec::loadCharmapFile()函数以构造一个数据驱动的编码器。

Qt所支持的字符编码类说明如表1。

表1 Qt中字符编码的类表
类名 功能说明
QEucJpCodec EUC到JP字符集之间的转换。
QEucKrCodec EUC到KR字符集之间的转换。
QGb18030Codec 中国GB18030/GBK/GB2312编码。
QGbkCodec 中国GBK编码。
QHebrewCodec Hebrew编码。
QJisCodec JIS字符集编码。
QSjisCodec Shift-JIS字符集编码。
QTextCodec 文本编码之间的转换。
QTextDecoder 基于状态(State-based)的解码器。
QTextEncoder 基于状态的编码器。
QTranslator 文本输出的国际化支持。
QTranslatorMessage Translator的信息及其属性。
QTsciiCodec Tscii编码。

5 QMetaObject元对象类

QMetaObject类装有有关Qt对象的元信息,包括运行时类型信息、属性系统。它负责信号与槽机制。每个在类声明中含有Q_OBJECT宏的类都有一个对应的元对象,即QMetaObject类对象,元对象是静态的,因此,只有一个元对象与这个类对应。

5.1 相关的数据结构

(1)QConnection、QConnectionList和QSignalVec类

QConnection类是内部使用的类,用于signal/slot机制中。不能直接用于应用程序。QObject中每个连接到外面的信号siganl有一个QConnection链表。QConnection类存储并可使用成员函数得到连接到的对象指针、连接的成员名及其类型、参数个数。QConnection类构造函数的声明列出如下:

QConnection::QConnection( const QObject *object, int member,
			  const char *memberName, int memberType )

QConnectionList类是一个QConnection对象链表,它从QPtrList继承,存储着各个QConnection对象指针。

QSignalVec类从QPtrVector继承,它是QConnection对象链表的数组,将每个信号的QConnection对象链表组织到一个数组中。

(2)元数据结构QMetaData及类属性结构QmetaEnum

结构QMetaData是信号与槽成员函数的元数据,用于描述信号或槽。结构QMetaData列出如下:

struct QMetaData				
{						
    const char *name;				// 信号或槽的名称
    const QUMethod* method;			// 信号或槽详细的方法描述
    enum Access { Private, Protected, Public };
    Access access;				//访问许可 
};
typedef const QMetaData QconstMetaData;

类QmemberDict从QasciiDict继承,是QmetaData的对象链表。

结构QmetaEnum是某类属性的描述,用于显示在Qt Designer等图形开发工具上。结构QmetaEnum说明如下:

{						
    const char *name;				// 某类属性名
    uint count;					// 属性条目数总数
    struct Item					// 一个属条目,name/value对
    {
	const char *key;
	int value;
    };
    const Item *items;				// 属性条目的数组
    bool set;					//这个类属性是否被看成一个集合
};

5.2 QmetaObject对象创建

每个含有Q_OBJECT宏的类都有一个元对象,这里以Qt类的元对象qtMetaObject为例说明元对象的建立及管理。

函数staticQtMetaObject是QObject类的成员函数,用来创建Qt类对应的元对象qtMetaObject,qtMetaObject是静态的QMetaObject类型指针。cleanUp_Qt是静态的全局QMetaObjectCleanUp对象。函数staticQtMetaObject分析如下:

// 将"QObject"及 &QObject::staticMetaObject插入到qt_metaobjects链表中
static QMetaObjectCleanUp cleanUp_Qt = QMetaObjectCleanUp( "QObject", 
                           Object::staticMetaObject );
 
QMetaObject* QObject::staticQtMetaObject()
{
    static QMetaObject* qtMetaObject = 0; //静态对象指针
    if ( qtMetaObject )
	return qtMetaObject;
 
#ifndef QT_NO_PROPERTIES
    //用于显示在Qt Designer工具中的属性对话框的表格中属性条目
    static const QMetaEnum::Item enum_0[] = {
	{ "AlignLeft",  (int) Qt::AlignLeft },
	...
	{ "AlignJustify", (int) Qt::AlignJustify },
	{ "WordBreak", (int) Qt::WordBreak }
    };
   ......
    static const QMetaEnum enum_tbl[] = {
   //在表格中的属性名,条目计数,各条目项,是否当作一个集合
	{ "Alignment", 10, enum_0, TRUE },
	{ "Orientation", 2, enum_1, FALSE },
	{ "TextFormat", 4, enum_2, FALSE },
	{ "BackgroundMode", 17, enum_3, FALSE },
	{ "DateFormat", 3, enum_4, FALSE }
    };
#endif
 
    qtMetaObject = new QMetaObject( "Qt", 0,
			  0, 0,
			  0, 0,
#ifndef QT_NO_PROPERTIES
			  0, 0,
			  enum_tbl, 5,
#endif
			  0, 0 );
    cleanUp_Qt.setMetaObject( qtMetaObject );
 
    return qtMetaObject;
}

下面分析上面的例子中用到的QMetaObject类成员函数及QMetaObjectCleanUp类成员函数。

类QMetaObject构造函数将各种元数据存入到QMetaObject元对象中,每个类有一个元对象对应,每个元对象使用链表存储槽或信号的元数据,使用私有类QMetaObjectPrivate对象存储属性数据。QMetaObject构造函数分析如下:

QMetaObject::QMetaObject( const char *const class_name, QMetaObject *super_class,
			  const QMetaData *const slot_data, int n_slots,//槽的元数据
			  const QMetaData *const signal_data, int n_signals,//信号的元数据
#ifndef QT_NO_PROPERTIES
			  const QMetaProperty *const prop_data, int n_props, //属性元数据,
			  const QMetaEnum *const enum_data, int n_enums, //属性条目数据
#endif
			  const QClassInfo *const class_info, int n_info )//类信息元数据
{
    classname = class_name;	// 类的名字
    superclass = super_class; //父元对象
    superclassname = superclass ? superclass->className() : 0;
    slotDict = init( slotData = slot_data, n_slots ); //建立槽的元数据链表
    signalDict = init( signalData = signal_data, n_signals ); //建立信号的元数据链表
 
    d = new QMetaObjectPrivate;
    reserved = 0;
  //将属性数据存入私有对象d中
#ifndef QT_NO_PROPERTIES
    d->propData = prop_data;
    d->numPropData = n_props;
    d->enumData = enum_data;
    d->numEnumData = n_enums;
#endif
    d->classInfo = class_info;
    d->numClassInfo = n_info;
 
    signaloffset = superclass ? ( superclass->signalOffset() + superclass->numSignals() ) : 0;
    slotoffset = superclass ? ( superclass->slotOffset() + superclass->numSlots() ) : 0;
#ifndef QT_NO_PROPERTIES
    propertyoffset = superclass ? ( superclass->propertyOffset() + superclass->numProperties() ) : 0;
#endif
}

函数init创建QMetaData对象链表并插入data数组所有元数据,函数init列出如下:

QMemberDict *QMetaObject::init( const QMetaData * data, int n )
{
    if ( n == 0 )				// nothing, then make no dict
	return 0;
  //创建QMetaData对象链表QAsciiDict
    QMemberDict *dict = new QMemberDict( optDictSize(n), TRUE, FALSE );
    Q_CHECK_PTR( dict );
    while ( n-- ) {				//将data数组所有元数据插入到链表中
	dict->insert( data->name, data );
	data++;
    }
    return dict;
}

类QMetaObjectCleanUp在moc产生的.cpp文件里被用作静态的全局对象,被用来删除由函数setMetaObject设置的QMetaObject元对象,函数setMetaObject设置QObject引用到metaObject。当metaObject被销毁时,metaObject为NULL。

所有类名及生成的对应静态元对象都存入到全局的链表qt_metaobjects中。构造函数QMetaObjectCleanUp用来将类名及生成的对应元对象func加入到链表qt_metaobjects中。函数列出如下:

static QAsciiDict<void> *qt_metaobjects = 0;
static int qt_metaobjects_count = 0;
typedef QMetaObject *(*QtStaticMetaObjectFunction)()
//func为元对象构造函数
QMetaObjectCleanUp::QMetaObjectCleanUp( const char *mo_name, QtStaticMetaObjectFunction func )
    : metaObject( 0 )
{
#ifdef QT_THREAD_SUPPORT
    QMutexLocker( qt_global_mutexpool ?
		  qt_global_mutexpool->get( &qt_metaobjects ) : 0 );
#endif // QT_THREAD_SUPPORT
    if ( !qt_metaobjects )
	qt_metaobjects = new QAsciiDict<void>( 257 );
    qt_metaobjects->insert( mo_name, (void*)func );
 
    qt_metaobjects_count++;
}
函数setMetaObject设置元对象给QMetaObjectCleanUp类内部使用的变量metaObject。函数列出如下:
void QMetaObjectCleanUp::setMetaObject( QMetaObject *&mo )
{
#if defined(QT_CHECK_RANGE)
    if ( metaObject )
	qWarning( "QMetaObjectCleanUp::setMetaObject: Double use of QMetaObjectCleanUp!" );
#endif
    metaObject = &mo;
}

5.3 Q_OBJECT宏及moc生成代码分析

使用信号与槽的类在类的声明中都必须加上Q_OBJECT宏,表示使用元对象系统,Q_OBJECT列出元对象的声明、类名、属性函数、字符串翻译函数等。Q_OBJECT的宏定义列出如下(在src/kernel/qobjectdefs.h中):

#define Q_OBJECT							/
public:									/
    virtual QMetaObject *metaObject() const { 				/
         return staticMetaObject();					///返回静态的元对象
    }									/
    virtual const char *className() const;				/
    virtual void* qt_cast( const char* ); 				/
    virtual bool qt_invoke( int, QUObject* ); 				/  //触发信号
    virtual bool qt_emit( int, QUObject* ); 				/ //发送信号
    QT_PROP_FUNCTIONS							/
    static QMetaObject* staticMetaObject();				/
    QObject* qObject() { return (QObject*)this; } 			/
    QT_TR_FUNCTIONS							/
private:								/
    static QMetaObject *metaObj;
 
 
//如果object对应的QMetaObject对象从T的QMetaObject对象继承,返回(T)object
template <typename T>
Q_INLINE_TEMPLATES T qt_cast(const QObject *object)
//如果object对应的QMetaObject对象为�T)0)->staticMetaObject()时,返回object,否则返回FALSE
{ return (T)qt_inheritedBy( ((T)0)->staticMetaObject(), object ); }
 
//属性函数
# define QT_PROP_FUNCTIONS /
    virtual bool qt_property( int id, int f, QVariant* v); /
    static bool qt_static_property( QObject* , int, int, QVariant* );
//翻译函数
#  define QT_TR_FUNCTIONS /
    static QString tr( const char *, const char * = 0 ); /
    static QString trUtf8( const char *, const char * = 0 );

元对象编译器moc根据类声明中的宏Q_OBJECT和Q_PROPERTY,以及signal及slot部分的定义生成附加的C++代码,下面用两个例子进行说明,示例1说明类声明在.h文件中,moc编译器生成moc_*.cpp附加文件,类声明中有slot部分。示例2说明类声明在*.cpp文件,moc编译器生成.moc附加文件。

示例1:QMessageBox 类经对象编译器moc产生的文件

$QTDIR/src/dialogs/qmessagebox.h文件是QMessageBox 类的定义,类声明中含有Q_OBJECT宏,经 moc编译器产生了$QTDIR/src/.moc/release-shared/moc-qmessagebox.cpp文件。

QMessageBox 类声明中与元对象相关的代码列出如下(在qmessagebox.h中):

class Q_EXPORT QMessageBox : public QDialog
{
    Q_OBJECT
    Q_ENUMS( Icon )
    Q_PROPERTY( QString text READ text WRITE setText )
    Q_PROPERTY( Icon icon READ icon WRITE setIcon )
    Q_PROPERTY( QPixmap iconPixmap READ iconPixmap WRITE setIconPixmap )
    Q_PROPERTY( TextFormat textFormat READ textFormat WRITE setTextFormat )
 
public:
    ......
private slots:
    void	buttonClicked();
    ......
}

moc编译器生成的QMessageBox 类对应的元对象函数列出如下(在moc-qmessagebox.cpp中):

#undef QT_NO_COMPAT
#include "../../dialogs/qmessagebox.h"
#include 
#include 
 
#include 
...
#include 
const char *QMessageBox::className() const
{
    return "QMessageBox";
}
 
QMetaObject *QMessageBox::metaObj = 0;
//将类名及元对象插入到qt_metaobjects链表中
static QMetaObjectCleanUp cleanUp_QMessageBox( "QMessageBox", &QMessageBox::staticMetaObject );
 
#ifndef QT_NO_TRANSLATION
QString QMessageBox::tr( const char *s, const char *c ) //字符串翻译函数
{
    if ( qApp )
	return qApp->translate( "QMessageBox", s, c, QApplication::DefaultCodec );
    else
	return QString::fromLatin1( s );
}
#ifndef QT_NO_TRANSLATION_UTF8
QString QMessageBox::trUtf8( const char *s, const char *c ) //字符串翻译函数
{
    if ( qApp )
	return qApp->translate( "QMessageBox", s, c, QApplication::UnicodeUTF8 );
    else
	return QString::fromUtf8( s );
}
#endif // QT_NO_TRANSLATION_UTF8
 
#endif // QT_NO_TRANSLATION
//得到各种元数据,生成元对象
QMetaObject* QMessageBox::staticMetaObject()
{
    if ( metaObj )
	return metaObj;
    QMetaObject* parentObject = QDialog::staticMetaObject();
#ifndef QT_NO_PROPERTIES
    static const QMetaEnum::Item enum_0[] = {
	{ "NoIcon",  (int) QMessageBox::NoIcon },
	{ "Information",  (int) QMessageBox::Information },
	{ "Warning",  (int) QMessageBox::Warning },
	{ "Critical",  (int) QMessageBox::Critical },
	{ "Question",  (int) QMessageBox::Question }
    };
    static const QMetaEnum enum_tbl[] = {
	{ "Icon", 5, enum_0, FALSE }
    };
#endif // QT_NO_PROPERTIES
    static const QUMethod slot_0 = {"buttonClicked", 0, 0 };
    static const QMetaData slot_tbl[] = {  //slot元数据
	{ "buttonClicked()", &slot_0, QMetaData::Private }
    };
#ifndef QT_NO_PROPERTIES // 类声明中Q_PROPERTY(…)对应的项经moc转化后得到属性元数据
    static const QMetaProperty props_tbl[4] = {
 	{ "QString","text", 0x3000103, &QMessageBox::metaObj, 0, -1 },
	{ "Icon","icon", 0x0107, &QMessageBox::metaObj, &enum_tbl[0], -1 },
	{ "QPixmap","iconPixmap", 0x6000103, &QMessageBox::metaObj, 0, -1 },
	{ "TextFormat","textFormat", 0x010f, &QMessageBox::metaObj, 0, -1 }
    };
#endif // QT_NO_PROPERTIES
  //创建元对象
    metaObj = QMetaObject::new_metaobject(
	"QMessageBox", parentObject,
	slot_tbl, 1,
	0, 0,
#ifndef QT_NO_PROPERTIES
	props_tbl, 4,
	enum_tbl, 1,
#endif // QT_NO_PROPERTIES
	0, 0 );
    cleanUp_QMessageBox.setMetaObject( metaObj );
    return metaObj;
}
//判断类名是不是这个类
void* QMessageBox::qt_cast( const char* clname )
{
    if ( !qstrcmp( clname, "QMessageBox" ) )
	return this;
    return QDialog::qt_cast( clname );
}
//判断激活的是否是这个元对象槽函数
bool QMessageBox::qt_invoke( int _id, QUObject* _o )
{
    switch ( _id - staticMetaObject()->slotOffset() ) {
    case 0: buttonClicked(); break;
    default:
	return QDialog::qt_invoke( _id, _o );
    }
    return TRUE;
}
//判断发出信号的是否是这个元对象
bool QMessageBox::qt_emit( int _id, QUObject* _o )
{
    return QDialog::qt_emit(_id,_o);
}
#ifndef QT_NO_PROPERTIES
//判断是否是这个元对象的属性
bool QMessageBox::qt_property( int id, int f, QVariant* v)
{
    switch ( id - staticMetaObject()->propertyOffset() ) {
    case 0: switch( f ) {
	case 0: setText(v->asString()); break;
	case 1: *v = QVariant( this->text() ); break;
	case 3: case 4: case 5: break;
	default: return FALSE;
    } break;
    case 1: switch( f ) {
	case 0: setIcon((Icon&)v->asInt()); break;
	case 1: *v = QVariant( (int)this->icon() ); break;
	case 3: case 4: case 5: break;
	default: return FALSE;
    } break;
    case 2: switch( f ) {
	case 0: setIconPixmap(v->asPixmap()); break;
	case 1: if ( this->iconPixmap() ) *v = QVariant( *iconPixmap() ); break;
	case 3: case 4: case 5: break;
	default: return FALSE;
    } break;
    case 3: switch( f ) {
	case 0: setTextFormat((TextFormat&)v->asInt()); break;
	case 1: *v = QVariant( (int)this->textFormat() ); break;
	case 3: case 4: case 5: break;
	default: return FALSE;
    } break;
    default:
	return QDialog::qt_property( id, f, v );
    }
    return TRUE;
}
 
bool QMessageBox::qt_static_property( QObject* , int , int , QVariant* ){ return FALSE; }
#endif // QT_NO_PROPERTIES

$QTDIR/src/dialogs/qmessagebox.cpp文件中的类QMessageBoxLabel声明含有Q_OBJECT宏,经 moc编译器产生了$QTDIR/src/.moc/release-shared/qmessagebox.moc文件,QMessageBoxLabel的声明后面加上了#include "qmessagebox.moc"语句。QMessageBoxLabel声明列出如下:

class QMessageBoxLabel : public QLabel
{
    Q_OBJECT
public:
    QMessageBoxLabel( QWidget* parent ) : QLabel( parent, "messageBoxText")
    {
	setAlignment( AlignAuto|ExpandTabs );
    }
};
#include "qmessagebox.moc" //包含有moc生成的元对象函数

6 进程间通信--QCopChannel

信号与槽提供了对象间通信的能力,而QCopChannel类提供了几个进程之间的通信能力。QCOP(Communication Protocol)是Qt的一种通信协议。它允许不同的客户在同一进程或不同的进程之间进行通信。目前,这种QCOP机制仅用于Qt/Embedded中。在Qt/X11和Windows上使用DCOP和 COM标准。

DCOP的全称是桌面通信协议(Desktop Communication Protocol),是给KDE桌面系统开发的一种基于信号的IPC/RPC机制,DCOP是一个客户端到客户端的通信协议,以基于X11R6标准库LibICE的服务器为中间媒介。这个协议支持消息传送和使用XML-RPC到DCOP"网关"的远端程序调用。它提供KDE应用程序的进程间及远程应用程序间通讯的功能。

QCopChannel继承自QObject 类,它提供了send()、isRegistered() 等静态函数,用户调用QCopChannel类构造函数创建一个信道,并重载receive() 函数或者使用 connect() 函数将信号与槽函数连接起来。receive()函数允许QCopChannel子类处理从它们的通道上接收的消息。

QCOP机制使用了Qt/Embedded内部的客户/服务器通信机制( ' ' ' '在第四章嵌入窗口系统中详细介绍),QCopChannel类构造函数及send函数分别使用了如下代码:

// 通知服务器注册这个信道。qt_fbdpy是QDisplay类的全局对象
qt_fbdpy->registerChannel( channel );
 
qt_fbdpy->sendMessage( channel, msg, data );

在嵌入设备的窗口系统Qtopia(QPE)中,提供了QCopEnvelope类用于进程间通信。该类实际上是对QCopChannel进行了封装,方便用户的使用。发送消息时使用QCopEnvelope 类,接收消息使用QCopChannel类。

发送消息的方法如下:

QCopEnvelope e(channelname, messagename);

如果消息携带参数,则还需要将消息参数发送出去,方法如下:

e << parameter1 << parameter2 << ...;

Qtopia中的通道名都以"QPE/"开始,在接收消息时,通通道与slot函数相连接,方法如下:

myChannel = new QCopChannel( "QPE/FooBar", this );
connect( myChannel, SIGNAL(received(const QCString &, const QByteArray &)),
          this, SLOT(fooBarMessage( const QCString &, const QByteArray &)) );

示例:QCopChannel通信过程

下面这个例子说明QCopChannel通信过程,先创建一个通道,这个通道实际上是一个在内存中的文件,再在一个类中建立发送消息函数,在另一个类中建立接收消息函数,具体如下:

(1)在一个应用程序的类构造函数中定义通道

//class1.cpp
MyClass1::MyClass1(QObject* parent, const char* name ) :
    QObject( parent, name )
{
    myChannel = new QCopChannel("System/Shell", this );
……
}

(2)在一个应用程序的类中发送消息。

//class2.cpp
MyClass2::sentMessage(QObject* parent, const char* name ) :
    QObject( parent, name )
{
    ......
    QByteArray ba;
    QDataStream stream( ba, IO_WriteOnly );
    stream << QString("cat") << QString("file.txt");
    QCopChannel::send( "System/Shell", "execute(QString,QString)", ba );
}

(3)在另一个应用程序的类中接收消息。

void MyClass::receive( const QCString &msg, const QByteArray &data )
{
    QDataStream stream( data, IO_ReadOnly );
    if ( msg == "execute(QString,QString)" ) {
        QString cmd, arg;
        stream >> cmd >> arg;
        ...
    } else if ( msg == "delete(QString)" ) {
        QString filenname;
        stream >> filename;
        ...
    } else ...
}

7 窗口部件类

窗口部件的父类一般是QWidget类、QFrame类和QScrollView类。这三个类是继承关系,分别是祖父、父、子的关系。QWidget类是所有窗口部件的基类,QFrame类是带有框架(如:带有凸出边界的QLabel窗口部件)的窗口部件的基类。QScrollView类是视图窗口部件(如:QIconView类)的基类。窗口部件类的继承关系见图13。

 

图13 窗口部件类的继承关系图

QWidget类、QFrame类和QScrollView类的窗口从外观上能看出区别,见图14。下面说明这三个窗口部件类的区别:

QWidget类是所有窗口部件的基类,它在一个区域内定义了窗口部件的各种事件、窗口显示、窗口层次管理等基本功能函数,不能单独作窗口部件使用,窗口部件类必须从QWidget类继承。如:QMainWindow类在继承QWidget类基础上加入了菜单栏。

QFrame类从QWidget类继承,它含有框架,即含有用户指定的边框(如:凸出的边框),并对边框的各种属性进行管理。因此,含有边框的窗口部件类都必须从它继承。

QScrollView类从QFrame类继承,是各种视图窗口类的基类。当视图中的控件超出窗口显示范围时,视图会自动加下滚动条。QScrollView类在QFrame类的基础上,增加对视图特性的管理函数。

 

图14 QWidget类、QFrame类和QScrollView类部件

7.1 窗口部件的基类QWidget

(1) QWidget类的基类说明

QWidget类从QObject类和QPaintDevice类继承,QObject类管理着对象树、信号与槽机制及其元对象等。QPaintDevice类是能被绘画的对象的基类。 它是绘画设备的抽象,是一个能使用QPainter在其中绘画的二维空间抽象。它被子类QWidget, QPixmap, QPicture和 QPrinter类继承。一个绘画设备的座标系统的原点在屏幕的左上角位置。X轴向右增长,Y轴向下增长,单位是像素点。

QPaintDevice类使用PDevCmd枚举类型列举了常用的绘画命令,其成员函数可以设置或得到绘制设备的一些通用属性,如:颜色映射、可视性等。对于Qt/X11来说,调用Xlib库函数,对于Qt/Embedded来说,使用Qt/Embedded服务器接口函数*qwsDisplay()等。全局的位图拷贝函数bitBlt提供了从源绘画设备对象向目的地绘画设备对象进行像素拷贝。

(2) QWidget类功能说明

QWidget类是所有用户界面对象的基类。QWidget窗口部件是用户界面的基本单元:它是屏幕上的矩形区域窗口,多个窗口按Z轴顺序排列,从窗口系统接收鼠标、键盘和其它事件。一个窗口部件可以被它的父窗口部件或者它前面的窗口部件盖住一部分。

不被嵌入到一个父窗口部件的窗口部件被叫做顶级窗口部件。一个没有父窗口部件的窗口部件一直是顶级窗口部件。通常顶级窗口部件是带有框架和标题栏的窗口。在Qt中,QMainWindow和和各种QDialog的派生类是最常见的顶级窗口。

非顶层窗口部件是子窗口部件。它们是它们的父窗口部件中的子窗口。你通常不能可见地从它的父窗口部件中区分一个子窗口部件。在Qt中的绝大多数其它窗口部件仅仅作为子窗口部件。

QWidget有很多成员函数,但是它们中的一些有几乎没有功能,有很多继承它的子类提供了实际的功能,比如QPushButton、QListBox和QTabDialog等等。QWidget类成员函数列出如表15。

表15 QWidget类成员函数分类
函数分类 方法函数名
窗口函数 show()、hide()、raise()、lower()、close()。
顶级窗口 caption()、setCaption()、icon()、setIcon()、iconText()、setIconText()、isActiveWindow()、setActiveWindow()、showMinimized()、showMaximized()、showFullScreen()、showNormal()。
窗口内容 update()、repaint()、erase()、scroll()、updateMask()。
几何形状 pos()、size()、rect()、x()、y()、width()、height()、sizePolicy()、setSizePolicy()、sizeHint()、updateGeometry()、layout()、move()、resize()、setGeometry()、frameGeometry()、geometry()、childrenRect()、adjustSize()、mapFromGlobal()、mapFromParent()、mapToGlobal()、mapToParent()、maximumSize()、minimumSize()、sizeIncrement()、setMaximumSize()、setMinimumSize()、setSizeIncrement()、setBaseSize()、setFixedSize()。
模式 isVisible()、isVisibleTo()、visibleRect()、isMinimized()、isDesktop()、isEnabled()、isEnabledTo()、isModal()、isPopup()、isTopLevel()、setEnabled()、hasMouseTracking()、setMouseTracking()、isUpdatesEnabled()、setUpdatesEnabled()。
外观、风格 style()、setStyle()、cursor()、setCursor()、font()、setFont()、palette()、setPalette()、backgroundMode()、setBackgroundMode()、colorGroup()、fontMetrics()、fontInfo()。
键盘焦点函数 isFocusEnabled()、setFocusPolicy()、focusPolicy()、hasFocus()、setFocus()、clearFocus()、setTabOrder()、setFocusProxy()。
事件处理 event()、mousePressEvent()、mouseReleaseEvent()、mouseDoubleClickEvent()、mouseMoveEvent()、keyPressEvent()、keyReleaseEvent()、focusInEvent()、focusOutEvent()、wheelEvent()、enterEvent()、leaveEvent()、paintEvent()、moveEvent()、resizeEvent()、closeEvent()、dragEnterEvent()、dragMoveEvent()、dragLeaveEvent()、dropEvent()、childEvent()、showEvent()、hideEvent()、customEvent()。
变化处理 enabledChange()、fontChange()、paletteChange()、styleChange()、windowActivationChange()。
系统函数 parentWidget()、topLevelWidget()、reparent()、polish()、winId()、find()、metric()。
这是什么的帮助 customWhatsThis()。
内部核心函数 focusNextPrevChild()、wmapper()、clearWFlags()、getWFlags()、setWFlags()、testWFlags()。

每一个窗口部件构造函数至少接受两个或三个标准参数,这些参数说明如下:

QWidget *parent = 0是新窗口部件的父窗口部件。如果为0(默认),新的窗口部件将是一个顶级窗口部件。如果不是,它将会使parent的一个孩子,并且被parent的几何形状所强迫(除非你指定WType_TopLevel作为窗口部件标记)。

const char *name = 0是新窗口部件的窗口部件名称。你可以使用name()来访问它。窗口部件名称很少被程序员用到,但是对于图形用户界面构造程序,比如Qt设计器,是相当重要的(你可以在Qt设计器中命名一个窗口部件,并且在你的代码中使用这个名字来连接它)。dumpObjectTree()调试函数也使用它。

WFlags f = 0(在可用的情况下)设置窗口部件标记,默认设置对于几乎所有窗口部件都是适用的,但是,举例来说,一个没有窗口系统框架的顶级窗口部件,你必须使用特定的标记。

7.2 QFrame类

QFrame类是有框架的窗口部件的基类。它绘制框架并可调用被子类重新实现的虚函数drawContents()填充这个框架。还有另外两个较少用到函数drawFrame()和frameChanged(),它们被用来绘制框架。

例如:QPopupMenu从QFrame类继承,可以将菜单边框凸出显示;QProgressBar从QFrame类继承,可以将进度条有"凹陷"的外观。这些边框外观可以通过QFrame类方法函数来改变,方法列出如下:

QLabel label(...);
    label.setFrameStyle( QFrame::Panel | QFrame::Raised );
    label.setLineWidth( 2 );
 
    QProgressBar pbar(...);
    label.setFrameStyle( QFrame::NoFrame );


QFrame类可以直接被用来创建没有任何内容的简单框架,但通常情况下,使用QFrame类的子类QHBox或QVBox,因为它们可以自动地布置你放到QFrame窗口中的窗口部件。

QFrame类窗口部件有四个属性描述窗口框架:frameStyle()、lineWidth()、midLineWidth()和margin()。 框架风格由框架外形和阴影风格决定。框架外形的种类有NoFrame、Box、Panel、StyledPanel、PopupPanel、WinPanel、ToolBarPanel、MenuBarPanel、HLine和VLine,阴影风格的类型有Plain、Raised和Sunken。

框架的外观还与线宽、中间线宽和边白有关。线宽指框架边界的宽度。中间线宽指在框架中间的另外一条线的宽度,它使用第三种颜色来得到一个三维的效果。注意中间线只有在Box、HLine和VLine这些凸起和凹陷的框架中才被绘制。边白指框架和框架内容之间的间隙。

由风格与线宽组成的框架外观组合如图15。图中列出了各种外观的QFrame类窗口部件。

 

图15 由风格与线宽组成的框架外观组合

7.3 QScrollView类

QScrollView窗口部件提供了在需要时可以出现滚动条的滚动区域。QScrollView是一个很大的画布,这个画布可它下面的窗口支持的坐标大得多。例如:一个很大的网页,长度超过了窗口限制,必须使用滚动条拖动才能阅读超出窗口的内容。

QScrollView类重新实现了drawContents()函数,并且可以使用resizeContents()设置查看区域的大小。使用addChild()和moveChild()在视图中增加和移动子窗口部件。

QScrollView有四个孩子窗口部件,它们分别是:viewport()、verticalScrollBar()、horizontalScrollBar()和cornerWidget()。viewport()是视图在窗口中的可见部分。通过滚动条的滚动,从viewport()上可按部分依次查看整个视图的内容。它们在窗口上的位置如图15。

QScrollView类有三种风格的窗口结构:单个较大的子窗口部件,带有一些窗口部件的平面区域和带有许多窗口部件的平面区域。下面对这三种风格的窗口分别进行说明:

(1) 单个较大的子窗口部件

QScrollView的最简单的用法是在一个不超过4000像素的滚动区域(这是X11服务器的最大可靠尺寸),放入一个大的子窗口部件在QScrollView中。这个子窗口部件是滚动视图的viewport()的孩子,它通过addChild()被添加。在视图中添加一个子窗口部件的方法如下:

QScrollView* sv = new QScrollView(...); //创建视图
    //给视图的viewport()类添加单个子窗口部件
    QVBox* big_box = new QVBox(sv->viewport()); 
    sv->addChild(big_box); //给单个子窗口部件添加孩子

 

还可继续在滚动视图中单一孩子中添加任意子窗口部件,方法如下:

QLabel* child1 = new QLabel("CHILD", big_box);
    QLabel* child2 = new QLabel("CHILD", big_box);
    QLabel* child3 = new QLabel("CHILD", big_box);


这样,在视图中,viewport()有一个孩子——大的QVBox。这个QVBox有三个QLabel对象作为子窗口部件。当视图被滚动,QVBox被移动,从viewport()窗口可以依次按部分查看QVBox及它的孩子,就好像几个子窗口部件被移动了一样。QScrollView类的viewport()子类有单个较大的子窗口部件的情况如图15。

文件:Linux Qt core introduction 09.png

图15 QScrollView类的viewport()子类有单个较大的子窗口部件

(2) 带有一些窗口部件的平面区域

QScrollView有非常大的滚动区域,在这个区域上少量窗口部件的高度或宽度超过4000像素。这时,你需要调用resizeContents()来设置区域的大小,并且重新实现drawContents()来绘制内容。你可以使用addChild()给viewport()添加孩子,而不是给viewport()的一个子类添加孩子。方法如下:

QScrollView* sv = new QScrollView(...);
    QLabel* child1 = new QLabel("CHILD", sv->viewport());
    sv->addChild(child1);
    QLabel* child2 = new QLabel("CHILD", sv->viewport());
    sv->addChild(child2);
    QLabel* child3 = new QLabel("CHILD", sv->viewport());
    sv->addChild(child3);


这样,viewport()有三个QLabel对象作为子窗口部件。当视图被滚动,viewport()会分别看到子窗口部件。QScrollView带有一些窗口部件的平面区域的情况如图17。

图17 QScrollView带有一些窗口部件的平面区域的情况

(3) 带有许多窗口部件的平面区域

QScrollView是非常大的滚动区域,在这个滚动区域中,有许多窗口部件的高度或宽度超过4000像素。这时,你需要调用resizeContents()设置区域的大小并重新实现drawContents()来绘制内容。然后你调用函数enableClipper(TRUE)把viewport()设置为比窗口大的区域,再向viewport()的添加孩子。方法如下:

QScrollView* sv = new QScrollView(...);
    sv->enableClipper(TRUE);
    QLabel* child1 = new Qlabel(“CHILD”, sv->viewport());
    sv->addChild(child1);
    Qlabel* child2 = new Qlabel(“CHILD”, sv->viewport());
    sv->addChild(child2);
    Qlabel* child3 = new Qlabel(“CHILD”, sv->viewport());
    sv->addChild(child3);


这样,clipper()有一个孩子:viewport()。viewport()有同样的三个标签作为子窗口部件。当视图被滚动时,viewport()被移动,它的孩子就像通常的子窗口部件那样被移动。QScrollView带有许多窗口部件的平面区域的情况如图18。

 

图18 QScrollView带有许多窗口部件的平面区域的情况

你可以使用上面所述三种方法中的一种来在视图中使用子窗口部件。你在滚动区域中看到的窗口部件是viewport()窗口部件,而不是QScrollView本身。因此你只有使用viewport()->setMouseTracking(TRUE)才能打开鼠标跟踪。

鼠标播放事件被传送给父对象,为了使拖放生效,你可以在QScrollView上设置setAcceptDrops(TRUE)。对于视图来说,常需要将鼠标在viewport()中的屏幕相对位置座标换算到视图的绝对座标,这需要使用viewportToContents()函数。

为了在滚动区域中处理鼠标事件,请重新实现contentsMousePressEvent(),而不是mousePressEvent(),因为contentsMousePressEvent()提供了滚动视图坐标系统中的转换事件。如果你重新实现mousePressEvent(),那么,只有当你QScrollView的cornerWidget()部件被点击时,mousePressEvent()才能得到调用。。

8 Qt风格(style)机制

Qt的风格机制实现了可以通过插件或风格类来改变的图形用户接口(GUI)的外观,例如Unix平台上通常使用Motif、CDE风格。在嵌入设备上常有主题(theme)或外观设置应用程序,它可以给嵌入设备设置不同风格的GUI图形界面。这个应用程序使用的就是Style(风格)机制。

8.1 风格类

QStyle是风格类的基类,为了设置窗口部件的UI界面风格,QStyle类定义了大量的枚举类型和十几个函数。枚举类型表示界面上的不同元素(如组合框中的按钮,按钮的边框等);函数控制图形用户界面的绘制,QStyle大多数成员函数只有声明而没有函数实现,他们的实现是通过在QCommonStyle、QWindowStyle、QMotifStyle及其子类中进行重载来实现的。风格类的继承关系图如图1。QStyle类实现了3个函数函数说明如下:

drawItem(): 负责绘制文本和象素图。

itemRect(): 返回文本或图像所占的区域。

visualRect(): 返回逻辑坐标,这个函数使Qt实现right-to-left风格(阿文、维文传统是文本从右向左显示,因此控件布局也是从右向左)。

图1显示了Qt中与风格相关的类的继承关系。

 

图1 风格相关类的继承关系图

按照对GUI图形界面的显示效果的影响,风格被分成不同枚举类型,每个枚举类型按照Qt的各种基本图形界面元素被分成不同的枚举项。表13对风格枚举类型进行说明:

表13 风格枚举类型说明
风格枚举类型 枚举类型说明
QStyle::ComplexControl 代表了一个复杂控制类型(如:SpinWidget、ComboBox、TitleBar等),它依赖于用户的点击或按键操作有不同的行为。
QStyle::ContentsType 代表了一个容积类型,它被用来计算各种widgets的容积尺寸。
QStyle::ControlElement 代表了一个控制元素,它是执行一些动作或显示信息给用户的widget的一部分。如:按钮的标签。
QStyle::PixelMetric 代表了一个像素点距离,它是风格依赖的尺寸,如:PM_ButtonMargin表示按钮标签与框架之间的空白区域的大小。
QStyle::PrimitiveElement 代表了一个风格的基本图形界面元素。它是一个通常的GUI元素,如:QStyle::PE_ArrowUp表示向上的箭头。
QStyle::StyleFlags 代表了一个基本图形界面元素的绘画标志。如:QStyle::Style_Enabled表示widget被激活时的风格。
QStyle::StyleHint 代表了一个StyleHint,它是一个常规的外观。如:QStyle::SH_ScrollBar_BackgroundMode表示一个QScrollBar的背景模式。
QStyle::StylePixmap 代表了一个风格位图。它是遵循一些存在的GUI风格的位图。如:QStyle::SP_MessageBoxWarning 表示警告信息的图标。
QStyle::SubControl 代表了在一个复杂控制的子控制。
QStyle::SubRect 代表了一个widget的子区域。Style使用这些区域画widget的不同部分。

如果用户需要修改界面风格的某一部分,就必须重载QStyle中描述这部分的相关绘制函数。下面只简单地说明这几个绘制函数,详细的说明可参考Qt文档:

drawPrimitive(......) 这个函数在指定区域绘制基本图形元素,如QSpinBox中的带箭头的按钮等。

drawComplexControl(......) 这个函数在指定区域用指定的颜色绘制复杂窗口部件,如SpinWidget,comboBox,slider,listView等。

drawComplexControlMask(...)这个函数在指定区域用指定的颜色以位掩码的方式绘制复杂窗口部件。

drawItem (......) 这个函数绘制指定区域中的文本或位图。

创建Style的方法是先选择一个风格基类,在用户的风格类中重载基类的函数。再从你的应用程序中使用这个新的风格类,或用一个插件来使用这个风格类。选择基类时,你可以选择QStyle或其它的风格派生类作为父类,如:QWindowsStyle或QMotifStyle。

示例:创建一个用户风格类CustomStyle

在这个例子中,我们首先改变在QWindowsStyle风格下的标准箭头的外观。箭头是PrimitiveElements类型(即GUI基本元素),使用函数drawPrimitive()绘画。在这个简单例子中,我们选择了QWindowsStyle作为父类。

CustomStyle类声明列出如下:

#include 
 
class CustomStyle : public QWindowsStyle {
    Q_OBJECT
public:
    CustomStyle();
    ~CustomStyle();
 
    void drawPrimitive( PrimitiveElement pe, //绘制基本图形元素
                        Qpainter *p,
                        const Qrect & r, //表示一个矩形区域
                       /*表示一个部件(widget)的颜色组,color group含有部件绘制自己时使用的各种颜色,如前景色、背景色等*/
                        const QcolorGroup & cg, 
                        //控制如何绘制图形界面元素的标志
                        Sflags flags = Style_Default,
                       /*绘制不同的部件(widget)时会需要不同的参数,如绘制面板(panel)可能需要线宽作为额外参数。*/
                        const QstyleOption & = QstyleOption::Default ) const;
 
private:
    //取消了拷贝构造函数和‘=’操作符。
    CustomStyle( const CustomStyle & );
    CustomStyle& operator=( const CustomStyle & );
};


下面是用户风格类CustomStyle的实现代码:

CustomStyle::CustomStyle()
{
}
 
CustomStyle::~CustomStyle()
{
}
 
void CustomStyle::drawPrimitive( PrimitiveElement pe,
                                 QPainter * p,
                                 const QRect & r,
                                 const QColorGroup & cg,
                                 SFlags flags,
                                 const QStyleOption & opt ) const
{
    // 我们仅对箭头感兴趣
    if (pe >= PE_ArrowUp && pe <= PE_ArrowLeft) {
        QPointArray pa( 3 );
        // 使箭头覆盖它所支持的绘画区域的一半。
        int x = r.x();
        int y = r.y();
        int w = r.width() / 2;
        int h = r.height() / 2;
        x += (r.width() - w) / 2;
        y += (r.height() - h) /2;
 
        switch( pe ) {
        case PE_ArrowDown:
            pa.setPoint( 0, x, y );
            pa.setPoint( 1, x + w, y );
            pa.setPoint( 2, x + w / 2, y + h );
            break;
        case PE_ArrowUp:
            pa.setPoint( 0, x, y + h );
            pa.setPoint( 1, x + w, y + h );
            pa.setPoint( 2, x + w / 2, y );
            break;
        case PE_ArrowLeft:
            pa.setPoint( 0, x + w, y );
            pa.setPoint( 1, x + w, y + h );
            pa.setPoint( 2, x, y + h / 2 );
            break;
        case PE_ArrowRight:
            pa.setPoint( 0, x, y );
            pa.setPoint( 1, x, y + h );
            pa.setPoint( 2, x + w, y + h / 2 );
            break;
        default: break;
 
        }
 
        //使用不同的颜色指示箭头enabled/disabled状态。
        if ( flags & Style_Enabled ) {
            p->setPen( cg.mid() );
            p->setBrush( cg.brush( QColorGroup::ButtonText ) );
        } else {
            p->setPen( cg.buttonText() );
            p->setBrush( cg.brush( QColorGroup::Mid ) );
        }
        p->drawPolygon( pa ); //画箭头
    } else {
        // 让基类处理其它的基本元素
        QWindowsStyle::drawPrimitive( pe, p, r, cg, flags, data );
    }
}


在Qt应用程序中有几种方法使用用户风格。最简单的方法是在应用程序的main()函数中加入下面的行:

int main( int argc, char ** argv )
{
    QApplication::setStyle( new CustomStyle() );
    //创建QApplication对象等其它通常的例程
  ......
}


注意你还必须在你的项目文件中包括customstyle.h和customstyle.cpp 文件。

你还可以以插件的形式创建stly,这时style作为共享对象在运行时被装载。编译你的style插件,并把它放在$QTDIR/plugins/styles目录里。对你已存在的应用程序可以使用下面的命令在运行时自动装载style插件:

./application -style custom

8.2 窗口系统如何更新风格

函数Global::applyStyle()是窗口系统风格更新的顶层函数,它通过系统信道"QPE/System"发送消息"applyStyle()"到各个应用程序,应用程序会根据这个消息调用函数applyStyle设置它的窗口风格。函数Global::applyStyle()调用层次图如图21。

 

Linux Qt core introduction 03.gif
图21 函数Global::applyStyle()调用层次图

函数Global::applyStyle()列出如下:

void Global::applyStyle()
{
#ifndef QT_NO_COP
    QCopChannel::send( "QPE/System", "applyStyle()" );
#else
    ((QPEApplication *)qApp)->applyStyle(); // apply without needing QCop for floppy version
#endif
}
 
 
void QPEApplication::systemMessage( const QCString &msg, const QByteArray &data)
{
    ......
    else if ( msg == "applyStyle()" ) {
	applyStyle();
    }
    ......
}


函数applyStyle应用风格,设置调色板及字体。还设置应用程序的装饰风格类。函数applyStyle读取配置文件中的风格类名、字体名、颜色值及位图,对应用程序的各个窗口设置风格,方法是得到应用程序的窗口部件链表,遍历各个窗口部件,调用风格类函数polish函数润饰窗口,设置窗口部件的调色板颜色值以及窗口部件的字体。

函数applyStyle分析如下:

void QPEApplication::applyStyle()
{
#ifdef Q_WS_QWS
    QString styleName;
    //读取qpe.conf配置文件中Appearance组中Style选项
    Config config( "qpe" );
    config.setGroup( "Appearance" );
    styleName = config.readEntry( "Style", "Qtopia" );
 
    //设置风格到窗口部件
    internalSetStyle( styleName );
 
    //从配置文件中读取颜色值,设置调色板    
    setPaletteEntry( tempPal, config, Button, "#F0F0F0" );
    setPaletteEntry( tempPal, config, Background, "#EEEEEE" );
    ......
    setPalette( pal, TRUE ); //设置应用程序的缺省调色板为pal
 
    color = config.readEntry( "AlternateBase", "#EEF5D6" );
    if ( color[0] == '#' ) //存在颜色值,使用颜色值
	style().setExtendedBrush(QStyle::AlternateBase, QColor(color));
    else {//不存在颜色值,则是位图名,装载位图
	QPixmap pix;
	pix = Resource::loadPixmap(color);
	style().setExtendedBrush(QStyle::AlternateBase, QBrush(QColor("#EEF5D6"),pix));
    }
   ......
    //读取窗口装饰配置,设置装饰对象,并重绘制窗口
    QString dec = config.readEntry( "Decoration", "Qtopia" );
    QString decTheme = config.readEntry( "DecorationTheme", "" );
    if ( dec != d->decorationName || !decTheme.isEmpty()) {
	qwsSetDecoration( new QPEDecoration( dec ) );// 设置装饰对象,并重绘制窗口
	d->decorationName = dec;
	d->decorationTheme = decTheme;
    }
 
    // 读取字体配置
    QString ff = config.readEntry( "FontFamily", font().family() );
    int fs = config.readNumEntry( "FontSize", font().pointSize() );
    QFont fn(ff,fs);
 
    //根据字体的大小算出图标高度,并将高度值存入配置文件
#ifndef QPE_FONT_HEIGHT_TO_ICONSIZE
#define QPE_FONT_HEIGHT_TO_ICONSIZE(x) (x+1)
#endif
    int is = config.readNumEntry( "IconSize", -1 );
    if ( is < 0 ) {
	QFontMetrics fm(fn);
	config.writeEntry( "IconSize", QPE_FONT_HEIGHT_TO_ICONSIZE(fm.height()) );
	config.write(); //写入图标高度(根据字体大小计算得到)
    }
 
    setFont( fn, TRUE ); //设置字体
 
#endif
}


函数internalSetStyle创建风格对象,并设置风格。函数列出如下:

void QPEApplication::internalSetStyle( const QString &style )
{
  ......
    QStyle *newStyle = 0;
    if ( style == "QPE"  || style == "Qtopia" ) {
	newStyle = new QPEStyle;
    } else {
	newStyle = QStyleFactory::create(style);
    }
 
    if ( !newStyle ) {
	newStyle = new QPEStyle;
	d->styleName = "QPE";
    } else {
	d->styleName = style;
    }
 
    if (&qApp->style() != newStyle)
	setStyle( newStyle ); //设置风格
 
}


函数setStyle设置应用程序的GUI风格到参数style。风格对象的拥有关系被传递给QApplication,这样在QApplication在删除或一个新风格被设置时,QApplication将删除style对象。

在切换风格时,颜色调色板将被设置回初始的或系统缺省状态,这是必须的,因为某一风格必须采用完全与风格相适应的颜色调色板。

函数setStyle分析如下:

void QApplication::setStyle( QStyle *style )
{
    QStyle* old = app_style;
    app_style = style;
#ifdef Q_WS_X11
    qt_explicit_app_style = TRUE;
#endif // Q_WS_X11
  //如果启动失败,即一个QApplication对象还没被创建,删除旧的风格对象
    if ( startingUp() ) {
	delete old;
	return;
    }
 
    // 清除旧的风格
    if (old) {
	if ( is_app_running && !is_app_closing ) {
	    QWidgetIntDictIt it( *((QWidgetIntDict*)QWidget::mapper) );
	    register QWidget *w;
	    while ( (w=it.current()) ) {		//遍历所有的窗口部件
		++it;
		if ( !w->testWFlags(WType_Desktop) &&	//除了Qtopia desktop外
		     w->testWState(WState_Polished) ) { // 已被润饰(Polished)
		    old->unPolish(w);//在窗口部件动态润饰之前,必须取消前一次的润饰参数
		}
	    }
	}
	old->unPolish( qApp );
    }
 
    /*在润饰应用程序之前关注某一GUI风格的可能的调色板需求。因为style可能通过函数QApplication::setStyle()调用它自己。*/
    if ( !qt_std_pal )
	qt_create_std_palette(); //创建标准调色板对象qt_std_pal
    QPalette tmpPal = *qt_std_pal;
    setPalette( tmpPal, TRUE ); //设置缺省的应用程序调色板app_pal = tmpPal
 
    // 使用新的风格初始化应用程序
    app_style->polish( qApp );
 
    //如果必要,重新润饰存在的窗口部件
    if (old) {
	if ( is_app_running && !is_app_closing ) {
	    QWidgetIntDictIt it( *((QWidgetIntDict*)QWidget::mapper) );
	    register QWidget *w;
	    while ( (w=it.current()) ) {		//遍历所有的窗口部件
		++it;
		if ( !w->testWFlags(WType_Desktop) ) {	//除了Qtopia desktop外
		    if ( w->testWState(WState_Polished) )
			app_style->polish(w);		//重新润饰
		    w->styleChange( *old );
		    if ( w->isVisible() ){
			w->update();
		    }
		}
	    }
	}
	delete old;
    }
}


窗口部件的润饰函数polish是用户的风格类中的重载函数,在这个函数中用户可以设置窗口部件的各种参数。每次窗口部件动态润饰之前,必须调用函数unPolish取消前一次的润饰参数。下面列出一个用户定义的FreshStyle风格的polish函数及unpolisy函数:

void FreshStyle::polish( QWidget *w )
{
    if ( w->inherits( "QListBox" ) ||   //是从QListBox继承的窗口部件
	 w->inherits( "QListView" ) ||
	 w->inherits( "QPopupMenu" ) ||
	 w->inherits( "QSpinBox" ) ) {
	QFrame *f = (QFrame *)w; //设置线条宽度
	f->setFrameShape( QFrame::StyledPanel ); //设置框架的样式
	f->setLineWidth( 1 );
    }
  //设置窗口部件w窗口状态为 WState_GlobalBrushOrigin,即画刷所在窗口
    QStyleHackWidget::setGlobalBrushOrigin(w);
}
 
void FreshStyle::unPolish( QWidget *w )
{
    if ( w->inherits( "QListBox" ) ||
	 w->inherits( "QListView" ) ||
	 w->inherits( "QPopupMenu" ) ||
	 w->inherits( "QSpinBox" ) ) {
	QFrame *f = (QFrame *)w;
	f->setFrameShape( QFrame::StyledPanel ); //设置框架的样式
	f->setLineWidth( 2 ); //设置线条宽度
    }
  
    QStyleHackWidget::clearGlobalBrushOrigin(w);
}


函数qwsSetDecoration设置QWSDecoration派生类对象并重新绘制窗口,这个方法仅在Qt/Embedded中使用。函数分析如下:

void QApplication::qwsSetDecoration( QWSDecoration *d )
{
    if ( d ) {
	delete qws_decoration;
	qws_decoration = d; //得到QWSDecoration派生类对象,用于QApplication内部
	QWidgetList *widgets = topLevelWidgets(); //得到所有窗口部件的链表
	QWidgetListIt it( *widgets );
	QWidget *w;
	while ( (w=it.current()) != 0 ) {
	    ++it;
	    if ( w->isVisible() && w != desktop() ) {
		((QETWidget *)w)->updateRegion(); //更新窗口各区域的大小
       //发出重画事件重新绘制desktop()->rect()区域
		((QETWidget *)w)->repaintDecoration(desktop()->rect(), FALSE);
		if ( w->isMaximized() ) //如果已是最大化,就最大化显示
		    w->showMaximized();
	    }
	}
	delete widgets;
    }
}

 

9 布局(layout)类

Qt的布局系统提供了设计子窗口部件布局的方法。Qt的布局系统的功能是在布局空间中布置子窗口部件(如:水平、垂直或网格布置),设置子窗口部件之间的空隙,管理在布局空间中布置子窗口部件等等。

当你添加一个窗口部件到一个布局中,布局中的所有窗口部件根据它们的QWidget::sizePolicy()策略被分配到一定空间中。并根据布局的需要来伸展窗口部件。

窗口部件基类QWidget中与布局相关的有属性sizeHint和尺寸策略类QSizePolicy分别说明如下:

QSize sizeHint 这个属性保存的是建议的窗口部件大小。

QSizePolicy类描述了水平和垂直扩展大小的策略。一个窗口部件的尺寸策略是指窗口重定义大小的各种方式。QSizePolicy使用枚举结构SizeType表示一个窗口部件尺寸策略,如:QSizePolicy::Fixed表示窗口部件固定为QWidget::sizeHint()大小;QSizePolicy::Minimum表示窗口部件最小尺寸为QWidget::sizeHint()大小。QSizePolicy使用枚举结构ExpandData表示一个窗口部件扩展策略,如:QSizePolicy::Horizontally 表示窗口部件能比sizeHint()大小扩展得更宽。

QLayout是Qt的布局系统抽象基类,QLayout中与布局相关的属性说明如下:

int margin – 布局的外边框的宽度。

ResizeMode resizeMode –布局的扩展方式,如:QLayout::Minimum 表示主窗口部件的最小尺寸设置到minimumSize(),不能再小了。

int spacing – 布局内的窗口部件间的空白空间大小。

(1) 布局窗口部件

让窗口部件有好的布局的最好方法是使用布局窗口部件:QHBox,QVBox和QGrid。布局窗口类QHBox、QVBox和QGrid,可以让子窗口部件按水平、垂直和网格排列。为了生成更复杂的布局,你可以嵌入一个布局窗口部件到其它的布局窗口部件。下面有5个QLabel子窗口部件,使用布局窗口部件QHBox、QVBox和QGrid后的显示如图2。

            

图2 布局窗口类QHBox、QVBox和QGrid排列QLabel部件图

图2演示的表格布局是由下面这些代码生成的:

QGrid *mainGrid = new QGrid( 2 ); // 一个2*n的网格
new QLabel( "One", mainGrid );
new QLabel( "Two", mainGrid );
new QLabel( "Three", mainGrid );
new QLabel( "Four", mainGrid );
new QLabel( "Five", mainGrid );


(2) 布局过程

当你添加窗口部件到一个布局中时,布局的工作过程如下:

1. 所有的窗口部件最初根据它们的QWidget::sizePolicy()而被分配到一定空间中。

2. 如果任何一个窗口部件设置了大于零的伸展因子,,那么它们就会按伸展因子比例分配空间。

3. 如果任何一个窗口部件的伸展因子为零,那么只有当其它窗口部件不需要空间时才会得到更多的空间。其中,空间会首先根据放大策略被分配给窗口部件。

4. 分配了小于窗口部件的最小尺寸的任何窗口部件(或者是在没有定义最小尺寸时的最小缺省尺寸)将使用窗口部件要求的最小尺寸。

5. 分配了大于窗口部件的最大尺寸的任何窗口部件,将使用窗口部件要求的最大尺寸。

放大因子被用来根据窗口部件互相间的比例来改变它们所被分配的空间。在没有设置放大因子时, QHBox布置的三个窗口部件显示如图18。

图18 QHBox布置的三个窗口部件

如果我们给每个窗口部件设置一个放大因子,它们将被按比例布置(但不能小于最小缺省尺寸),QHBox布置的设置了放大因子三个窗口部件显示如图19。

图19 QHBox布置的设置了放大因子三个窗口部件

(3)布局类使用

如果你需要更多地控制布局,可以使用QLayout的子类。Qt包括的布局类有QGridLayout和QBoxLayout。QHBoxLayout和QVBoxLayout是QBoxLayout的子类,它们使代码简单易读。

当你使用一个布局,你必须布局子类插入到父窗口部件和布局类中,这样,你可以给出每个窗口部件的布局参数,定义象对齐方式、放大因子和位置这样的属性。

你可以使用网格布局类QGridLayout将窗口部件按网格布置,网格布局类比网格窗口类QGrid可提供更多对窗口布局参数的控制。下面的代码生成图2中的网格布局:

QWidget *main = new QWidget;
 
    // 生成一个1*1的网格布局对象,它可以自动自动展开
    QGridLayout *grid = new QGridLayout( main, 1, 1 );
 
    // 根据(行, 列)编址来添加前四个窗口部件
    grid->addWidget( new QLabel( "One", main ), 0, 0 );
    grid->addWidget( new QLabel( "Two", main ), 0, 1 );
    grid->addWidget( new QLabel( "Three", main ), 1, 0 );
    grid->addWidget( new QLabel( "Four", main ), 1, 1 );
 
    // 添加最后一个窗口部件到第2行,并占用第0列和第1列,中间对齐
    grid->addMultiCellWidget( new QLabel( "Five", main ), 2, 2, 0, 1,
                              Qt::AlignCenter );
 
    // 设置第0列和第1列的宽度比例为2:3
    grid->setColStretch( 0, 2 );
    grid->setColStretch( 1, 3 );


你可以插入一个子布局到父布局中,通过把父布局作为子布局构造函数的一个参数来把布局添加到另一个布局中。 下面代码是插入一个子布局到父布局的例子:

QWidget *main = new QWidget;
    QLineEdit *field = new QLineEdit( main );
    QPushButton *ok = new QPushButton( "OK", main );
    QPushButton *cancel = new QPushButton( "Cancel", main );
    QLabel *label = new QLabel( "Write once, compile everywhere.", main );
 
    // 父窗口部件布局
    QVBoxLayout *vbox = new QVBoxLayout( main );
    vbox->addWidget( label );
    vbox->addWidget( field );
 
    // 在父布局中创建子布局
    QHBoxLayout *buttons = new QHBoxLayout( vbox );
    buttons->addWidget( ok );
    buttons->addWidget( cancel );


如果你对默认放置位置不满意,你创建没有父布局的布局对象,调用addLayout()把这个布局插入到其它布局中,并且插入的布局对象成为子布局,被插入的布局对象变成父布局。

当你创建自己的窗口部件类时,你应该与它的布局属性联系。如果这个窗口部件有一个QLayout,则说明布局已被进行处理。如果这个窗口部件没有任何子窗口部件,或者没有人工定义布局,你应该重实现QWidget的下面成员函数:

QWidget::sizeHint()返回窗口部件的优先选用的大小。

QWidget::minimumSizeHint()返回窗口部件所能有的最小尺寸。

QWidget::sizePolicy()返回一个QSizePolicy;它的值描述了这个窗口部件所需要的空间。

当缺省尺寸、最小缺省尺寸和尺寸策略发生改变时,就调用函数QWidget::updateGeometry()。这会引起布局的重新计算。对updateGeometry()的多次调用只会引起一次重新计算。

如果窗口部件的优先选用的高度依赖于它的实际宽度(比如一个自动断词的标签),在sizePolicy()中设置hasHeightForWidth()标识,并且重新实现函数QWidget::heightForWidth()。

即使实现了heightForWidth(),还应提供一个好的函数sizeHint()。sizeHint()提供了窗口部件优先选用宽度,并且它被不支持heightForWidth()的QLayout子类使用(QGridLayout和QBoxLayout都支持heightForWidth())。

你还可以通过QWidget::event()来进行人工布局。当布局需要重新计算的时候,窗口部件会得到LayoutHint类型事件。重新实现函数QWidget::event()并处理LayoutHint类型事件。

(4) 布局类设计

设计一个用户自己的布局类需要以QLayout为基类,先创建一个QPtrList对象用来存放布局处理的条目对象。每个条目是一个QLayoutItem,它常对应一个窗口部件。再实现一个QGLayoutIterator类的派生类来实现对QPtrList对象(链表)中每个条目的查找。然后,才能实现对每个条目的布局的管理,如:给每一个子窗口部件调用setGeometry()来设置尺寸。

如果你要生成一种特殊的布局,你重新实现QWidget::resizeEvent()来计算所需要分配的大小并且给每一个子窗口部件调用setGeometry()。

当布局需要重新计算的时候,窗口部件会得到一个类型是LayoutHint的事件。重新实现被通知LayoutHint事件窗口部件的QWidget::event()。

下面分别说明QLayoutItem和QGLayoutIterator的作用:

类QLayoutItem提供了QLayout操作的抽象条目,如:布局空间的一个窗口部件对象。这个类被用户的布局类使用。它提供了纯函数返回布局的信息,包括sizeHint(), minimumSize(), maximumSize()和expanding()。通过setGeometry()和geometry()函数可设置或得到布局的几何尺寸。通过iterator()返回布局的条目,如:具体的窗口部件。

QGLayoutIterator是内部布局迭代器的抽象基类。用于得到用户布局的各个条目,必须实现的函数是next(), current(), 和takeCurrent()。

示例:用户的布局类CardLayout

下面是布局类CardLayout的声明,例子中使用一个QPtrList存放被布局处理的条目,每一个条目是一个QLayoutItem对象。

头文件card.h列出如下:

#ifndef CARD_H
#define CARD_H
 
#include 
#include 
 
class CardLayout : public QLayout
{
public:
    CardLayout( QWidget *parent, int dist )
        : QLayout( parent, 0, dist ) { }
    CardLayout( QLayout* parent, int dist)
        : QLayout( parent, dist ) { }
    CardLayout( int dist )
        : QLayout( dist ) { }
    ~CardLayout();
 
    void addItem(QLayoutItem *item); //如何向布局中添加一个item。
    QSize sizeHint() const;  //布局优先选用的尺寸。
    QSize minimumSize() const;  //最小尺寸。
    QLayoutIterator iterator(); //用于遍历查找布局中的每个条目。
    void setGeometry(const QRect &rect);
 
private:
    QPtrList<QLayoutItem> list; //存放在布局中的各个对象(如:窗口部件)。
};
 
#endif


类CardLayoutIterator用于遍历查找QPtrList链表中存放的对象。实现QGLayoutIterator类中的虚函数:current()、next()和takeCurrent()。

类CardLayoutIterator的声明和实现列出如下:

类实现文件card.cpp列出如下:

#include "card.h"
class CardLayoutIterator : public QGLayoutIterator
{
public:
    CardLayoutIterator( QPtrList<QLayoutItem> *l )
        : idx( 0 ), list( l ) { }
 
    QLayoutItem *current() //从对象链表中得到当前项
    { return idx < int(list->count()) ? list->at(idx) : 0;  }
 
    QLayoutItem *next()  //从对象链表中得到下一项
    { idx++; return current(); }
 
    QLayoutItem *takeCurrent()
    { return list->take( idx ); } //从对象链表中删除当前项
 
private:
    int idx;
    QPtrList<QLayoutItem> *list;
};

在CardLayout类中创建iterator(),使用类CardLayoutIterator来构造。

QLayoutIterator CardLayout::iterator()
{       
    return QLayoutIterator( new CardLayoutIterator(&list) );
}
 
 
void CardLayout::addItem( QLayoutItem *item ) //向布局中加入条目。
{
    list.append( item );
}
 
 
CardLayout::~CardLayout()
{
    deleteAllItems(); //从布局中删除所有条目
}

下面是对布局的实现函数:

void CardLayout::setGeometry( const QRect &rect ) //设置尺寸
{
    QLayout::setGeometry( rect ); //设置布局的区域大小
 
    QPtrListIterator<QLayoutItem> it( list );
    if (it.count() == 0)
        return;
 
    QLayoutItem *o;
 
    int i = 0;
  //使用spacing()得到条目(窗口部件)之间的空白距离。
    int w = rect.width() - ( list.count() - 1 ) * spacing();
    int h = rect.height() - ( list.count() - 1 ) * spacing();
 
    while ( (o = it.current()) != 0 ) {
        ++it;
        QRect geom( rect.x() + i * spacing(), rect.y() + i * spacing(),
                    w, h );
        o->setGeometry( geom );
        ++i;
    }
}
 
QSize CardLayout::sizeHint() const
{
    QSize s( 0, 0 );
    int n = list.count();
    if ( n > 0 )
        s = QSize( 100, 70 ); // start with a nice default size
    QPtrListIterator<QLayoutItem> it( list );
    QLayoutItem *o;
    while ( (o = it.current()) != 0 ) {
        ++it;
        s = s.expandedTo( o->minimumSize() );
    }
    return s + n * QSize( spacing(), spacing() );
}
 
QSize CardLayout::minimumSize() const
{
    QSize s( 0, 0 );
    int n = list.count();
    QPtrListIterator<QLayoutItem> it( list );
    QLayoutItem *o;
    while ( (o = it.current()) != 0 ) {
        ++it;
        s = s.expandedTo( o->minimumSize() );
    }
    return s + n * QSize( spacing(), spacing() );
}

10 Qt插件

Qt提供了一个简单地插件接口,可以轻松地生成作为独立组件的定制数据库驱动、图象格式、文本编解码器(text codec)、风格(style)和作为独立的窗口部件的控件。写一个插件需要选择合适的插件基类,有5个插件基类,从它们派生的插件缺省保存于标准插件目录下,见表1:

表1 插件基类及插件保存缺省路径表
基类 缺省路径
QImageFormatPlugin $QTDIR/plugins/imageformats
QSqlDriverPlugin $QTDIR/plugins/sqldrivers
QStylePlugin $QTDIR/plugins/styles
QTextCodecPlugin $QTDIR/plugins/codecs
QWidgetPlugin $QTDIR/plugins/designer

关于每种基类的插件必须要实现哪些虚拟函数,根据基类而定。可以在一个插件里实现任意数目的插件子类,只要他们都是由同一个基类,比如说,所有插件子类都从QStylePlugin派生出来的。

示例:用户风格类插件

下面这个例子说明如何创建一个用户风格类MyStyle的插件MyStylePlugin 。

class MyStylePlugin : public QStylePlugin
{
public:
    MyStylePlugin() {}
    ~MyStylePlugin() {}
     //返回在插件中实现的类的一个字符串列表
    QStringList keys() const { 
        return QStringList() << "MyStyle"; 
    }
    //返回所要的类的一个对象
    QStyle* create( const QString& key ) { 
        if ( key == "MyStyle" ) 
            return new MyStyle;
        return 0;
    }
};
 
Q_EXPORT_PLUGIN( MyStylePlugin )


对于数据库驱动、图象格式、定制部件和文本编解码器,不需要显式的对象生成过程。Qt会按需求查找并生成他们。风格则是例外,因为你可能想以代码显式地设置一个风格。应用一个风格,可以使用类似代码:

QApplication::setStyle( QStyleFactory::create( "MyStyle" ) );

如果插件保存在的插件的缺省目录中,Qt的应用程序会自动识别哪些插件可用,应用程序中不需要任何查找和装载插件的代码。插件可以与应用程序一起编译,或者插件编译为动态链接库(在linux中以.so结尾文件,在windows中以.dll结尾的文件)。

11 Qt模板库

Qt模板库(QT Template Library 简称QTL)是一套提供对象容器的模板。如果你的编译器没有适当的STL(标准模板库)可用,QTL将被代替使用。QTL提供了对象的链表、对象的矢量(动态数组)、从一个类型到另一个类型的映射(或称为字典)和相关的迭代器和算法。一个容器是包含和管理其它对象的一个对象,并且提供迭代器对被包含的对象进行访问。 Qt模板类说明如表2。

表2 Qt模板类说明
模板类名称 说明
QMap 提供基于值的一个字典的模板类
QMapConstIterator QMap的常量迭代器
QMapIterator QMap的迭代器
QPair 提供基于值的一对元素的模板类
QValueList 提供基于值的一个双向链表的模板类
QValueListConstIterator QValueList的常量迭代器
QValueListIterator QValueList的迭代器
QValueStack 提供基于值的一个堆栈模板类
QValueVector 提供基于值的一个动态数组模板类

QTL类的命名约定与其他Qt类一致(比如,count()、isEmpty())。它们还提供额外的函数来兼容STL算法,比如size()和empty()。可以像使用STL的函数map一样来使用它们。

与STL相比,QTL仅仅包含了STL容器应用程序接口的最重要的特性,没有平台差异,通常要慢一些并且经常扩展为更少的对象代码。

如果你不想拷贝存储对象,你最好使用QPtrCollection及派生类。它们就是被设计用来处理各种类指针的。QObject没有拷贝构造函数,因此QObject不能作为一个值使用。但可以存储指向QObject的指针到QValueList。当然,直接使用QPtrList更好。QPtrList像所有其它的基于QPtrCollection的容器一样,提供了比速度优化了、基于值的容器更多健全的检查。

如果你有一些使用值的对象,并且在你的目标平台没有可用的STL,Qt模板库就可以替代它。使用值的对象至少需要一个拷贝构造函数、一个赋值操作符和一个默认构造函数(如:一个没有任何参数的构造函数)。

注意一个快速的拷贝构造函数对于容器的高性能是关键的,因为许多拷贝操作将会发生。如果你想排序你的数据,你必须在你的数据类中实现operator<()。

Qt模板库是为高性能而设计,迭代器是非常快的。为了实现这样的性能,Qt模板库比基于QPtrCollection的集合类做更少的错误检查。一个QTL容器,例如:QTL容器没有跟踪任何相关的迭代器。这样在诸如删除条目时没有执行有效性检查,但它提供了很好的执行性能。

11.1 迭代器(Iterators)

Qt模板库打交道的是值对象,而不是指针对象。迭代器是最好的遍历容器方法。遍历一个容器可使用像下面的循环:

typedef QValueList<int> List;
List l;
for( List::Iterator it = l.begin(); it != l.end(); ++it )
     printf( "Number is %i/n", *it );


begin()返回第一个元素的迭代器,end()返回的是最后一个元素之后的一个迭代器。end()标明的是一个无效的位置,它永远不能被解除引用。它只是任何一次迭代的终止条件,迭代可以从begin()或end()开始。同样的概念也适用于其它容器类,例如,用于QMap和QValueVector的迭代方法如下:

typedef QMap<QString,QString> Map;
Map map;
for( Map::iterator it = map.begin(); it != map.end(); ++it )
    printf( "Key=%s Data=%s/n", it.key().ascii(), it.data().ascii() );
 
typedef QValueVector<int> Vector;
Vector vec;
for( Vector::iterator it = vec.begin(); it != vec.end(); ++it )
    printf( "Data=%d/n", *it );


11.2 算法

Qt模板库定义了大量操作容器的算法。这些算法用模板库函数实现,还提供了有迭代器的容器的通用代码。例如:qHeapSort()和qBubbleSort()提供了著名的堆排序和冒泡排序算法。你可以象下面这样使用它们:

typedef QValueList<int> List;
List l;
l << 42 << 100 << 1234 << 12 << 8;
qHeapSort( l );
 
List l2;
l2 << 42 << 100 << 1234 << 12 << 8;
List::Iterator b = l2.find( 100 );
List::Iterator e = l2.find( 8 );
qHeapSort( b, e );
 
double arr[] = { 3.2, 5.6, 8.9 };
qHeapSort( arr, arr + 3 );


第一个例子对整个列表排序。第二个例子对两个迭代器之间的所有元素排序,即100、1234和12。第三个例子表明迭代器是作为指针使用的。

一些常用的模板函数说明如下:

(1)函数qSwap()用来交换两个变量的值,例如:

QString second( "Einstein" );
    QString name( "Albert" );
qSwap( second, name );


(2)函数qCount()用于统计容器中一个值出现的次数。例如:

QValueList<int> l;
    l.push_back( 1 );        //放入1到l链表中  
    l.push_back( 1 );          
    l.push_back( 1 );          
    l.push_back( 2 );          
    int c = 0;
qCount( l.begin(), l.end(), 1, c ); //统计1的个数c, c = 3


(3)函数qFind()用于查找容器中一个值的第一次出现位置。例如:

QValueList<int> l;
    l.push_back( 1 );          
    l.push_back( 1 );          
    l.push_back( 1 );          
    l.push_back( 2 );          
  //查找2所在的位置
    QValueListIterator<int> it = qFind( l.begin(), l.end(), 2 );


(4)函数qFill()用于将一个值拷贝填充到一个范围。例如:

QValueVector<int> v(3);
qFill( v.begin(), v.end(), 99 ); //将99填充整个v数组, v包含99, 99, 99


(5)函数qEqual()用来比较两个范围的元素是否相等,两个范围的元素个数不一定相等。只要第一个范围的元素与第二个范围的对应元素都相等时,就认为这两个范围相等。例如:

QValueVector<int> v1(3);
    v1[0] = 1;
    v1[2] = 2;
    v1[3] = 3;
 
    QValueVector<int> v2(5);
    v1[0] = 1;
    v1[2] = 2;
    v1[3] = 3;
    v1[4] = 4;
    v1[5] = 5;
 
    bool b = qEqual( v1.begin(), v2.end(), v2.begin() );
    // b == TRUE


(6)函数qCopy()用于拷贝一个范围的元素到输出迭代器,例如:

QValueList<int> l;
    l.push_back( 100 );
    l.push_back( 200 );
    l.push_back( 300 );
    QTextOStream str( stdout );
  //拷贝l中所有元素到输出迭代器QTextOStreamIterator
    qCopy( l.begin(), l.end(), QTextOStreamIterator(str) );


(7)函数qCopyBackward()用于拷贝一个容器或者它的一部分到一个输出迭代器,拷贝的次序是从后面开始,例如:

QValueVector<int> vec(3);
    vec.push_back( 100 );
    vec.push_back( 200 );
    vec.push_back( 300 );
    QValueVector<int> another;
  // “another”包含的是按倒序排列的(300、200、100)。
    qCopyBackward( vec.begin(), vec.end(), another.begin() );


如果你写了新的算法,请考虑把它们写成模板函数,这样就可以使它们能够用在尽可能多的容器上了。在上一个例子中,你可以很容易地使用qCopy()打印出一个标准C++数组,方法列出如下:

int arr[] = { 100, 200, 300 };
    QTextOStream str( stdout );
    qCopy( arr, arr + 3, QTextOStreamIterator( str ) );


11.3 数据流串行化

所有提到的容器(如:QValueList 、QStringList、QValueStack和QMap等)都可被相应的流操作符串行化。下面是一个例子。

QDataStream str(...);
    QValueList<QRect> l;
    // ……在这里填充这个列表
    str << l;


容器还能象下面这样被再一次读入:

QValueList<QRect> l;
    str >> l;


12 集合类

一个集合类是装有多个条目的容器,每个条目是某种数据结构,集合类能执行对容器中的条目的插入、删除及查找等操作。

Qt有几个基于值和基于指针的集合类。基于指针的集合类使用指向条目的指针来工作,而基于值的集合类存储着它们条目的拷贝。基于值的集合类类似于STL容器类,能和STL算法和容器一起使用。

基于值的集合类说明如表4所示:

表4 基于值的集合类表
类名称 说明
QValueList 基于值的链表
QValueVector 基于值的矢量结构
QValueStack 基于值的栈结构
QMap 基于值的字典结构


基于指针的集合类说明如表5所示:

表5 基于指针的集合类表
类名称 说明
QCache和QIntCache LRU(least recently used)缓存结构。
QDict、QIntDict和QPtrDict 字典结构。
QPtrList 双向链接的链表结构。
QPtrQueue FIFO 先进先出(first in,first out)队列结构。
QPtrStack LIFO后进先出(last in, first out)栈结构。
QPtrVector 矢量结构。

QMemArray 是一个例外,它既不是基于指针也不是基于值,而是基于内存的结构。用于在有简单数据结构的数组中使用QMemArray效率最高,QMemArray在拷贝和数组元素比较时使用位逻辑运算符操作。

这些类中有一些具有迭代器,迭代器是遍历集合类中的条目的类。在Qt模板库里,基于值的集合和算法集成在一起。下面讨论基于指针的容器。

12.1 基于指针的容器的结构

基于指针的容器有4个内部基类(QGCache, QGDict, QGList和QGVector)操作void类型指针。通过增加/删除条目指针,一个由这4个类组成的薄模板层实现了实际的集合。

允许Qt的模板类的策略使得在空间上很经济(实现这些模板类仅增加了对基类的内联调用),而且还不影响执行效率。

示例:QPtrList使用

下面的例子说明了如何存储Employee条目到一个链表,并将它们以相反的次序打印出来。

#include 
#include 
#include 
 
class Employee
{
public:
    Employee( const char *name, int salary ) { n=name; s=salary; }
    const char *name()   const               { return n; }
    int         salary() const               { return s; }
private:
    QString     n;
    int         s;
};
 
int main()
{
    QPtrList<Employee> list;        // 指向Employee的指针链表。
list.setAutoDelete( TRUE );     //当链表条目被移动时,删除条目。
 
    list.append( new Employee("Bill", 50000) ); //链表追加新的对象。
    list.append( new Employee("Steve",80000) );
    list.append( new Employee("Ron",  60000) );
 
    QPtrListIterator<Employee> it(list); //遍历Employee链表。
    for ( it.toLast(); it.current(); --it) ) { //从尾向头遍历。
        Employee *emp = it.current();
        printf( "%s earns %d/n", emp->name(), emp->salary() );
    }
 
    return 0;
}


程序运行结果如下:

Ron earns 60000

Steve earns 80000

Bill earns 50000

12.2 管理集合条目

所有基于指针的集合继承了QPtrCollection基类。这个类仅知道集合中的条目个数和删除策略。

当集合中的条目被移去时,缺省时它们不被删除。QPtrCollection::setAutoDelete()定义了删除策略。在上述QPtrList使用示例子,我们激活了自动删除功能来进行链表删除。

当插入一个条目到一个集合时,仅指针被拷贝,而不是拷贝条目本身。这称为浅拷贝。当插入一个条目时,拷贝所有条目的数组到集合中也是可能的,这称为深拷贝。

所有的集合类函数在插入条目时调用虚拟函数QPtrCollection::newItem()。如果你想进行深拷贝,你需要重载它。

当从一个链表中移去一个条目时,调用虚拟函数QPtrCollection::deleteItem()。如果自动删除功能被激活,在所有集合类中的缺省实现函数被调用来删除条目。

基于指针的集合类,如:QPtrList,定义了指向对象的指针集合。我们在这里只讨论QPtrList类,其它的基于指针的集合类和所有集合类迭代器都有同样的使用方法。

模板实例化方法如下:

QPtrList list;

在这个例子中,条目的类或类型是Employee,它必须在链表定义之前被定义。例如:

class Employee {
    ...
};
QPtrList<Employee> list;


12.3 迭代器(Iterators)

QPtrListIterator能在链表被修改的同时非常安全的遍历链表。在同一个集合上,多个迭代器能独立地工作。

QPtrList有一个指向所有迭代器的内部链表,这些迭代器当前操作链表。当一个链表条目被移去时,链表更新所有的指向这个条目的迭代器。

QDict和QCache集合没有遍历函数。为了遍历集合,你必须使用QDictIterator或 QCacheIterator。

Qt预定义的集合类有字符串链表:QStrList, QStrIList (在qstrlist.h中)和 QStringList (在qstringlist.h中)。在绝大多数情况下你将选择QStringList,它是一个共享的QString Unicode字符串的值链表。QPtrStrList和 QPtrStrIList仅存储字符指针,而不是字符串本身。

基于指针的集合类和相关的迭代器类说明如表4。

表4 基于指针的集合类和相关的迭代器类列表
QAsciiCache 基于char* keys 的缓存的模板类。
QAsciiCacheIterator QAsciiCache集合的迭代器。
QAsciiDict 基于char* keys 的字典的模板类。
QAsciiDictIterator QAsciiDict集合的迭代器。
QBitArray Bit位数组。
QBitVal 与 QBitArray 一起用的内部类
QBuffer 在QByteArray上操作的I/O设备。
QByteArray 字节数组
QCache 基于QString keys的模板类。
QCacheIterator QCache集合的迭代器。
QCString C语言中0结尾的字符数组(char *)。
QDict 基于QString keys 的字典的模板类。
QDictIterator QDict collections的迭代器。
QIntCache 基于long keys 的缓存的模板类。
QIntCacheIterator QIntCache集合的迭代器。
QIntDict 基于long keys 的字典的模板类。
QIntDictIterator QIntDict集合的迭代器。
QObjectList QObjects的QPtrList链表。
QObjectListIt QObjectLists的迭代器。
QPtrCollection 大多数基于指针的Qt集合的基类。
QPtrDict 基于void* keys的字典的模板类。
QPtrDictIterator QPtrDict的迭代器。
QPtrList 双向链表的模板类。
QPtrListIterator QPtrList集合的迭代器。
QPtrQueue 一个队列的模板类。
QStrIList 对于大小写敏感的char*双向链接的链表。
QStrList char* 双向链接的链表。

13 Qt线程

Qt对线程提供了支持,它引入了一些基本与平台无关的线程类、线程安全传递事件的方式和全局Qt库互斥量允许你从不同的线程调用Qt的方法。Qt中与线程应用相关的类如表6所示。

表6 Qt中与线程相关的类
QMutex 线程之间的互斥锁
QSemaphore 整数信号量
QThread 与平台无关的线程
QWaitCondition 线程之间允许等待/唤醒的条件

使用线程需要Qt提供相应的线程库的支持,因此,在编译安装Qt时,需要加上线程支持选项。

当在Windows操作系统上编译Qt时,线程支持是在一些编译器上的一个选项。在Windows操作系统上编译应用程序时,通过在qconfig.h文件中增加一个选项来解决来解决这个问题。

在Mac OS X和Unix上编译Qt时,你应在运行configure脚本时添加-thread选项。在Unix平台上,多线程程序必须用特殊的线程支持库连接,多线程程序必须连接线程支持库libqt-mt,而不是标准的Qt库。编译应用程序时,你应该使用宏定义QT_THREAD_SUPPORT来编译(如:编译时使用-DQT_THREAD_SUPPORT)。

13.1 线程类QThread

在 Qt中提供了QThread线程类,它提供了创建一个新线程的方法。线程通过重载 QThread::run()函数开始执行的,这一点与Java中的线程类相似。

示例1:一个简单的线程

下面的例子实现了一个简单的继承自QThread的用户线程类,并运行2个线程,线程b在线程a运行完后运行。代码列出如下:

class MyThread : public QThread {
public:
    virtual void run();
};
 
void MyThread::run()   //运行线程
{
    for( int count = 0; count < 20; count++ ) {
        sleep( 1 );
        qDebug( "Ping!" );
    }
}
 
int main()
{
    MyThread a;
    MyThread b;
    a.start(); //通过调用run()函数来执行
    b.start();
    a.wait();
    b.wait();
}


只有一个线程类是不够的,对于支持多线程的程序来说,还需要保护两个不同的线程对数据的同时访问,因此 Qt 提供了QMutex 类,一个线程可以锁住互斥量,当互斥量被锁住时,将阻塞其它线程访问临界数据,直到这个线程释放互斥量。这样,可以保护临界数据一次只能被一个线程访问。

Qt库互斥量(qApp->lock()和qApp->unlock())是在访问Qt的GUI界面资源时用到的互斥量。在Qt中没有使用互斥量而调用一个函数通常会导致不可预知的行为。从另外一个线程中调用Qt的一个GUI相关函数需要使用Qt库互斥量。在这种情况下,所有访问图形或窗口系统资源的函数都与GUI相关。如果对象仅被一个线程访问,使用容器类,字符串或者输入/输出类不需要任何互斥量。

13.2 线程安全的事件传递

在Qt中,一个线程总是一个事件线程,线程从窗口系统中拉出事件并且把它们分发给窗口部件。静态方法QThread::postEvent从线程中邮递事件,而不是从事件线程。事件线程被唤醒并且事件象一个正常窗口系统的事件一样在事件线程中被分发。例如,你可以从不同的线程强制一个窗口部件进行重绘,方法如下:

QWidget *mywidget;
QThread::postEvent( mywidget, new QPaintEvent( QRect(0, 0, 100, 100) ) );


上述代码将异步地使mywidget在它区域中重绘一块100*100的正方形区域。

另外,还需要一些机制使得处于等待状态的线程在给定条件下被唤醒。QWaitCondition 类就提供了这种功能。线程等待的条件QWaitCondition满足,QWaitCondition表明发生了什么事情,它阻塞直到这件事情发生。当发生给定的事情时,QWaitCondition 将唤醒等待该事情的所有线程或者唤醒任意一个被选中的线程。(这和POSIX线程条件变量具有相同功能,是Unix上的一种实现。)

示例2:QWaitCondition类应用

下面这个例子的功能是:当你按下按钮,这个程序就会唤醒worker线程,这个线程在按钮上显示工作状态:等待(Waiting)还是正在工作(Working)。当按钮被按下时,worker线程正在工作,那么对线程不产生影响。当run函数再次循环到mycond.wait()时,线程阻塞并等待。当按钮再被按下时,触发slotClicked()函数运行,唤醒等待的线程。

#include 
#include 
 
// 全局条件变量
QWaitCondition mycond;
 
// Worker类实现
class Worker : public QPushButton, public QThread
{
    Q_OBJECT
 
public:
    Worker(QWidget *parent = 0, const char *name = 0)
            : QPushButton(parent, name)
    {
        setText("Start Working");
 
        // 将QPushButton继承来的信号与槽slotClicked()连接起来。
        connect(this, SIGNAL(clicked()), SLOT(slotClicked()));
 
        // 调用从QThread继承来的start()方法开始线程的执行
        QThread::start();
    }
 
public slots:
    void slotClicked()
    {
        // 唤醒等待这个条件变量的一个线程
        mycond.wakeOne();
    }
 
protected:
    void run() //重载run函数
    {
          while ( TRUE ) {
            // 锁定应用程序互斥锁,并且设置窗口标题来表明我们正在等待开始工作
            qApp->lock();
            setCaption( "Waiting" );
            qApp->unlock();
 
            // 等待直到我们被告知可以继续
            mycond.wait();
 
            // 如果到了这里,表示我们已经被另一个线程唤醒
            qApp->lock();
            setCaption( "Working!" );// 设置标题,表示正在工作
            qApp->unlock();
      }
    }
};
 
int main( int argc, char **argv )
{
    QApplication app( argc, argv );
 
    // 创建一个worker
    Worker firstworker( 0, "worker" );
 
    app.setMainWidget( &worker ); //将worker设置为应用程序的主窗口。
    worker.show();
 
    return app.exec();
}


当进行线程编程时,需要注意的一些事项:

(1)在持有Qt库互斥量时不要做任何阻塞操作。这将冻结事件循环。

(2)确认你锁定一个递归QMutex的次数等于解锁的次数,不能多也不能少。

(3)在调用除了Qt容器和工具类外的任何东西之前锁定Qt应用程序互斥量。

(4)谨防隐含的共享类,如果你需要在线程之间指定它们,你应该用detach()分离它们。

(5)小心没有被设计成线程安全的Qt类,例如,QPtrList的API接口不是线程安全的,并且如果不同的线程需要遍历一个QPtrList,它们应该在调用QPtrList::first()之前锁住,在到达终点后解锁。

(6)确信仅在GUI线程中创建继承自QWidget、QTimer和QSocketNotifier的对象。在一些平台上,创建在线程中而不是GUI线程的对象永远不会接收到底层窗口系统的事件。

(7)和上面很相似,只在GUI线程中使用QNetwork类。因为所有的QNetwork类都是异步的,没必要把QSocket用在多线程中。

(8)永远不要尝试在不是GUI线程的线程中调用processEvents()函数。这也包括QDialog::exec()、QPopupMenu::exec()、QApplication::processEvents()和其它一些函数。

(9)在你的应用程序中,不要把普通的Qt库和支持线程的Qt库混合使用。这意味着如果你的程序使用了支持线程的Qt库,你就不能连接普通的Qt库、动态的载入普通Qt库或者动态地连接其它依赖普通Qt库的库或者插件。在一些系统上,这样做会导致Qt库中使用的静态数据崩溃。

14 鼠标拖放

拖放提供了一种用户在应用程序之间或之内传递信息的一种简单可视机制。在术语中,这被称为"直接操作模型"。拖放在功能上类似剪贴板的剪切和粘贴机制。拖放机制包括拖动、放下、剪贴板、拖放操作、添加新的拖放类型、高级拖放以及和其它应用程序之间的操作几个方面。下面从这几个方面分别进行说明:

(1)拖动

开始一个拖动,比如是在鼠标移动事件,创建一个适合你的媒体的QDragObject的子类的对象,例如:对于文本使用QTextDrag,对于图片使用QImageDrag。然后调用drag()方法。例如,从一个窗口部件中开始拖动一些文本:

void MyWidget::startDrag()
  {
    QDragObject *d = new QTextDrag( myHighlightedText(), this );
    d->dragCopy(); //拷贝选中文本
    // 不要删除d。
  }


注意在拖动之后,QDragObject没有被删除。在拖放明显完成后,这个QDragObject需要被保存。因为它还可能需要与其它进程通信。最后Qt会删除这个对象。如果拥有拖动对象的窗口部件在删除拖动对象之前被删除,那么任何没有完成的放下操作将会被取消,并且拖动对象会被删除。因为这个原因,你应该小心对待对象引用。

(2)放下

为了能在一个窗口部件中接收被放下的媒体,这个窗口部件调用setAcceptDrops(TRUE)(如:在它的构造函数中),并且重载事件处理方法dragEnterEvent()和dropEvent()。对于更复杂的应用程序,重载dragMoveEvent()和dragLeaveEvent()也是必需的。

例如,当拖动后放下文本或图片时,窗口部件接受并处理放下操作的代码如下:

MyWidget::MyWidget(...) :
    QWidget(...)
  {
     ...
     setAcceptDrops(TRUE); //接收被放下的媒体。
  }
//当一个拖动正在进行并且鼠标进入这个窗口部件,这个事件处理函数被调用。
  void MyWidget::dragEnterEvent(QDragEnterEvent* event)
  {
     event->accept( QTextDrag::canDecode(event) ||
         QImageDrag::canDecode(event)  );
  }
//当拖动在这个窗口部件上被放下,这个事件处理器被调用。
  void MyWidget::dropEvent(QDropEvent* event)
  {
     QImage image;
     QString text;
 
     if ( QImageDrag::decode(event, image) ) {//解码图片
        insertImageAt(image, event->pos()); //在窗口部件中插入图片
     } else if ( QTextDrag::decode(event, text) ) {
        insertTextAt(text, event->pos()); 
     }
  }


(3)剪贴板

QDragObject、QDragEnterEvent、QDragMoveEvent和QDropEvent类都是QMimeSource(提供类型信息的类)的子类。如果你在QDragObject中基于你的数据进行传递,你不仅可使用拖放,而且还可以使用传统的剪切和粘贴。QClipboard有两个函数:

setData(QMimeSource*)
      QMimeSource* data()const


使用这些函数,你可以把你的拖放初始信息放到剪贴板中:

void MyWidget::copy()
{
    QApplication::clipboard()->setData( new QTextDrag(myHighlightedText()) );
}
 
void MyWidget::paste()
{
    QString text;
    if ( QTextDrag::decode(QApplication::clipboard()->data(), text) )
        insertText( text );
}


你甚至能使用QDragObject的子类作为文件I/O部分。例如,如果你的程序有一个QDragObject的子类把CAD设计编码成DXF格式,你可以象下面这样存储和装载这个格式的文件:

void MyWidget::save()
{
    QFile out(current_file_name);
    out.open(IO_WriteOnly);
    MyCadDrag tmp(current_design); // MyCadDrag是QDragObject的子类
    out.writeBlock( tmp->encodedData( "image/x-dxf" ) );
}
 
void MyWidget::load()
{
    QFile in(current_file_name);
    in.open(IO_ReadOnly);
    if ( !MyCadDrag::decode(in.readAll(), current_design) ) {
        QMessageBox::warning( this, "Format error",
            tr("The file /"%1/" is not in any supported format")
             .arg(current_file_name)
        );
    }
}


(4)拖放操作

在一些简单的情况下,拖放的目标接收一个被拖动的数据的拷贝,并且由源来决定是否删除初始的拖动对象。这是QDropEvent中的"Copy"操作。目标也可以选择理解其它操作,特别是"Move"和"Link"操作。如果目标理解了"Move"操作,目标负责拷贝和删除操作,源不会尝试删除数据。如果目标理解为"Link"操作,它存储它自己的引用到初始信息中,并且源不会删除初始信息。最通用的拖放操作是在同一个窗口部件中执行一个"Move"操作。

拖动操作的另一个主要用途是当使用一个引用类型,比如text/uri-list,实际上被拖动的数据是文件或对象的引用。

(5)添加新的拖放类型

拖放不仅仅局限于文本和图片,任何信息都可以被拖放。为了在应用程序之间拖放信息,两个应用程序必须指明彼此都能接受和产生的数据格式。这个可以通过使用MIME类型来获得。拖动的源提供一个它能产生的MIME类型列表(按从最合适的到最少合适的顺序排列),并且放下的目标选择一种它能接受的类型。例如,QTextDrag提供了"text/plain"MIME类型(普通的没有格式的文本),还有"text/utf16"和"text/utf8"的Unicode格式的类型。QImageDrag提供了"image/*"类型,*是QImageIO支持的任何一种图片格式,并且QUriDrag子类提供了"text/uri-list"的支持,它是传输一个文件名列表(或URL)的标准格式。

为了实现一些还没有可用QDragObject子类的信息类型的拖放,首先和最重要的步骤是查找合适的存在格式:IANA(Internet Assigned Numbers Authority)在ISI(Information Sciences Institute)提供了一个MIME媒体类型的分级列表。使用标准的MIME类型将会使你的应用程序现在及未来能更好地与其它软件互相操作。

为了支持另外的媒体类型,从QDragObject或QStoredDrag派生类。当你需要提供多种媒体类型的支持时,从QDragObject派生类。当一个类型足够时,就从更简单的QStoredDrag派生类。

QDragObject的子类将会重载const char* format(int i) const和QByteArray encodedData(const char* mimetype) const成员,并且提供一套方法编码媒体数据,提供静态成员canDecode()和decode()解码输入的数据,QImageDrag的成员函数bool canDecode(QMimeSource*) const和QByteArray decode(QMimeSource*) const在子类中需要类似的重载。

QStoredDrag的子类提供了提供一套方法编码媒体数据,静态成员canDecode()和decode()对进入的数据进行解码。

(6)高级拖放

在剪贴板模式中,用户可以剪切或复制资源信息,然后粘贴它。相似地,在拖放模式中,用户可以拖动信息的拷贝或者拖动信息本身到一个新的位置(移动它)。拖放模式对于程序员来说都是更多的复杂性:程序直到放下(粘贴)完成才会知道用户是想剪切还是复制。在应用程序之间拖动,这个没有什么区别,但是在一个应用程序之内进行拖动,应用程序必须小心不要将拷贝粘贴到同一个地方。例如,在同上窗口部件中拖动文本,拖动的开始点和放下事件处理函数应象下面这样重载:

void MyEditor::startDrag()
{
    QDragObject *d = new QTextDrag(myHighlightedText(), this);
    if ( d->drag() && d->target() != this )
       cutMyHighlightedText(); //剪切选中的文本
}
 
void MyEditor::dropEvent(QDropEvent* event)
{
    QString text;
 
    if ( QTextDrag::decode(event, text) ) {
       if ( event->source() == this && event->action() == QDropEvent::Move ) {
          // 在同一个窗口部件时,不能使用粘贴拷贝,而应是移到到这个位置。
          event->acceptAction();
          moveMyHighlightedTextTo(event->pos());
       } else {
          pasteTextAt(text, event->pos()); //粘贴拷贝
       }
    }
}


一些窗口部件在数据被拖动到它们上面时需要指定"是"或"否"接收。例如,一个CAD程序也许只接收在视图中的文本对象上放下的文本。在这种情况下,dragMoveEvent()被使用并且给定接受或者忽略拖动的区域。代码列出如下:

void MyWidget::dragMoveEvent(QDragMoveEvent* event)
{
    if ( QTextDrag::canDecode(event) ) {
      MyCadItem* item = findMyItemAt(event->pos()); 
      if ( item )
        event->accept();
    }
}


(7)和其它应用程序之间的操作

在X11上,拖动使用公有的XDND协议,而Qt在Windows上使用OLE标准,Qt在Mac上使用Carbon拖动管理器。在X11上,XDND使用MIME,所以不需要转换。Qt的应用编程接口与平台无关。在Windows上,识别MIME的应用程序可以通过使用MIME类型的剪贴板格式名字进行通信。一些Windows应用程序已经对它们的剪贴板格式使用MIME命名规范了。在内部,Qt有能力在专有的剪贴板格式和MIME类型之间转换。在X11上,Qt也支持使用Motif拖放协议的拖动。

15 键盘焦点

Qt的窗口部件在图形用户界面中按用户的习惯的方式来处理键盘焦点。基本出发点是用户的击键能定向到屏幕上窗口中的任何一个,和在窗口中任何一个部件中。当用户按下一个键,他们期望键盘焦点能够到达正确的位置,并且软件必须尽量满足这种希望。系统必须确定击键定位在哪一个应用程序、应用程序中的哪一个窗口和窗口中的哪一个窗口部件。

15.1 焦点移动的方式

把焦点定位特殊的窗口部件的习惯方式有:

(1)用户按下Tab键(或者Shift键+Tab键)(或者有时是Enter键)。

(2)用户点击一个窗口部件。

(3)用户按下一个键盘快捷键。

(4)用户使用鼠标滚轮。

(5)用户移动焦点到一个窗口,并且应用程序必须决定窗口中的哪个窗口部件应该得到焦点。

这些移动机制的每个都是不同的,并且不同类型的窗口部件只能接收它们中的一些方式的焦点。下面我们将按次序介绍它们。

1. Tab或者Shift+Tab.

按Tab键是到目前为止用键盘移动焦点的最通用的方法。有时在输入数据的应用程序中Enter键和Tab键的作用是一样的。我们暂时忽略这一点。

所有窗口系统中的有关焦点的最通用使用方法是:按Tab键移动键盘焦点到每个窗口的窗口部件循环列表中的下一个窗口部件。Tab键按照循环列表的一个方向移动焦点,Shift键+Tab键按另一个方向移动焦点。按Tab键从一个窗口部件到下一个窗口部件移动焦点的次序叫做Tab键次序。

在Qt中,窗口部件循环列表存放在QFocusData类中。每个窗口有一个QFocusData对象,并且当选择合适的QWidget::FocusPolicy焦点策略的QWidget::setFocusPolicy()被调用的时候,窗口部件自动把它们自己追加到列表的末尾。你可以使用QWidget::setTabOrder()来自定义Tab键控制次序。如果你没有定义这个次序,那么Tab键会按照窗口部件构造的顺序移动焦点。Qt designer工具提供了一个可视化的改变Tab键控制次序的方法。

因为按Tab键是如此的常用,大多数含有焦点的窗口部件应该支持Tab焦点。主要例外情况是几乎没用到的窗口部件,并且在窗口部件上有一些移动焦点的键盘快捷键或者错误处理。

2. 用户点击一个窗口部件。

在使用鼠标或者其它指针设备的计算机中,用鼠标点击一个窗口部件是一种比按Tab键更常用的方法。

当鼠标点击把焦点移到一个窗口部件时,对于编辑器窗口部件,它也会移动文本光标(窗口部件的内部焦点)到鼠标被点击的地点。

鼠标点击移动焦点是大多数窗口部件必须支持的,有时窗口部件需要避免鼠标点击移动焦点。例如:在一个字处理程序中,当用户点击"B"(粗体)工具按钮,键盘焦点应该保留在原来的位置。在Qt中,只有QWidget::setFocusPolicy()函数影响点击焦点。

3. 用户按下一个键盘快捷键。

使用键盘快捷键来移动焦点不是很常用。这种情况可能会隐含地发生在打开的模式对话框中,但是也会显式地发生在使用焦点加速器中,例如在QLabel::setBuddy()、QGroupBox和QTabBar提供的加速器中。

用户想让焦点跳到的窗口部件都应支持快捷键焦点。例如:一个Tab对话框为它的每一个页提供键盘快捷键,所以用户可以按下比如Alt+P来跳到打印页面。但只能有少量的快捷键,并且为命令提供键盘快捷键也很重要,如:在标准快捷键列表中,Alt+P也可以用来粘贴、播放或打印。

4. 用户使用鼠标滚轮。

在Microsoft Windows上,鼠标滚轮的用法是一直由有键盘焦点的窗口部件处理。在Mac OS X和X11上,它由获得其它鼠标事件的窗口部件处理。

Qt处理这种平台差异的方法是当滚轮被使用时,让窗口部件移动键盘焦点。每个窗口部件上有合适的焦点策略,应用程序可以在Windows、Mac OS X和X11上按照习惯正确地处理焦点。

5. 用户移动焦点到这个窗口。

在这种情况下,应用程序必须决定窗口中的哪一个窗口部件接收焦点。Qt自动实现这样的做法:如果焦点以前在这个窗口中,那么窗口中有焦点的最后一个窗口部件应该重新获得焦点。如果以前焦点就从来没有来到过这个窗口,并且你知道焦点应该从哪里开始,就在你调用QWidget::show()显示它之前,在应该接收焦点的窗口部件上调用QWidget::setFocus()。如果你不知道,Qt会选择一个合适的窗口部件。

15.2 焦点策略及操作函数

键盘焦点的策略及操作函数说明如下:

(1) 焦点策略属性变量

focusPolicy 焦点策略属性变量保存的是窗口部件接收键盘焦点的策略。如果窗口部件通过tab来接收键盘焦点,这个策略就是QWidget::TabFocus;如果窗口部件通过点击来接收键盘焦点,这个策略就是QWidget::ClickFocus;如果窗口部件上述两种方式都使用,是QWidget::StrongFocus;并且如果它不接收焦点(QWidget的默认值),是QWidget::NoFocus。

如果一个窗口部件处理键盘事件,你必须使键盘焦点生效。这通常在窗口部件的构造函数中完成。例如,QLineEdit的构造函数调用setFocusPolicy(QWidget::StrongFocus)。

(2)void QWidget::setFocus() [虚槽]

函数setFocus()把键盘输入焦点赋给这个窗口部件(或者它的焦点代理)。

首先,一个焦点移出事件会被发送给焦点窗口部件(如果有的话)告诉它关于失去焦点的事情。然后一个焦点进入事件被发送给这个窗口部件告诉它刚刚接收到焦点。(如果焦点移出和进入的窗口部件是同一个的话,就什么都没有发生。)

函数setFocus()会把焦点给一个窗口部件,而不管它的焦点策略,但是不会清空任何键盘捕获(grabKeyboard())。请注意如果窗口部件是被隐藏的,它将不接收焦点。

(3)void QWidget::setFocusProxy(QWidget * w) [虚]

函数setFocusProxy设置这个窗口部件的焦点代理为窗口部件w。如果w为0,这个函数重置这个窗口部件没有焦点代理。

一些窗口部件,比如QComboBox,能够"拥有焦点",但创建一个子窗口部件来实际处理这个焦点。例如,QComboBox创建了一个QLineEdit来处理焦点。

当"这个窗口部件"获得焦点时,setFocusProxy()设置的这个窗口部件实际获得焦点。如果有了一个焦点代理,focusPolicy()、setFocusPolicy()、setFocus()和hasFocus()都在这个焦点代理上操作。

16 会话管理

一个会话是一组正在运行的应用程序,它们每个都一个特殊的状态。会话被一个称为会话管理器(Session manager)的服务程序来控制。在会话里每个参与的应用程序被称为会话客户。

会话管理器为用户发出命令给它的客户。这些命令会使客户提交没有保存的变化(如:保存打开的文件),使客户为将来的会话保存状态或关机。这样的一些操作被称为会话管理。

通常情况下,一个会话由用户在其桌面上同时运行的所有应用程序组成。在Unix/X11下,一个会话可能包括运行在不同计算机和多个显示器上的应用程序。

16.1 会话管理

(1)关闭一个会话

一个会话可以被会话管理器关闭,通常在用户logout时为用户关闭的。一个系统可以在紧急情况下执行自动关闭,例如:在掉电时。正常关机和掉电关机有很大的不同,在正常关机时,用户可能想与应用程序交互,并确定哪些文件应该保存,哪些应该删除。在掉电关机时,没有时间进行交互,甚至于用户不在现场。

(2)不同平台上的协议和支持

Mac OS X和MS-Windows对应用程序还没有完全的会话管理,如:没法恢复以前的会话。它们支持正常的logout,在得到用户确认后,应用程序有机会取消进程。这是与QApplication::comm.itData()方法相对应的功能。X11自从X11R6后,支持完整的会话管理。

(3)让会话管理与Qt一起工作

通过重载QApplication::comm.itData()来使用你的应用程序参加正常的logout处理。如果你仅应用在MS-Windows平台上,就只能提供这个重载了。你的应用程序最好提供一个如图5的程序关闭对话框。

图5 程序关闭对话框

对于完全的会话管理(目前仅X11R6),你还应该关心应用程序的状态存储和下一个会话生命周期恢复状态。

16.2 测试和调试会话管理

在Mac OS X和Windows上的会话管理支持由于这些操作系统本身的这种功能的缺乏而受到限制。为了简单地关闭会话并验证你的应用程序是否如期望的那样执行,你最好启动一个其它的应用程序。这个应用程序将随后得到关闭消息,这样允许你取消关闭。

在Unix上你能使用一个支持标准的X11R6会话管理,或使用X联盟提供的会话管理器xsm。xsm是标准X11R6安装的一部分。它是一个具有图形界面的会话管理器,你可能用它来管理会话。

下面是使用xsm的一个简单的方法:

(1)运行X11R6.

(2)在你的home目录下将创建仅包含下面一行的.xsmstartup文件:

xterm

这将告诉xsm的default/failsafe(缺省/失败安全)会话仅有xterm。否则,xsm将尝试触发包括窗口管理器twm在内多个客户,这没有什么用途。

(3)现在从另一个终端窗口启动xsm。一个会话管理器和xterm将出现。xterm有一个其它的shell没有的属性:在xterm的shell里,SESSION_MANAGER环境变量指向了你刚启动的会话管理器。

(4)在新的xterm窗口中启动你的应用程序,应用程序将自动连接它到会话管理器。你能使用ClientList按钮检查连接是否成功。

注意:当你启动或关闭会话管理的客户时,不要保护ClientList为打开状态。否则,xsm可能崩溃。

(5)使用会话管理器的Checkpoint和Shutdown按钮的不同设置检查你的应用程序执行行为。本地保存类型表示客户端应该保存它们的状态。它对应着QApplication::saveState()函数。全局保存类型请求应用程序保存它们没存储的变化到永久的全局可访问的存储中。它触发QApplication::commitData()。

(6)在用户桌面上xsm是一个有用的会话管理器。作为一个测试环境它是稳定的而有用的。

17 调试技术

Qt应用程序的调试可以通过DDD进行跟踪调试和打印各种调试或警告信息。DDD(Data Display Debugger)是使用gdb调试工具的图形工具,它安装在Linux操作系统中,使用方法可参考DDD的帮助文档。下面说明如何打印各种调试或警告信息

17.1 命令行参数

当你运行Q应用程序时,你可以指定几个命令行参数来帮助你调试。这几个命令行参数说明如下:

-nograb 应用程序不再捕获鼠标或者键盘。当程序在Linux下运行在gdb调试器中时这个选项是默认的。

-dograb 忽略任何隐含的或明显得-nograb。即使-nograb出现在命令行的最后,-dograb也会超过-nograb生效的。

-sync 在X同步模式下运行应用程序。同步模式强迫X服务器立即执行每一个X客户端的请求,而不使用缓存优化。它使得程序更加容易测试并且通常会更慢。-sync模式只对X11版本的Qt有效。

17.2 打印警告和调试消息

Qt使用三个全局函数qDebug、qWarning和qFatal来打印警告和调试信息到标准错误输出stderr(它在缺省情况下为显示屏,也可指定为文件)。 这三个函数说明如下:

qDebug()用来打印调试信息,在调试版本中输出信息,在发布版本中,函数将不起作用。

qWarning()用来在程序发生错误时打印警告信息。

qFatal()用来打印致命错误消息并且退出。

这些函数的Qt实现在Unix/X11下把文本打印到标准错误输出(stderr),在Windows下会打印到调试器。你可以通过安装一个消息处理器,qInstallMsgHandler()来接收这些函数。

因为这3个函数的实现类似,这里只分析函数qDebug,qDebug函数的参数格式与函数printf类似,打印格式化字符串。qDebug函数列出如下(在src/tools/qglobal.cpp中):

static QtMsgHandler handler = 0;		//指向用户定义的打印输出函数的句柄。
static const int QT_BUFFER_LENGTH = 8196; //内部buffer长度。
 
void qDebug( const char *msg, ... ) //msg格式化的需要打印的字符串。
{
    char buf[QT_BUFFER_LENGTH];
    va_list ap;
    va_start( ap, msg );			//使用可变的参数链表。
#if defined(QT_VSNPRINTF)
    QT_VSNPRINTF( buf, QT_BUFFER_LENGTH, msg, ap );
#else
    vsprintf( buf, msg, ap ); //将需要打印的信息放入到buf中。
#endif
    va_end( ap );
    if ( handler ) {//如果用户指定的输出函数存在,使用它来输出信息。
	(*handler)( QtDebugMsg, buf );
    } else {
#if defined(Q_CC_MWERKS)
        mac_default_handler(buf); //mac系统下的缺省输出函数。
#elif defined(Q_OS_TEMP)
	QString fstr( buf );
	OutputDebugString( (fstr + "/n").ucs2() );
#else
	fprintf( stderr, "%s/n", buf );		// 输出到stderr
#endif
    }
}


在src/tools/qglobal.h中定义了QtMsgHandler的函数类型,并将函数qInstallMsgHandler定义为从动态库中输出函数名。这两个定义列出如下:

typedef void (*QtMsgHandler)(QtMsgType, const char *);
// Q_EXPORT表示动态库中输出这个函数名
Q_EXPORT QtMsgHandler qInstallMsgHandler( QtMsgHandler );


函数qInstallMsgHandler被用户用来定义一个安装处理函数,并返回以前定义的消息处理函数的指针。在一个应用程序中只能定义一个消息处理函数。恢复以前的消息处理函数时,调用qInstallMsgHandler(0)。函数列出如下(在src/tools/qglobal.cpp中):

QtMsgHandler qInstallMsgHandler( QtMsgHandler h )
{
    QtMsgHandler old = handler;
    handler = h;
    return old;
}


示例:应用qInstallMsgHandler

下面的例子说明如果在一个应用程序中安装自己的程序运行信息输出函数。这个例子先定义了信息输出函数myMessageOutput,然后,在程序的main函数中安装了信息输出函数。当这个应用函数运行时,就会使用函数myMessageOutput输出运行信息。代码如下:

#include 
#include 
#include 
 
void myMessageOutput( QtMsgType type, const char *msg )//定义信息输出函数
{
    switch ( type ) {
		case QtDebugMsg: //输出调试信息
		    fprintf( stderr, "Debug: %s/n", msg );
		    break;
		case QtWarningMsg: //输出警告信息
		    fprintf( stderr, "Warning: %s/n", msg );
		    break;
		case QtFatalMsg: //输出致命信息
		    fprintf( stderr, "Fatal: %s/n", msg );
		    abort();			//中断运行,退出程序
    }
}
 
int main( int argc, char **argv )
{
    qInstallMsgHandler( myMessageOutput ); //安装信息输出函数
    QApplication a( argc, argv );
    ...
    return a.exec();
}


还有另外两个打印对象信息的调试函数QObject::dumpObjectTree()和QObject::dumpObjectInfo()。它们只在程序调试版本下,输出信息,在发布版本中,这两个函数不起作用。函数QObject::dumpObjectInfo()打印一个对象信号连接等方面的信息。函数QObject::dumpObjectTree()打印出子对象树。

17.3 调试宏

在程序运行中还常使用宏Q_ASSERT和Q_CHECK_PTR来输出信息,这两个宏说明如下:

(1)Q_ASSERT(b)中的b是一个布尔表达式,当b是FALSE的时候,打印出类似的警告信息:"ASSERT:‘b’ in file file.cpp (234)"。

(2)Q_CHECK_PTR(p)中的p是一个指针。如果p是空的话,打印出类似的警告信息:"In file file.cpp, line 234: Out of memory"。

宏Q_ASSERT实质上是调用函数qFatal或qWarning输出信息,列出如下(在src/tools/qglobal.h中):

#if !defined(Q_ASSERT)
#  if defined(QT_CHECK_STATE)
#    if defined(QT_FATAL_ASSERT)
#      define Q_ASSERT(x)   //打印x,文件名,在程序源代码中的行号
#    else
#      define Q_ASSERT(x)  
#    endif
#  else
#    define Q_ASSERT(x)
#  endif
#endif


宏Q_CHECK_PTR实质上调用函数qWarning输出信息,宏定义Q_CHECK_PTR列出如下(在src/tools/qglobal.h中):

#if defined(QT_CHECK_NULL)
#  define Q_CHECK_PTR(p) (qt_check_pointer
#else
#  define Q_CHECK_PTR(p)
#endif
 
Q_EXPORT bool qt_check_pointer( bool c, const char *, int );


函数qt_check_pointer实现信息输出操作,函数列出如下(在src/tools/qglobal.cpp中):

bool qt_check_pointer( bool c, const char *n, int l )
{
    if ( c )
	qWarning( "In file %s, line %d: Out of memory", n, l );
    return TRUE;
}

示例2:运行宏Q_ASSERT和Q_ASSERT

宏Q_ASSERT和Q_ASSERT常用来检测程序错误,下面例子使用了这两个宏:

char *alloc( int size )
{
    Q_ASSERT( size > 0 ); //如果size > 0表达式不成立,打印警告信息。
    char *p = new char[size];
    Q_CHECK_PTR( p ); //如果指针p为空,打印警告信息。
    return p;
}

Qt基于不同的调试标记打印不同类型的警告信息。Qt使用了下面的宏定义说明了不同的调试标记(在src/tools/qglobal.h中):

QT_CHECK_STATE:检测一致的/期望的对象状态

QT_CHECK_RANGE:检测变量范围错误

QT_CHECK_NULL:检测危险的空指针

QT_CHECK_MATH:检测危险的数学,比如被0除

QT_NO_CHECK:关闭所有的QT_CHECK_...标记

QT_DEBUG:使调试代码生效

QT_NO_DEBUG:关闭QT_DEBUG标记

默认情况下,QT_DEBUG和所有的QT_CHECK标记都是打开的。如果要关闭QT_DEBUG,请定义QT_NO_DEBUG。如果要关闭QT_CHECK标记,请定义QT_NO_CHECK。

示例3: 打印不同类型的警告信息

下面的例子根据不同的宏定义打印不同类型的警告信息。代码如下:

void f( char *p, int i )
{
#if defined(QT_CHECK_NULL) //检测危险的空指针
      if ( p == 0 )
          qWarning( "f: Null pointer not allowed" );
#endif
 
#if defined(QT_CHECK_RANGE) //检测变量范围错误
      if ( i < 0 )
          qWarning( "f: The index cannot be negative" );
#endif
}

你可能感兴趣的:(Qt)