存档

文章标签 ‘InsideQt’

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

2009年9月15日

前面我们介绍了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图来说明一下:

qte.input.method

====================================
声明:
《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=192

前一篇:Qt/e输入法程序设计指南
http://www.insideqt.com/bbs/viewthread.php?tid=146
后一篇:(Writing now . . . . . . 敬请关注)
====================================

Noshow ,

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

2009年9月15日

注,本输入法设计指南针对Qt for Embedded Linux 4.5.1,并且以中文输入法为例做说明,并且本文只是侧重于说明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;
};

首先,我们定义了一个类的私有数据成员结构体,这种方法也是从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();
}
}
<pre>显示上一页
<pre lang="cpp">void XInputMethod::showPreviousPage()
{
if ((mpdata-&gt;imedata.first_visible - mpdata-&gt;imedata.counts_per_page) &gt;= 0)
{
mpdata-&gt;imedata.first_visible -= mpdata-&gt;imedata.counts_per_page;
mpdata-&gt;pframe-&gt;update();
}
}

另外,我们还需要一个窗口来显示用户的输入字符,和经过中文编码转换后的中文,我们称之为XIMWindow。这个用户界面窗口的代码,就不做详细解释了,它是很简单的,附件文件包含了完整的代码,有兴趣的朋友可以下载下来读一下。
关于这个窗口,有一点需要注意的就是,由于输入法需要在最顶层显示出来,免得被其它窗口给覆盖了,所以在创建窗口的时候,需要设置好相应的Widget Flag才行。
#define IME_WND_FLAG (Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool)
下面这张图片是这个程序的截图:
xinputmethod
本文只是一个最简单的Qte输入法指南,只演示了最重要的输入法接口,仅仅只能起到一个入门的作用,一个实用的输入法还要包括字符转换,用户界面,根据需要,可能还需要鼠标事件处理,等等,有兴趣的朋友请参考Qt的在线文档和源代码。
这里是本设计指南的源代码:xinputmethod.tar
====================================

声明:

《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。

本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。

作者保留版权,未经作者同意,不得用于任何商业用途
《Inside Qt Series》专栏文章总索引:

http://www.insideqt.com/bbs/viewthread.php?tid=9

本文原始地址:

http://www.insideqt.com/bbs/viewthread.php?tid=146
前一篇:Qt/e体系结构概述

http://www.insideqt.com/bbs/viewthread.php?tid=113

后一篇:Qt/e 输入法,How it works?

http://www.insideqt.com/bbs/viewthread.php?tid=192

====================================

Noshow ,

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

2009年9月15日

各位朋友好,从本节(Inside Qt Series 十三)开始,本专栏文章的内容开始转向Qt for Embedded Linux的技术内幕介绍。在后续内容中,我对Qt for Embedded Linux一律简称为Qt/e,不再对这个term做更多的解释。需要您注意的一点是,在本系列文章中的任何部分,这个term都是指Qt for Embedded Linux,而不是指Qt for Windows CE。

这些内容所适用的软件版本是:Qt for Embedded Linux 4.5.1, 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,又会加快显示速度。这种区别如图所示:

qte.architecture

但 就是这一个改变,导致了在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实例都是由一个QWSServer类来实现的。

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

qt-embedded-clientservercommunication

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

qt-embedded-pointerhandlinglayer

qt-embedded-characterinputlayer

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

qt-embedded-clientrendering

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

qt-embedded-setwindowattribute

qt-embedded-reserveregion

====================================
声明:
《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=113

前一篇:Qt对象之间的父子关系
http://www.insideqt.com/bbs/viewthread.php?tid=72
后一篇:Qt/e输入法程序设计指南
http://www.insideqt.com/bbs/viewthread.php?tid=146
====================================

Noshow ,

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

2009年9月15日

很多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;
}

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

qt.object.parent.child

在上述代码片段中,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", &amp;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(&amp;window);
}

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

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

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

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

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

====================================
声明:
《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=72

前一篇:emit,幕后的故事
http://www.insideqt.com/bbs/viewthread.php?tid=55
后一篇:Qt/e体系结构概述
http://www.insideqt.com/bbs/viewthread.php?tid=113
====================================

Noshow ,

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

2009年9月15日

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

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

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

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

// SIGNAL 0
void ZMytestObj::sigMenuClicked()
{
QMetaObject::activate(this, &amp;staticMetaObject, 0, 0);
}
 
// SIGNAL 1
void ZMytestObj::sigBtnClicked()
{
QMetaObject::activate(this, &amp;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核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=55

前一篇:connect,幕后的故事
http://www.insideqt.com/bbs/viewthread.php?tid=37
后一篇:Qt对象之间的父子关系
http://www.insideqt.com/bbs/viewthread.php?tid=72
====================================

Noshow ,

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

2009年9月15日

我们都知道,把一个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&lt;int&gt; argumentTypes;
};
 
typedef QList&lt;Connection&gt; ConnectionList;
 
QObjectConnectionListVector *connectionLists;
 
struct Sender
{
QObject *sender;
int signal;
int ref;
};
 
QList&lt;Sender&gt; senders;
}

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

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

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

====================================
声明:
《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=37

前一篇:QMetaObject class data members
http://www.insideqt.com/bbs/viewthread.php?tid=23
后一篇:emit,幕后的故事
http://www.insideqt.com/bbs/viewthread.php?tid=55
====================================

Noshow ,

Inside Qt Series (九):QMetaObject class data members

2009年9月15日

我们来看一下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是如何来索引这些字符串,以便于在需要的时候能正确的找到他们呢?第三个成员正式登场了。

Noshow ,

Inside Qt Series (八):Meta Object Class overview

2009年9月15日

前面我们介绍了 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:

qt.meta.data.tables

QMetaObject 对象与 QT 类之间的关系

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

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

qt.meta.class.to.q.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》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=17

前一篇:Signal & Slot
http://www.insideqt.com/bbs/viewthread.php?tid=12
后一篇:QMetaObject class data members
http://www.insideqt.com/bbs/viewthread.php?tid=23
====================================

Noshow ,

Inside Qt Series (七):Signal & Slot

2009年9月15日

本节介绍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++源文件。

qt.signal.and.slots

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

qt.signal.slot.sample_1

====================================
声明:
《Inside Qt Series》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=12

前一篇:元对象编译器 – Meta Object Compiler (moc)
http://www.insideqt.com/bbs/viewthread.php?tid=11
后一篇:Meta Object Class overview
http://www.insideqt.com/bbs/viewthread.php?tid=17
====================================

Noshow ,

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

2009年9月15日

元对象编译器用来处理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》专栏文章是Qt核心技术论坛(InsideQt.com)原创技术文章。
本系列专栏文章可随意转载,但必须保留本段声明和每一篇文章的原始地址。
作者保留版权,未经作者同意,不得用于任何商业用途

《Inside Qt Series》专栏文章总索引:
http://www.insideqt.com/bbs/viewthread.php?tid=9
本文原始地址:
http://www.insideqt.com/bbs/viewthread.php?tid=11

前一篇:Inside QT Series (五):元对象系统(Meta-Object System)
http://www.insideqt.com/bbs/viewthread.php?tid=10
后一篇:Inside QT Series (七):Signal & Slot
http://www.insideqt.com/bbs/viewthread.php?tid=12
====================================

Noshow ,