应用程序框架设计(2):SW系统的窗口类
解释了消息分派机制后,接下来我们开始介绍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系统的类体系图

类说明:
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系统决定仍然使用原先的方案。
1 评论:
中国呼吸网 癌症康复网 感冒 支气管炎 气管炎 哮喘 肺癌 肺炎 肺结核 打鼾 鼻炎 咳嗽 咽炎 肺心病 肺气肿 鼻窦炎 鼻息肉 扁桃体炎 喉炎 支气管扩张 肺水肿 肺脓肿 肺不张 尘肺病 肺栓塞 鼻咽癌 鼻窦炎 呼吸衰竭 呼吸道感染 呼吸困难 口咽癌 咽部异物 喉癌 喉麻痹 喉头水肿 新生儿窒息 胸腔积液 气胸 胸膜炎 鼻疖 咯血 胸膜癌 急性会厌炎 禽流感 麻疹 风疹 猩红热 百日咳 呼吸机 氧气机 婉转的夜曲 淋过雨的空气 带著一根烟.浪迹天涯 工作总结
发表评论