winx官方站点改版了!
网站建设了一个星期,正式对外发布了。
中文:http://www.winxcn.com 或者 http://www.winxgui.cn
英文:http://www.winxgui.com(建设中,目前指向中文站点)
网站建设了一个星期,正式对外发布了。
中文:http://www.winxcn.com 或者 http://www.winxgui.cn
英文:http://www.winxgui.com(建设中,目前指向中文站点)
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。
解释了消息分派机制后,接下来我们开始介绍WINX的窗口类。为了产生比较的效果,我决定从之前我写的“SW系统”的窗口类讲起。在你理解了SW系统的窗口类后,我们再来看6年后WINX中的窗口类在设计上发生了什么样的变化。——这自然也是我个人在窗口类观念上的改变。
1、SW系统的“Hello,World!”程序
#define Uses_SApp
#include <sw.h>
// 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系统的类体系图
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系统决定仍然使用原先的方案。
《应用程序框架设计》是我大学毕业时(2000年)写的毕业论文。在我给公司内部作“应用程序架构”方面的讲座时,曾经作为入门级的参考资料附上。后来不知如何就流传到Internet上,不过是不完整的版本(可尝试在Google中搜索“应用程序框架设计:SW系统”)。回头看这篇文字,最大的感受觉得自己的文字功底是越来越退步了:-) 由于与界面库有关,大家不妨看看。
应用程序框架设计
许式伟
2000年6月
一、摘要
随着面向对象技术的发展成熟,已经出现了许多著名的应用程序框架,如在Windows平台下有MFC、VCL、OWL等;在旧的DOS系统下有Turbo Vision。在这里我希望通过我设计的“SW系统”来阐述我对设计应用程序框架一些想法。其中涉及的内容主要有:
二、应用程序框架设计的基本内容
一个应用程序到底有多少“骨头”,多少“肉”?这里所说的“肉”是指程序中用于解决问题的那一部分,而“骨头”是指“肉”所依附的程序框架部分,它们是为了实现与用户交互、使界面友好必需做的事情。
对于解决问题的逻辑,我们很难能够找到一个一般做法来简化这件事。只有在具体定位到某一具体的方向时,才有可能做到这一点。例如你要进行数值计算,可能会需要一个功能完善的数学包;你要进行图象处理,可能需要一个图象处理库;等等。严格的说,这些东西说不上是一个框架,只是一个个工具包(Utilities)。因为它们一般没有复杂的调用规则,函数之间相当独立。
对于应用程序与用户的交互,在DOS时代编程的人一定对此感触很深。DOS时期基本上程序与用户交互的动作都是自己完成的。这样做的结果往往不是觉得在界面设计上浪费了太多时间,就是觉得界面设计得不尽人意。各个程序的框架代码有大量的反复,但由于编程方法的局限,程序代码的重用效率往往比较低。
面向对象思想的成熟促使了种种应用程序框架的诞生。面向对象语言中,类的继存可以完成对大量现成代码重用的重用;动态束定(即虚函数机制)技术有效地将具体的实现代码延迟到设计阶段。而一种称为“事件驱动模式”的应用程序结构使程序的框架代码与实现细节彻底发生了分离。尽管现在的应用程序框架种类挺多,但它们在实现思想上相当一致:所有的这些应用程序框架都是“事件驱动模式”的一个应用。这也包括本文要介绍的SW系统。
“事件驱动”的核心自然是事件。从事件角度说,事件驱动程序的基本结构是由一个事件收集器、一个事件发送器和一个事件处理器组成。事件收集器专门负责收集所有事件,包括来自用户的(如鼠标、键盘事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器负责将收集器收集到的事件分发到目标对象中。事件处理器做具体的事件响应工作,它往往要到实现阶段才完全确定,因而需要运用虚函数机制(函数名往往取为类似于HandleMsg的一个名字)。对于框架的使用者来说,他们唯一能够看到的是事件处理器。这也是他们所关心的内容。
视图(即我们通常所说的“窗口”)是“事件驱动”应用程序的另一个要元。它是我们所说的事件发送器的目标对象。视图接受事件并能够对其进行处理。当我们将事件发送到具体的视图时,实际上我们完成了一个根本性的变化:从传统的流线型程序结构到事件触发方式的转变。这样应用程序具备相当的柔性,可以应付种种离散的、随机的事件。
由于Windows本身是基于“事件驱动”模型的。因而在Windows操作系统下实现应用程序框架有相当的便利。在事件驱动程序的基本单元中,事件收集器已经由Windows系统完成;事件发送器也已经由Windows完成了部分内容。之所以是部分而非完全是因为Windows是用C语言实现的,而不是C++。由于没有对象,Windows将事件发送到所谓的“窗口函数”中(尽管不是发送到具体的对象,但应该说这是面向对象方式实现的一个变体)。要感谢Windows做了这件事。确定事件的目标所要做的工作的复杂可能要超出我们的想象。我们要对此进行定性的讨论。
根据事件的发送路线,事件可以分为以下几种:
你已经了解了WINX的消息分派,这里我们总结一下,并交代一些前文为了思路紧凑而略过的一些细节,内容包括:
开发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*>
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;
}
};
我们继续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源代码时,你可以发现这个技巧有不少变种(消息分派的实现就与此有细节上的不同),但是其中的道理是完全一致的。
和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函数进行处理。涉及的几个关键函数功能如下:
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 ...
本博客内容除非特殊说明均属原创,如需转载、引用其中的部分文字,请注意以下几点:
1)请在转载(引用)的内容开始添加本人署名,并提供本博客中相应文章的链接。如你的作品为非电子读物或纯文本,请给出链接的url。
2)请勿用于商业用途。
3)如果愿意,请给我邮件:xushiweizh@gmail.com,让我知道我的东西到哪去了。谢过。
WINX是卓越的,您需要了解以下内容:
*) 卓越的消息分派机制。正是因为有这个核心支撑,使得WINX区别于传统的界面库(如MFC、WTL)。
*) 简单易用(SIMPLE)是第一目标,尽量使可视化(WYSIWYG)界面开发成为可能。
*) WINX是一个界面库,不是开发框架。WINX代码是可以和WTL、MFC等界面库的代码共存的。
*) 兼容。尽管有更简洁的方法,但WINX还是提供了MFC程序员熟悉的调用界面,并尽量使得MFC代码可以轻松移植到WINX下。
*) 不重复制造轮子。在没有一个卓越的解决方案以区别于现有系统之前,先沿用现有的。事实上,WINX建立于WTL之上,重用了多数的WTL组件。