星期六, 三月 03, 2007

winx官方站点改版了!

网站建设了一个星期,正式对外发布了。
中文:http://www.winxcn.com 或者 http://www.winxgui.cn
英文:http://www.winxgui.com(建设中,目前指向中文站点)

星期三, 十一月 08, 2006

My blog was migrated...

Because of blockage, the readers from china were unable to read these posts. So I decide to migrate my blog, which is written in chinse, to CSDN.net. Here is the homepage of my new blog: http://blog.csdn.net/xushiweizh. And late I will write something in english to http://codeproject.com.

转移战场...

由于blogspot被盾,不得不考虑转移战场了...

原先winxcn.blogspot.com主要访问来源有二:搜索引擎和一些一直关注WINX的朋友。目前只剩下几个骨灰级的朋友,和从sourceforge的链接(http://winxcn.com)无意中过来一些老外。

决定中文blog主要在CSDN上维护,网址:http://blog.csdn.net/xushiweizh
下阶段可能会增加英文文档,目前考虑转战CodeProject。

星期一, 十一月 06, 2006

应用程序框架设计(2):SW系统的窗口类

解释了消息分派机制后,接下来我们开始介绍WINX的窗口类。为了产生比较的效果,我决定从之前我写的“SW系统”的窗口类讲起。在你理解了SW系统的窗口类后,我们再来看6年后WINX中的窗口类在设计上发生了什么样的变化。——这自然也是我个人在窗口类观念上的改变。

1、SW系统的“Hello,World!”程序

#define Uses_SApp
#include <sw.h>
// SW系统中,你需要记住头文件只有,它是SW系统总控文件。
// 你只需要告诉它,你用了什么,它可以为你检测需要的头文件并包含它们。
// 例如,这里我们用了SApp类,故有 #define Uses_SApp 一句。

class SHelloApp : public SApp
{
public:
 void OnDraw(SHDC dc);
};

void SHelloApp::OnDraw(SHDC dc)
{
 dc.TextOut(1, 1, _T(“Hello, World!”));
}

// SW系统没有封装WinMain函数,这应该是一个好消息吧?
// 你不必对主函数是main()而非是WinMain太过关心。它只是为了与DOS
// 习惯兼容以及书写的方便而实现的宏而已。
int main()
{
 return Desktop.Execute(new SHelloApp);
 // Desktop就是Windows系统桌面,它是一个特殊的窗口对象。
 // 特别注意,SW系统中App类是一个窗口!
}

2、SW系统的类体系图


类说明:
 SHWnd:对Windows窗口句柄的简单包装
 SObject:类库的根
 SString:字符串类
 SArchive:文档类,用于存盘
 SHxxxCtrl:各种控件
 SWnd:窗口类基本类
 SHDC:对DC句柄的简单包装
 SMsg:消息类
 SSubclass:派生子类
 SDlg:对话框类
 SApp:应用程序类(SDI)
 SMDIChild:MDI程序文档窗口
 SMDIApp:应用程序类(MDI)

注:SW系统中对话框不分模态/非模态,只有在调用时才有区别。

如果希望是模态的:
 hWndParent.ExecDlg(new SxxxDlg); // 调用DialogBoxParam
或:
 hWndParent.Execute(new SxxxDlg, SW_SHOW);
  // 使用SW系统的消息循环

如果希望是非模态的:
 hWndParent.Insert(new SxxxDlg);
  // 像普通窗口一样插入到父窗口中即可

3、独特的窗口(视图)模型

注: 请注意术语上的变化,由于SW系统在Windows下实现,使用了Windows中的一些术语。在这里视图被称为窗口,事件被称为消息。

窗口应该具备哪些行为才合理?象显示/隐藏、选择(激活)、移动、改变大小、关闭等这些基本行为是很容易想到的。SW系统与经典应用程序框架如MFC不同的是,它引入了另两个非常有用的方法:

1)Insert操作:

SW系统中,窗口都通过Insert操作插入到父窗口中。

2)Execute操作:

SW系统中,可以在一个窗口A中运行(Execute)另一个窗口B。此时,窗口A作为窗口B的父窗口,已经不再接受消息,即窗口B是模态视图。不过,在窗口A是Desktop(这里Desktop是Windows系统桌面)时稍微有点不同。这是由于我们使用得是多任务系统所致。

在SW系统中,应用程序是一个窗口,由SWnd类派生;这不同于MFC,在那里应用程序是一个线程,由CWinThread类派生。这一点也许MFC的设计可能严谨一些,但是SW系统的做法的优点是,程序的逻辑变得相当简单。SW系统如何运行程序?非常清楚,只要一句话:
  Desktop.Execute(new SxxxApp);
这个我们在例子程序中已经看到了。

Execute操作很复杂吗?打开SW系统的源代码,你可能很失望,代码只有几句话:

UINT SHWnd::Execute(SWnd *pWnd, int nCmdShow)
{
 if (this->Insert(pWnd))
 {
  pWnd->Show(nCmdShow);
  return pWnd->MsgLoop();
 }
 return –1;
}

很显然,Desktop.Execute(new SxxxApp)的含义是:

1)将应用程序插入(Insert)到Desktop;
2)显示应用程序;
3)进入消息循环;

Insert操作又做了什么?它的代码就相对复杂,且与Windows系统密切相关,这里不具体写出它的代码。它完成的工作主要是:

1)检测窗口类是否已经注册;如果没有,注册窗口类;
2)调用窗口创建函数创建窗口对象;
3)由事件发送器将WM_CREATE发送给窗口对象;

消息循环(MsgLoop)的实现流程我们一开始就已经给出,即“事件驱动”应用中的消息流动过程。我们也提到其中事件发送器Windows只实现了一部分。现在问题的关键在于,如何将消息由窗口过程发送给具体的窗口对象?

MFC采用一张Hash表将窗口句柄与窗口实例指针关联起来。在这一点上SW系统采取了一个取巧的办法,避免去维护一张Hash表。它利用窗口句柄的USERDATA字节保存对象指针。具体代码如下:

// 为了突出问题的关键,本文的代码一般比较简略,省略了没有大多数的出错
// 处理。作为实际的程序,其风格当然不应该这样。
LONG CALLBACK WndProc(
 HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 SWnd* pWnd = (SWnd*)GetWindowLong(hwnd, GWL_USERDATA);
 if (pWnd)
 {
  SMsg ev;
  ev.uMsg = uMsg;
  ev.lParam = lParam;
  ev.wParam = wParam;
  return pWnd->HandleMsg(ev);
 }
 if (uMsg == WM_CREATE)
 {
  pWnd = (SWnd*)((CREATESTRUCT*)lParam)->lpCreateParams;
  // 这是从CreateWindow函数传进来的窗口实例指针!
  pWnd->m_hWnd = hwnd;
  SetWindowLong(hwnd, GWL_USERDATA, (LONG)pWnd);
  return pWnd->OnCreate() ? 0 : -1;
 }
 return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
}

在这个技巧中存在的问题是,所有在WM_CREATE之前的消息被“遗失”了。解决这个限制的方案是简单的。核心思想是用全局变量传递窗口实例指针,而不是在CreateWindow函数的参数中传递。见下:

创建窗口代码:
 LOCK();
 x_pThisWnd = 窗口实例指针;
 调用CreateWindow创建窗口;
 UNLOCK();

窗口过程代码:
 LONG CALLBACK WndProc(
   HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
 {
  SWnd* pWnd = (SWnd*)GetWindowLong(hwnd, GWL_USERDATA);
  if (!pWnd)
  {
   pWnd = x_pThisWnd;
   pWnd->m_hWnd = hwnd;
   SetWindowLong(hwnd, GWL_USERDATA, (LONG)pWnd);
  }
  SMsg ev;
  ev.uMsg = uMsg;
  ev.lParam = lParam;
  ev.wParam = wParam;
  return pWnd->HandleMsg(ev);
 }

这里LOCK()/UNLOCK()是多线程互斥代码。由于使用了全局变量,线程互斥是必要的。

可以看到,所有的消息都是经由HandleMsg处理的。这是一个进步。但是这中间还有一个不妥当的地方。事实上我们已经习惯于在WM_CREATE消息中对窗口实例进行初始化。因此在WM_CREATE之前的消息处理时,对象其实还没有初始化完毕。这一点如果被遗忘就会犯错。而且,从概念上说,初始化总应该是对象接收到第一个消息。正是由于这一点,SW系统决定仍然使用原先的方案。

不小心上了CSDN首页...

呵呵,我申请csdn的blog才一个月哩。

星期日, 十一月 05, 2006

应用程序框架设计(1):SW系统简介

《应用程序框架设计》是我大学毕业时(2000年)写的毕业论文。在我给公司内部作“应用程序架构”方面的讲座时,曾经作为入门级的参考资料附上。后来不知如何就流传到Internet上,不过是不完整的版本(可尝试在Google中搜索“应用程序框架设计:SW系统”)。回头看这篇文字,最大的感受觉得自己的文字功底是越来越退步了:-) 由于与界面库有关,大家不妨看看。


应用程序框架设计

许式伟
2000年6月


一、摘要

随着面向对象技术的发展成熟,已经出现了许多著名的应用程序框架,如在Windows平台下有MFC、VCL、OWL等;在旧的DOS系统下有Turbo Vision。在这里我希望通过我设计的“SW系统”来阐述我对设计应用程序框架一些想法。其中涉及的内容主要有:

  • 应用程序框架设计的基本内容

    这一部分主要是讨论应用程序框架的必要性、可行性,以及设计应用程序框架的基本思路。
     
  • SW系统的总体内容与实现

    这一部分主要讨论作为一个应用程序框架,SW系统的总体结构和内容,并对SW系统一些重要的实现细节做出说明。其中主要有:SW系统中的窗口模型、属性、SW系统的RuntimeClass支持和序列化等。在最后,我们要分析经典应用程序框架的缺陷。同时也说明由SW系统向COM转变的必然
     
  • SW系统的新方向:基于COM(组件)思想的应用程序框架

    这一部分主要介绍组件思想的基本内容。SW系统的对组件思想的实现。

二、应用程序框架设计的基本内容

一个应用程序到底有多少“骨头”,多少“肉”?这里所说的“肉”是指程序中用于解决问题的那一部分,而“骨头”是指“肉”所依附的程序框架部分,它们是为了实现与用户交互、使界面友好必需做的事情。

对于解决问题的逻辑,我们很难能够找到一个一般做法来简化这件事。只有在具体定位到某一具体的方向时,才有可能做到这一点。例如你要进行数值计算,可能会需要一个功能完善的数学包;你要进行图象处理,可能需要一个图象处理库;等等。严格的说,这些东西说不上是一个框架,只是一个个工具包(Utilities)。因为它们一般没有复杂的调用规则,函数之间相当独立。

对于应用程序与用户的交互,在DOS时代编程的人一定对此感触很深。DOS时期基本上程序与用户交互的动作都是自己完成的。这样做的结果往往不是觉得在界面设计上浪费了太多时间,就是觉得界面设计得不尽人意。各个程序的框架代码有大量的反复,但由于编程方法的局限,程序代码的重用效率往往比较低。

面向对象思想的成熟促使了种种应用程序框架的诞生。面向对象语言中,类的继存可以完成对大量现成代码重用的重用;动态束定(即虚函数机制)技术有效地将具体的实现代码延迟到设计阶段。而一种称为“事件驱动模式”的应用程序结构使程序的框架代码与实现细节彻底发生了分离。尽管现在的应用程序框架种类挺多,但它们在实现思想上相当一致:所有的这些应用程序框架都是“事件驱动模式”的一个应用。这也包括本文要介绍的SW系统。

“事件驱动”的核心自然是事件。从事件角度说,事件驱动程序的基本结构是由一个事件收集器、一个事件发送器和一个事件处理器组成。事件收集器专门负责收集所有事件,包括来自用户的(如鼠标、键盘事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器负责将收集器收集到的事件分发到目标对象中。事件处理器做具体的事件响应工作,它往往要到实现阶段才完全确定,因而需要运用虚函数机制(函数名往往取为类似于HandleMsg的一个名字)。对于框架的使用者来说,他们唯一能够看到的是事件处理器。这也是他们所关心的内容。

视图(即我们通常所说的“窗口”)是“事件驱动”应用程序的另一个要元。它是我们所说的事件发送器的目标对象。视图接受事件并能够对其进行处理。当我们将事件发送到具体的视图时,实际上我们完成了一个根本性的变化:从传统的流线型程序结构到事件触发方式的转变。这样应用程序具备相当的柔性,可以应付种种离散的、随机的事件。

由于Windows本身是基于“事件驱动”模型的。因而在Windows操作系统下实现应用程序框架有相当的便利。在事件驱动程序的基本单元中,事件收集器已经由Windows系统完成;事件发送器也已经由Windows完成了部分内容。之所以是部分而非完全是因为Windows是用C语言实现的,而不是C++。由于没有对象,Windows将事件发送到所谓的“窗口函数”中(尽管不是发送到具体的对象,但应该说这是面向对象方式实现的一个变体)。要感谢Windows做了这件事。确定事件的目标所要做的工作的复杂可能要超出我们的想象。我们要对此进行定性的讨论。

根据事件的发送路线,事件可以分为以下几种:

  • 位置事件(鼠标事件)

    它的目标比较明确,即包含鼠标所在点的视图。不过这也有不适用的时候。在拖动时,往往希望鼠标消息的接收者是鼠标刚开始拖动时的视图,而非真正包含鼠标点位置的那个视图。正是考虑到这一点,Windows提出了“捕获鼠标消息”的概念。

  • 焦点事件(键盘事件、命令事件等)

    对于焦点事件消息,情形就比较复杂。为了说明白这一点,需要引入模态视图、焦点视图等概念。

    一个应用程序的视图结构通常由一个树型结构维护。在某个时刻,总存在一个视图,它的所有祖先视图不再接受事件,而它及其所有子视图仍然处理事件。这个视图被称为模态视图。它称得上是“活动着的根视图”。模态视图的一个典型例子是对话框。在它运行时是应用程序的其他部分都失去了响应。

    什么是焦点视图?模态视图首先是一个焦点视图。模态视图的子视图中,有一个视图是被激活的(或者称,它获得了“焦点”),它也是一个焦点视图。如果它也是复合视图(存在子视图的视图),它也可以有一个子焦点视图。这个逻辑一直到一个被激活的简单视图(对于它,还有一个名称,叫“热点视图”)。“热点视图”的所有父视图、父视图的父视图等等,直到模态视图,构成一个“焦点视图”链。

    总的说来,焦点事件首先发送给获当前的模态视图。在它不知道如何处理时,可能将事件顺着焦点视图链往下传。很多人在编程时往往发现某个事件处理函数总是不能够被执行。这种情况总是发生在焦点事件。其原因是由于祖先视图已经截获了该事件。如果能够对事件传递的过程理解了,应该怎样改写代码也就明确了。

    然而在焦点事件上有太多例外。有些视图没有获得焦点是也希望能够处理键盘消息。典型例子是菜单,对话框中的一些按钮等。针对这些Windows中已经有一些操作惯例,同时Windows也是不可能完全确定目标视图的。
     
  • 广播事件

    广播事件并不知道自己的目标,它的处理方式很简单,它将事件发送给所有视图。Windows并不支持广播消息。SW系统实现广播消息主要是为了逻辑的简化,例如关闭所有窗口、保存所有文件等,应用广播的概念是方便的。

WINX的消息分派机制(终结篇)

你已经了解了WINX的消息分派,这里我们总结一下,并交代一些前文为了思路紧凑而略过的一些细节,内容包括:

  • WINX消息分派的总体特色。
  • 与MFC、WTL相比它有什么优点与劣势。
  • Default函数是如何实现的。

开发WINX的时候,尽管我决定尽量重用WTL,以便这个界面库不至于和Sourceforge上其他众多的界面库一样,最后只是一个实验品(它们无法流行的原因多数在于体系封闭而个人精力有限而无法提供与当前流行的界面库同等的功能),只是拥有少量的忠实拥护者,但是最终无法成为工业级的产品(在我看来,C++界面库比较成功的有MFC、QT、wxWidgets,而WTL只能算半个)。但是,消息分派机制上我决定不沿袭WTL,而是自己提供全新的实现。

WTL的消息机制是极其灵活的,通过它你很容易将功能划分为一个个独立而且含义完整的功能切片。但是问题在于,这种灵活给用户带来了困惑:深奥的模板技术、复杂的消息机制、晦涩而丑陋的代码,用户望而却步了。

WINX的消息机制给用户最简洁的界面,并尽量与MFC的消息兼容。另一方面,WINX的消息分派的智能带来了另一个好处:WINX不断可以增加新的消息,只要派生类没用响应它,就没有任何额外开销。进而,我们可以为WINX加入任何新特性,只要用户没用使用该特性,那么就没用额外代价。——这很有趣。你马上可以想到,WTL的一个个功能切片,WINX也可以提供,并且可以以更为简洁的方式提供。

WINX的消息分派是高效的。一部分原因你已经了解到了:WINX它可以智能的了解派生类并做出优化。还有一个细节,同样与性能有关:WINX的消息响应次序在DispatchMessage中已经精心安排好了。你可以想象,把WM_PAINT消息放到switch..case的最后,还是放在最前,这对消息函数的执行效率产生了怎样的影响。而WTL中是由你自己去安排消息响应的次序,这对使用者是一个额外的负担。——这不只是因为性能的因素,WTL中某些功能切片的安放是有次序要求的,交换次序后会有细节上的行为差异。

WINX的消息机制最大的问题在于,由于我们随时可能会为了实现一个作用于所有窗口的新特性而添加新消息,故此,为了方便你未来升级WINX到更高的版本,你派生的窗口类需要小心定义其成员函数名。我的一个建议是,请尽量不要自定义一些以On开头的函数名。如果确实需要,建议引入类似命名空间的法则:例如,所有响应命令的函数统一以OnCmdXXX命名之。

MFC在你响应消息中希望执行默认处理时,除了调用基类的同名方法外,更为常见的方法是调用Default()函数。同样,WINX也提供了该函数,并且实现方式类似,大体代码如下:

__declspec(thread) PackedMessage _g_currMsg;

template <class T>
class WindowMessage
{
public:
 LRESULT Default()
 {
  T* pThis = static_cast<T*>(this);
  return pThis->InternalDefault(
   _g_currMsg.hWnd, _g_currMsg.message,
   _g_currMsg.wParam, _g_currMsg.lParam);
 }

 LRESULT ProcessMessage(
   HWND hWnd, UINT message ,
   WPARAM wParam, LPARAM lParam)
 {
  LRESULT lResult = 0;
  PackedMessage oldMsg = _g_currMsg;
  _g_currMsg.hWnd = hWnd;
  _g_currMsg.message = message;
  _g_currMsg.wParam = wParam;
  _g_currMsg.lParam = lParam;
  BOOL fProcess = DispatchMessage(
    hWnd, message, wParam, lParam, lResult);
  if (!fProcess)
   lResult = Defalut();
  _g_currMsg = oldMsg;
  return lResult;
 }
};

WINX的消息分派机制(续2)

我们继续Inside WINX's Message Dispatch。现在开始我们进入了最为关键的部分——WINX是怎么进行消息分派的。

从原理上来讲,WINX的消息分派函数(DispatchMessage)其实与上一篇:《WINX的消息分派机制(续)》中的并无多大的不同,只不过更加智能而已。其中最为关键的是,WINX引入了一种技巧,它可以在编译期判断一个函数是否被重载。简单来说,WINX的消息分派伪代码如下:

template <class T>
class WindowMessage
{
 ...

 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  T* pThis = static_cast<T*>(this);
  if (派生类重载了OnPaint && message == WM_PAINT)
    pThis->OnPaint(hWnd);
  else if (派生类重载了OnKeyDown && message == WM_KEYDOWN)
    pThis->OnKeyDown(hWnd, wParam, lParam);
  else if (...)
    ...
  else
    return FALSE;
  return TRUE;
 }
};

简单看一个实际的例子,这样做的好处就很明了了。设想WindowMessage的派生类只重载了OnPaint,那么WindowMessage类看起来是这样的:

template <class T>
class WindowMessage
{
 ...

 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  T* pThis = static_cast<T*>(this);
  if (true && message == WM_PAINT)
    pThis->OnPaint(hWnd);
  else if (false && message == WM_KEYDOWN)
    pThis->OnKeyDown(hWnd, wParam, lParam);
  else if (...)
    ...
  else
    return FALSE;
  return TRUE;
 }
};

并最终被编译器优化为:

template <class T>
class WindowMessage
{
 ...

 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  T* pThis = static_cast<T*>(this);
  if (message == WM_PAINT)
    pThis->OnPaint(hWnd);
  else
    return FALSE;
  return TRUE;
 }
};

特别地,如果WindowMessage派生类没有响应任何消息,则优化后DispatchMessage为一个空函数,如下:

template <class T>
class WindowMessage
{
 ...

 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  return FALSE;
 }
};

这就是WINX的消息分派机制为何比MFC、WTL以及其他任何界面库高效(无论是编译后的代码尺寸上,还是执行效率上)的原因。

好了,现在该是解释WINX如何做到这一点——检测派生类是否重载某个函数的时候了。我们假设,基类(名为Base)中有一个成员函数Func(假设有两个参数),现在有另一个成员函数Caller希望根据派生类是否重载Func来做事情。如下:

template <class T>
class Base
{
 RetType Func(ArgType1 arg1, ArgType2 arg2) { ... }
 void Caller() {
  if (派生类重载了Func) { ... }
  else { ... }
 }
};

一个办法是,略微修改一下基类中的Func原型,加上一个无用参数int unused:
 RetType Func(ArgType1 arg1, ArgType2 arg2, int unused = 0);
或者直接改为可变参数:
 RetType Func(ArgType1 arg1, ArgType2 arg2, ...);
当然,派生类重载Func原型还是需要按我们预期的:
 RetType Func(ArgType1 arg1, ArgType2 arg2);

如此,判断“派生类是否重载了Func”就变成了判断函数原型是否为
 RetType Func(ArgType1 arg1, ArgType2 arg2);
而这正是编译器的拿手好戏。

最后提醒一下,阅读WINX源代码时,你可以发现这个技巧有不少变种(消息分派的实现就与此有细节上的不同),但是其中的道理是完全一致的。

WINX的消息分派机制(续)

和MFC、WTL等界面库不太一样的是,WINX认为消息分派是一个可独立于窗口存在的基础服务。所以WINX中负责消息分派的不是 winx::Window<T>类,而是 winx::WindowMessage<T>类。 winx::Window<T>只是从winx::WindowMessage <T>继承。

上一篇我故意买了个关子。如果有读者在看了《WINX的消息分派机制 》一文后去亲自看winx的头文件了解实地了解一下的话,我将觉得很安慰。这一篇我们继续这个话题。

WindowMessage<T>的基本规格是这样的:

template <class T>
class WindowMessage
{
 void OnDestroy(HWND hWnd);
 void OnPaint(HWND hWnd);
 void OnKeyDown(HWND hWnd , UINT uVKChar, UINT uKeyData);
 ...

 LRESULT InternalDefault (
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam);

 BOOL DispatchMessage (
   HWND hWnd , UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult);

 LRESULT ProcessMessage(
   HWND hWnd, UINT message ,
   WPARAM wParam, LPARAM lParam);
};

WindowMessage<T>的契约,其客户必须将发送给窗口的所有消息全部转发给ProcessMessage函数进行处理。涉及的几个关键函数功能如下:

  • ProcessMessage函数先调用DispatchMessage对消息分发,如果DispatchMessage 没有处理该消息,则调用InternalDefault来处理该消息。
  • DispatchMessage函数根据消息的ID,即UINT message参数,对消息进行分派。例如WM_PAINT消息发送给OnPaint处理,WM_DESTROY发送给OnDestroy处理,WM_KEYDOWN发送给OnKeyDown处理等等。
  • InternalDefault函数则是抽象不同类型的窗口。对于普通窗口,它调用DefWindowProc;但对于MDIFrame窗口,它需要调用DefFrameProc;对于MDIChildFrame窗口,则调用DefMDIChildProc;对于对话框,则它只需要直接返回FALSE即可。
显然,这里面最为关键的是 DispatchMessage 。如果不考虑优化,它看起来并不复杂。我们这里实作一个基于虚函数(virtual)机制的版本:

class WindowMessage
{
 virtual void OnPaint(HWND hWnd) { Default(); }
 virtual void OnKeyDown(
   HWND hWnd, UINT uVKChar, UINT uKeyData) { Default(); }
 ...
 
 LRESULT Default();
 
 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  switch (message)
  {
  case WM_PAINT: OnPaint(hWnd); break;
  case WM_KEYDOWN: OnKeyDown(hWnd, wParam, lParam); break;
  ...
  default: return FALSE;
  }
  return TRUE;
 }
};
 
你一定对此不屑一顾:除了Default函数看起来有点意思(容后介绍它的实现)外,用一个超庞大switch..case来实现DispatchMessage,有"创意",但实在是有些乏味。
 
有人马上提建议说,改用模板(template)吧�D�D性能高些。于是,就有了基于template的版本:
 
template <class T>
class WindowMessage
{
 void OnPaint(HWND hWnd) { Default(); }
 void OnKeyDown(HWND hWnd, UINT uVKChar, UINT uKeyData)
  { Default(); }
 ...
 
 LRESULT Default();
 
 BOOL DispatchMessage(
   HWND hWnd, UINT message,
   WPARAM wParam , LPARAM lParam, LRESULT& lResult)
 {
  T* pThis = static_cast<T*>(this);
  switch (message)
  {
  case WM_PAINT: pThis->OnPaint(hWnd); break;
  case WM_KEYDOWN: pThis->OnKeyDown(hWnd, wParam, lParam); break;
  ...
  default: return FALSE;
  }
  return TRUE;
 }
};
 
在很多时候,模板(template)与虚拟(virtual)是相通的。这两个版本的WindowMessage并无本质的不同,事实上只是作了机械的代码变换而已。了解这个变换的等价性是很有必要的。不可否认,经此一变,性能提高了不少。前文《 ATL界面类——兼谈多态与泛型 》我们对此作了细述。
 
这个基于模板的WindowMessage类同样乏味。尽管没有了虚函数调用的开销,但这个DispatchMessage函数无疑仍然是个庞然大物,与WTL的精巧相去甚远。
 
那么,我们还有甚么办法可想吗?
 
to be continued...

星期三, 十一月 01, 2006

WINX的消息分派机制

WINX的消息分派是卓越的。我们先简单回顾一下WINX的SDI风格的Hello程序与MFC/WTL/SDK的对比(我们关注的是窗口类中的消息处理相关):

MFC和WTL有着类似MessageMap(尽管内部机制大不一样),是通过宏实现消息分派的。也许你已经习惯了响应消息时提供MessageMap,但在WINX中这不需要响应任何消息你均只需要直接覆盖消息处理函数即可。示意如下:

class MyWindow : public winx::Window<MyWindow>
{
public:
 void OnPaint(HWND hWnd) { ... }
};

你可能担忧WINX的消息分派的便利,是牺牲性能为代价的。——可是我郑重告诉你,这种担忧是多余的。随着本文对winx消息机制的一步步剖解,你将发现,事实恰恰相反,WINX在消息分派的性能上考虑甚多,其消息分派的代码的无论是编译后的执行代码尺寸,还是效率,均优于MFC、WTL。

to be continued ...