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

保护的指针QGuardedPtr<X>使用的方法如下面的例子:
QGuardedPtr<QLabel> label = new QLabel( 0, "label" );
        label->setText( "I like guarded pointers" );
 
        delete (QLabel*) label; // 模拟别人销毁了label
 
        if ( label)
            label->show();
        else
            qDebug("The label has been destroyed");

  程序将输出"The label has been destroyed",而不是引用了一个无效的地址。 如果你需要知道别人拥有的多个QObject何时已被删除,QObjectCleanupHandler将是很有用的,如:分配在一个共享库里的应用程序卸载共享库时必须知道共享库的所有对象必须销毁。一个使用QObjectCleanupHandler来监视对象销毁的例子如下:
class FactoryComponent : public FactoryInterface, public QLibraryInterface
    {
     
    public:
        ...
 
        QObject *createObject();
 
        bool init();
        void cleanup();
        bool canUnload() const;
 
    private:
        QObjectCleanupHandler objects;
    };
 
    // 分配一个新的对象并把它加入到cleanup handler中
    QObject *FactoryComponent::createObject()
    {
     
        return objects.add( new QObject() );
    }
 
    // QLibraryInterface接口应用
    bool FactoryComponent::init()
    {
     
        return TRUE;
    }
 
    void FactoryComponent::cleanup()
    {
     
    }
 
    //当所有QObject对象被销毁时,卸载库才是安全的。
    bool FactoryComponent::canUnload() const
    {
     
        return objects.isEmpty();
    }

元对象系统不能对信号与槽使用模板。一个简单的理由是:由于各个编译器的不充分,Qt不能在多个平台的应用程序中完全应用模板。即使今天,许多使用很广的C++编译器应用到先进模板时有问题。例如:你不能安全依赖于部分模板实例。

即使C++编译器对模板有优秀的支持,我们也不能抛弃元对象编译器使用的基于字符串访问。原因如下:

Qt信号与槽的语法是直观的、易用易读,在类定义中声明了信号确保了信号在保护成员函数中被保护。

Qt的元编译器moc (Meta Object Compiler)产生能被任何标准C++编译器访问的附加C++代码。moc读取C++代码文件,如果它发现类声明中含有"Q_OBJECT"宏,它将给这些类产生另外的C++代码,其中装有元对象代码。这些被moc产生有C++源代码必须被编译连接到这个类(或它能被#include进这个类的源文件里)。moc通常不被手动调用,而是被编译系统自动调用,这样它不需要编程员做另外的工作。

由于给信号与槽增加了moc,我们能加其它有用的东西到Qt,这是模板类不能做的。如:使用tr()函数翻译,先进的属性系统和扩展的运行类型信息。属性系统可用于象Qt Designer这样的通用用户界面设计工具。带有moc预处理器的C++基本上提供了面向对象的C的灵活性或类似Java的运行环境,并保持了C++的执行效率和扩展性。

2.1 信号(signal)和槽(slot)

signal和slot用于对象间的通讯。信号/槽机制是Qt的一个重要特征。 在图形用户界面编程中,常需要将一个窗口部件的变化通知给另一个窗口部件,或者说希望对象间进行通讯。一般的图形用户界面编程中采用回调函数进行对象间通信,这样回调和处理函数捆绑在一起,没有signal和slot机制的简便和灵活。信号和槽连接的有原理图如图1。

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

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

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

 

图3 信号和槽连接示意图一个带有信号和槽的Qt类Foo声明如下:
class Foo : public QObject
 {
      
        Q_OBJECT //包含信号和/或者槽的类必须声明Q_OBJECT
    public:
        Foo();
        int value() const {
       return val; }
    public slots:
        void setValue( int ); //槽的声明
    signals:
        void valueChanged( int ); //信号声明
    private:
        int val;
};
 
void Foo::setValue( int v )
{
      
        if ( v != val ) {
      
            val = v;
            emit valueChanged(v); //发送信号
        }
}

下面把两个对象连接在一起:
Foo a, b;
   connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));//信号与槽连接
   b.setValue( 11 ); // a == undefined  b == 11
   a.setValue( 79 ); // a == 79         b == 79
b.value();

调用a.setValue(79)会使a发射一个valueChanged() 信号,b将会在它的setValue()槽中接收这个信号,也就是b.setValue(79) 被调用。接下来b会发射同样的valueChanged()信号,但是因为没有槽被连接到b的valueChanged()信号,所以没有发生任何事(信号消失了)。

signals、slots和emit不是C++的标准关键字,需要预处理程序改变或者移去了signals、slots和emit 这些关键字,然后再使用标准的C++编译器。对包含有信号和槽的类定义运行MOC(Meta Object Compiler)。生成一个可以和其它对象文件编译和连接成引用程序的C++源文件。

槽是普通成员函数,它和普通成员函数一样分为public、protected和private三类,public slots表示声明的是任何信号都可以相连的槽。protected slots表示这个类的槽和它的子类的信号才能连接。private slots表示这个类本身的信号可以连接这个类的槽。

元对象编译器(moc)解析一个C++文件中的类声明并且生成初始化元对象的C++代码。元对象包括所有信号和槽函数的名称,还有这些函数的指针。

2.2 元对象系统

Qt中的元对象系统是用来处理对象间通讯的信号/槽机制、运行时的类型信息和动态属性系统。它基于QObject类、类声明中的私有段中的Q_OBJECT宏和元对象编译器(moc)。

moc读取C++源文件,如果它发现类的声明中含有Q_OBJECT宏,它就会给含有Q_OBJECT宏的类生成另一个含有元对象代码的C++源文件。这个生成的源文件可以被类的源文件包含(#include)并和这个类的实现一起编译连接。

除了提供对象间通信的信号和槽机制之外(这是使用元对象最主要的原因),QObject中的元对象代码还实现其它特征:

className()函数在运行的时候以字符串返回类的名称,不需要C++编译器中的本地运行类型信息的支持。

inherits()函数返回本对象一个在QObject继承树中一个特定类的实例。

tr()和trUtf8() 两个函数是用于国际化中的字符串翻译。

setProperty()和property()函数用来通过名称动态设置和获得对象属性。

metaObject()函数返回这个类所关联的元对象。

在类的定义中声明了Q_OBJECT宏,这个类才能使用元对象系统相关的牲。建议QObject 的所有子类使用Q_OBJECT宏,而不管它们是否实际使用了信号、槽和属性。

元对象编译器读取一个C++源文件。如果它发现其中的一个或多个类的声明中含有Q_OBJECT宏,它就会给这个使用Q_OBJECT宏的类生成另外一个包含元对象代码的C++源文件。

如果你是用qmake来生成你的Makefile文件,当需要的时候,编译规则中会包含调用元对象编译器,所以你不需要直接使用元对象编译器。

元对象编译器生成的输出文件必须被编译和连接,就像你的程序中的其它的C++代码一样,这种操作是用下述两种方式之一解决的:

 

方法1:类的声明放在一个头文件(.h文件)中。

如果在文件myclass.h中发现类的声明,元对象编译器的输出文件将会被放在一个叫moc_myclass.cpp的文件中。这个文件象普通文件一样被编译,输出对象文件的结果是moc_myclass.o(在Unix下)或者moc_myclass.obj(在Windows下)。这个对象接着会被包含到一个对象文件列表中,它们将在程序的最后连接阶段被连接在一起。

方法2:类的声明放在一个实现文件(.cpp文件)中。

如果在文件myclass.cpp中发现类的声明,元对象编译器的输出文件将会被放在一个叫myclass.moc的文件中。这个文件需要被实现文件包含(#include),也就是说myclass.cpp需要包含下面这行放在所有的代码之后:

#include "myclass.moc"

这样,元对象编译器生成的代码将会和myclass.cpp中普通的类定义一起被编译和连接。

方法1是常规的方法。方法2用在你想让实现文件自包含,或者Q_OBJECT类是内部实现的并且在头文件中不可见的这些情况下使用。

建议使用自由makefile生成工具qmake来生成你的Makefile。这个工具可以识别方法一和方法二风格的源文件,并建立一个可以做所有必要的元对象编译操作的Makefile。

如果你想自己建立你的Makefile,根据上面的方法1和方法2所提的声明Q_OBJECT宏不同方法,下面说明如何在makefile中包含元对象编译操作:

对于在头文件中声明了Q_OBJECT宏的类,如果你只使用GNU的make,在makefile如下添加元对象编译规则:
moc_%.cpp: %.h
            moc $< -o $@

你还可以按下面的格式写单个文件的规则:
moc_NAME.cpp: NAME.h
            moc $< -o $@

你必须把moc_NAME.cpp添加到你的SOURCES变量中并且把moc_NAME.o(Linux操作系统下)或者moc_NAME.obj(windows操作系统下)添加到你的OBJECTS变量中。 对于在实现文件(.cpp文件)中声明Q_OBJECT的类,你可使用下面这样的makefile规则:
NAME.o: NAME.moc
 
    NAME.moc: NAME.cpp
            moc -i $< -o $@

这将会保证make程序会在编译NAME.cpp之前运行元对象编译器。然后你可以把下面这行放在NAME.cpp的末尾,这样在这个文件中的所有的类声明都知道这个元对象。
#include "NAME.moc"

元对象编译中常出现的错误是:

YourClass::className() is undefined

或者

YourClass lacks a vtbl

出现这种错误的绝大多数情况是你忘记了编译或者#include元对象编译器产生的C++代码,或者(在前面的情况下)没有在连接命令中包含那个对象文件。

2.3 元对象编译器限制

元对象编译器并不展开#include或者#define,它简单地忽略它所遇到的所有预处理程序。元对象编译器无法处理所有的C++语法。主要的问题是类模板不能含有信号和槽。下面是一个错误的例子:
class SomeTemplate<int> : public QFrame {
        
    Q_OBJECT
    ...
signals:
    void bugInMocDetected( int );
};

元对象编译器的限制说明如下:

(1)多重继承把QObject的子类作为第一个父类

如果你使用了多重继承,元对象编译器认为第一个继承类是QObject的子类。这是因为元对象编译器并不展开#include或#define,它无法发现基类中哪个是QObject。例如:
class SomeClass : public QObject, public OtherClass {
        
    ...
};

(2)函数指针不能作为信号或槽的参数 下面是一个不合法的语法的例子:
class SomeClass : public QObject {
        
    Q_OBJECT
    ...
public slots:
    // 不合法的
    void apply( void (*apply)(List *, void *), char * );
};

可以象下面这样纠正这个错误:
typedef void (*ApplyFunctionType)( List *, void * );

  class SomeClass : public QObject {

   Q_OBJECT
   ...

public slots:

   void apply( ApplyFunctionType, char * );
};

(3)不能把友声明friend放在信号或者槽的声明部分 通常情况下,友声明friend不能放在信号或者槽的声明部分。把它们替换到private、protect或public部分中。这里是一个不合法的例子:
class SomeClass : public QObject {
        
    Q_OBJECT
    ...
signals:
    friend class ClassTemplate<char>; // 错的
};

(4)信号和槽不能被升级 把继承的成员函数升级为公有状态这一个C++特征对信号和槽并不适用。下面是一个不合法的例子:
class Whatever : public QButtonGroup {
        
    ...
public slots:
    void QButtonGroup::buttonPressed; // 错的,槽是保护的。
    ...
};

(5)宏不能用在信号和槽的参数中 因为元对象编译器不能展开#define,在信号和槽中类型宏作为一个参数是不能工作的。下面是一个不合法的例子:
#ifdef ultrix
#define SIGNEDNESS(a) unsigned a
#else
#define SIGNEDNESS(a) a
#endif
 
class Whatever : public QObject {
        
    ...
signals:
    void someSignal( SIGNEDNESS(int) );
    ...
};

(6)嵌套类不能放在信号部分或者槽部分,也不能含有信号和槽 下面是一个例子:
class A {
        
        Q_OBJECT
    public:
        class B {
        
        public slots:   // 错的
            void b();
            ...
        };
    signals:
        class B {
               // 错的
            void b();
            ...
        }:
    };

(7)构造函数不能用于信号或槽的声明部分 下面是一个错误的例子:
class SomeClass : public QObject {
        
    Q_OBJECT
public slots:
    SomeClass( QObject *parent, const char *name )
         : QObject( parent, name ) {
         } // 错的
    ...
};

(8)属性的声明应该public声明部分之前 如果在public之后声明属性,元对象编译器将不能找到函数或解析这个类型。下面是一个错误的例子:
class SomeClass : public QObject {
        
    Q_OBJECT
public:
    ...
    Q_PROPERTY( Priority priority READ priority WRITE setPriority ) // 错的
    Q_ENUMS( Priority ) // 错的
    enum Priority {
         High, Low, VeryHigh, VeryLow };
    void setPriority( Priority );
    Priority priority() const;
    ...
};

这个例子纠正错误后列出如下:
class SomeClass : public QObject {
        
   Q_OBJECT
   Q_PROPERTY( Priority priority READ priority WRITE setPriority )
   Q_ENUMS( Priority )

public:

   ...
   enum Priority {
          High, Low, VeryHigh, VeryLow };
   void setPriority( Priority );
   Priority priority() const;
   ...
};

2.4 属性

Qt的属性也基于元对象系统,在类声明中用宏Q_PROPERTY来声明。属性只能在继承于QObject的子类中声明。宏Q_OVERRIDE用来覆盖一些子类中由继承得到的属性。属性也是一个类的成员。

元对象系统中设置属性和得到属性的成员函数列出如下:

QObject::setProperty() 可以让你控制类中那些在编译时不可用的属性。

QMetaObject::propertyNames() 返回所有可用属性的名称。

QMetaObject::property() 返回一个指定属性的属性数据:一个QMetaProperty对象。

下面两个设置函数是等效的:

// QButton *b和QObject *o指向同一个按钮时
    b->setDown( TRUE );
    o->setProperty( "down", TRUE );

示例:使用元对象系统的属性

下面的类MyClass用来设置和得到优先级,但还不能使用元对象系统的属性。类MyClass列出如下:

class MyClass : public QObject
{
         
    Q_OBJECT
public:
    MyClass( QObject * parent=0, const char * name=0 );
    ~MyClass();
 
    enum Priority {
          High, Low, VeryHigh, VeryLow };
    void setPriority( Priority );
   Priority priority() const;
};

为了使用元对象的属性系统,必须用宏Q_PROPERTY来声明属性。宏Q_PROPERTY语法如下:

Q_PROPERTY( type name READ getFunction [WRITE setFunction]
            [RESET resetFunction] [DESIGNABLE bool] 
            [SCRIPTABLE bool] [STORED bool] )

宏Q_PROPERTY说明如下:

type name是属性的类型及名字,它可以是一个QVariant支持的类型或者在类中已经定义的枚举类型。枚举类型必须使用Q_ENUMS宏来进行注册。

READ getFunction表示用于读取属性的函数是getFunction。

WRITE setFunction表示用于写(或设置)属性的函数是setFunction。

RESET resetFunction表示用函数resetFunction设置属性到缺省状态(这个缺省状态可能和初始状态不同)。这个函数必须返回void并且不带有参数。

DESIGNABLE bool声明这个属性是否适合被一个图形用户界面设计工具修改。bool缺省为TRUE,说明这个属性可写,否则,FALSE说明不能被图形用户界面设计工具修改。

  SCRIPTABLE bool声明这个属性是否适合被一个脚本引擎访问。bool缺省为TRUE,说明可以被访问。

STORED bool声明这个属性的值是否必须作为一个存储的对象状态而被记住。STORED只对可写的属性有意义。缺省是TRUE。

这样,修改成使用了宏Q_PROPERTY的MyClass类列出如下:

class MyClass : public QObject
{
         
    Q_OBJECT
    Q_PROPERTY( Priority priority READ priority WRITE setPriority )
    Q_ENUMS( Priority )
public:
    MyClass( QObject * parent=0, const char * name=0 );
    ~MyClass();
 
    enum Priority {
          High, Low, VeryHigh, VeryLow };
    void setPriority( Priority );
    Priority priority() const;
};

当枚举数据可以被同时被读或写时,必须使用Q_SETS来注册这个枚举类型。

宏Q_CLASSINFO可以用来把名称/值这样一套的属性添加到一个类的元对象中,例如:

Q_CLASSINFO( "Version", "3.0.0" )

和其它元数据一样,类信息在运行时是可以通过元对象QMetaObject::classInfo()访问的。

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 <qevent.h>
#include <qstring.h>
 
#define REFRESHUI_EVENT QEvent::User+2
class TEST_Event: public QCustomEvent
{
            
public:
    TEST_Event();
};

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

#include <test_event.h>
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国际化

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值