注:以下摘自侯捷老师《深入浅出MFC》部分内容,有删节。原文基于VC5.0,部分之处陈旧但不影响整体。
Windows程序简述
Windows 程序分为「程序代码」和「UI(User Interface)资源」两大部份,两部份最后以连接器整合为一个完整的EXE 文件。所谓UI 资
源是指功能菜单、对话框外貌、程序图标、光标形状等等东西。这些UI 资源的实际内容(二进制代码)系借助各种工具产生,并以各种扩展名
存在,如.ico、.bmp、.cur 等等。程序员必须在一个所谓的资源描述档(.rc)中描述它们。RC 编译器(RC.EXE)读取RC 档的描述后将所
有UI资源档集中制作出一个.RES 档,再与程序代码结合在一起,这才是一个完整的Windows可执行档。
(.LIB)
众所周知Windows 支持动态联结。换句话说,应用程序所调用的Windows API 函数是在「执行时期」才联结上的。那么,「联结时期」所
需的函数库做什么用?有哪些?并不是延伸档名为.dll 者才是动态联结函数库(DLL,Dynamic Link Library),事实
上.exe、.dll、.fon、.mod、.drv、.ocx 都是所谓的动态联结函数库。Windows 程序调用的函数可分为C Runtimes 以及Windows API
两大部分。
以下是它们的命名规则与使用时机:
■ LIBC.LIB - 这是C Runtime 函数库的静态联结版本。
■ MSVCRT.LIB - 这是C Runtime 函数库动态联结版本(MSVCRT40.DLL)的
import 函数库。如果联结此一函数库,你的程序执行时必须有MSVCRT40.DLL在场。
另一组函数,Windows API,由操作系统本身(主要是Windows 三大模块GDI32.DLL 和USER32.DLL 和KERNEL32.DLL)提供。虽说动
态联结是在执行时期才发生「联结」事实,但在联结时期,联结器仍需先为调用者(应用程序本身)准备一些适当的信息,才能够在执行时期
顺利「跳」到DLL 执行。如果该API 所属之函数库尚未加载,系统也才因此知道要先行加载该函数库。这些适当的信息放在所谓的「import
函数库」中。32 位Windows 的三大模块所对应的import 函数库分别为GDI32.LIB 和USER32.LIB和KERNEL32.LIB。
Windows 发展至今,逐渐加上的一些新的API 函数(例如Common Dialog、ToolHelp)并不放在GDI 和USER 和KERNEL 三大模块中,
而是放在诸如COMMDLG.DLL、TOOLHELP.DLL 之中。如果要使用这些APIs,联结时还得加上这些DLLs 所对应的import 函数库,诸如
COMDLG32.LIB 和TH32.LIB。
可参考:
2.关于形如--error LNK2005: xxx 已经在 msvcrtd.lib ( MSVCR90D.dll ) 中定义--的问题分析解决
(.H)
所有Windows 程序都必须包含WINDOWS.H。除非你十分清楚什么API 动作需要什么头文件,否则为求便利,单单一个WINDOWS.H
也就是了。不过,WINDOWS.H 只照顾三大模块所提供的API 函数,如果你用到其它system DLLs,例如COMMDLG.DLL 或MAPI.DLL 或
TAPI.DLL 等等,就得包含对应的头文件,例如COMMDLG.H 或MAPI.H 或TAPI.H 等等。
事件为驱动,消息为基础
Windows 程序的进行系依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个while 回路),等待任何可能的输入,然后做判
断,然后再做适当的处理。上述的「输入」是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中。操作系统如何捕捉外围设
备(如键盘和鼠标)所发生的事件呢?噢,USER 模块掌管各个外围的驱动程序,它们各有侦测回路。如果把应用程序获得的各种「输入」分
类,可以分为由硬件装置所产生的消息(如鼠标移动或键盘被按下),放在系统队列(system queue)中,以及由Windows 系统或其它
Windows 程序传送过来的消息,放在程序队列(application queue)中。以应用程序的眼光来看,消息就是消息,来自哪里或放在哪里其
实并没有太大区别,反正程序调用GetMessage API 就取得一个消息,程序的生命靠它来推动。所有的GUI 系统,包括UNIX的X Window
以及OS/2 的Presentation Manager,都像这样,是以消息为基础的事件驱动系统。
图 :Windows程序本体与操作系统的关系
可想而知,每一个Windows 程序都应该有一个回路如下:
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 以上出现的函数都是Windows API 函数
消息,也就是上面出现的MSG 结构,其实是Windows 内定的一种资料格式:
/* Queued message structure */
typedef struct tagMSG
{
HWND hwnd;
UINT message; // WM_xxx,例如WM_MOUSEMOVE,WM_SIZE...
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
窗口过程函数
接受并处理消息的主角就是窗口。每一个窗口都应该有一个函数负责处理消息,程序员必须负责设计这个所谓的「窗口函数」(window
procedure,或称为window function)。如果窗口获得一个消息,这个窗口函数必须判断消息的类别,决定处理的方式。
以上就是Windows 程序设计最重要的观念。至于窗口的产生与显示,十分简单,有专门的API 函数负责。稍后我们就会看到Windows 程序
如何把这消息的取得、分派、处理动作表现出来。
程序进入点WinMain
main 是一般C 程序的进入点:
int main(int argc, char *argv[ ], char *envp[ ]);
{
...
}
WinMain 则是Windows 程序的进入点:
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
...
}
在Win32 中CALLBACK 被定义为__stdcall,是一种函数调用习惯,关系到参数挤压到堆栈的次序,以及处理堆栈的责任归属。其它的函数调
用习惯还有 _pascal 和_cdecl。
可参考:
当Windows 的「外壳」(shell)侦测到使用者意欲执行一个Windows 程序,于是调用加载器把该程序加载,然后调用C startup code,
后者再调用WinMain,开始执进程序。WinMain 的四个参数由操作系统传递进来。
窗口类别之注册与窗口之诞生
一开始,Windows 程序必须做些初始化工作,为的是产生应用程序的工作舞台:窗口。这没有什么困难,因为API 函数CreateWindow 完
全包办了整个巨大的工程。但是窗口产生之前,其属性必须先设定好。所谓属性包括窗口的「外貌」和「行为」,一个窗口的边框、颜色、标
题、位置等等就是其外貌,而窗口接收消息后的反应就是其行为(具体地说就是指窗口函数本身)。程序必须在产生窗口之前先利用API 函数
RegisterClass设定属性(我们称此动作为注册窗口类别)。RegisterClass 需要一个大型数据结构WNDCLASS 做为参数,
CreateWindow 则另需要11 个参数。
图:RegisterClass与CreateWindow
初始化工作完成后,WinMain 进入所谓的消息循环:
while (GetMessage(&msg,...)) {
TranslateMessage(&msg); // 转换键盘消息
DispatchMessage(&msg); // 分派消息
}
其中的TranslateMessage 是为了将键盘消息转化,DispatchMessage 会将消息传给窗口函数去处理。没有指定函数名称,却可以将消息传
送过去,岂不是很玄?这是因为消息发生之时,操作系统已根据当时状态,为它标明了所属窗口,而窗口所属之窗口类别又已经明白标示了窗
口函数(也就是wc.lpfnWndProc 所指定的函数),所以DispatchMessage自有脉络可寻。DispatchMessage 经过USER 模块的协助,才
把消息交到窗口函数手中。
消息循环中的DispatchMessage 把消息分配到哪里呢?它透过USER 模块的协助,送到该窗口的窗口函数去了。窗口函数通常利用
switch/case 方式判断消息种类,以决定处置方式。由于它是被Windows 系统所调用的(我们并没有在应用程序任何地方调用此函数),所
以这是一种call back 函数,意思是指「在你的程序中,被Windows 系统调用」的函数。这些函数虽然由你设计,但是永远不会也不该被你调
用,它们是为Windows 系统准备的。
程序进行过程中,消息由输入装置,经由消息循环的抓取,源源传送给窗口并进而送到窗口函数去。窗口函数的体积可能很庞大,也可能很精
简,依该窗口感兴趣的消息数量多寡而定。
至于窗口函数的形式,相当一致,必然是:
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
注意,不论什么消息,都必须被处理,所以switch/case 指令中的default: 处必须调用DefWindowProc,这是Windows 内部预设的消息处
理函数。
对话框
Windows 的对话框依其与父窗口的关系,分为两类:
1. 「令其父窗口除能,直到对话框结束」,这种称为modal 对话框。
2. 「父窗口与对话框共同运行」,这种称为modeless 对话框。比较常用的是modal 对话框。
为了做出一个对话框,程序员必须准备两样东西:
1. 对话框模板(dialog template)。这是在RC 文件中定义的一个对话框外貌,以各种方式决定对话框的大小、字形、内部有哪些控制组
件、各在什么位置...等等。
2. 对话框函数(dialog procedure)。其类型非常类似窗口函数,但是它通常只处理WM_INITDIALOG 和WM_COMMAND 两个消息。对
话框中的各个控制组件也都是小小窗口,各有自己的窗口函数,它们以消息与其管理者(父窗口,也就是对话框)沟通。而所有的控制组件传
来的消息都是WM_COMMAND,再由其参数分辨哪一种控制组件以及哪一种通告(notification)。Modal 对话框的激活与结束,靠的是
DialogBox 和EndDialog 两个API 函数。
图:对话框的诞生、运行与结束
RC文件
RC 文件是一个以文字描述资源的地方。常用的资源有如下:ICON、CURSOR、BITMAP、FONT、DIALOG、MENU、TOOLBAR、
ACCELERATOR、STRING、VERSIONINFO。还可能有新的资源不断加入。这些文字描述需经过RC 编译器,才产生可使用的二进制代码。
总结窗口的生命周期:
1. 程序初始化过程中调用CreateWindow,为程序建立了一个窗口,做为程序的萤幕舞台。CreateWindow 产生窗口之后会送出
WM_CREATE 直接给窗口函数,后者于是可以在此时机做些初始化动作(例如配置内存、开文件、读初始资料...)。
2. 程序活着的过程中,不断以GetMessage 从消息贮列中抓取消息。如果这个消息是WM_QUIT,GetMessage 会传回0 而结束while 循
环,进而结束整个程序。
3. DispatchMessage 透过Windows USER 模块的协助与监督,把消息分派至窗口函数。消息将在该处被判别并处理。
4. 程序不断进行2. 和3. 的动作。
5. 当使用者按下系统菜单中的Close 命令项,系统送出WM_CLOSE。通常程序的窗口函数不栏截此消息,于是DefWindowProc 处理它。
6. DefWindowProc 收到WM_CLOSE 后, 调用DestroyWindow 把窗口清除。DestroyWindow 本身又会送出WM_DESTROY。
7. 程序对WM_DESTROY 的标准反应是调用PostQuitMessage。
8. PostQuitMessage 没什么其它动作,就只送出WM_QUIT 消息,准备让消息循环中的GetMessage 取得,如步骤2,结束消息循环。
图:窗口的生命周期