Windows程序内部运行原理
为了理解Visual C++应用程序开发过程,先要理解Windows程序的运行机制。因为 Visual C++是 Windows 开发语言,需要明白在 Windows 环境下编程和在其它环境下编程的一些根本性的差别。全面地讨论 Windows 的内部工作机制将需要整整一本书的容量,没有必要深入了解所有的技术细节。但是对于Windows程序运行的一些根本性的概念,是一个Visual C++程序员所必须掌握的知识。
2.1 Windows应用程序,操作系统,计算机硬件之间的相互关系
WINDOWS程序设计是一种完全不同于传统的DOS方式的程序设计方法,它是一种事件驱动方式的程序设计模式,主要是基于消息的。当用户需要完成某种功能时会调用操作系统的某种支持,然后操作系统将用户的需要包装成消息,并投递到消息队列中,最后应用程序从消息队列中取走消息并进行响应。
我们这样解释上面的图例,向下的箭头1表示操作系统能够操纵输出设备,以执行特定的功能,如让声卡发出声音,让显卡画出图形。向上的箭头2表示操作系统能够感知输入设备状态的变化,如鼠标移动,键盘按下,并且能够知道鼠标移动的具体位置,键盘按下的哪个字符。这就是操作系统和计算机硬件之间的交互关系,应用程序开发者通常不需知道其具体实现细节。
2.1.1 关于API
向下的箭头3表示应用程序可以通知操作系统执行某个具体的动作,如操作系统能够控制声卡发出声音,但它并不知道何时发出何种声音,得由应用程序告诉操作系统该发出什么样的声音。这个关系好比有个机器人能够完成行走的功能,但是,如果人们不告诉它往哪个方向上走,机器人是不会主动行走的。这里的机器人就是操作系统,人们就是应用程序。
那么,应用程序是如何通知操作系统执行某个功能的呢?有过编程经验的读者都应该知道,在应用程序中要完成某个功能,都是以函数调用的形式实现的,同样,应用程序也是以函数调用的方式来通知操作系统执行相应的功能的。操作系统所能够完成的每一个特殊功能通常都有一个函数与其对应,也就是说,操作系统把它所能够完成的功能以函数的形式提供给应用程序使用,应用程序对这些函数的调用就叫做系统调用,这些函数的集合就是Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称Windows API。如CreateWindow就是一个API函数,应用程序中调用这个函数,操作系统就会按照该函数提供的参数信息产生一个相应的窗口。大家不妨看看EX02_00中的源程序,体会一下在程序中是如何调用这个CreateWindow API函数的,关于这个函数的详细解释,请参阅MSDN(微软开发编程的开发系统)。
顺便提一下,对于一个真正的程序员来说,不可能死记硬背每一个API函数及其各参数的详细信息。通常都是只记住其英文拼写,有时甚至是凭着语意拼读出来的,如显示窗口用ShowWindow,退出Windows操作系统用ExitWindows等等,API函数的正确拼写格式及各参数的祥尽信息都是在MSDN迅速检索到的,没必要刻意去死记这些信息,等用的次数多了,这些信息也就在不知不觉中掌握了,但一定要具备在需要的时候能够从帮助系统中检索想要的信息的能力,这样就能做到事半功倍。学习VC++,一定要有一套真实的练习环境,学会查阅帮助系统,决不能纸上谈兵,照着书本亦步亦趋,否则就真的是没有一两年的时间,是学不好VC++的了。
注意:请不要将这里的API与java API以及其他API混淆。API正如其语义一样,已成为一种被广泛使用的专业术语。如果某个系统或某个设备提供给某种应用程序对其进行编程操作的函数,类,组件等的集合,就称作该系统的API。曾经有学员问我这样的问题,Java API与windows API有何关系,是不是指java也可以调用windows里的API?读者现在应该明白这个问题了,不需我来回答了吧?
2.1.2 关于消息及消息队列
向上的箭头4表示操作系统能够将输入设备的变化上传给应用程序。如用户在某个程序活动时按了一下键盘,操作系统马上能够感知到这一事件,并且能够知道用户按下的是哪一个键,操作系统并不决定对这一事件如何作出反应,而是将这一事件转交给应用程序,由应用程序决定如何对这一事件作出反应。好比有个蚊子叮了我们一口,我们的神经末梢(相当于操作系统)马上感知到这个事件,并传递给了我们的大脑(相当于应用程序),我们的大脑最终决定如何对这一事件作出反应,如将蚊子赶走,或是将蚊子拍死。对事件作出反应的过程就是消息响应,由水平箭头5表示。
操作系统是怎样将感知到的事件传递给应用程序的呢?这是通过消息机制(Message)来实现的。操作系统将每个事件都包装成一个称为消息的结构体MSG来传递给应用程序的,参看MSDN,
MSG结构定义如下:
Code
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
看不懂这种定义的读者,请赶快复习
C语言,其基本意义是定义一个struct tagMSG的结构体,并在以后的应用中用MSG来代替struct tagMsg。该结构体中各成员变量的作用如下:
第一个成员变量hwnd即代表消息所属的窗口,一个消息一般都是与某个窗口相联系的,如在某个活动窗口中按下键盘,该键盘消息就是发给该窗口的,在VC中,用HWND变量类型来标识窗口。有关窗口的知识,在稍后有详细解释。
第二个成员变量message代表消息代号,无论是键盘按下,还是鼠标移动,都是用一个数字来表示的,不同的数值对应不同的消息。由于数值不便于记忆,在VC中将消息对应的数值定义为WM_xxx宏的形式,xxx对应某种消息的英文拼写的大写,如鼠标移动消息为WM_MOUSEMOVE,键盘按下消息为WM_KEYDOWN,输入一个字符消息为WM_CHAR等等。我们在程序中一般以WM_xxx宏的形式来使用消息。
提示:如果想知道WM_xxx消息对应的具体数值,请在程序中选中WM_xxx,单击右键,在弹出菜单中选择goto definition即可看到该宏的具体定义。跟踪,查看某个变量的定义,使用此方法非常有效。
第三个,四个成员变量分别为wParam、lParam,用于对消息进行补充说明,如message成员表示字符消息,但没有说明输入的是哪个字符,这就需要用其他变量对其进行补充说明。wParam,lParam代表的意义是随消息的不同而异。读者可用goto definition功能查看WPARAM、LPARAM的定义,发现它们分别为unsigned int和long,并不是什么神秘莫测的变量类型。VC++中之所以要这样做,是希望从变量定义的类型上,就能区分出变量的用途。对于同一种变量类型,可按其用途细分定义成多种其他的形式。这种概念在VC++中被广泛使用,也是导致初学者困惑的一个因素。
最后两个变量分别代表发出消息的时间和鼠标的当前位置,这里没有什么需要特殊解释的。
明白了消息,我们再来看看消息队列(Queue)。如上面的图例所示,每个Windows程序都有一个消息队列。队列是一个先进先出的缓冲区,通常是一个某种变量类型的数组。消息队列里的每一个元素就是一条消息,操作系统将生成的每个消息按先后顺序放进消息队列里,第一条消息放入第一格,第二条消息放入第二格,依次类推…...。应用程序总是取走队列里的第一条消息,消息取走后,第二条消息成为第一条,剩余的消息依次前移。应用程序取得消息后,便能够知道用户的操作和程序状态的变化。
例如,若应用程序从队列里取到了一条WM_CHAR消息,那一定是用户输入了一个字符,并且能够知道输入的是哪个字符。应用程序得到消息后,就要对消息进行处理,这即我们通常说的消息响应,消息响应是我们通过编码实现的,这也是Windows程序的主要代码区。在消息响应代码中,我们很可能又要调用操作系统提供的API函数,以便完成特定的功能。如果我们收到窗口的WM_CLOSE消息,我们可以调用DestroyWindow这个API函数来关闭该窗口,或是用MessageBox这个API函数来提示用户是否真的要关闭窗口。
通过上面的分析,我们可以想像到,要用VC++编写Windows程序,除了要具备良好的C语言功底外,还要求掌握两点知识:1.不同的消息所代表的用户操作和程序状态,2.要让操作系统执行某个功能所对应的API函数。
2.2 关于句柄
在Windows编程中我们时刻接触到一个称为句柄(HANDLE)的东西。可以这样去理解句柄,Windows程序中产生的任何资源(要占用某一块或大或小的内存),如图标,光标,窗口,应用程序的实例(已加载到内存运行中的程序)等等,操作系统每产生一个这样的资源时,都要将它们放入相应的内存,并为这些内存指定一个唯一的标识号,这个标识号即该资源的句柄。
操作系统要管理和操作这些资源,都是通过句柄来找到对应的资源的。按资源的类型,又可将句柄细分成图标句柄(HICON),光标句柄(HCURSOR),窗口句柄(HWND),应用程序实例句柄(HINSTANCE)等等各种类型的句柄。操作系统给每一个窗口指定的一个唯一的标识号即窗口句柄。
2.3 谈谈WinMain函数
使用过传统的WINDOWS编程方法的人都知道,在应用程序中有一个重要的函数WinMain函数,这个函数是应用程序的基础,当Windows操作系统启动一个程序时,它调用的就是该程序的Winmain函数,用户的操作所产生的消息正是经过这个函数的处理派送到对应的对象中进行处理。WinMain就是Windows程序的入口点函数,同dos程序的入口点函数main的
作用相同,当WinMain函数结束或返回时,Windows应用程序结束。
下面,让我们来看看一个完整的Win32程序,该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序实现的大致步骤为:
1. WinMain函数的定义;
2. 创建一个窗口;
3. 进行消息循环;
4. 完成回调函数.
2.3.1 WinMain函数的定义及功能
WinMain函数的原型如下:
Code
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // pointer to command line
int nCmdShow // show state of window
);
该函数接受四个参数,这些参数都是系统调用
WinMain函数时,传递给应用程序的。
第一个参数hInstance表示该程序的当前运行的实例句柄。同一应用程序在同一计算机上可运行多份实例,每启动一个这样的实例,操作系统都要给该实例分配一个标识号,即实例句柄,随后系统调用程序中的WinMain函数,并将该实例句柄传递给参数hInstance。
第二个参数hPrevInstance表示当前实例的上一个正在运行的,由同一个应用程序所产生的实例的句柄,即当前实例的"哥哥"的句柄。如果该值为NULL,则表示当前实例是该程序正在运行的第一份实例,是“长子”,是“老大”。如果该值不为NULL,只能表示当前实例不是该程序正在运行的第一份实例,不是“长子”,不是“老大”,但到底是“老几”,就无从得知了。这个参数到底有什么作用呢?如果想让我们的程序只能有一份实例运行,不能同时有多份实例运行,我们可以在WinMain函数的开始部分加上如下代码实现。
if(hPrevInstance) return 0;
大多数没有实际开发经验的读者都很难理解这段简单的代码。大家平时见到的if语句通常是if(x!=0)或if(x==0),其实if判断的是括号中的表达式的结果为“真”,还是为“假”。在C语言中,结果为0即“假”,非0即“真”。所以,我们也可以直接用if对某个变量的
值进行判断,if(变量)代表“如果变量不等于0”,if(!变量)代表“如果变量等于0”。我们再来看看if(hPrevInstance) return 0;的作用,如果hPrevInstance为NULL(即0),说明当前运行的实例是程序的第一个实例,WinMain函数将不返回,程序正常向下运行。只要hPrevInstance不为NULL,说明已经有同样程序的实例在运行,WinMain函数将返回,当前实例启动后立马结束,这样就保证了只有程序的一个实例可以运行。这个过程好比“计划生育”,只能要一个孩子,如果第二个孩子已经出世,当他发现自己不是老大,属于计划之外,便进行“自杀”,在程序中对应的是return 0语句。顺便说一下:“计划生育”的比喻只是为了方便大家理解问题,并不是我们的现实生活中真的要如此做法。
第三个参数lpCmdLine是一个字符串,里面包含有传递给应用程序的参数串,如:双击C盘下的1.txt文件方式启动notepad.exe程序,传递给notepad.exe程序的参数串即"c:"1.txt",不包含应用程序名本身。要在VC开发环境中给应用程序传递参数,请选择菜单Project->Settings...,在弹出的Project Settings对话框中选择Debug标签,在该标签页的Program arguments编辑框中输入你想传递给应用程序的参数。我们在WinMain函数的入口点设置一运行断点,以调试方式启动程序运行至该断点处,将鼠标移动到参数lpCmdLine上,在弹出的黄色小浮框中便能观察到该变量的值。在我们的程序调试中,经常要用到这种方法查看变量的值和状态。
第四个参数nCmdShow指定的程序的窗口应该如何显示,如最大化,最小化,隐藏等。WinMain函数前的修饰符WINAPI的解释,请参看下面关于__stdcall的讲解,我们使用goto definition功能,发现WINAPI其实就是__stdcall。
Winmain函数的程序代码按功能划分主要有两部分:1.产生并显示程序的主窗口。窗口创建并显示后,用户便可以在窗口上进行各种操作了,用户的操作及程序状态的变化都以消息的形式放到了应用程序的消息队列中。2.从消息队列循环取走消息,并将消息派发到窗口过程函数中去处理。当消息循环取到一条WM_QUIT消息时,将结束循环,WinMain函数返回,结束整个程序的运行。
如果WinMain在消息循环之前返回,程序没有正常运行,返回值为0。如果在消息循环之后返回,返回值为WM_QIUT消息的wParam参数。
2.3.2 窗口及其生成
不妨简单地将窗口看做带有边界的矩形区域。除文字处理程序中的文档窗口或者弹出提示有约会信息的对话框等这些最普通的窗口外,实际上还有许多其它类型的窗口。如命令按钮、文本框、选项按钮等都是窗口。
一个通常的Windows程序都具有窗口。通过窗口,用户可以对应用程序进行各种操作;反之,应用程序可以通过窗口收集用户的操作信息,如在窗口上移动鼠标,按下键盘等等。可以说,窗口是应用程序和用户之间交互的界面,沟通的桥梁,联系的纽带。所以窗口的编写与管理在Windows程序中占有重要的地位。进而, 窗口的创建也不是一步就可以完成的,它至少要经过下面几个操作步骤:
1) 设计一个窗口类;
2) 注册窗口类;
3) 创建窗口;
4) 显示及刷新窗口。
下面的四个小分节将分别介绍创建窗口的过程。完整的例程请参见光盘中的例子代码EX02-00。
2.3.2.1 如何设计一个窗口类——步骤1
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状),图标,菜单,背景色等等。产生窗口的过程类似汽车的生产过程。试想想,在生产任何汽车之前,都必须先在图纸上设计好一个该车的雏形(选择搭配汽车的各个部件),并要为这种新设计好的车型起个名称,如“奔驰200”。以后,便可以生产“奔驰200”这款汽车了,可以按照这个型号生产若干辆汽车,同一型号的车,可以具有不同的颜色。
同样,产生一个窗口前,也必须先设计好该窗口(指定窗口的那些特征)。窗口的特性是由一个WNDCLASS结构体进行定义的。参看MSDN,WNDCLASS定义如下:
Code
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
下面是对该结构体成员变量的说明:
第一个成员变量style成员指定了这一类型窗口的样式,比较典型的取值有:
l CS_NOCLOSE,这一类型的窗口没有关闭按钮,请实验体会。
l CS_VREDRAW,当改变窗口的垂直方向上的高度时,将引发窗口重画。窗口的重画过程好比汽车重新喷漆一样,汽车车身上原有的文字与图案将被擦除。同样,当窗口重画时,窗口上原有的文字和图形将被擦除。如果没有指定该值,当垂直方向上拉动窗口时,窗口不会重画,窗口上原有的文字和图形将被保留。
l CS_HREDRAW,当改变窗口的水平方向上的宽度时,将引发窗口重画。
l CS_DBLCLKS,设置该值可以接受到用户双击的消息。
其他的设置值请参阅MSDN,在一些特殊的场合可能要用到这些置。
提示:
在我们的程序中经常要用到一类变量,这个变量里的每一位(bit)都对应某一种特性。当该变量的某位为1时,表示有该位对应的那种特性,当该位为0时,即没有那位所对应的特性。当变量中的某几位同时为1时,就表示同时具有那几种特性的组合。一个变量中的哪一位代表哪种意义,不容易记忆,所以我们经常根据特征的英文拼写的大写去定义一些宏,该宏所对应的数值中仅有与该特征相对应的那一位(bit)为1,其余的bit都为0。我们再次使用goto definition就能发现CS_VREDRAW=0x0001,CS_HREDRAW=0x0002, CS_DBLCLKS =0x0008,CS_NOCLOSE=0x0200。他们的共同点就是只有一位为1,其余位都为0。如果我们希望某一变量的数值即有CS_VREDRAW特性,又有CS_HREDRAW特性,我们只需使用(|)操作符将他们进行或运算相组合,如style=CS_VREDRAW| CS_HREDRAW | CS_NOCLOSE。如果我们希望在某一变量原有的几个特征上去掉其中一个特征,用(&~)取反之后再进行与运算,就能够实现,如在刚才的style的基础上去掉CS_NOCLOSE特征,可以用style & ~CS_NOCLOSE实现。
第二个成员变量lpfnWndProc指定了这一类型窗口的过程函数,也称回调函数。回调函数的原理是这样的,当应用程序收到给某一窗口的消息时(还记得前面讲过的消息通常与窗口相关的吗?),就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己来实施,而由操作系统来完成,但是,回调函数本身的代码必须由应用程序自己完成。对于一条消息,操作系统到底调用应用程序中的哪个函数(回调函数)来处理呢?操作系统调用的就是接受消息的窗口所属的类型中的lpfnWndProc成员指定的函数。每一种不同类型的窗口都有自己专用的回调函数,该函数就是通过pfnWndProc成员指定的。举个例子,汽车厂家生产汽车好比应用程序创建窗口,用户使用汽车好比操作系统管理窗口,某种汽车在销售前就指定好了修理站(类似回调函数),当用户的汽车出现故障后(类似窗口收到消息),汽车用户(类似操作系统)自己直接找到修理站去修理,不用厂家(类似应用程序)亲自将车送到修理站去修理,但修理站还得由厂家事先建造好。
提示:lpfnWndProc成员的变量类型为WNDPROC,我们使用goto definition将发现WNDPROC是被如下定义的:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
读者不要被新的数据类型LRESULT,CAllBACK所吓倒,只要再次使用goto definition就可以看到他们的庐山真面目分别为long和__stdcall。
顺便帮助大家复习一下C语言的知识。首先是关于用typedef定义指向函数的指针类型的问题,其次是__stdcall修饰符的问题。
typedef int (*PFUN)(int x,int y);
上面的语句实际上就是定义了一个函数指针类型PFUN。以后便可以用PFUN来定义变量。应用如下:
int add(int x,int y);
PFUN pfun=add;
int sum=pfun(3,5);
注意,能够赋值给pfun的函数原型必须严格与PFUN的定义相同。WNDPROC定义了指向窗口回调函数的指针类型,回调函数的格式必须与WNDPROC相同。
__stdcall与__cdecl是两种不同的函数调用习惯,定义了参数的传递顺序、堆栈清除等。关于它们的详细信息请参看msdn。由于除了那些可变参数的API函数外,其余的API函数都是__stdcall习惯。由于VC++程序默认的编译选项是__cdecl,所以在VC++中调用这些__stdcall习惯的API函数,必须在声明这些函数的原型时加上__stdcall修饰符,以便对该函数的调用使用__stdcall习惯。我们曾有这样的经验,在Delphi(默认的编译选项是__stdcall)中编写的dll中的函数,在VC++中被调用时,总是造成程序崩溃,在函数的原型声明中加上__stdcall修饰符,便解决了这个问题。回调函数也必须是__stdcall调用习惯,在这里是用CALLBACK来标识的,否则,在NT4.0环境,程序将崩溃,但win98和win2000却没有这种现象。
第三个,四个成员变量cbClsExtra,cbWndExtra一般都被初始化为0,如需进一步了解这两个参数更详细的信息,请参看msdn。
第五个成员变量hInstance指定了提供回调函数的程序实例句柄。
第六个成员变量hIcon指定了这一类型窗口的图标句柄。
在填写hIcon变量时通常用LoadIcon函数来加载所需的图标。LoadIcon函数不仅可以加载Windows系统提供的标准图标到内存中,还可以加载由用户自己制作的图标资源到内存中并返回系统分配给该图标的句柄。LoadIcon函数的详细信息请参阅msdn,但要注意的是,如果加载的是系统的标准图标,第一个参数必须为NULL,如果加载的是应用程序中自定义的图标,对于第二个参数要用MAKEINTRESOURCE宏转换,请看下面的解释:
LoadIcon的第二个参数是LPCTSTR类型,用goto definition功能发现它实际被定义成CONST CHAR *,是字符串常量指针,而图标的ID是一个整数。参看msdn中的提示,对于这样的情况我们需用MAKEINTRESOURCE这个宏把资源ID标识符转换为需要的LPCTSTR类型。使用goto definition功能,或在MSDN中都可以看到MAKEINTRESOURCE的定义:
#define MAKEINTRESOURCE(i) (LPTSTR) ((DWORD) ((WORD) (i)))
之所以可以这样做,是因为字符串变量本身代表的就是一个字符数组的首地址,本身就是一个数字。所以字符串变量可以类型转换成整数,反之,一个整数也可以类型转换成字符串型。
在VC++中,对于自定义的菜单,图标,光标,对话框等都是以资源的形式进行管理的,它们的定义与描述存放在资源文件中(扩展名为.rc),资源文件是文本格式,读者可以用notepad.exe打开,阅读里面的信息。在VC++中是以“所见即所得”的方式打开资源文件的,在编辑窗口中看到的和编辑完后的结果即程序运行时的效果。对于每一个资源及资源中的子项都是用一个标识号来标识的,通常称为ID,同一个ID可以标识多个不同的资源。注意区别资源的ID号与句柄的区别,ID号是应用程序指定的,可以在资源还没在内存中产生前指定,也可在设计阶段就指定,基本上是固定的。而句柄则是资源在内存中产生时由操作系统临时安排的,每次产生的句柄可能都不一样,一个ID号标识的资源可在内存中产生多个实例句柄。资源文件中的ID标识符必须在"resouce.h"头文件中用宏定义成一个整数,这样程序中用到的一个ID号标识符实际上就是那个整数。
第七个成员变量hCursor指定了这一类型窗口的光标句柄,LoadCursor函数可以加载一个光标资源到内存中并返回系统分配给该光标的句柄。除了加载的是光标外,其特点与LoadIcon函数一样。
第八个成员变量hbrBackground指定了这一类型窗口重画时所使用的刷子句柄。当窗口重画时会使用这里指定的刷子去刷新窗口背景。刷子是具有颜色和形状的,我们可以使用GetStockObject返回一个系统刷子,也可以直接使用msdn中提供的宏,如COLOR_WINDOWTEXT,还可以用CreateBrushIndirect函数产生具有一定形状的刷子。由于GetStockObject参数能够返回标准的刷子、笔、字体、调色板等图形设备对象,定义该函数时,是无法确定该函数到底返回的是刷子还是笔,所以该函数返回类型是HGDIOBJECT(图形设备对象的总称)。由于编译器的需要,在这里我们必须HGDIOBJECT转换成HBRUSH。
顺便提示:在VC++开发Windows程序中,类型转换的频率非常高,在这有必要重点介绍一下。比如有个函数为“去叫一个人来帮忙”,定义该函数时,其返回值只能是“人”,但实际来的“人”,要么是“男人”,要么是“女人”。即使叫来的是一个“男人”,如果将该函数的返回值直接赋给一个“男人”类型的变量,编译时是没法确定返回的是“男人”,还是“女人”,将不会通过。只有我们写程序的人才知道运行时返回的是“男人”,还是“女人”,我们可以对返回值进行类型转换,以便编译器通过。在类型转换时,程序员要对转换完的后果负责,要确保在内存中存在的对象本身确实可以被看成那种要转换成的类型,如果来的是“女人”,我们将其转换成“男人”后,编译能够通过,但程序运行时将会出错。作者在编码和调试时,总是用意境的方式,仿佛看到变量或对象在内存中的真实布局和状态,以及是如何进行转换的,这样编码时比较容易一气呵成,极少犯错。
第九个成员变量lpszMenuName指定了这一类型窗口的菜单。可见菜单本身不是一个窗口,同图标、光标一样,是窗口的一个元素。不少的人和书都错以为菜单也是一个窗口,其实我们用Spy++实用工具的Find Window功能就能够区分出桌面上的哪些元素为窗口,哪些不是。lpszMenuName是LPCTSTR类型,需用MAKEINTRESOURCE这个宏把资源ID标识符转换为lpszMenuName需要的LPCTSTR类型。
第十个成员变量lpszClassName指定了这一类型窗口的名称,是字符串变量。与设计一辆新型汽车后,要为该汽车型号指定名称一样,设计了一种新型窗口后,也要为这种新型窗口起个名称。我们先将这里的名称指定成"VC教学",等会我们将看到如何使用这个名称。
2.3.2.2 注册设计好的窗口类——步骤2
设计完WNDCLASS后,需调用RegisterClass函数对其进行注册,经过注册之后,才可以创建该窗口,就好比你好成立一家公司,在公司成立之前,至少应该先去注册吧。注册函数的原形如下,只有一个参数,该参数的内容就是上一步骤中所设计的类的类名。
RegisterClass( CONST WNDCLASS *lpWndClass // class data);
2.3.2.3 创建窗口——步骤3
设计好了窗口类并且将其注册好以后就可以用CreateWindow函数产生这种类型的窗口了。CreateWindow函数的定义如下:
Code
HWND CreateWindow(
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu or child-window identifier
HANDLE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);
参数lpClassN
ame即我们刚才在WNDCLASS的lpszClassName成员里指定的名称,在这里应该为" VC教学",表示要产生" VC教学"这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow函数之前,还没有用RegisterClass函数注册过名称为" VC教学"的窗口类型,操作系统无法得知这种窗口类型的配置信息,窗口产生过程失败。
参数lpWindowName指定产生的窗口实例上显示的标题文字。
参数dwStyle指定产生的窗口实例的样式,就象同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区别WNDCLASS中的style成员与参数dwStyle,前者是针对一个大类,后者是针对个别。
参数x,y,nWidth,nHeight指定了窗口左上角的x,y坐标,窗口的宽度,高度。如果x被设置成CW_USEDEFAULT,系统将窗口的左上角设置为确省值,参数y将被忽略。如果nWidth被设置成CW_USEDEFAULT,系统将窗口的大小设置为确省值,参数nHeight将被忽略。
参数hWndParent指定了窗口的父窗口句柄。窗口之间可以组合成父子关系,子窗口必须具有WS_CHILD样式,当父窗口被破坏,隐藏,移动,显示时,也会破坏,隐藏,移动,显示子窗口。当lpWindowName为NULL时,桌面就成为当前窗口的父窗口。
参数hMenu指定了窗口的菜单或子窗口句柄。
参数hInstance指定了窗口所属的应用程序的句柄。
参数lpParam可以为窗口附加补充信息。
如果窗口创建成功,函数将返回系统为该窗口分配的句柄,否则,返回NULL。因此,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。
2.3.2.4 怎样显示创建的窗口——步骤4
窗口创建之后,我们要让它显示出来,函数ShowWindow和UpdateWindow分别表示显示和刷新窗口,其函数原形分别如下:
Code
BOOL ShowWindow(
HWND hWnd, // handle to window
int nCmdShow // show state
);
该函数有两
个参数,第一个参数就是在上一步骤中创建窗口成功之后返回的那个窗口句柄;第二个参数指定了窗口显示的状态,详细内容请参见MSDN。
Code
BOOL UpdateWindow(
HWND hWnd // handle to window
);
刷新窗口函
数很简单,只有一个参数就是创建窗口成功之后返回的窗口句柄。
到目前为止,一个窗口基本建立完成,但是编译的时候会报错,即使不报错也不能响应用户的任何事件,那是因为我们没有进行消息循环,并且还没有完成回调函数的建立。下面让我们将这个程序编写完整。
2.3.3 消息循环
通常的消息循环代码如下:
GetMessage函数从应用程序消息队列中取走一条消息,该函数的原型如下:
Code
BOOL GetMessage(
LPMSG lpMsg, // address of structure with message
HWND hWnd, // handle of window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
参数lpMsg是接受消息的变量的指针。
参数hWnd指定了接收属于哪个窗口的消息。
参数wMsgFilterMin,wMsgFilterMax指定了接受某一范围内的消息。
如果队列中没有满足条件的消息,该函数将一直等待,不会返回。除了WM_QUIT消息外,该函数返回非零值。对WM_QUIT消息,该函数返回零。也就是说,只有收到WM_QUIT消息,上述代码才能退出while循环,程序才有可能结束运行。
TranslateMessage函数的作用是对取到的消息进行转换。用户按动一下某个键,系统将发出WM_KEYDOWN, WM_KEYUP,并且参数中提供的是该键的虚拟扫描码。但有时用户按动一下某个键, 我们想得到一条表示用户输入了某个字符的消息,并在消息补充参数中提供字符的编码。TranslateMessage能将可行的WM_KEYDOWN, WM_KEYUP消息对转换成一条WM_CHAR消息,将可行的WM_SYSKEYDOWN或 WM_SYSKEYUP消息对转换成一条WM_SYSCHAR消息,并将转换后得到的新消息投递到程序的消息队列中。转换过程不会影响原来的消息,只在消息队列中增加新消息。
DispatchMessage函数将取到的消息传递到窗口的回调函数中去处理。可以理解成该函数通知操作系统,让操作系统去调用窗口的回调函数来处理收到的消息。
所有Windows程序的消息处理代码基本上都是相同的,如没特殊需要,可以照搬照抄上述代码。
顺便提示:从队列中取消息还有PeekMessage函数,PeekMessage函数有两种取消息的方式,第一种与GetMessage一样,从队列中直接取走消息,第二种是只取消息的一个副本,并不将消息从队列中取走。无论哪种方式,PeekMessage都不会因队列中没有满足条件的消息而阻塞,当取到满足条件的消息,该函数返回非零值反之,返回零。
向队列中发送消息有PostMessage和SendMessage。PostMessage函数发送消息后立即返回,而SendMessage需等到发送的消息被处理完后才能返回。还有一个PostThreadMessage用于向线程发送消息,关于线程,请在以后的章节再学,但我们也因此想到消息不一定总是与窗口相关的,也就是说,对于某些消息,其对应的MSG结构中的hwnd可以为NULL。
2.3.4 完成回调函数
回调函数的原型为:
Code
我们可以将函数名
WindowProc改为我们喜欢的名称,如mywndproc,该函数的四个参数对应消息的窗口句柄,消息码,消息码的两个补充参数。在该函数内部是一个庞大的switch语句,用于对各种感兴趣的消息进行处理。请看下面清单2-00程序中的代码:
清单2-00 响应消息的回调函数
——————————————————————————————————————
Code
1 LRESULT CALLBACK mywndproc(
2 HWND hwnd, // handle to window
3 UINT uMsg, // message identifier
4 WPARAM wParam, // first message parameter
5 LPARAM lParam // second message parameter
6 )
7 {
8 switch(uMsg)
9 {
10 case WM_CHAR:
11 char str[10];
12 sprintf(str,"char is %d",wParam);
13 MessageBox(hwnd,str,"it315.org",MB_OKCANCEL);
14 break;
15 case WM_LBUTTONDOWN:
16 MessageBox(hwnd,"mouse click","it315.org",MB_OK);
17 HDC hDC;
18 hDC=GetDC(hwnd);
19 TextOut(hDC,0,50,"http://www.it315.org.com",strlen("http://www.it315.org.com"));
20 ReleaseDC(hwnd,hDC);
21 break;
22 case WM_CLOSE:
23 if(IDOK==MessageBox(NULL,“真的退出?”,”确认”,
24 ",MB_OKCANCEL|MB_ICONQUESTION))
25 {
26 DestroyWindow(hwnd);
27 }
28 break;
29 case WM_PAINT:
30 PAINTSTRUCT ps;
31 hDC=BeginPaint(hwnd,&ps);//在WM_PAINT里必须用这个函数
32 TextOut(hDC,0,0,"http://www.it315.org.com",strlen("http://www.it315.org.com"));
33 EndPaint(hwnd,&ps);
34 break;
35 case WM_DESTROY:
36 PostQuitMessage(0);
37 break;
38 default:
39 return(DefWindowProc(hwnd,uMsg, wParam,lParam ));
40 }
41 return 0;
42 }
———————————————————————————————————————
下面我们来分析一下清单2-00中的代码:
注意10~14行代码,当用户在窗口上按下一个字符键,程序将得到一条WM_CHAR消息,参看msdn,在其wParam参数中含有字符的码值。使用程序中的代码,我们可以随时获得某个字符的码值,不用为此去专门查找书籍。MessageBox函数可以弹出一个显示信息的对话框,如果我们按下BackSpace键,在这里将弹出"the char code is 8"消息。
注意15~21行代码,当用户在窗口上按下鼠标左键时,程序弹出一条消息框对收到的WM_LBUTTONDOWN进行响应,以证明按下鼠标左键动作与WM_LBUTTONDOWN消息的这种对应关系,另外程序还将在窗口上鼠标所按下的位置写上一串文字。当我们要在窗口上写字、绘图,并不直接在这些设备上操作,而是在一个称为设备描述表(Device Contexts,简称DC)的资源上操作的。使用DC,程序不用为图形的显示与打印输出分别单独处理了。无论是打印还是显示,我们都是在DC上操作,然后由DC映射到这些设备上。使用DC,我们不用担心程序在不同的硬件上执行的差异性,DC能够为我们装载合适的设备驱动程序,实现程序的图形操作与底层设备无关。
第18行代码,GetDC函数能够返回同窗口相关连的DC的句柄。
TextOut函数在当前DC上指定的位置输出一个字符串,当按下鼠标左按钮,消息WM_LBUTTONDOWN的补充参数lParam中含有鼠标位置的x,y坐标,其中的低16位含有x坐标,高16位含有y坐标,可以分别通过LOWORD,HIWORD宏获得。
对于每次成功调用GetDC返回的DC句柄,在执行完所有的图形操作后,必须调用ReleaseDC释放该句柄所占用的资源。否则,会造成内存泄露。我曾经帮助一个学员调试他的On-job项目程序,他发现他的程序所占用的内存总是以4k的增量向上增长,每运行几小时后,程序便因内存不足而崩溃了。后来发现就是因为程序在定时器中反复使用GetDC而没有使用ReleaseDC释放造成的。读者可以将例子程序中的ReleaseDC一句注释掉,编译后运行,在NT4.0/win2000下启动任务管理器,切换到进程标签,查看你的程序所使用的内存量,在程序中不断点击鼠标左键,程序将不断调用GetDC函数,你将发现你的程序占用的内存量不断向上增长,我们通常使用这样的方式来检测程序的内存泄露的。
注意22~27行代码,当用户点击窗口上的关闭按钮时,系统将给应用程序发送一条WM_CLOSE消息,如果程序不做进一步处理,窗口是不会关闭的。我们在程序中利用一个选择对话框,如果用户确认真的要退出,程序调用DestroyWindow函数将窗口关闭。
窗口关闭后,系统将给应用程序发送一条WM_DESTROY消息,第35~37行代码,需要注意的是,主窗口的关闭,不代表应用程序结束,在WinMain函数中的消息循环代码中,GetMessage函数必须取到一条WM_QUIT,消息循环才能结束。要让程序正常退出,必须在WM_DESTROY消息响应代码中,调用PostQuitMessage函数向程序的消息队列中发送一条WM_QUIT消息,PostQuitMessage函数的参数值传递给WM_QUIT消息的wParam参数,通常用作WinMain函数的返回值。
当窗口第一次产生移动,改变大小,从其他窗口后面切换到前面等情况都会导致窗口的重画。重画时将使用设计窗口类时指定的刷子粉刷窗口的背景,窗口上原有的文字和图形都将被擦除掉。要想让图形和文字总显示在窗口的表面,只能是在这些图形和文字被擦除后,立即又将它们画上去。这个过程对用户来说,是感觉不到的,他们只能感觉到这些图形和文字永远都和窗口一并存在。
当系统粉刷完窗口的背景后,都会发送一条WM_PAINT消息,以便通知应用程序原有的图形和文字已被擦除,如果还想保留哪些图形和文字,请在此处加入处理代码。也就是说,我们在WM_PAINT消息响应中作出的图形和文字是“永远”存在的。
注意第29~34行代码,对于WM_PAINT消息响应代码中要获得窗口的DC,只能使用BeginPaint函数,除此之外的消息响应代码中必须用GetDC获得窗口的DC,BeginPaint获得的DC最后必须用EndPaint释放。提醒:水平或垂直改变窗口的大小时,窗口是否重画,取决于WNDCLASS结构中style成员的设置中是否包含CS_VREDRAW与CS_HREDRAW。
DefWindowProc函数提供了对所有消息的缺省处理方式,对于大多数不想特殊处理的消息,程序都可以调用这个函数来处理,所以程序在switch的default语句中调用此函数进行处理。