Inside Qt Series 全 (一)----(十六)

来源:互联网 发布:面板数据因子分析 编辑:程序博客网 时间:2024/05/16 09:38

Inside Qt Series (一):Starting From the QObject

作者:Q-Kevin @ http://www.qkevin.com

    QObject 这个 class 是 Qt 对象模型的核心,绝大部分的 Qt 类都是从这个类继承而来。这个模型的中心特征就是一个叫做信号和槽(signal and slot)的机制来实现对象间的通讯,你可以把一个信号和另一个槽通过 connect(…) 方法连接起来,并可以使用 disconnect(…) 方法来断开这种连接,你还可以通过调用 blockSignal(…) 这个方法来临时的阻塞信号,

    QObject 把它们自己组织在对象树中。当你创建一个 QObject 并使用其它对象作为父对象时,这个对象会自动添加到父对象的 children() list 中。父对象拥有这个对象,比如,它将在它的析构函数中自动删除它所有的 child 对象。你可以通过 findChild() 或者 findChildren()函数来查找一个对象。

    每个对象都有一个对象名称(objectName())和类名称(class name), 他们都可以通过相应的 metaObject 对象来获得。你还可以通过 inherits() 方法来判断一个对象的类是不是从另一个类继承而来。

    当对象被删除时,它发出destroyed()信号。你可以捕获这个信号来避免对QObject的无效引用。

    QObject可以通过event()接收事件并且过滤其它对象的事件。详细情况请参考installEventFilter()和eventFilter()。

    对于每一个实现了信号、槽和属性的对象来说,Q_OBJECT 宏都是必须要加上的。
    QObject 实现了这么多功能,那么,它是如何做到的呢?让我们通过它的 Source Code 来解开这个秘密吧。

QObject 类的实现文件一共有四个:
    * qobject.h,QObject class 的基本定义,也是我们一般定义一个类的头文件
    * qobject.cpp,QObject class 的实现代码基本上都在这个文件
    * qobjectdefs.h,这个文件中最重要的东西就是定义了 QMetaObject class,这个class是为了实现 signal、slot、properties,的核心部分。
    * qobject_p.h,这个文件中的 code 是辅助实现 QObject class 的,这里面最重要的东西是定义了一个 QObjectPrivate 类来存储 QOjbect 对象的成员数据。

    理解这个 QObjectPrivate class 又是我们理解 Qt kernel source code 的基础,这个对象包含了每一个 Qt 对象中的数据成员,好了,让我们首先从理解 QObject 的数据存储代码开始我么的 Qt Kernel Source Code 之旅。

敬请关注下一节:QObject 对象数据存储

Inside Qt Series (二):对象数据存储(A)

作者:Q-Kevin @ http://www.qkevin.com

前言,为什么先说这个?

     我们知道,在C++中,几乎每一个类(class)中都需要有一些类的成员变量(class member variable),在通常情况下的做法如下:

class Person
{
private:
    string mszName; // 姓名
    bool mbSex;    // 性别
    int mnAge;     // 年龄
};

     就是在类定义的时候,直接把类成员变量定义在这里,甚至于,把这些成员变量的存取范围直接定义成是 public 的,您是不是这是这样做的呢?

在Qt中,却几乎都不是这样做的,那么,Qt是怎么做的呢

     几乎每一个C++的类中都会保存许多的数据,要想读懂别人写的C++代码,就一定需要知道每一个类的的数据是如何存储的,是什么含义,否则,我们不可能读懂别人的C++代码。在这里也就是说,要想读懂Qt的代码,第一步就必须先搞清楚Qt的类成员数据是如何保存的。

    为了更容易理解Qt是如何定义类成员变量的,我们先说一下Qt 2.x 版本中的类成员变量定义方法,因为在 2.x 中的方法非常容易理解。然后在介绍 Qt 4.6 中的类成员变量定义方法。

Qt 2.x 中的方法

在定义class的时候(在.h文件中),只包含有一个类成员变量,只是定义一个成员数据指针,然后由这个指针指向一个数据成员对象,这个数据成员对象包含所有这个class的成员数据,然后在class的实现文件(.cpp文件)中,定义这个私有数据成员对象。示例代码如下:

//————————————————————————————————————–
// File name:  person.h

struct PersonalDataPrivate; // 声明私有数据成员类型

class Person
{
public:

    Person ();   // constructor
    virtual ~Person ();  // destructor
    void setAge(const int);
    int getAge();

private:

    PersonalDataPrivate* d;
};

 

//————————————————————————————————————–
// File name:  person.cpp

struct PersonalDataPrivate  // 定义私有数据成员类型
{
    string mszName; // 姓名
    bool mbSex;    // 性别
    int mnAge;     // 年龄
};

// constructor
Person::Person ()
{
    d = new PersonalDataPrivate;
};

// destructor
Person::~Person ()
{
    delete d;
};

void Person::setAge(const int age)
{
    if (age != d->mnAge)
        d->mnAge = age;
}

int Person::getAge()
{
    return d->mnAge;
}

在最初学习Qt的时候,我也觉得这种方法很麻烦,但是随着使用的增多,我开始很喜欢这个方法了,而且,现在我写的代码,基本上都会用这种方法。具体说来,它有如下优点:

    * 减少头文件的依赖性
      把具体的数据成员都放到cpp文件中去,这样,在需要修改数据成员的时候,只需要改cpp文件而不需要头文件,这样就可以避免一次因为头文件的修改而导致所有包含了这个文件的文件全部重新编译一次,尤其是当这个头文件是非常底层的头文件和项目非常庞大的时候,优势明显。
      同时,也减少了这个头文件对其它头文件的依赖性。可以把只在数据成员中需要用到的在cpp文件中include一次就可以,在头文件中就可以尽可能的减少include语句
    * 增强类的封装性
      这种方法增强了类的封装性,无法再直接存取类成员变量,而必须写相应的 get/set 成员函数来做这些事情。
      关于这个问题,仁者见仁,智者见智,每个人都有不同的观点。有些人就是喜欢把类成员变量都定义成public的,在使用的时候方便。只是我个人不喜欢这种方法,当项目变得很大的时候,有非常多的人一起在做这个项目的时候,自己所写的代码处于底层有非常多的人需要使用(#include)的时候,这个方法的弊端就充分的体现出来了。

还有,我不喜欢 Qt 2.x 中把数据成员的变量名都定义成只有一个字母,d,看起来很不直观,尤其是在search的时候,很不方便。但是,Qt kernel 中的确就是这么干的。

那么,在 Qt4.6 里面是如何实现的呢?请关注下一节。

Inside Qt Series (三):对象数据存储(B)

作者:Q-Kevin @ http://www.qkevin.com

Qt 4.6.x 中的方法

在 Qt 4.6 中,类成员变量定义方法的出发点没有变化,只是在具体的实现手段上发生了非常大的变化,下面具体来看。

在 Qt 4.6 中,使用了非常多的宏来做事,这凭空的增加了理解 Qt source code 的难度,不知道他们是不是从MFC学来的。就连在定义类成员数据变量这件事情上,也大量的使用了宏。

在这个版本中,类成员变量不再是给每一个class都定义一个私有的成员,而是把这一项common的工作放到了最基础的基类 QObject 中,然后定义了一些相关的方法来存取,好了,让我们进入具体的代码吧。

//————————————————————————————————————–
// file name: qobject.h

class QObjectData
{
public:
    virtual ~QObjectData() = 0;
    // 省略
};

class QObject
{
    Q_DECLARE_PRIVATE(QObject)

public:

    QObject(QObject *parent=0);

protected:

    QObject(QObjectPrivate &dd, QObject *parent = 0);
    QObjectData *d_ptr;
}

这些代码就是在 qobject.h 这个头文件中的。在 QObject class 的定义中,我们看到,数据员的定义为:QObjectData *d_ptr; 定义成 protected 类型的就是要让所有的派生类都可以存取这个变量,而在外部却不可以直接存取这个变量。而 QObjectData 的定义却放在了这个头文件中,其目的就是为了要所有从QObject继承出来的类的成员变量也都相应的要在QObjectData这个class继承出来。而纯虚的析构函数又决定了两件事:

    * 这个class不能直接被实例化。换句话说就是,如果你写了这么一行代码,new QObjectData, 这行代码一定会出错,compile的时候是无法过关的。
    * 当 delete 这个指针变量的时候,这个指针变量是指向的任意从QObjectData继承出来的对象的时候,这个对象都能被正确delete,而不会产生错误,诸如,内存泄漏之类的。

我们再来看看这个宏做了什么,Q_DECLARE_PRIVATE(QObject)

#define Q_DECLARE_PRIVATE(Class) \
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } \
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } \
    friend class Class##Private;

这个宏主要是定义了两个重载的函数,d_func(),作用就是把在QObject这个class中定义的数据成员变量d_ptr安全的转换成为每一个具体的class的数据成员类型指针。我们看一下在QObject这个class中,这个宏展开之后的情况,就一幕了然了。

Q_DECLARE_PRIVATE(QObject) 展开后,就是下面的代码:

inline QObjectPrivate* d_func() { return reinterpret_cast<QObjectPrivate *>(d_ptr); }
inline const QObjectPrivate* d_func() const
{ return reinterpret_cast<const QObjectPrivate *>(d_ptr); } \
friend class QObjectPrivate;

宏展开之后,新的问题又来了,这个QObjectPrivate是从哪里来的?在QObject这个class中,为什么不直接使用QObjectData来数据成员变量的类型?

还记得我们刚才说过吗,QObjectData这个class的析构函数的纯虚函数,这就说明这个class是不能实例化的,所以,QObject这个class的成员变量的实际类型,这是从QObjectData继承出来的,它就是QObjectPrivate !

这个 class 中保存了许多非常重要而且有趣的东西,其中包括 Qt 最核心的 signal 和 slot 的数据,属性数据,等等,我们将会在后面详细讲解,现在我们来看一下它的定义:

下面就是这个class的定义:

class QObjectPrivate : public QObjectData
{
    Q_DECLARE_PUBLIC(QObject)

public:

    QObjectPrivate(int version = QObjectPrivateVersion);
    virtual ~QObjectPrivate();
    // 省略
}

那么,这个 QObjectPrivate 和 QObject 是什么关系呢?他们是如何关联在一起的呢?


Inside Qt Series (四):对象数据存储(C)

作者:Q-Kevin @ http://www.qkevin.com

接上节,让我们来看看这个 QObjectPrivate 和 QObject 是如何关联在一起的。

//————————————————————————————————————–
// file name: qobject.cpp

QObject::QObject(QObject *parent)
    : d_ptr(new QObjectPrivate)
{
  // ………………………
}

QObject::QObject(QObjectPrivate &dd, QObject *parent)
    : d_ptr(&dd)
{
   // …………………
}

怎么样,是不是一目了然呀?

从第一个构造函数可以很清楚的看出来,QObject class 中的 d_ptr 指针将指向一个 QObjectPrivate 的对象,而QObjectPrivate这个class是从QObjectData继承出来的。

这第二个构造函数干什么用的呢?从 QObject class 的定义中,我们可以看到,这第二个构造函数是被定义为 protected 类型的,这说明,这个构造函数只能被继承的class使用,而不能使用这个构造函数来直接构造一个QObject对象,也就是说,如果写一条下面的语句,编译的时候是会失败的,
        new QObject(*new QObjectPrivate, NULL)

为了看的更清楚,我们以QWidget这个class为例说明

QWidget是QT中所有UI控件的基类,它直接从QObject继承而来,

class QWidget : public QObject, public QPaintDevice
{
    Q_OBJECT
    Q_DECLARE_PRIVATE(QWidget)
    // …………………
}

我们看一个这个class的构造函数的代码:
QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
    : QObject(*new QWidgetPrivate, 0), QPaintDevice()
{
    d_func()->init(parent, f);
}

非常清楚,它调用了基类QObject的保护类型的构造函数,并且以 *new QWidgetPrivate 作为第一个参数传递进去。也就是说,基类(QObject)中的d_ptr指针将会指向一个QWidgetPrivate类型的对象。

再看QWidgetPrivate这个class的定义:

class QWidgetPrivate : public QObjectPrivate
{
    Q_DECLARE_PUBLIC(QWidget)
    // …………………
}

好了,这就把所有的事情都串联起来了。

关于QWidget构造函数中的唯一的语句 d_func()->init(parent, f) 我们注意到在class的定义中有这么一句话: Q_DECLARE_PRIVATE(QWidget)

我们前面讲过这个宏,当把这个宏展开之后,就是这样的:

inline QWidgetPrivate* d_func() { return reinterpret_cast<QWidgetPrivate *>(d_ptr); }
inline const QWidgetPrivate* d_func() const
{ return reinterpret_cast<const QWidgetPrivate *>(d_ptr); } \
friend class QWidgetPrivate;

很清楚,它就是把QObject中定义的d_ptr指针转换为QWidgetPrivate类型的指针。

小结:

要理解Qt Kernel的code,就必须要知道Qt中每一个Object内部的数据是如何保存的,而Qt没有象我们平时写code一样,把所有的变量直接定义在类中,所以,不搞清楚这个问题,我们就无法理解一个相应的class。其实,在Qt4.6中的类成员数据的保存方法在本质是与Qt2.x中的是一样的,就是在class中定义一个成员数据的指针,指向成员数据集合对象(这里是一个QObjectData或者是其派生类)。初始化这个成员变量的办法是定义一个保护类型的构造函数,然后在派生类的构造函数new 一个派生类的数据成员,并将这个新对象赋值给QObject的数据指针。在使用的时候,通过预先定义个宏里面的一个inline函数来把数据指针在安全类型转换,就可以使用了。


Inside Qt Series (五):元对象系统(Meta-Object System)

作者:Q-Kevin @ http://www.qkevin.com

从本节开始,我们讲解 Qt Meta-Object System 的功能,以及实现。

在使用 Qt 开发的过程中,大量的使用了 signal 和 slot. 比如,响应一个 button 的 click 事件,我们一般都写如下的代码:

class MyWindow : public QWidget
{
    Q_OBJECT
public:
    MyWindow(QWidget* parent) : QWidget(parent)
    {
      QPushButton* btnStart = new QPushButton(“start”, this);
      connect(btnStart, SIGNAL(clicked()), SLOT(slotStartClicked()));
    }

private slots:
    void slotStartClicked();
};

void MyWindow:: slotStartClicked()
{
    // 省略
}

在这段代码中,我们把 btnStart 这个 button 的clicked() 信号和 MyWindow 的 slotStartClicked() 这个槽相连接,当 btnStart 这个 button 被用户按下(click)的时候,就会发出一个 clicked() 的信号,然后,MyWindow:: slotStartClicked() 这个 slot 函数就会被调用用来响应 button 的 click 事件。

    这段代码是最为典型的 signal/slot 的应用实例,在实际的工作过程中,signal/slot 还有更为广泛的应用。准确的说,signal/slot 是QT提供的一种在对象间进行通讯的技术,那么,这个技术在QT 中是如何实现的呢?

    这就是 Qt 中的元对象系统(Meta Object System)的作用,为了更好的理解它,让我先来对它的功能做一个回顾,让我们一起来揭开它神秘的面纱。

Meta-Object System 的基本功能

Meta Object System 的设计基于以下几个基础设施:

    * QObject 类
      作为每一个需要利用元对象系统的类的基类
    * Q_OBJECT 宏,
      定义在每一个类的私有数据段,用来启用元对象功能,比如,动态属性,信号和槽
    * 元对象编译器moc (the Meta Object Complier),
      moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++ 源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++ 源文件名前面加上 moc_ 作为新文件的文件名。其具体过程如下图所示:

除了提供在对象间进行通讯的机制外,元对象系统还包含以下几种功能

    * QObject::metaObject() 方法
      它获得与一个类相关联的 meta-object
    * QMetaObject::className() 方法
      在运行期间返回一个对象的类名,它不需要本地C++编译器的RTTI(run-time type information)支持
    * QObject::inherits() 方法
      它用来判断生成一个对象类是不是从一个特定的类继承出来,当然,这必须是在QObject类的直接或者间接派生类当中
    * QObject::tr() and QObject::trUtf8()
      这两个方法为软件的国际化翻译字符串
    * QObject::setProperty() and QObject::property()
      这两个方法根据属性名动态的设置和获取属性值

    除了以上这些功能外,它还使用qobject_cast()方法在QObject类之间提供动态转换,qobject_cast()方法的功能类似于标准C++的dynamic_cast(),但是qobject_cast()不需要RTTI的支持,在一个QObject类或者它的派生类中,我们可以不定义Q_OBJECT宏。如果我们在一个类中没有定义Q_OBJECT宏,那么在这里所提到的相应的功能在这个类中也不能使用,从meta-object的观点来说,一个没有定义Q_OBJECT宏的类与它最接近的那个祖先类是相同的,那就是所,QMetaObject::className() 方法所返回的名字并不是这个类的名字,而是与它最接近的那个祖先类的名字。所以,我们强烈建议,任何从QObject继承出来的类都定义Q_OBJECT 宏。

下一节,我们来了解另一个重要的工具:Meta-Object Compiler


Inside Qt Series (六):元对象编译器 – Meta Object Compiler (moc)

作者:Q-Kevin @ http://www.qkevin.com

元对象编译器用来处理Qt 的C++扩展,moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++ 源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++ 源文件名前面加上 moc_ 作为新文件的文件名。

   如果使用qmake工具来生成Makefile文件,所有需要使用moc的编译规则都会给自动的包含到Makefile文件中,所以对程序员来说不需要直接的使用moc

    除了处理信号和槽之外,moc还处理属性信息,Q_PROPERTY()宏定义类的属性信息,而Q_ENUMS()宏则定义在一个类中的枚举类型列表。 Q_FLAGS()宏定义在一个类中的flag枚举类型列表,Q_CLASSINFO()宏则允许你在一个类的meta信息中插入name/value 对。

    由moc所生成的文件必须被编译和链接,就象你自己写的另外一个C++文件一样,否则,在链接的过程中就会失败。

Code example:

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Priority priority READ priority WRITE setPriority)
    Q_ENUMS(Priority)
    Q_CLASSINFO("Author", "Oscar Peterson")
    Q_CLASSINFO("Status", "Active")

public:
    enum Priority { High, Low, VeryHigh, VeryLow };

    MyClass(QObject *parent = 0);
    virtual ~MyClass();

    void setPriority(Priority priority);
    Priority priority() const;
};

Inside Qt Series (七):Signal & Slot

作者:Q-Kevin @ http://www.qkevin.com

本节介绍Signal和slot的基本知识。

    信号和槽是用来在对象间通讯的方法,当一个特定事件发生的时候,signal会被 emit 出来,slot 调用是用来响应相应的 signal 的。Qt 对象已经包含了许多预定义的 signal,但我们总是可以在派生类中添加新的 signal。Qt 对象中也已经包含了许多预定义的 slog,但我们可以在派生类中添加新的 slot 来处理我们感兴趣的 signal.

    signal 和 slot 机制是类型安全的,signal 和 slot必须互相匹配(实际上,一个solt的参数可以比对应的signal的参数少,因为它可以忽略多余的参数)。signal 和 slot是松散的配对关系,发出signal的对象不关心是那个对象链接了这个signal,也不关心是那个或者有多少slot链接到了这个 signal。Qt的signal 和 slot机制保证了,如果一个signal和slot相链接,slot会在正确的时机被调用,并且是使用正确的参数。Signal和slot都可以携带任何数量和类型的参数,他们都是类型安全的。

    所有从QObject直接或者间接继承出来的类都能包含信号和槽,当一个对象的状态发生变化的时候,信号就可以被emit出来,这可能是某个其它的对象所关心的。这个对象并不关心有那个对象或者多少个对象链接到这个信号了,这是真实的信息封装,它保证了这个对象可以作为一个软件组件来被使用。

    槽(slot)是用来接收信号的,但同时他们也是一个普通的类成员函数,就象一个对象不关心有多少个槽链接到了它的某个信号,一个对象也不关心一个槽链接了多少个信号。这保证了用Qt创建的对象是一个真实的独立的软件组件。

    一个信号可以链接到多个槽,一个槽也可以链接多个信号。同时,一个信号也可以链接到另外一个信号。所有使用了信号和槽的类都必须包含 Q_OBJECT 宏,而且这个类必须从QObject类派生(直接或者间接派生)出来,

    当一个signal被emit出来的时候,链接到这个signal的slot会立刻被调用,就好像是一个函数调用一样。当这件事情发生的时候,signal和slot机制与GUI的事件循环完全没有关系,当所有链接到这个signal的slot执行完成之后,在 emit 代码行之后的代码会立刻被执行。当有多个slot链接到一个signal的时候,这些slot会一个接着一个的、以随机的顺序被执行。

    Signal 代码会由 moc 自动生成,开发人员一定不能在自己的C++代码中实现它,并且,它永远都不能有返回值。Slot其实就是一个普通的类函数,并且可以被直接调用,唯一特殊的地方是它可以与signal相链接。C++的预处理器更改或者删除 signal, slot, emit 关键字,所以,对于C++编译器来说,它处理的是标准的C++源文件。


 

如下图所示:假定 QPushButton 的 signal clicked() 已经和 QLineEdit 的 signal clear() 连接成功,那么当 QPushButton 的 clicked() signal 被 emit 出来的时候,QLineEdit 的 clear() slot 就会被调用。

 

Inside Qt Series (八):Meta Object Class overview

作者:Q-Kevin @ http://www.qkevin.com

前面我们介绍了 Meta Object 的基本功能,和它支持的最重要的特性之一:Signal & Slot的基本功能。现在让我们来进入 Meta Object 的内部,看看它是如何支持这些能力的。

    Meta Object 的所有数据和方法都封装在一个叫QMetaObject 的类中。它包含并且可以查询一个Qt类的 meta 信息,meta信息包含以下几种:
     * 信号表(signal table),其中有这个对应的 Qt 类的所有Signal的名字
     * 槽表(slot table),其中有这个对应的Qt类中的所有Slot的名字。
     * 类信息表(class info table),包含这个Qt类的类型信息
     * 属性表(property table),其中有这个对应的Qt类中的所有属性的名字。
     * 指向parent meta object的指针(pointers to parent meta object)

请参考下图, Qt Meta Data Tables:

QMetaOb ject 对象与 Qt 类之间的关系:

    * 每一个 QMetaObject 对象包含了与之相对应的一个 Qt 类的元信息
    * 每一个 Qt 类(QObject 以及它的派生类) 都有一个与之相关联的静态的(static) QMetaObject 对象(注:class的定义中必须有 Q_OBJECT 宏,否则就没有这个Meta Object)
    * 每一个 QMetaObject 对象保存了与它相对应的 Qt 类的父类的 QMetaObject 对象的指针。   或者,我们可以这样说:“每一个QMetaObject对象都保存了一个其父亲(parent)的指针”.注意:严格来说,这种说法是不正确的,最起码是不严谨的。

请参考下图,Qt Meta Class 与 Qt class 之间的对应关系:

Q_OBJECT宏

Meta Object 的功能实现,这个宏立下了汗马功劳。首先,让我们来看看这个宏是如何定义的:

#define Q_OBJECT \
public: \
     Q_OBJECT_CHECK \
     static const QMetaObject staticMetaObject; \
     virtual const QMetaObject *metaObject() const; \
     virtual void *qt_metacast(const char *); \
     QT_TR_FUNCTIONS \
     virtual int qt_metacall(QMetaObject::Call, int, void **); \
private:

这里,我们先忽略Q_OBJECT_CHECK 和QT_TR_FUNCTIONS 这两个宏。

我们看到,首先定义了一个静态类型的类变量staticMetaObject,然后有一个获取这个对象指针的方法metaObject()。这里最重要的就是类变量staticMetaObject 的定义。这说明所有的 QObject 的对象都会共享这一个staticMetaObject 类变量,靠它来完成所有信号和槽的功能,所以我们就有必要来仔细的看看它是怎么回事了。

Inside Qt Series (九):QMetaObject class data members

作者:Q-Kevin @ http://www.qkevin.com

我们来看一下QMetaObject的定义,我们先看一下QMetaObject对象中包含的成员数据。

struct Q_CORE_EXPORT QMetaObject
{
    // ……
    struct { // private data
        const QMetaObject *superdata;
        const char *stringdata;
        const uint *data;
        const void *extradata;
    } d;
};

上面的代码就是QMetaObject类所定义的全部数据成员。就是这些成员记录了所有signal,slot,property,class information这么多的信息。下面让我们来逐一解释这些成员变量:

const QMetaObject *superdata:
这个变量指向与之对应的QObject类的父类,或者是祖先类的QMetaObject对象。

如何理解这一句话呢?我们知道,每一个QMetaObject对象,一定有一个与之相对应的QObject类(或者由其直接或间接派生出的子类),注意:这里是类,不是对象。

那么每一个QObject类(或其派生类)可能有一个父类,或者父类的父类,或者很多的继承层次之前的祖先类。或者没有父类(QObject)。那么 superdata 这个变量就是指向与其最接近的祖先类中的QMetaObject对象。对于QObject类QMetaObject对象来说,这是一个NULL指针,因为QObject没有父类。

下面,让我们来举例说明:

class Animal : public QObject
{
    Q_OBJECT
    //………….
};

class Cat : public Animal
{
    Q_OBJECT
    //………….
}

那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是 Animal::staticMetaObject
而 Animal::staticMetaObject.d.superdata 这个指针变量指向的对象是 QObject::staticMetaObject.
而 QObject::staticMetaObject.d.superdat 这个指针变量的值为 NULL。

但如果我们把上面class的定义修改为下面的定义,就不一样了:

class Animal : public QObject
{
    // Q_OBJECT,这个 class 不定义这个
    //………….
};

class Cat : public Animal
{
    Q_OBJECT
    //………….
}

那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是 QObject::staticMetaObject
因为 Animal::staticMetaObject 这个对象是不存在的。

const char *stringdata:
顾名思义,这是一个指向string data的指针。但它和我们平时所使用的一般的字符串指针却很不一样,我们平时使用的字符串指针只是指向一个字符串的指针,而这个指针却指向的是很多个字符串。那么它不就是字符串数组吗?哈哈,也不是。因为C++的字符串数组要求数组中的每一个字符串拥有相同的长度,这样才能组成一个数组。那它是不是一个字符串指针数组呢?也不是,那它到底是什么呢?让我们来看一看它的具体值,还是让我们以QObject这个class的QMetaObject为例来说明吧。

下面是QObject::staticMetaObject.d.stringdata指针所指向的多个字符串数组,其实它就是指向一个连续的内存区,而这个连续的内存区中保存了若干个字符串。

static const char qt_meta_stringdata_QObject[] =
{
    "QObject\0\0destroyed(QObject*)\0destroyed()\0"
    "deleteLater()\0_q_reregisterTimers(void*)\0"
    "QString\0objectName\0parent\0QObject(QObject*)\0"
    "QObject()\0"
};

这个字符串都是些什么内容呀?有,Class Name, Signal Name, Slot Name, Property Name。看到这些大家是不是觉得很熟悉呀,对啦,他们就是Meta System所支持的最核心的功能属性了。

既然他们都是不等长的字符串,那么Qt是如何来索引这些字符串,以便于在需要的时候能正确的找到他们呢?第三个成员正式登场了。

const uint *data;
这个指针本质上就是指向一个正整数数组,只不过在不同的object中数组的长度都不尽相同,这取决于与之相对应的class中定义了多少signal,slot,property。

这个整数数组的的值,有一部分指出了前一个变量(stringdata)中不同字符串的索引值,但是这里有一点需要注意的是,这里面的数值并不是直接标明了每一个字符串的索引值,这个数值还需要通过一个相应的算法计算之后,才能获得正确的字符串的索引值。

下面是QObject::staticMetaObject.d.data指针所指向的正整数数组的值。

static const uint qt_meta_data_QObject[] =
{
// content:
       2,       // revision
       0,       // classname
       0,    0, // classinfo
       4,   12, // methods
       1,   32, // properties
       0,    0, // enums/sets
       2,   35, // constructors

// signals: signature, parameters, type, tag, flags
       9,    8,    8,    8, 0×05,
      29,    8,    8,    8, 0×25,

// slots: signature, parameters, type, tag, flags
      41,    8,    8,    8, 0x0a,
      55,    8,    8,    8, 0×08,

// properties: name, type, flags
      90,   82, 0x0a095103,

// constructors: signature, parameters, type, tag, flags
     108,  101,    8,    8, 0x0e,
     126,    8,    8,    8, 0x2e,

       0        // eod
};

简单的说明一下,

第一个section,就是 //content 区域的整数值,这一块区域在每一个QMetaObject的实体对象中数量都是相同的,含义也相同,但具体的值就不同了。专门有一个struct定义了这个section,其含义在上面的注释中已经说的很清楚了。

struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData;
};

这个 struct 就是定义第一个secton的,和上面的数值对照一下,很清晰,是吧?

第二个section,以 // signals 开头的这段。这个section中的数值指明了QObject这个class包含了两个signal,

第三个section,以 // slots 开头的这段。这个section中的数值指明了QObject这个class包含了两个slot。

第四个section,以 // properties 开头的这段。这个section中的数值指明了QObject这个class包含有一个属性定义。

第五个section,以 // constructors 开头的这段,指明了QObject这个class有两个constructor。

const void *extradata;
这是一个指向QMetaObjectExtraData数据结构的指针,关于这个指针,这里先略过。

对于每一个具体的整数值与其所指向的实体数据之间的对应算法,实在是有点儿麻烦,这里就不讲解细节了,有兴趣的朋友自己去读一下源代码,一定会有很多发现。

Inside Qt Series (十):connect,幕后的故事

作者:Q-Kevin @ http://www.qkevin.com

我们都知道,把一个signal和slot连接起来,需要使用QObject类的connect方法,它的作用就是把一个object的signal和另外一个object的slot连接起来,以达到对象间通讯的目的。

connect 在幕后到底都做了些什么事情?为什么emit一个signal后,相应的slot都会被调用?好了,让我们来逐一解开其中的谜团。

SIGNAL 和 SLOT 宏定义

我们在调用connect方法的时候,一般都会这样写:
obj.connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt()));
我们看到,在这里signal和slot的名字都被包含在了两个大写的SIGNAL和SLOT中,这两个是什么呢?原来SIGNAL 和 SLOT 是Qt定义的两个宏。好了,让我们先来看看这两个宏都做了写什么事情:

这里是这两个宏的定义:
# define SLOT(a)      "1"#a
# define SIGNAL(a)   "2"#a

原来Qt把signal和slot都转化成了字符串,并且还在这个字符串的前面加上了附加的符号,signal前面加了’2’,slot前面加了’1’。也就是说,我们前面写了下面的connect调用,在经过moc编译器转换之后,就便成了:
obj.connect(&obj, "2destroyed()", &app, "1aboutQt()”));

当connect函数被调用了之后,都会去检查这两个参数是否是使用这两个宏正确的转换而来的,它检查的根据就是这两个前置数字,是否等于1或者是2,如果不是,connect函数当然就会失败啦!

然后,会去检查发送signal的对象是否有这个signal,方法就是查找这个对象的class所对应的staticMetaObject对象中所包含的d.stringdata所指向的字符串中是否包含这个signal的名字,在这个检查过程中,就会用到d.data所指向的那一串整数,通过这些整数值来计算每一个具体字符串的起始地址。同理,还会使用同样的方法去检查slot,看响应这个signal的对象是否包含有相应的slot。这两个检查的任何一个如果失败的话,connect函数就失败了,返回false.

前面的步骤都是在做一些必要的检查工作,下一步,就是要把发送signal的对象和响应signal的对象关联起来。在QObject的私有数据类QObjectPrivate中,有下面这些数据结构来保存这些信息:

class QObjectPrivate : public QObjectData
{
    struct Connection
    {
        QObject *receiver;
        int method;
        uint connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
        QBasicAtomicPointer<int> argumentTypes;
    };

    typedef QList<Connection> ConnectionList;

    QObjectConnectionListVector *connectionLists;

    struct Sender
    {
        QObject *sender;
        int signal;
        int ref;
    };

    QList<Sender> senders;
}

在发送signal的对象中,每一个signal和slot的connection,都会创建一个QObjectPrivate::Connection对象,并且把这个对象保存到connectionList这个Vector里面去。

在响应signal的对象中,同样,也是每一个signal和slot的connection,都会一个创建一个Sender对象,并且把这个对象附加在Senders这个列表中。

以上就是connect的过程,其中,创建QObjectPrivate::Connection对象和Sender对象的过程有一点点复杂,需要仔细思考才可以,有兴趣的朋友可以去读一下源代码。

Inside Qt Series (十一):emit,幕后的故事

作者:Q-Kevin @ http://www.qkevin.com

当我们写下一下emit signal代码的时候,与这个signal相连接的slot就会被调用,那么这个调用是如何发生的呢?让我们来逐一解开其中的谜团。

让我们来看一段例子代码:

class ZMytestObj : public QObject
{
    Q_OBJECT
signals:
    void sigMenuClicked();
    void sigBtnClicked();
};

MOC编译器在做完预处理之后的代码如下:

// SIGNAL 0
void ZMytestObj::sigMenuClicked()
{
    QMetaObject::activate(this, &staticMetaObject, 0, 0);
}

// SIGNAL 1
void ZMytestObj::sigBtnClicked()
{
    QMetaObject::activate(this, &staticMetaObject, 1, 0);
}

哈哈,看到了把,每一个signal都会被转换为一个与之相对应的成员函数。也就是说,当我们写下这样一行代码:
emit sigBtnClicked();
当程序运行到这里的时候,实际上就是调用了void ZMytestObj::sigBtnClicked() 这个函数。

大家注意比较这两个函数的函数体,
void ZMytestObj::sigMenuClicked()  void ZMytestObj::sigBtnClicked(),
它们唯一的区别就是调用 QMetaObject::activate 函数时给出的参数不同,一个是0,一个是1,它们的含义是什么呢?它们表示是这个类中的第几个signal被发送出来了,回头再去看头文件就会发现它们就是在这个类定义中,signal定义出现的顺序,这个参数可是非常重要的,它直接决定了进入这个函数体之后所发生的事情。

当执行流程进入到QMetaObject::activate函数中后,会先从connectionLists这个变量中取出与这个signal相对应的connection list,它根据的就是刚才所传入进来的signal index。这个connection list中保存了所有和这个signal相链接的slot的信息,每一对connection(即:signal 和 slot 的连接)是这个list中的一项。

在每个一具体的链接记录中,还保存了这个链接的类型,是自动链接类型,还是队列链接类型,或者是阻塞链接类型,不同的类型处理方法还不一样的。这里,我们就只说一下直接调用的类型。

对于直接链接的类型,先找到接收这个signal的对象的指针,然后是处理这个signal的slot的index,已经是否有需要处理的参数,然后就使用这些信息去调用receiver的qt_metcall 方法。

在qt_metcall方法中就简单了,根据slot的index,一个大switch语句,调用相应的slot函数就OK了。

Inside Qt Series (十二):Qt对象之间的父子关系

作者:Q-Kevin @ http://www.qkevin.com

很多C/C++初学者常犯的一个错误就是,使用malloc、new分配了一块内存却忘记释放,导致内存泄漏。Qt的对象模型提供了一种Qt对象之间的父子关系,当很多个对象都按一定次序建立起来这种父子关系的时候,就组织成了一颗树。当delete一个父对象的时候,Qt的对象模型机制保证了会自动的把它的所有子对象,以及孙对象,等等,全部delete,从而保证不会有内存泄漏的情况发生。

任何事情都有正反两面作用,这种机制看上去挺好,但是却会对很多Qt的初学者造成困扰,我经常给别人回答的问题是:1,new了一个Qt对象之后,在什么情况下应该delete它?2,Qt的析构函数是不是有bug?3,为什么正常delete一个Qt对象却会产生segment fault?等等诸如此类的问题,这篇文章就是针对这个问题的详细解释。

在每一个Qt对象中,都有一个链表,这个链表保存有它所有子对象的指针。当创建一个新的Qt对象的时候,如果把另外一个Qt对象指定为这个对象的父对象,那么父对象就会在它的子对象链表中加入这个子对象的指针。另外,对于任意一个Qt对象而言,在其生命周期的任何时候,都还可以通过setParent函数重新设置它的父对象。当一个父对象在被delete的时候,它会自动的把它所有的子对象全部delete。当一个子对象在delete的时候,会把它自己从它的父对象的子对象链表中删除。

QWidget是所有在屏幕上显示出来的界面对象的基类,它扩展了Qt对象的父子关系。一个Widget对象也就自然的成为其父Widget对象的子Widget,并且显示在它的父Widget的坐标系统中。例如,一个对话框(dialog)上的按钮(button)应该是这个对话框的子Widget。

关于Qt对象的new和delete,下面我们举例说明。

例如,下面这一段代码是正确的:

int main()
{
  QObject* objParent = new QObject(NULL);
  QObject* objChild = new QObject(objParent);
  QObject* objChild2 = new QObject(objParent);
  delete objParent;
}

我们用一张图来描述这三个对象之间的关系:

在上述代码片段中,objParent是objChild的父对象,在objParent对象中有一个子对象链表,这个链表中保存它所有子对象的指针,在这里,就是保存了objChild和objChild2的指针。在代码的结束部分,就只有delete了一个对象objParent,在objParent对象的析构函数会遍历它的子对象链表,并且把它所有的子对象(objChild和objChild2)一一删除。所以上面这段代码是安全的,不会造成内存泄漏。

如果我们把上面这段代码改成这样,也是正确的:

int main()
{
  QObject* objParent = new QObject(NULL);
  QObject* objChild = new QObject(objParent);
  QObject* objChild2 = new QObject(objParent);
  delete objChild;
  delete objParent;
}

在这段代码中,我们就只看一下和上一段代码不一样的地方,就是在delete objParent对象之前,先delete objChild对象。在delete objChild对象的时候,objChild对象会自动的把自己从objParent对象的子对象链表中删除,也就是说,在objChild对象被delete完成之后,objParent对象就只有一个子对象(objChild2)了。然后在delete objParent对象的时候,会自动把objChild2对象也delete。所以,这段代码也是安全的。

Qt的这种设计对某些调试工具来说却是不友好的,比如valgrind。比如上面这段代码,valgrind工具在分析代码的时候,就会认为objChild2对象没有被正确的delete,从而会报告说,这段代码存在内存泄漏。哈哈,我们知道,这个报告是不对的。

我们在看一看这一段代码:

int main()
{
  QWidget window;
  QPushButton quit("Exit", &window);
}

在这段代码中,我们创建了两个widget对象,第一个是window,第二个是quit,他们都是Qt对象,因为QPushButton是从QWidget派生出来的,而QWidget是从QObject派生出来的。这两个对象之间的关系是,window对象是quit对象的父对象,由于他们都会被分配在栈(stack)上面,那么quit对象是不是会被析构两次呢?我们知道,在一个函数体内部声明的变量,在这个函数退出的时候就会被析构,那么在这段代码中,window和quit两个对象在函数退出的时候析构函数都会被调用。那么,假设,如果是window的析构函数先被调用的话,它就会去delete quit对象;然后quit的析构函数再次被调用,程序就出错了。事实情况不是这样的,C++标准规定,本地对象的析构函数的调用顺序与他们的构造顺序相反。那么在这段代码中,这就是quit对象的析构函数一定会比window对象的析构函数先被调用,所以,在window对象析构的时候,quit对象已经不存在了,不会被析构两次。

如果我们把代码改成这个样子,就会出错了,对照前面的解释,请你自己来分析一下吧。

int main()
{
  QPushButton quit("Exit");
  QWidget window;
  quit.setParent(&window);
}

但是我们自己在写程序的时候,也必须重点注意一项,千万不要delete子对象两次,就像前面这段代码那样,程序肯定就crash了。

最后,让我们来结合Qt source code,来看看这parent/child关系是如何实现的。

在本专栏文章的第一部分“对象数据存储”,我们说到过,所有Qt对象的私有数据成员的基类是QObjectData类,这个类的定义如下

typedef QList<QObject*> QObjectList;
class QObjectData
{
public:
  QObject *parent;
  QObjectList children;
  // 忽略其它成员定义
};

我们可以看到,在这里定义了指向parent的指针,和保存子对象的列表。其实,把一个对象设置成另一个对象的父对象,无非就是在操作这两个数据。把子对象中的这个parent变量设置为指向其父对象;而在父对象的children列表中加入子对象的指针。当然,我这里说的非常简单,在实际的代码中复杂的多,包含有很多条件判断,有兴趣的朋友可以自己去读一下Qt的源代码。

Inside Qt Series (十三):Qt/e体系结构概述

作者:Q-Kevin @ http://www.qkevin.com

在后续内容中,我对Qt for Embedded Linux一律简称为Qt/e,不再对这个term做更多的解释。需要您注意的一点是,在本系列文章中的任何部分,这个term都是指Qt for Embedded Linux,而不是指Qt for Windows CE 或者 Qt for Symbian.

这些内容所适用的软件版本是:Qt for Embedded Linux 4.6, Open Source edition

首先,让我们来看看Qt/e的系统结构介绍:

Qt for destop Linux 和 Qt for Embedded Linux 最大的区别就在于他们所依赖的底层显示基础的不同,这也就导致了他们在体系结构上的差异。对于Qt for desktop Linux来说,底层的显示技术构建在X Window System之上,完全依赖于X System,他们在下层完全是调用了X Lib的系统方法来把界面上的东西显示出来。

Qt for embedd linux在这方面则完全不同,它并没有构建在X Window之上,而是构建在Linux的Framebuffer之上,把在界面上需要显示的内容直接写入了framebuffer。因为在嵌入式系统上把X System给省略了,这样会节省许多的系统开销。而直接写framebuffer,又会加快显示速度。这种区别如图所示:

但就是这一个改变,导致了在Qt/E凭空多出了一个Server这么一层,这一层负责监听系统事件,尤其是键盘和鼠标事件,屏幕输出,管理region,管理顶层窗口,管理光标和屏幕保护程序等等诸多功能。系统产生的键盘鼠标事件,首先就传给了这个server application,然后server在根据具体的情况把这些事件分发给相应的应用程序。

每一个Qt/e应用程序,都需要这样一个server存在。一个程序运行起来后,如是自己成为Server进程,就是连接到一个已经存在的Server进程。所以,第一个运行起来的Qt/E应用程序就会启动这个server让自己成为这个Server进程,后续运行的程序就会连接到这个Server来管理自己。

在Server端,每一个连接到QWSServer的client都有一个QWSClient对象与之对应,这个对象主要记录了client ID。在应用程序中每创建一个顶层窗口,那么在server端就会有创建一个QWSWindow实例来与之对应。

每当Server收到一个event 的时候,它需要判断应该发送给那一个窗口,这时候,它就会从QWSWindow列表中去找,然后根据这个窗口去找对应的client application,然后用一个QWSEvent对象来包装这个event,通过socket机制发送给具体的client application。如果当前系统安装了一个输入法,那么每一次键盘事件产生的时候,都会去调用输入法的相应方法。如图所示(取自Qte文档):


 

鼠标事件的处理和键盘事件的处理也符合上面的流程。鼠标驱动由一个QWSMouseHandler对象封装,键盘驱动由一个QWSKeyboardHandler封装。这两个驱动程序对象都会通过Qt的plugin机制加载。具体的鼠标和键盘事件发生之后,都会封装成为一个QWSEvent对象并发送给具体的client。如图所示(取自Qte文档):

 


 

图形输出,Qte的缺省行为是每一个widget会把自己画在一块内存中,然后由Server负责把这快内存copy到Linux的Framebuffer上去,如图所示(取自Qte文档):


 

但是对于大多数嵌入式系统来说,其中的显示子系统都是确定的,这样对于client应用程序来说,就可以直接输出到Framebuffer上面去。有两种方法可以实现这一点,第一种是为每一个Widget都设置Qt::WA_PaintOnScreen属性,另一种是QDirectPainter来在Framebuffer中保留一块区域,如图所示(取自Qte文档):

Inside Qt Series (十四):Qt/e输入法程序设计指南

作者:Q-Kevin @ http://www.qkevin.com

注,本输入法设计指南针对Qt for Embedded Linux 4.6,并且以中文输入法为例做说明,并且本文只是侧重于说明Qt/Embedded对输入法的支持接口,并不涉及到如何把键盘输入转换为中文所对应的编码方法。对其它Qt版本的适用性未曾验证。

大家都知道,Qt for Embedded Linux是Client/Server结构,在Server端负责监听系统消息,尤其是键盘和鼠标消息,而输入法又是一个全局性的模块,所以在Qt/Embedded中,就把输入法的设计放在了Server这一层上。具体来说,就是,输入法是属于Server层的一部分。

Qt/e 输入法基类,QWSInputMethod,在这个基类中定义了一些接口用以支持输入法程序设计,我们需要做的就是从QWSInputMethod这个类继承出一个输入法类,在这个类中处理键盘和鼠标事件,把接收到的键盘事件按照输入法的编码规则转换为对应的中文,一个汉字,或者是一个中文短语,我们可以把这个正在输入过程中的汉字或者短语发送给当前的编辑窗口,或者把最终用户的选择发送到当前编辑窗口。我们需要自己定义一个输入法窗口来显示用户当前的输入,我们可以称之为IME Window。

文字的输入一般分为三个步骤
1,开始输入
当用户在键盘上按下第一个按键的时候,输入法上下文就被创建出来,这个输入法上下文包含键盘输入字符
2,编辑
当有任何一个新的按键被按下的时候,输入法就会尝试着去创建与键盘输入相对应的中文字符,这个时候,输入法上下文处于激活状态,用户可以在这个输入法上下文中前后移动光标。
3,完成输入
在用户认为输入已经完成的时候,用户会选择以某种方式来选择最终的字符串,通常是使用键盘按键;或者鼠标点击;用户所选择的字符串最终应该被发送到当前的编辑窗口。

QWSInputMethod类是Qte提供的、专门为输入法程序设计的基类,这个类定义了一系列的通用接口来对输入法提供支持,现在,让我们来看看这个类所定义的几个主要的接口:

virtual bool filter(int unicode, int keycode, int modifiers, bool isPress, bool autoRepeat );

这个接口的作用就是过滤键盘事件,详细一点儿说,就是我们可以在这个函数中处理键盘输入,并且根据相应的输入法规则把键盘输入转换为相应的中文。这个函数的参数含义如下:

unicode:Qte统一使用的键盘按键编码,本文中,我们不使用这个参数

keycode: 键值,Qt定义了一系列的键值与键盘一一对应,具体定义在Qt namespace中,比如说,Qt::Key_Left, Qt::Key_Up, Qt::Key_Right, Qt::Key_Down,这四个定义对应到四个方向键,Qt::Key_0则对应数字键0,Qt::Key_A则对应大写字母A,等等。详细列表请参考Qt在线文档

modifiers: 这个参数表示是否有其它的辅助按键同时被按下,比如,Alt, Ctrl, Shift,等,其预定义值如下:
Qt::NoModifier, 没有辅助键被按下
Qt::ShiftModifier, Shift键被按下
Qt::ControlModifier, Ctrl键被按下
Qt::AltModifier, Alt键被按下
Qt::MetaModifier, Meta键被按下
Qt::KeypadModifier, keypad 的按键被按下
Qt::GroupSwitchModifier,仅用于X11,Mode_switch键被按下
更多解释请参考Qt在线文档

这些定义相互之间并不冲突,它们是按照“与”的关系组合在一起,在我们的使用中,我们可以用C++的&操作符来判断某一个建是否被按下,比如,如果我们需要判断Alt键是否被按下,就应该这样做:
if (Qt::AltModifier & modifiers)
{
  //Alt键被按下
}

isPress: 这个参数表示键是被按下(press),还是被释放(release)

autoRepeat: 这个参数表示这个按键事件是否是自动重复产生的

返回值:返回true表示这个按键事件已经被处理了,不需要继续分发;返回false表示这个按键没有被处理,Qt会继续分发这个事件

void sendCommitString(const QString & commitString, int replaceFromPosition = 0, int replaceLength = 0);

这个接口函数表示把相应的字符串发送到当前编辑窗口,一般用于在用户作出最终的选择之后,把相应的字符串发送出去。

void sendPreeditString(const QString & preeditString, int cursorPosition, int selectionLength = 0);

把当前正在编辑的字符串发送给当前编辑窗口

下面我们写一个最简单的例子,

首先我们从QWSInputMethod派生出一个类来,在这个类中,我们集成了安装/卸载输入法,响应键盘事件,响应用户选择,上翻页,下翻页的功能。

// file: xinputmethod.h

struct XInputMethodPrivate;

class XInputMethod : public QWSInputMethod
{
    Q_OBJECT

public:

    static void installInputMethod();
    static void releaseInputMethod();
    static XInputMethod* instance();

    virtual bool filter(int unicode, int keycode, int modifiers, bool isPress, bool autoRepeat);
    virtual ~XInputMethod();

private:
    XInputMethod();
 
    void toggleIME();

    void newCharacter(char);
    bool makeSelection(int);
    void showNextPage();
    void showPreviousPage();

    XInputMethodPrivate* mpdata;
};

// File: xinputmethod.cpp

首先,我们定义了一个类的私有数据成员结构体,这种方法也是从Qt学来的。关于这个方法的详细解释,请看本系列文章的2,3,4篇,《对象数据存储》。
这里,我们定义了一个XWindow 类型的pframe指针变量,注意,这个XWindow和Linux系统的XWindow不是一回事,这个XWindow是本文中的输入法用户界面窗口类。

struct XInputMethodPrivate
{
        static XInputMethod* pInputMethod;

        XWindow* pframe;

        XData imedata;

        XInputMethodPrivate(): pframe(NULL) {}
};

XInputMethod* XInputMethodPrivate::pInputMethod = NULL;

// File: xinputmethod.cpp

首先,我们定义了一个类的私有数据成员结构体,这种方法也是从Qt学来的。关于这个方法的详细解释,请看本系列文章的2,3,4篇,《对象数据存储》。
这里,我们定义了一个XWindow 类型的pframe指针变量,注意,这个XWindow和Linux系统的XWindow不是一回事,这个XWindow是本文中的输入法用户界面窗口类。

struct XInputMethodPrivate
{
        static XInputMethod* pInputMethod;
        XWindow* pframe;
        XData imedata;
        XInputMethodPrivate(): pframe(NULL) {}
};

XInputMethod* XInputMethodPrivate::pInputMethod = NULL;
复制代码我们开发了一个输入法,最重要的就是需要install,这样系统中才会有输入法模块,输入法才能工作。我们来看一下最重要的install和release输入法的代码。这里就是调用QWSServer类中的成员函数来实现的。
QWSServer::setCurrentInputMethod 这个函数为当前的Qt/Embedded 安装一个输入法,如果把参数设置为NULL,就是卸载输入法。

void XInputMethod::installInputMethod()
{
    XInputMethod* pim = instance();
  
    if (pim)
    {
        QWSServer::setCurrentInputMethod(pim);
    }
}

void XInputMethod::releaseInputMethod()
{
    if (XInputMethodPrivate::pInputMethod)
    {
        QWSServer::setCurrentInputMethod(NULL);
        delete XInputMethodPrivate::pInputMethod;
        XInputMethodPrivate::pInputMethod = NULL;
    }
}

XInputMethod* XInputMethod::instance()
{
    if (NULL == XInputMethodPrivate::pInputMethod)
    {
        XInputMethodPrivate::pInputMethod = new XInputMethod();
    }
   
    return XInputMethodPrivate::pInputMethod;
}

输入法安装完成之后,在我们的输入法类中就可以接收到键盘事件了,这是在QWSInputMethod类中定义的虚函数filter完成的;我们重新实现这个函数,

在这里,我们用ALT+Z按键来显示/隐藏输入法用户界面。

当用户界面显示出来之后,就处理键盘点击事件,当用户输入’a’ – ‘z’,或者 ‘A’ – ‘Z’的时候,就启动输入法引擎,把用户输入安装编码规则转换为相应的汉字,或者短语;紧接着,就在用户界面窗口上显示出来用户的输入和转换后的中文字符。

当用户输入数字0 – 9 的时候,用户处理用户选择候选字。

当用户输入PageDown的时候,用来处理下翻页

当用户输入PageUp的时候,用来处理上翻页

bool XInputMethod::filter(int /*unicode*/, int keycode, int modifiers,  bool isPress, bool /*autoRepeat*/)
{
    if (isPress && (Qt::AltModifier & modifiers) && (Qt::Key_Z == keycode))
    {
        toggleIME();

        return true;
    }

    if (mpdata && mpdata->pframe && mpdata->pframe->isVisible() && isPress)
    {
        if ((Qt::Key_A <= keycode) && (Qt::Key_Z >= keycode))
        {
            char ch = (char)((Qt::ShiftModifier & modifiers) ?
                             keycode : (keycode – Qt::Key_A + 'a'));

            newCharacter(ch);

            return true;
        }

        if ((Qt::Key_0 <= keycode) && (Qt::Key_9 >= keycode))
        {
            return makeSelection(keycode – Qt::Key_0);
        }

        if (Qt::Key_PageDown == keycode)
        {
            showNextPage();
            return true;
        }

        if (Qt::Key_PageUp == keycode)
        {
            showPreviousPage();
            return true;
        }
    }

    return false;
}

void XInputMethod::toggleIME()
{
    if (mpdata->pframe->isVisible())
    {
        mpdata->pframe->hide();
        mpdata->imedata.reset();
    }
    else
    {
        mpdata->pframe->show();
    }
}

在这个函数中把通过编码转换后的中文字符加入到mpdata->imedata.listHanzi 这个变量中,就可以在界面上显示出来了。
由于本文仅仅只是为了讲解Qt/Embedded的输入法设计接口,没有编码方面的内容,所以这里就加入了两个字符做为示例。

void XInputMethod::newCharacter(char ch)
{
    mpdata->imedata.strPinyin += ch;

    mpdata->imedata.listHanzi << "a";
    mpdata->imedata.listHanzi << "b";

    mpdata->pframe->update();
}

用户按下数字键,选择当前显示的字符,注意,这里有一个很重要的地方,就是使用QWSInputMethod类的方法sendCommitString,把用户选择的字符发送给当前的应用程序编辑窗口。

bool XInputMethod::makeSelection(int number)
{
    number–;

    if ((mpdata->imedata.first_visible + number) < mpdata->imedata.listHanzi.count())
    {
        QString result = mpdata->imedata.listHanzi[mpdata->imedata.first_visible + number];

        if (!result.isEmpty())
        {
            sendCommitString(result);
            mpdata->imedata.reset();
            mpdata->pframe->update();

            return true;
        }
    }

    return false;
}

显示下一页

void XInputMethod::showNextPage()
{
    if ((mpdata->imedata.first_visible + mpdata->imedata.counts_per_page) < mpdata->imedata.listHanzi.count())
    {
        mpdata->imedata.first_visible += mpdata->imedata.counts_per_page;
        mpdata->pframe->update();
    }
}

显示上一页

void XInputMethod::showPreviousPage()
{
    if ((mpdata->imedata.first_visible – mpdata->imedata.counts_per_page) >= 0)
    {
        mpdata->imedata.first_visible -= mpdata->imedata.counts_per_page;
        mpdata->pframe->update();
    }
}

另外,我们还需要一个窗口来显示用户的输入字符,和经过中文编码转换后的中文,我们称之为XIMWindow。这个用户界面窗口的代码,就不做详细解释了,它是很简单的,附件文件包含了完整的代码,有兴趣的朋友可以下载下来读一下。

关于这个窗口,有一点需要注意的就是,由于输入法需要在最顶层显示出来,免得被其它窗口给覆盖了,所以在创建窗口的时候,需要设置好相应的Widget Flag才行。

#define IME_WND_FLAG (Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool)

下面这张图片是这个程序的截图:

本文只是一个最简单的Qte输入法指南,只演示了最重要的输入法接口,仅仅只能起到一个入门的作用,一个实用的输入法还要包括字符转换,用户界面,根据需要,可能还需要鼠标事件处理,等等,有兴趣的朋友请参考Qt的在线文档和源代码。

这里是本设计指南的源代码:xinputmethod.tar.gz

 

Inside Qt Series (十五):
Qt/e 输入法,How it works?

 

作者:Q-Kevin @ http://www.qkevin.com

前面我们介绍了Qte输入法的基本设计思路,以及一个最简单的例子,那么,Qte的输入法是如何工作的呢?本节我们就来看一下Qte的源代码,一起来解开这个谜团。

在Qte的Client/Server体系结构中,QWSServer类负责管理Qte的Server,监听系统事件,尤其是键盘和鼠标事件。当这些监听的事件发生的时候,server会做出判断,这些事件应该发送给那一个客户端。

如果当前系统安装了输入法,那么键盘和鼠标事件在派发之前,就会先送给输入法,让输入法来做一下判断,看输入法是否会处理这个键盘按键,如果输入法已经处理,就不在继续分发这个事件,否则就会按照原先的事件分发机制继续分发这个事件。也就是说,输入法会在应用程序之前接收到键盘事件。

Qte已经定义了一个输入法基类QWSInputMethod,在这个类中封装了一些基本的输入法函数。我们一起来看看QWSInputMethod类的定义:

class QWSInputMethod : public QObject
{
    Q_OBJECT
public:
    QWSInputMethod();
    virtual ~QWSInputMethod();

    enum UpdateType {Update, FocusIn, FocusOut, Reset, Destroyed};

    virtual bool filter(int unicode, int keycode, int modifiers,
                        bool isPress, bool autoRepeat);

    virtual bool filter(const QPoint &, int state, int wheel);

    virtual void reset();
    virtual void updateHandler(int type);
    virtual void mouseHandler(int pos, int state);
    virtual void queryResponse(int property, const QVariant&);

protected:
    uint setInputResolution(bool isHigh);
    uint inputResolutionShift() const;
    void sendMouseEvent(const QPoint &pos, int state, int wheel);

    void sendEvent(const QInputMethodEvent*);
    void sendPreeditString(const QString &preeditString, int cursorPosition, int selectionLength = 0);
    void sendCommitString(const QString &commitString, int replaceFrom = 0, int replaceLength = 0);
    void sendQuery(int property);

private:
    bool mIResolution;
};

这个类从QObject类继承而来,定义了 Q_OBJECT 宏,说明这个类支持Qt对象模型的操作,signal/slot,property,都没有问题,这里最关键的几个函数有,两个重载的filter函数,一个用来过滤键盘事件,另一个用来过滤鼠标事件,sendEvent函数用来发送输入法事件,在这个事件中可以打包preedit string, commit string,它还有一个list,可以添加任意多的其它数据。sendPreeditString函数用来把正在输入过程中的字符串发送到当前编辑窗口,而sendCommitString则用来把最终的用户选择的字符串发送到当前编辑窗口。

QWSServer类提供了一个函数来安装输入法,void setCurrentInputMethod ( QWSInputMethod * method),这个函数的参数就是一个QWSInputMethod类的指针。QWSServer是如何管理QWSInputMethod的呢?在Server端,定义了这么几个变量,

static QWSInputMethod *current_IM = 0;
static QWSWindow *current_IM_composing_win = 0;
static int current_IM_winId = -1;
static bool force_reject_strokeIM = false;

其中,最重要的就是current_IM了,这个指针指向当前安装的输入法对象,它就是在QWSServer::setCurrentInputMethod函数中赋值的。

这里是QWSServer::setCurrentInputMethod这个函数的源代码:

void QWSServer::setCurrentInputMethod(QWSInputMethod *im)
{
    if (current_IM)
        current_IM->reset();
    current_IM = im;
}

再看看这个键盘事件处理函数:

void QWSServer::sendKeyEvent(int unicode, int keycode, Qt::KeyboardModifiers modifiers,
                             bool isPress, bool autoRepeat)
{
    //………………………..
#ifndef QT_NO_QWS_INPUTMETHODS

    if (!current_IM || !current_IM->filter(unicode, keycode, modifiers, isPress, autoRepeat))
        QWSServerPrivate::sendKeyEventUnfiltered(unicode, keycode, modifiers, isPress, autoRepeat);
#else
    QWSServerPrivate::sendKeyEventUnfiltered(unicode, keycode, modifiers, isPress, autoRepeat);
#endif
}

在QWSServer::sendKeyEvent函数中,会去检查当前是否安装了输入法,如果是,就会去调用这个输入法的filter函数来过滤键盘事件,如果这个函数返回值为true,就不在继续分发这个key事件。

再看看这个鼠标事件处理函数:

void QWSServer::sendMouseEvent(const QPoint& pos, int state, int wheel)
{
// ————————–
    const int btnMask = Qt::LeftButton | Qt::RightButton | Qt::MidButton;
    int stroke_count; // number of strokes to keep shown.
    if (force_reject_strokeIM || !current_IM)
    {
        stroke_count = 0;
    } else {
        stroke_count = current_IM->filter(tpos, state, wheel);
    }
}

在 QWSServer::sendMouseEvent 函数里面,同样会去检查当前是否安装了输入法,如果是,就会去调用输入法的filter函数来过滤鼠标事件,如果这个函数返回值为true,就不在继续分发这个key事件。

看,Qt/Embedded 输入法的工作原理其实就是这么简单!

下面以一张简单的UML sequence图来说明一下:

Inside Qt Series (十六):Event Overview

 

Inside Qt Series (十六):Event Overview

 

作者:Q-Kevin @ http://www.qkevin.com

Qt 的应用程序都是事件驱动的,以前我一直都很好奇,事件驱动到底是如何工作的?好在Qt是开放源代码的,让我得以有机会从Qt的源代码中学习到一个事件驱动的体系结构是什么样子的。

在Qt中,所有的Event对象都是由从QEvent类派生出来的类来描述的。通过调用QObject::event()函数来发送到从QObject继承出来的Qt对象,这个对象会有相应的方法来处理这个事件。

事件的来源主要有两种

1, 系统产生的
通常是window system把从系统得到的消息,比如鼠标按键,键盘按键等, 放入系统的消息队列中. Qt事件循环的时候读取这些事件,转化为QEvent,再依次处理.

2, Qt应用程序程序自身产生的
程序产生事件有两种方式, 一种是调用QApplication::postEvent(). 例如QWidget::update()函数,当需要重新绘制屏幕时,程序调用update()函数,new出来一个paintEvent,调用QApplication::postEvent(),将其放入Qt的消息队列中,等待依次被处理. 另一种方式是调用sendEvent()函数. 这时候事件不会放入队列, 而是直接被派发和处理, QWidget::repaint()函数用的就是这种方式.

但是对于Qt来说,它并不需要区分这些事件的来源,他们都会采用同样的方式来一致的处理。而且几乎每一种事件都会有一个特定的类相对应。如:QMouseEvent对应鼠标事件,QKeyEvent对应键盘就十分,QCloseEvent对应窗口关闭事件,等等。

 

QtEvent 的类型很多, 常见的 event类型如下:

** 键盘事件: 按键按下(press)和松开(release)
** 鼠标事件:鼠标按键的按下(press)和松开(release),鼠标移动(move),双击(double click)
** 拖放事件: 用鼠标进行拖放(drag and drop)
** 滚轮事件: 鼠标滚轮滚动(wheel event)
** 绘屏事件: 重绘屏幕的某些部分(paint)
** 定时事件: 定时器到时(timer)
** 焦点事件: 键盘焦点移动(focus)
** 进入和离开事件: 鼠标移入widget之内,或是移出(focus in & focus out)
** 大小改变事件: widget的大小改变(resize)
** 显示和隐藏事件: widget显示和隐藏(show & hide)
** 窗口事件: 窗口是否为当前窗口
** 还有一些不太常见的event,比如socket事件,剪贴板事件,字体改变,布局改变等

在大多数情况下,Qt类都是通过一个虚函数来实现对事件的响应的,这样对于继承出来的类来说,就有机会重新实现这个事件的响应方法。比如,我们会经常写QWidget::paintEvent(…)函数来对一个widget做paint处理,其实,这就是在响应一个QPaintEvent事件。

事件发送的方法主要有两种:

1, send event:
这可以通过调用QCoreApplication::sendEvent(QObject* receiver, QEvent* event)这个函数来实现。当通过这个函数调用来发送一个事件的时候,事件响应函数立刻就会被调用到,也就是说,这是一个同步调用。

2, post event:
这可以通过调用QCoreApplication::postEvent(QObject* receiver, QEvent* event)这个函数来实现。当通过这个函数调用来发送一个事件的事件,事件响应函数并没有立刻被调用,而是把事件先放到一个事件队列里面去,然后在Qt的Event Loop下次才会检索到这个event,然后调用相应的事件处理函数。
Qt的event loop还有这样一个特性,他会把相同的event进行合并,以避免这些事件被执行多次。如:你一次post了多个QPaintEvent事件,那么Qt会把这些事件合并成为一个QPaintEvent事件,这样做的好处是节约系统资源,只paint一次就可以了,而且还有效的避免的界面的闪烁。

Qt有以下几种不同的事件处理方法:

1, Qt的类已经实现的那些事件处理方法,如:paintEvent(..), mousePressEvent(..), keyPressEvent(..), 等等。这些事件处理函数都是虚函数,我们可以在自己定义的类里面重新实现这些函数来处理相应的事件。这是最经常实用,这是最通用,最容易的方法。

2, 重新实现QCoreApplication::notify(QObject* receiver, QEvent* event)函数。这也是一个非常强大,而且可以对事件进行完全控制的方法。但是在一个应用程序中,只能有一个QCoreApplication类(或者是它的子类)的实例存在。

3, 在QCoreApplication(或者其子类)的实例上面安装event filter来处理事件。这样的event filter可以处理到所有的widget event,所有它和重写QCoreApplication::notify函数功能一样强大。另外,在一个应用程序中可以有多个全局性的event filter。需要注意的是,application的event filter只有在那些在主线程中的对象调用。

4, 重新实现QObject::event函数。在这个函数中,你能获得更多的事件控制权限,它会在一个对象的特定事件处理函数之前被调用。比如说:paintEvent, keyPressEvent,等等。

5, 给一个对象安装一个event filter,这个filter将会获得这个对象的所有事件调用。

事件运行机制

有两种event调度方式,一种是同步的, 一种是异步.

** 当应用程序的main函数调用QCoreApplication的exec()方法是,应用程序进入Qt的主事件循环,它从事件队列中取出本窗口及系统事件,然后把他们打包成相应的QEvent的子类对象,并且使用函数QCoreApplication::notify函数发送事件到相应的QObject对象。在这个主事件循环中,是异步处理的。

** QApplication::sendEvent()的时候, 消息会立即被处理,是同步的. 实际上QApplication::sendEvent()是通过调用QApplication::notify(), 直接进入了事件的派发和处理环节
 

Event Filter:

一个event filter是一个能够接收所有发送到这个对象上面的事件的对象,并且能够停止或者转发到这个对象上的事件。它通过eventFilter()函数来接收事件,如果这个事件应该被过滤掉(如停止这个事件,或者这个事件已经被处理了,下面不再需要了),那么eventFilter函数返回true,如果不应该被过滤掉,就返回false。

QObject还提供了event filter的安装和卸载方法,如下所示:

a) QObject::installEventFilter(const QObject* filterObject)

b) QObject::removeEventFilter(QObject* filterObject)
 

 



原创粉丝点击