解释了消息分派机制后,接下来我们开始介绍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系统决定仍然使用原先的方案。