windows编程 第二回 windows程序的生与死(上)
-----路过的朋友,若发现错误或有好的建议,欢迎在下面留言,谢谢!-----
引子
“Windows 程序分为‘程序代码’和‘UI(User Interface)资源’两大部份,两部份最后以RC编译器(资源编译器)整合为一个完整的EXE 文件。所谓UI 资源是指功能菜单、对话框外貌、程序图标、光标形状等等东西。这些UI 资源的实际内容(二进制代码)系借助各种工具产生,并以各种扩展名存在,如.ico、.bmp、.cur 等等。程序员必须在一个所谓的资源描述档(.rc)中描述它们。RC 编译器读取RC 档的描述后将所有UI资源档集中制作出一个.RES 档,再与程序代码结合在一起,这才是一个完整的Windows可执行件。”
以上是侯捷先生在《深入浅出MFC》中关于windows程序开发流程的一段论述,我觉得他说的很是精辟,简明扼要的说清楚了Windows程序的两方面,特放在此与大家分享。下文中我还还要引用他的这本书中的内容。
我的忠告
说到windows编程,我认为首先要先了解清楚windows程序由生到死的整个机制和过程。当你对微软定的这个“游戏规则”了然于胸后,我想后面的学习将会对你来说很轻松。所以,请你认真看懂这一回的内容。
这两回我要交代的东西还真是不少,既要讲解windows程序机制又要引进windows中很多概念。在此我很欣赏王爽老师的教学观点——知识屏蔽。有关概念等到用的时候我再解释,不用的我先不提,很多书开始都是先一股脑的把很多概念都抛出来,让读者看的迷迷糊糊的,反而增加了读者的畏惧心理。我认为一边用一边在介绍概念反而会让读者更好理解一点。(至少对于我来说是这样)。
我还要再强调一下,这两回的的主要内容是讲解运行机制,我打算以一边讲机制一边读程序的方式来进行。考虑到大家对很多概念和很多函数都是第一次接触,所以我只是以“写意”形式来搞。只要你对这机制和过程能宏观和整体上掌握了解,那你就是成功的。遇到概念我会简单介绍(小字内容是对概念的扩展,有余力的读者可以看看),看了不理解,这很正常,请继续看下去,从下面我对概念的运用上你再慢慢体会这概念的含义,这个概念理解过程正如我们小的时候学说话,听得多了自然就懂了(现在不必一时纠结于此)。很多函数我只说它的作用,具体用法和为什么这么用请你在这一回不要操心(函数用法我以后会讲解,以后你看得多了,闭着眼都能写出来,不必急于一时),以防分心从而破坏了对了解windows机制和程序有生到死过程的连贯性,请记住我们的目的 ——对windows机制和程序有生到死过程宏观和整体了解,其他是浮云,哈哈。
先来上一段代码——请别害怕
1. #include <windows.h> 2. LRESULT CALLBACK WinSunProc( 3. HWND hwnd, // handle to window 4. UINT uMsg, // message identifier 5. WPARAM wParam, // first message parameter 6. LPARAM lParam // second message parameter 7. ); 8. int WINAPI WinMain( 9. HINSTANCE hInstance, // handle to current instance 10. HINSTANCE hPrevInstance, // handle to previous instance 11. LPSTR lpCmdLine, // command line 12. int nCmdShow // show state 13. ) 14. { 15. WNDCLASS wndcls; //定义一个wndcls窗口类对象 16. wndcls.cbClsExtra=0; //类变量占用的存储空间 17. wndcls.cbWndExtra=0; //实例变量占用的存储空间 18. wndcls.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);//指定窗口类画刷句柄 19. wndcls.hCursor=LoadCursor(NULL,IDC_ARROW); //指定窗口类光标句柄 20. wndcls.hIcon=LoadIcon(NULL,IDI_APPLICATION); //指定窗口类图标句柄 21. wndcls.hInstance=hInstance; //包含窗口过程的程序的实例句柄 22. wndcls.lpfnWndProc=WinSunProc; //指向窗口过程函数 23. wndcls.lpszClassName="Hello World"; //指定窗口类名字 24. wndcls.lpszMenuName=NULL; //指定菜单资源名字 25. wndcls.style=CS_HREDRAW | CS_VREDRAW; //指定窗口类型样式 26. RegisterClass(&wndcls); //注册窗口 27. HWND hwnd; //定义句柄变量 28. hwnd=CreateWindow //创建窗口,返回系统为窗口分配的句柄 29. ("Hello World", //类名,指定该窗口所属的类 30. "Hello World Program", //窗口的名字,即在标题栏中显示的文本 31. WS_OVERLAPPEDWINDOW, //该窗口的风格 32. 0, //窗口左上角相对于屏幕左上角的初始X坐标 33. 0, //窗口左上角相对于屏幕左上角的初始Y坐标 34. 600, //窗口的宽度 35. 400, //窗口的高度 36. NULL, //一个子窗口的父窗口的句柄,或隶属窗口的拥有者窗口的句柄 37. NULL, //菜单句柄 38. hInstance, //创建窗口对象的应用程序的实例句柄 39. NULL); //创建窗口时指定的额外参数 40. ShowWindow(hwnd,SW_SHOWNORMAL); 显示窗口 41. UpdateWindow(hwnd);更新窗口 42. MSG msg; 43. while(GetMessage(&msg,NULL,0,0)) 44. { 45. TranslateMessage(&msg); 46. DispatchMessage(&msg); 47. } 48. return msg.wParam; 49. } 50. LRESULT CALLBACK WinSunProc( 51. HWND hwnd, // handle to window 52. UINT uMsg, // message identifier 53. WPARAM wParam, // first message parameter 54. LPARAM lParam // second message parameter 55. ) 56. { 57. switch(uMsg) 58. { 59. case WM_CREATE: 60. MessageBox(hwnd,"Window Created","message",MB_OK); 61. break; 62. case WM_PAINT: 63. HDC hDC; 64. PAINTSTRUCT ps; 65. hDC=BeginPaint(hwnd,&ps); 66. TextOut(hDC,0,0,"Hello World!",strlen("Hello World!")); 67. EndPaint(hwnd,&ps); 68. break; 69. case WM_DESTROY: 70. PostQuitMessage(0); 71. break; 72. default: 73. return DefWindowProc(hwnd,uMsg,wParam,lParam); 74. } 75. return 0; 76. }
在Windows XP环境下打开VC6.0(虽然有点老了,但对我们来说功能够用了),从File菜单中选择New,在Nem对话框中,单击Project标签,选择Win32 Application。输入Project Name与Location后点确定。在下一个出现的对话框选Empty Workspace,再按下Finish。进入项目后,再新建一个C++ Sourse File,将以上代码贴上后,点击BuildExecute按钮后将出现一个提示框(如图),点击确定后,会出现一个窗口(如图)。
概念解释:
窗口是屏幕上与一个应用程序相关的矩形区域,它是用户与产生该窗口的应用程序之间的可视界面,他接收用户的输入,并以文本或图形的格式显示输出内容。对应用程序来说,窗口是应用程序控制下的屏幕上的一个矩形区 域,应用程序创建并控制窗口的所有方面。当用户启动一个应用程序时,一个窗口就被创建。每当用户操作窗口中的对象时,程序就有所响应。
它一般由边框、标题栏、控制栏最小化图标、最大化图标、关闭图标水平滚动条、垂直滚动条、菜单栏和客户区构成。①
如常见的记事本就是一个很好的例子:
Windows程序运行的机制——一消息为基础,事件驱动之(引用侯捷老师《深入浅出MFC》)
概念解释:
事件与消息
事件由用户(操作电脑的人)触发且只能由用户触发(如用户按下了鼠标按钮,就产生一鼠标事件),操作系统能够感觉到由用户触发的事件,并将此事件转换为一个(特定的)消息发送到程序的消息队列中。②
消息队列
消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。(来自百度百科)
Windows 程序的进行系依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个while 回路),等待任何可能的输入,然后做判断,然后再做适当的处理。上述的“输入”是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中。操作系统如何捕捉外围设备(如键盘和鼠标)所发生的事件呢?噢,USER 模块③(不懂先不管它)掌管各个外围的驱动程序,它们各有侦测回路。
如果把应用程序获得的各种“输入”分类,可以分为由硬件装置所产生的消息(如鼠标移动或键盘被按下),放在系统消息队列(system message queue)中,以及由Windows 系统或其它Windows 程序传送过来的消息,放在程序消息队列(application message queue)中。以应用程序的眼光来看,消息就是消息,来自哪里或放在哪里其实并没有太大区别,反正程序调用GetMessage API 就取得一个消息,程序的生命靠它来推动。所有的GUI(Graphical User Interface,图形用户界面) 系统,包括UNIX的X Window 以及OS/2 的Presentation Manager,都像这样,是以消息为基础的事件驱动系统。
可想而知,每一个Windows 程序都应该有一个回路如下(先熟悉一下即可):
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 以上出现的函数都是Windows API 函数
仅了解这个while循环有可以不断处理消息的作用即可。
Windows程序生与死——开始分析代码了!
不要被上面的代码行数吓坏了,对就是这么多行才产生一个窗口,没办法微软就是这么规定的,这个简简单单的窗口可以说是麻雀虽小,五脏俱全。但是几乎所有的windows程序都是有它为骨架扩展而来的,弄懂它你就会了一半的windows基本编程,剩下的工作顶多就是学学函数,填充一下它罢了。
一个程序的一生要经历出生、运行、死亡三个阶段,且听我慢慢道来……
出生:
正如在C程序中的进入点是函数main一样,Windows程序的进入点是WinMain,总是像这样出现:见行8 (再强调一下,只讲函数作用,具体用法参数含义后面讲)
引用孙鑫老师的精彩讲解:
“创建一个完整的窗口,需要经过下面几个操作步骤:
设计一个窗口类;
注册窗口类;
创建窗口;
显示及更新窗口。
(请先记住这个顺序,先不要问为什么这样)
1.设计一个窗口类
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状)、图标、背景色等。窗口的创建过程类似于汽车的制造过程。我们在生产一个型号的汽车之前,首先要对该型号的汽车进行设计,在图纸上画出汽车的结构图,设计各个零部件,同时还要给该型号的汽车取一个响亮的名字,例如“奥迪A6”。在完成设计后,就可以按照“奥迪A6”这个型号生产汽车了。
类似地,在创建一个窗口前,也必须对该类型的窗口进行设计,指定窗口的特征。当
然,在我们设计一个窗口时,不像汽车的设计这么复杂,因为Windows 已经为我们定义好
了一个窗口所应具有的基本属性,我们只需要像考试时做填空题一样,将需要我们填充的部分填写完整,一种窗口就设计好了。(精彩的比喻)”
行15-行25:定义一个wndcls窗口类对象,并填充它。见代码注释(遇到新概念或看不懂先跳过,以后详细讲,下同)
“2.注册窗口类
在设计完汽车后,需要报经国家有关部门审批,批准后才能生产这种类型的汽车。同
样地,设计完窗口类(WNDCLASS)后,需要调用RegisterClass 函数对其进行注册,注
册成功后,才可以创建该类型的窗口。”
行26:注册窗口
“3.创建窗口——步骤3
设计好窗口类并且将其成功注册之后,就可以用CreateWindow 函数产生这种类型的
窗口了。”
行27-行39:创建窗口(遇到新概念或看不懂先跳过,以后详细讲)
“4.显示及更新窗口
(1)显示窗口
窗口创建之后,我们要让它显示出来,这就跟汽车生产出来后要推向市场一样。调用
函数ShowWindow 来显示窗口。
(2)更新窗口
在调用 ShowWindow 函数之后,我们紧接着调用UpdateWindow 来刷新窗口,就好像
我们买了新房子,需要装修一下。”
行41、41:显示及更新窗口
运行:
请回顾上回“windows程序运行机制——一消息为基础,以事件驱动之”,程序由一系列复杂的过程创建出来显示在屏幕上后,程序就时刻在等待消息,然后处理消息,这就是它“活着”。
程序是如何等待消息的呢,又是怎样处理的呢?请看代码,我们就来到了while消息循环
见行43-49 (只大致知道函数作用即可,现在不必深究其原理和用法)
函数GetMessage可以从消息队列中检索出与此应用程序窗口有关连的消息(一般情况,函数返回非零值让循环一直进行,什么时候函数返回零,消息循环结束,我们后面讲),并将消息的具体内容存于具有MSG类型的一个变量中(即msg,MSG类型后面讲),然后交由函数TranslateMessage对该消息进行翻译(翻译后面讲),紧接着,函数DispatchMessage将消息发送到适当的“对象”处理。
while消息循环如此往复进行,程序就能“活着”,不断进行等待消息、处理消息。
关于这个神秘的“对象”叫做窗口过程,它是一个函数(即行50-76,行2-7是此函数的声明),用来处理消息。你仔细观察你会发现这个函数的主体就是switch选择语句,那他选择的WM_CREATE,WM_PAINT,WM_DESTROY这又是什么呢,或许你已经猜到了,这些就是要特意处理的消息,如果传进来的消息没有响应它的case 语段,那就一律交由default语段处理(看来它是必不可少的)。仿佛windows一切神秘的面纱已经渐渐地被我们掀开了!一个windows程序原来是这样这样的:它首先要被创建并显示在桌面上,这要经过设计一个窗口类、注册窗口类、创建窗口、显示及更新窗口等过程,你想要它有什么功能,就要在窗口过程函数switch选择语句中特意加一case语段来处理与你这功能相关的消息,对于不相关的消息一率交由default语段处理。可以这样说很多应用程序之所以不同,区别就在于其窗口过程的不同。至于while消息循环,这是固定的写法,我们只要这么写,具体消息的路由都是windows系统自动完成的,我们不用操心。
死亡:
讲到此或许读者应该可以推断出一点有关程序死亡的一点信息了:窗口程序要 “死亡”肯定是要窗口函数收到某种消息并响应,让窗口销毁,同时还要结束while消息循环。(就先了解到这儿,下回再深入)
这次就先讲到这了,希望读者对windows程序的“一生”和它的运行机制有那么一点印象,能以宏观的角度看待它,那我就能感到心满意足了。这一回大家主要是理解,下一回我将详细讲代码中函数的具体用法和相关细节,需要大家多记忆。好了,大家后会有期。
① 说一下大家可能不熟悉的:
控制框是每个窗口左上方的小图片,每个应用程序都使用它。在控制图标上单击鼠标键会使Windows显示系统菜单。系统菜单它提供了诸如还原、移动、大小、最小化、最大化以及关闭这样的标准操作。
通常客户区占据了窗口最大的部分(即记事本中可以写字的区域)。这是应用程序的基本输出区域。应当由应用程序来复杂管理客户区。另外,应用程序可以输出到客户区。
② 补充:
我们通常说:“某一件事发生了”和“向什么发送某一个消息”。比如在桌面上单击鼠标时,某一件事发生了,Windows首先知道这件事的发生,然后使用函数SendMessage向桌面发送一个消息,证明有某件事发生了。这就是“事件驱动、消息处理”的原理。
事件是一个动作——用户触发的动作。
消息是一个信息——传递给系统的信息。这里强调的是:
可以说“用户触发了一个事件”,而不能说“用户触发了一个消息”。
用户只能触发事件,而事件只能由用户触发。
一个事件产生后,将被操作系统转换为一个消息,所以一个消息可能是由一个事件转换而来(或者由操作系统产生)。
一个消息可能会产生另一个消息,但一个消息决不能产生一个事件——时间只能由用户触发。
总结(事件:消息的来源)
事件:只能由用户通过外设的输入产生。
消息:(产生消息的来源有三个)
(1) 由操作系统产生。
(2) 由用户触发的事件转换而来。
(3) 由另一个消息产生。
以上援引自《事件与消息的区别》
Windows的消息可分为四种类型:
(1)输入消息:对键盘和鼠标输入作反应。这类输入消息首先放在系统消息队列中,然后Windows将它们送入应用程序的消息队列,使消息得到处理。
(2)控制消息:用来与Windows的特殊控制对象,例如,对话框、列表框、按钮等进行双向通信。这类消息一般不通过应用程序的消息队列,而是直接发送到控制对象上。
(3)系统消息:对程式化的事件或系统时钟中断作出反应。有些系统消息,例如大部分DDE消息(程序间进行动态数据交换时所使用的消息)要通过Windows的系统消息队列。而有些系统消息,例如窗口的创建及删除等消息直接送入应用程序的消息队列。
(4)用户消息:这些消息是程序员创建的,通常,这些消息只从应用程序的某一部分进入到该应用程序的另一部分而被处理,不会离开应用程序。用户消息经常用来处理选单操作:一个用户消息与选单中的一选项相对应,当它在应用程序队列中出现时被处理。
③ 模块
在Windows中,术语“模块”一般是指任何能被装入内存中运行的可执行代码和数据的集合。更明确地讲,模块指的就是一个.EXE文件(又称为应用程序模块),或一个动态链接库(DLL — Dynamic Linking Library,又被称为动态链接库模块或DLL模块),或一个设备驱动程序,也可能是一个程序包含的能被另一个程序存取的数据资源。模块一词也被用于特指自包含的一段程序。例如,一个可单独编译的源文件,或该源文件被编译器处理之后所生成的目标程序。当制作一个程序时,模块一词用于指被连接在一起的许多模块中的某个模块。
Windows本身由几个相关的模块组成,Windows API函数就是在Windows启动时装入内存中的几个动态链接库模块实现的。其中的三个主要模块是USER.EXE(用于窗口管理等)、KERNEL.EXE(用于内存管理的多任务调度)和GDI.EXE(图形设备接口,用于图形输出等)。
注:如未另注明,概念解释均来自《Windows编程基础 --概述》一文。
文中很多内容都引自《Windows编程基础 --概述》《深入浅出MFC》《VC++深入详解》,在此特向这三位作者致敬!