星期一, 十一月 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系统决定仍然使用原先的方案。

没有评论: