浅谈Windows应用程序一般框架
关键词:Windows 应用程序 Win32API 消息驱动
摘要:本文从一个使用者的角度入手,逐步分析了Windows应用程序的概念模型,又在Win32API的层面简要分析了一个一般的Windows应用程序的基本框架,并且展示了如何使用C代码和原始的API来开发简单的Windows应用程序。
正文:
自从1985年,第一个Windows操作系统Microsoft Windows 1.0发布以来,MS Windows的大名已经走进了千家万户,全球有无数的个人电脑,都运行着Windows操作系统和各式各样Windows风格的应用程序,在“桌面”市场,几乎形成了微软一家独霸的态势。与其说是Windows操作系统垄断了整个“桌面”市场,不如说是基于Windows操作系统的应用程序所倡导的方便、易用的开放式、向导式、会话式操作风格赢得了全球数以亿计的个人电脑用户的芳心。那么对于程序员来说,这样的Windows应用程序是如何编写的呢?
在回答这个问题之前我想做一些解释。
如果您以前曾经使用过VB、DELPHI或者是VC,基于VCL、MFC等框架开发过Windows应用程序的话,那么请您注意,您以前所看到的并不是Windows应用程序真实的面貌,或者说那些都不是底层的代码,今天我所讲到的则是更为底层些的技术,而且我相信这些底层的代码又会在一定程度上加深您对VCL或MFC的理解。当然,如果您以前并没有使用过这些RAD(Rapid Application Development,快速应用程序开发)工具也没有关系,甚至从某种意义上讲您更具有学习这种技术的优势。
今天所讲到的东西被一些人认为是古老的(第一代Windows程序员就是这样“劳作”的)、应被淘汰的,但是,如果能够了解一些本质的事情一定会使您区别于其他人。
另外,为了能够更好的理解我将要讲到的技术,您需要有一定的基础,这只是十分简单的两条:一是熟悉Windows应用程序的一般操作,二是有一定的C∕C++语言基础。
好,下面让我们一起进入主题。
一、宏观概览
这一部分只是介绍了些概念化、模型化的事情,非常容易。
1、 请求∕应答模型(Request/Response)
回想一下,平日里,我们使用MS WORD时,我们滑动鼠标,点击菜单或者工具按钮,WORD就会根据我们的要求做相应的工作(当然它也有可能什么也不做),这里我们明显的感到了一个人机之间的请求∕应答模型(如图1)
图1
请 求
应 答
事实上,基于一点常识,我们知道人与计算机之间是不能像图1那样直接交互的,人的请求需要被翻译成应用程序能认知的形式,这一翻译的过程可能由硬件和软件共同完成。
硬件方面很复杂,比如鼠标、键盘等输入设备,又如PS∕2、USB等连接设备——所幸,这些并不属于我们讨论的范畴。
软件方面,负责驱动硬件设备,主要就是将硬件信号转换、翻译成应用程序能接受的指令。这样的翻译软件,想必您也猜到了,就是Windows操作系统,这样我们就得到了一个清晰一些的请求∕应答模型,如图2。
图 2
硬
件
设
备
请求
应答
请求
应答
Windows 操作系统
请求
应答
Windows
应用程序
出于本人的爱好和本文的主旨,我们需要细致研究的是下面的模型,如图3。
图3
Windows 操作系统
请求
应答
Windows
应用程序
值得注意的,这里的请求与应答是程序与程序的“对话”,并不是我们平日里熟悉的“点鼠标、看结果”的过程。
2、消息驱动模型(Message-Driven)
再请回想一下,如果您的鼠标或键盘并不对应用程序做什么动作,也就是说,并不产生任何请求的话,应用程序是否有应答呢?我想,一般来说是不会的(除了病毒等不听命于您的程序),如果不知什么时候,您突然发出了请求,应用程序会不会立即应答呢?显然,一般的,应用程序会立即做出回应。它怎么会如此及时的对我们的请求做出应答呢?
实际上,我们很难想象,应用程序竟可以准确的预知我们何时将要做出何种动作,所以,您一定想到了——有一个家伙一直等在那里为应用程序监视着一个个请求并把它们交给应用程序。
现在,我要把这一个个请求称为消息(Message)。
请回顾一下图3的模型,那里的请求是由Windows操作系统发出的为了避免混淆和尊重微软,这里用一个专业一点的词汇——消息。对于操作系统与应用程序之间的请求和应答,微软把它们统称为消息。
下面是一个新的模型,见图4。
图 4
Windows 操作系统
消息
消息
Windows
应用程序
图3和图4似乎并没有什么区别,但因为引入了一个非常重要的概念——消息,所以我还是决定把它做为一个新的模型来对待。事实上,它带来了一种新的说法:“操作系统发送消息给应用程序,应用程序响应这个消息,并执行相应的操作。”这就是所谓的消息驱动(Message-Driven)。
实际使用中,您可能连续给应用程序发出了多个请求,您会发现应用程序会不慌不忙、按部就班的返回一个个相应的应答,这里提示我们,Windows操作系统发给应用程序的消息一定被暂存在某个地方,供应用程序逐个逐次的处理。
这个暂存消息的地方由操作系统负责为每个应用程序进行维护,我们称这个地方叫消息队列(Message Queue),正如我们在数据结构中学到的,消息队列就是那种先进先出、尾进头出的内存空间。看一看下面的模型,如图5
图5
Windows 操作系统
翻译
和
发出
消息
消 息 队 列
Windows
应用程序
这里我武断的将Windows操作系统分为两个部分,请您注意这只是在概念上的分割,至于Windows操作系统内部究竟是什么样子,我是不知道的,您如果感兴趣,可以去询问微软的工程师们并且希望您能将答案告知我。
细心的读者一定会发现,在图5中,由Windows应用程序发送给操作系统的消息并没有原路返回,事实就是这样——设想,怎么能将应用程序的应答放入请求的队列中呢?
而对于应用程序的应答,可能是发送消息给操作系统,或者调用一些系统提供的Api函数,总之这样的应答是要通过操作系统的。
另外,值得一提的,一个应用程序可能会和多个消息队列相关联,而且有些消息还可能加塞儿,由于本文“浅谈”的主旨,我就不深入探讨了。
图5深化了原先的消息驱动模型,它告诉我们,Windows操作系统为应用程序维护一个或多个消息队列,并将请求封装成消息,再将消息填入消息队列,应用程序则从消息队列中逐个获得消息,并针对不同的消息执行不同的动作,做出不同的响应。
更为简单的说,应用程序所做的工作就是,一个获取消息、执行操作,再获取消息、执行操作的周而复始的循环过程,这里也有一个专业的词汇来形容它——消息循环(Message loop)。
消息驱动模型是Windows应用程序最根本的编程模型,也是本文开头时提到的开放式、向导式、会话式操作风格的基础,请您一定要悉心体会,它并不像看上去那么简单。
二、微观实现
这部分会有一大段艰涩的代码和一大堆晦涩的文字,我并没想改变这种窘境,因为它本来就是那样。另外您如果想区别于其它读者,请您消化掉这些苦涩。
1、 窗口类(Window Class)
如果只有消息队列与消息循环,我们一般是无法使用应用程序的,普通的Windows应用程序会提供给用户一个图形界面,用来接受用户发出的请求。这个图形界面就是我们众所周知的Windows
窗口的建立需要一个数据结构来描述,毕竟你得告诉Windows你需要什么模样的窗口。
这里只要调用一个相应的API函数即可,原型如下:
RegisterClass(CONST WNDCLASS *lpWndClass);
我们需要为这个函数填入非常重要的、也是唯一的一个参数,这个参数是一个结构体类型,原型如下:
typedef struct {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;
这个结构体定义了一系列用于描述窗口类的属性,您只需赋给每个字段相应的值就能定制您的窗口类。为了避免枯燥,我不想列举出每个字段的意义,您可以在MSDN中查询到细致、权威的解释。
2、 消息
在消息驱动模型中我已经描述过消息的概念,那么如何用C的代码来描述消息呢?或者说在Windows操作系统与应用程序之间究竟传递着些什么呢?
勿庸置疑,操作系统与应用程序传递的必然是无差别的0,1数据,但操作系统并不是能理解任何形式的信息,只有下面这种格式的信息它能明白:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
这样就定义了一个所谓的消息。
上面已经提到过,应用程序所获得的消息是由Windows操作系统发送的,那么,必然的,这个消息类型中的一个个字段也是由操作系统填充的,它们的意义如下:
hwnd: 接受消息的窗口的句柄(对于句柄,我们可以简单的理解为,操作系统为了标定它所掌管的各类资源而给它们起的代号)。
message:消息识别字,它是一个32位无符号整数,它标志着不同的消息。而且在Windows.h这个头文件中对应每一个不同的整数还定义了许多常量标识符。
wParam: 一个32位的数据,它的值随消息的不同而不同,主要描述消息的附加信息。
lParam: 同上。
time: 消息放入消息队列中的时间。
pt: 消息放入消息队列时鼠标的坐标。
至于为什么消息会定义成这样的一个结构,相信您会在我后面的讲述中体会到。
3、 消息循环与消息队列
我在前面已经概念性的描述了这两个词汇,下面我将讲解一般我们是如何实现的。
消息循环确实被描述成一个While循环:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
GetMessage函数负责从消息队列中获得消息,并用引用的参数传递方式将消息填入了变量msg中(显然,变量msg属于MSG类型)
当GetMessage函数从消息队列中获得一个“退出消息”时,它会返回一个0值,从而退出While循环。
TranslateMessage函数负责转译某些键盘消息(我并不想深入解释这一点,因为这样又会引出一大堆内容,请您查阅MSDN)
DispatchMessage 函数将消息(即变量msg)回传给Windows操作系统,再由操作系统将消息发送到处理这个消息的地方,这个地方就是下面将要讲到的消息处理函数。
4、 消息处理函数(window procedure)
先说明一点,对于window procedure 很多书译做:窗口过程或窗口函数,我觉得这样的直译没说明任何问题(不过似乎已经成了标准译名),所以我用了一个更达意的名字——消息处理函数。
前面讲到的3个部分,应该说都是规程化的,是一个程序中最没有创意的部分,而将要讲到的消息处理函数则给您提供了一个展现才华的舞台。
这里将处理您的应用程序所接受到的所有消息,如何让应用程序更强大、更友好都取决于消息处理函数的编写。这里是应用程序中最繁忙的地方,也是整个框架的核心部分。
结合C语言的语法特性,我们必然的使用Switch…Case处理不同种类的消息(其实可以应用更好的模式),例如:
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE: /* WM_CREATE以及后面的WM_PAINT等是消息的常量标识符*/
return 0 ;
case WM_PAINT:/*这一段显示了一句话Hello, Windows 98!更细致的叙述请查阅MSDN*/
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;/* 这一句发送一个“退出消息”到消息队列中,这个消息使得GetMessage函数的返回值为0 */
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
给消息处理函数传递消息,函数内部通过Switch…Case结构在对应的Case块中做相应的动作(应答),如果在Switch…Case结构中没有对应消息的处理块或者已经处理完消息而跳出了处理块,那么该消息会被传递给DefWindowProc函数,这个函数被称为默认消息处理函数(default window procedure),它是由操作系统提供的,这个函数做了许多善后的工作,您当然可以不调用这个函数以屏蔽掉操作系统对您工作的干预,但前提是您得清楚自己在干什么,因为通常DefWindowProc函数做了许多非常有用的事情,与您的应用程序的稳定性是相关的。
操作系统定义的消息有很多(您还可以定义自己的消息),它们大都是在特定事件发生的情况下由操作系统传递给应用程序的,这里仅就这段代码讲述几个常见消息。
WM_CREATE:当应用程序建立一个窗口时,消息处理函数就会收到这个消息,您可以在这里做一些初始化的工作。
WM_PAINT:这是一个非常重要的消息,一般来说也是一个非常忙碌的消息,只要窗口的显示区域需要改变时,都要处理这个消息,一些漂亮的应用程序界面都是在这个消息的处理重画出来的。最大化、最小化、改变窗口的大小等也都会传递这个消息,所以这个消息的处理通常都要考虑到时间效率的问题。
WM_DESTROY:当销毁一个窗口时,消息处理函数会接受到这条消息,一般我们就是简单的调用PostQuitMessage函数,这个函数会将WM_QUIT消息放到消息队列中,并且将调用时的参数填充到这个消息的wParam字段中。
正如前面所讲到的,DispatchMessage函数将消息传回给操作系统,操作系统以接受到的消息为参数调用消息处理函数,那么操作系统是如何得知消息处理函数的存在的呢?其实这在注册窗口类时,我们所填入的WNDCLASS结构中已经将这个函数和窗口类关联上了,也就是告知了操作系统消息处理函数的名称(后面还会讲到,但也请查询MSDN)。
5、 一个完整的HELLO WORLD程序和其它细节
下面是一段很简单但很冗长的代码,它摘自Programming Windows程式开发设计指南,您应该看到编写一个Windows应用程序是一件多么繁杂的事情,而如果您使用过那些RAD工具,那么您将对比出这些工具的伟大之处。
您应该结合代码后面的解释弄懂整个流程。
请一定坚持!
HELLOWIN.C
/*------------------------------------------------------------------------
HELLOWIN.C -- Displays "Hello, Windows 98!" in client area
(c) Charles Petzold, 1998
-----------------------------------------------------------------------*/
1:#include <windows.h>
2:LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
3:int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
4: LPSTR szCmdLine, int iCmdShow)
{
5: static TCHAR szAppName[] = TEXT ("HelloWin") ;
6: HWND hwnd ;
7: MSG msg ;
8: WNDCLAS wndclass ;
9: wndclass.style = CS_HREDRAW | CS_VREDRAW ;
10:wndclass.lpfnWndProc = WndProc ;
11:wndclass.cbClsExtra = 0 ;
12:wndclass.cbWndExtra = 0 ;
13:wndclass.hInstance = hInstance ;
14:wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
15:wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
16:wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
17:wndclass.lpszMenuNam = NULL ;
18:wndclass.lpszClassName = szAppName ;
19:if (!RegisterClass (&wndclass))
{
20:MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
21: szAppName, MB_ICONERROR) ;
22: return 0 ;
}
23:hwnd = CreateWindow( szAppName, // window class name
24: TEXT ("The Hello Program"), // window caption
25: WS_OVERLAPPEDWINDOW, // window style
26: CW_USEDEFAULT, // initial x position
27: CW_USEDEFAULT, // initial y position
28: CW_USEDEFAULT, // initial x size
29: CW_USEDEFAULT, // initial y size
30: NULL, // parent window handle
31: NULL, // window menu handle
32: hInstance, // program instance handle
33: NULL) ; // creation parameters
34:ShowWindow (hwnd, iCmdShow) ;
35:UpdateWindow (hwnd) ;
36:while (GetMessage (&msg, NULL, 0, 0))
{
37: TranslateMessage (&msg) ;
38: DispatchMessage (&msg) ;
}
39:return msg.wParam ;
}
40:LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM 41:lParam)
{
42: HDC hdc ;
43: PAINTSTRUCT ps ;
44: RECT rect ;
45: switch (message)
{
46: case WM_CREATE:
47: return 0 ;
48: case WM_PAINT:
49: hdc = BeginPaint (hwnd, &ps) ;
50: GetClientRect (hwnd, &rect) ;
51: DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
52: DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
53: EndPaint (hwnd, &ps) ;
54: return 0 ;
55: case WM_DESTROY:
56: PostQuitMessage (0) ;
57: return 0 ;
}
58: return DefWindowProc (hwnd, message, wParam, lParam) ;
}
虽然,整个程序的几大部分要点我已经在前面讲述过,但仍有许多您要学习的地方,我们从第1行开始讲解。
第1行:这里包含了Windows.h头文件,这里面定义或声明了后续程序中用到的常量、类型和Win32Api函数等,实际上,在它内部还包含了其它的关键头文件(比如后面提到的WINDEF.h文件)。
第2行:本行预声明了消息处理函数。返回类型LRESULT CALLBACK指明这是一个由操作系统(准确的说是动态链接库,由CALLBACK标识,很多书上称其为回调函数)调用的,返回消息处理结果(由LRESULT标识)的函数。实际上通过WINDEF.h文件中的两条语句:
#define CALLBACK __stdcall
typedef LONG LRESULT;
LRESULT CALLBACK最终被转换成:LONG __stdcall, __stdcall代表标准调用约定,详情请查阅MSDN。
第3、4行:这里是程序入口点,正如DOS下C程序的Main函数,一个Windows应用程序以一个WinMain函数开始,WINAPI在WINDEF.h文件中定义如下:
#define WINAPI __stdcall
操作系统传递调用WinMain函数并且传递给函数4项参数:
hInstance 标明正在运行的应用程序实例句柄。
hPrevInstance 标明以前运行的该应用程序(现在仍在运行)的实例句柄,这个参数在现在的32位操作系统中始终被置为NULL,这一点和Win32的多任务处理有关,这个参数只是在16位的Windows操作系统中是有用处的,保留它只是为了兼容。
szCmdLine 指明应用程序的参数列表,LPSTR是一个字符串指针类型。
iCmdShow 标明应用程序初始时是如何显示的,比如全屏或是最小化。
第5-8行:这几行声明了前面提到的几个重要的变量,句柄、消息、窗口类等
第9-18行:这里我们填写了窗口类结构的各项字段,比如应用程序的图标、鼠标图案、菜单等,也就是说我们定制了想显示的窗口的样子。请注意第10行:
10:wndclass.lpfnWndProc = WndProc ;
就是这一句将窗口类和消息处理函数关联起来。
第19-22行:我们通过一个条件语句,判断是否成功的注册了窗口类,如果不成功将给出一个错误信息。MessageBox函数负责显示一个消息框。
第23-33行:我们调用CreateWindow函数正式建立一个具体的窗口,这时会使得消息处理函数接收到WM_CREATE消息。正如您看到的,这个函数有一大串的参数,它们大都是对这个窗口的描述,出于我的懒惰,详情仍然请您查阅MSDN。
第34-35行:建立一个窗口并不代表将它显示出来,我们需要这两个函数将它显示出来。
第36-38行:消息循环,前面已经讲到过。
第39行:这是主函数的最后一行,意思很明显,将消息循环的最后一个消息做为返回值(这个值通常是0),然后结束程序。
第40-58行:消息处理过程,前面已经讲过。
我想讲述的就是这些了,但仍有些要点请您特别注意:
1、一个窗口类与一个消息处理函数一一对应。有几个窗口类就有几个消息处理函数。
2、一个窗口类可以与多个具体窗口对应,同时这多个具体窗口的消息处理都由一个消息处理函数负责,也即是一个消息处理函数可以与多个具体窗口对应(消息处理函数需要以具体窗口的句柄为参数,它就依此来判定应处理哪个具体窗口的消息)。
3、一个应用程序(准确的说是线程)对应一个消息循环,那么一个消息循环则可对应这个应用程序中的所有窗口类、消息处理函数。这也说明了另一个问题——消息队列也和应用程序一一对应。
4、消息处理函数的指针是在注册窗口类时就已经传递给操作系统了,对于操作系统来说这时建立了消息处理过程,即,还未建立具体窗口时,就可以处理窗口的消息了,事实上,这时唯一能处理的消息就只有 WM_CREATE,所以好似消息处理函数是随着窗口的建立而建立的。
本文只讲述了Windows应用程序的一般性框架,对于Windows应用程序设计这只是沧海一粟,纵使是对于这个框架本身,也有许多细节没有讲述,所以还有许多东西值得您去学习,正如我在文中反覆提到的:“请查阅msdn”,真的,请查阅msdn,本文只能给您提供一份不完善的索引,便于您进一步的学习。
希望这篇文章能让您对Windows应用程序有一个更深刻的认识,谢谢并祝好运!
参考资料:
[1]Charles Petzold著,余孟学(台湾)译. Programming Windows程式开发设计指南.第五版.2000
[2]Microsoft Developer Network.
Yves
2003年10月1日,国庆节
于郑州