分类:
2006-06-12 16:51:31
接触过Windows编程方法的读者都知道,在应用程序中有一个重要的函数WinMain,这个函数是应用程序的基 础。当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数(实际是由插入到可执行文件中的启动代码调用的)。WinMain 是Windows程序的入口点函数,与DOS程序的入口点函数main的作用相同,当WinMain函数结束或返回时,Windows应用程序结束。
下面,让我们来看一个完整的Win32程序,该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序实现的步骤为:
1. WinMain函数的定义;
2. 创建一个窗口;
3. 进行消息循环;
4. 编写窗口过程函数。
WinMain函数的原型声明如下:
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // command line
int nCmdShow // show state
);
WinMain函数接收4个参数,这些参数都是在系统调用WinMain函数时,传递给应用程序的。
第一个参数hInstance表示该程序当前运行的实例的 句柄,这是一个数值。当程序在Windows下运行时,它唯一标识运行中的实例(注意,只有运行中的程序实例,才有实例句柄)。一个应用程序可以运行多个 实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给WinMain函数。
第二个参数hPrevInstance表示当前实例的前一个实例的句柄。通过查看MSDN我们可以知道,在Win32环境下,这个参数总是NULL,即在Win32环境下,这个参数不再起作用。
第三个参数lpCmdLine是一个以空终止的字符串,指 定传递给应用程序的命令行参数。例如:在D盘下有一个sunxin.txt文件,当我们用鼠标双击这个文件时将启动记事本程序 (notepad.exe),此时系统会将D:\sunxin.txt作为命令行参数传递给记事本程序的WinMain函数,记事本程序在得到这个文件的 全路径名后,就在窗口中显示该文件的内容。要在VC++开发环境中向应用程序传递参数,可以单击菜单【Project】→【Settings】,选择 “Debug”选项卡,在“Program arguments”编辑框中输入你想传递给应用程序的参数。
第四个参数nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。
关于WinMain函数前的修饰符WINAPI,请参看下面关于__stdcall的介绍。读者可以利用goto definition功能查看WINAPI的定义,可以看到WINAPI其实就是__stdcall。
创建一个完整的窗口,需要经过下面几个操作步骤:
1. 设计一个窗口类;
2. 注册窗口类;
3. 创建窗口;
4. 显示及更新窗口。
下面的四个小分节将分别介绍创建窗口的过程。完整的例程请参见光盘中的例子代码Chapter1目录下WinMain。
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状)、图标、背景色等。窗口的创建过程类似于汽车的制 造过程。我们在生产一个型号的汽车之前,首先要对该型号的汽车进行设计,在图纸上画出汽车的结构图,设计各个零部件,同时还要给该型号的汽车取一个响亮的 名字,例如“奥迪A6”。在完成设计后,就可以按照“奥迪A6”这个型号生产汽车了。
类似地,在创建一个窗口前,也必须对该类型的窗口进行设计,指定窗口的特征。当然,在我们设计一个窗口时,不像汽车 的设计这么复杂,因为Windows已经为我们定义好了一个窗口所应具有的基本属性,我们只需要像考试时做填空题一样,将需要我们填充的部分填写完整,一 种窗口就设计好了。在Windows中,要达到作填空题的效果,只能通过结构体来完成,窗口的特征就是由WNDCLASS结构体来定义的。 WNDCLASS结构体的定义如下(请读者自行参看MSDN):
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指定这一类型窗口的样式,常用的样式如下:
■ CS_HREDRAW
当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘时,窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。
■ CS_VREDRAW
当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。
■ CS_NOCLOSE
禁用系统菜单的Close命令,这将导致窗口没有关闭按钮。
■ CS_DBLCLKS
当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
style成员的其他取值请参阅MSDN。
知 识点 在Windows.h中,以CS_开头的类样式(Class Style)标识符被定义为16位的常量,这些常量都只有某1位为1。在VC++开发环境中,利用goto definition功能,可以看到CS_VREDRAW=0x0001,CS_HREDRAW=0x0002,CS_DBLCLKS =0x0008,CS_NOCLOSE=0x0200,读者可以将这些16进制数转换为2进制数,就可以发现它们都只有1位为1,并且为1的位各不相同。 用这种方式定义的标识符称为“位标志”,我们可以使用位运算操作符来组合使用这些样式。例如,要让窗口在水平和垂直尺寸发生变化时发生重绘,我们可以使用 位或(|)操作符将CS_HREDRAW和CS_VREDRAW组合起来,如style=CS_HREDRAW | CS_VREDRAW。假如有一个变量具有多个样式,而我们并不清楚该变量都有哪些样式,现在我们想要去掉该变量具有的某个样式,那么可以先对该样式标识 符进行取反(~)操作,然后再和这个变量进行与(&)操作即可实现。例如,要去掉先前的style变量所具有的CS_VREDRAW样式,可以编 写代码:style=style & ~ CS_VREDRAW。
在Windows程序中,经常会用到这种位标志标识符,后面我们在创建窗口时用到的窗口样式,也是属于位标志标识符。
第二个成员变量lpfnWndProc是一个函数指针,指向窗口过程函数,窗口过程函数是一个回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。回调函数实现的机制是:
(1)定义一个回调函数。
(2)提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。
(3)当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
针对Windows的消息处理机制,窗口过程函数被调用的过程如下:
(1)在设计窗口类的时候,将窗口过程函数的地址赋值给lpfnWndProc成员变量。
(2)调用RegsiterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。
(3)当应用程序接收到某一窗口的消息时,调用DispatchMessage(&msg)将消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。
一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联(通过WNDCLASS结构体中的lpfnWndProc成员变量指定),基于该窗口过程。
lpfnWndProc成员变量的类型是WNDPROC,我们在VC++开发环境中使用goto definition功能,可以看到WNDPROC的定义:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
在这里又出现了两个新的数据类型LRESULT和CALLBACK,再次使用goto definition,可以看到它们实际上是long和__stdcall。
从WNDPROC的定义可以知道,WNDPROC实际上是函数指针类型。
注意:WNDPROC被定义为指向窗口过程函数的指针类型,窗口过程函数的格式必须与WNDPROC相同。
知 识点 在函数调用过程中,会使用栈。__stdcall与__cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数 弹出栈,以及产生函数修饰名的方法。关于这两个调用约定的详细信息,读者可参看MSDN。对于参数个数可变的函数,例如printf,使用的是 __cdecl调用约定,Win32的API函数都遵循__stdcall调用约定。在VC++开发环境中,默认的编译选项是__cdecl,对于那些需 要__stdcall调用约定的函数,在声明时必须显式地加上__stdcall。在Windows程序中,回调函数必须遵循__stdcall调用约 定,所以我们在声明回调函数时要使用CALLBACK。使用CALLBACK而不是__stdcall的原因是为了告诉我们这是一个回调函数。注意,在 Windows 98和Windows 2000下,声明窗口过程函数时,即使不使用CALLBACK也不会出错,但在Windows NT4.0下,则会出错。
WNDCLASS结构体第三个成员变量cbClsExtra:Windows为系统中的每一个窗口类管理一个 WNDCLASS结构。在应用程序注册一个窗口类时,它可以让Windows系统为WNDCLASS结构分配和追加一定字节数的附加内存空间,这部分内存 空间称为类附加内存,由属于这种窗口类的所有窗口所共享,类附加内存空间用于存储类的附加信息。Windows系统把这部分内存初始化为0。一般我们将这 个参数设置为0。
第四个成员变量cbWndExtra:Windows系统为每一个窗口管理一个内部数据结构,在注册一个窗口类时, 应用程序能够指定一定字节数的附加内存空间,称为窗口附加内存。在创建这类窗口时,Windows系统就为窗口的结构分配和追加指定数目的窗口附加内存空 间,应用程序可用这部分内存存储窗口特有的数据。Windows系统把这部分内存初始化为0。如果应用程序用WNDCLASS结构注册对话框(用资源文件 中的CLASS伪指令创建),必须给DLGWINDOWEXTRA设置这个成员。一般我们将这个参数设置为0。
第五个成员变量hInstance指定包含窗口过程的程序的实例句柄。
第六个成员变量hIcon指定窗口类的图标句柄。这个成员变量必须是一个图标资源的句柄,如果这个成员为NULL,那么系统将提供一个默认的图标。
在为hIcon变量赋值时,可以调用LoadIcon函数来加载一个图标资源,返回系统分配给该图标的句柄。该函数的原型声明如下所示:
HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName)
LoadIcon函数不仅可以加载Windows系统提供的标准图标到内存中,还可以加载由用户自己制作的图标资源 到内存中,并返回系统分配给该图标的句柄,请参看MSDN关于LoadIcon的解释。但要注意的是,如果加载的是系统的标准图标,那么第一个参数必须为 NULL。
LoadIcon的第二个参数是LPCTSTR类型,利用goto definition命令将会发现它实际被定义成CONST CHAR*,即指向字符常量的指针,而图标的ID是一个整数。对于这种情况我们需要用MAKEINTRESOURCE宏把资源ID标识符转换为需要的 LPCTSTR类型。
知 识点 在VC++中,对于自定义的菜单、图标、光标、对话框等资源,都保存在资源脚本(通常扩展名为.rc)文件中。在VC++开发环境中,要访问资源文件,可 以单击左边项目视图窗口底部的ResourceView选项卡,你将看到以树状列表形式显示的资源项目。在任何一种资源上双击鼠标左键,将打开资源编辑 器。在资源编辑器中,你可以以“所见即所得”的方式对资源进行编辑。资源文件本身是文本文件格式,如果你了解资源文件的编写格式,也可以直接使用文本编辑 器对资源进行编辑。
在VC++中,资源是 通过标识符(ID)来标识的,同一个ID可以标识多个不同的资源。资源的ID实质上是一个整数,在“resource.h”中定义为一个宏。我们在为资源 指定ID的时候,应该养成一个良好的习惯,即在“ID”后附加特定资源英文名称的首字母,例如,菜单资源为IDM_XXX(M表示Menu),图标资源为 IDI_ XXX(I表示Icon),按钮资源为IDB_ XXX(B表示Button)。采用这种命名方式,我们在程序中使用资源ID时,可以一目了然。
WNDCLASS结构体第七个成员变量hCursor指定窗口类的光标句柄。这个成员变量必须是一个光标资源的句柄,如果这个成员为NULL,那么无论何时鼠标进入到应用程序窗口中,应用程序都必须明确地设置光标的形状。
在为hCursor变量赋值时,可以调用LoadCursor函数来加载一个光标资源,返回系统分配给该光标的句柄。该函数的原型声明如下所示:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
LoadCursor函数除了加载的是光标外,其使用方法与LoadIcon函数一样。
第八个成员变量hbrBackground指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来 擦除窗口的背景。我们既可以为hbrBackground成员指定一个画刷的句柄,也可以为其指定一个标准的系统颜色值。关于hbrBackground 成员的详细说明,请参看MSDN。
我们可以调用GetStockObject函数来得到系统的标准画刷。GetStockObject函数的原型声明如下所示:
HGDIOBJ GetStockObject( int fnObject);
参数fnObject指定要获取的对象的类型,关于该参数的取值,请参看MSDN。GetStockObject函 数不仅可以用于获取画刷的句柄,还可以用于获取画笔、字体和调色板的句柄。由于GetStockObject函数可以返回多种资源对象的句柄,在实际调用 该函数前无法确定它返回哪一种资源对象的句柄,因此它的返回值的类型定义为HGDIOBJ,在实际使用时,需要进行类型转换。例如,我们要为 hbrBackground成员指定一个黑色画刷的句柄,可以调用如下:
wndclass. hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);
当窗口发生重绘时,系统会使用这里指定的黑色画刷擦除窗口的背景。
第九个成员变量lpszMenuName是一个以空终止的字符串,指定菜单资源的名字。如果你使用菜单资源的ID 号,那么需要用MAKEINTRESOURCE宏来进行转换。如果将lpszMenuName成员设置为NULL,那么基于这个窗口类创建的窗口将没有默 认的菜单。要注意,菜单并不是一个窗口,很多初学者都误以为菜单是一个窗口。
第十个成员变量lpszClassName是一个以空终止的字符串,指定窗口类的名字。这和汽车的设计类似,设计一 款新型号的汽车,需要给该型号的汽车取一个名字。同样的,设计了一种新类型的窗口,也要为该类型的窗口取个名字,这里我们将这种类型窗口的命名为 “sunxin2006”,后面将看到如何使用这个名称。
在设计完汽车后,需要报经国家有关部门审批,批准后才能生产这种类型的汽车。同样地,设计完窗口类(WNDCLASS)后,需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。
设计好窗口类并且将其成功注册之后,就可以用CreateWindow函数产生这种类型的窗口了。CreateWindow函数的原型声明如下:
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
);
参数lpClassName指定窗口类的名称,即我们在步骤1设计一个窗口类中为WNDCLASS的 lpszClassName成员指定的名称,在这里应该设置为“sunxin2006”,表示要产生“sunxin2006”这一类型的窗口。产生窗口的 过程是由操作系统完成的,如果在调用CreateWindow函数之前,没有用RegisterClass函数注册过名称为“sunxin2006”的窗 口类型,操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败。
参数lpWindowName指定窗口的名字。如果窗口样式指定了标题栏,那么这里指定的窗口名字将显示在标题栏上。
参数dwStyle指定创建的窗口的样式。就好像同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同 的外观样式。要注意区分WNDCLASS中的style成员与CreateWindow函数的dwStyle参数,前者是指定窗口类的样式,基于该窗口类 创建的窗口都具有这些样式,后者是指定某个具体的窗口的样式。
在这里,我们可以给创建的窗口指定WS_OVERLAPPEDWINDOW这一类型,该类型的定义为:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_CAPTION | \
WS_SYSMENU | \
WS_THICKFRAME | \
WS_MINIMIZEBOX | \
WS_MAXIMIZEBOX)
可以看到,WS_OVERLAPPEDWINDOW是多种窗口类型的组合,其原理和前面知识点所讲的内容是一致的。下面是这几种常用窗口类型的说明。
n WS_OVERLAPPED:产生一个层叠的窗口,一个层叠的窗口有一个标题栏和一个边框。
n WS_CAPTION:创建一个有标题栏的窗口。
n WS_SYSMENU:创建一个在标题栏上带有系统菜单的窗口,要和WS_CAPTION类型一起使用。
n WS_THICKFRAME:创建一个具有可调边框的窗口。
n WS_MINIMIZEBOX:创建一个具有最小化按钮的窗口,必须同时设定WS_ SYSMENU类型。
n WS_MAXIMIZEBOX:创建一个具有最大化按钮的窗口,必须同时设定WS_ SYSMENU类型。
使用WS_OVERLAPPEDWINDOW类型的窗口如图1.1所示。
CreateWindow函数的参数x,y,nWidth,nHeight分别指定窗口左上角的x,y坐标,窗口的宽度,高度。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。如果参数nWidth被设为CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
参数hWndParent指定被创建窗口的父窗口句柄。在1.2节中已经介绍了,窗口之间可以有父子关系,子窗口必须具有WS_CHILD样式。对父窗口的操作同时也会影响到子窗口,表1.1列出了对父窗口的操作如何影响子窗口。
表1.1 对父窗口的操作对子窗口的影响
父 窗 口 |
子 窗 口 |
销毁 |
在父窗口被销毁之前销毁 |
隐藏 |
在父窗口被隐藏之前隐藏,子窗口只有在父窗口可见时可见 |
移动 |
跟随父窗口客户区一起移动 |
显示 |
在父窗口显示之后显示 |
参数hMenu指定窗口菜单的句柄。
参数hInstance指定窗口所属的应用程序实例的句柄。
参数lpParam:作为WM_CREATE消息的附加参数lParam传入的数据指针。在创建多文档界面的客户窗口时,lpParam必须指向CLIENTCREATESTRUCT结构体。多数窗口将这个参数设置为NULL。
如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。
(1)显示窗口
窗口创建之后,我们要让它显示出来,这就跟汽车生产出来后要推向市场一样。调用函数ShowWindow来显示窗口,该函数的原型声明如下所示:
BOOL ShowWindow(
HWND hWnd, // handle to window
int nCmdShow // show state
);
ShowWindow函数有两个参数,第一个参数hWnd就是在上一步骤中成功创建窗口后返回的那个窗口句柄;第二个参数nCmdShow指定了窗口显示的状态,常用的有以下几种。
n SW_HIDE:隐藏窗口并激活其他窗口。
n SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。
n SW_SHOWMAXIMIZED:激活窗口并将其最大化显示。
n SW_SHOWMINIMIZED:激活窗口并将其最小化显示。
n SW_SHOWNORMAL:激活并显示窗口。如果窗口是最小化或最大化的状态,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。
关于nCmdShow参数的详细内容请参见MSDN。
(2)更新窗口
在调用ShowWindow函数之后,我们紧接着调用UpdateWindow来刷新窗口,就好像我们买了新房子,需要装修一下。UpdateWindow函数的原型声明如下:
BOOL UpdateWindow(
HWND hWnd // handle to window
);
其参数hWnd指的是创建成功后的窗口的句柄。UpdateWindow函数通过发送一个WM_PAINT消息来刷 新窗口,UpdateWindow将WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到我们前面所说的消息队列里,请读者注意这一点。关 于WM_PAINT消息的作用和窗口过程函数,后面我们将会详细讲解。
到此,一个窗口就算创建完成了。
在创建窗口、显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:
BOOL GetMessage(
LPMSG lpMsg, // address of structure with message
HWND hWnd, // handle of window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
参数lpMsg指向一个消息(MSG)结构体,GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中。
参数hWnd指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。
参数wMsgFilterMin指定要获取的消息的最小值,通常设置为0。
参数wMsgFilterMax指定要获取的消息的最大值。如果wMsgFilterMin和wMsgFilter Max都设置为0,则接收所有消息。
GetMessage函数接收到除WM_QUIT外的消息均返回非零值。对于WM_QUIT消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数hWnd是无效的窗口句柄或lpMsg是无效的指针时。
通常我们编写的消息循环代码如下:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
前面已经介绍了,GetMessage函数只有在接收到WM_QUIT消息时,才返回0。此时while语句判断的 条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows应用程序就通过这个while循环来保证程序始终处于运行 状态。
TranslateMessage函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当 下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附 加参数(wParam和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII码, TranslateMessage这个函数就可以将WM_KEYDOWN和WM_ KEYUP消息的组合转换为一条WM_CHAR消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到调用线程的消息 队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。
DispatchMessage函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理(响应)。
Windows应用程序的消息处理机制如图1.2所示。
(1)操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。
(2)应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。
(3)应用程序调用DispatchMessage,将消息回传给操作系统。消息是由MSG结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此,DispatchMessage函数总能进行正确的传递。
(4)系统利用WNDCLASS结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)。
以上就是Windows应用程序的消息处理过程。
提示:
(1)从消息队列中获取消息还可以调用PeekMessage函数,该函数的原型声明如下所示:
BOOL PeekMessage(
LPMSG lpMsg, // message information
HWND hWnd, // handle to window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax, // last message
UINT wRemoveMsg // removal options
);
前4 个参数和GetMessage函数的4个参数的作用相同。最后1个参数指定消息获取的方式,如果设为PM_NOREMOVE,那么消息将不会从消息队列中 被移除;如果设为PM_REMOVE,那么消息将从消息队列中被移除(与GetMessage函数的行为一致)。关于PeekMessage函数的更多信 息,请参见MSDN。
(2) 发送消息可以使用SendMessage和PostMessage函数。SendMessage将消息直接发送给窗口,并调用该窗口的窗口过程进行处理。 在窗口过程对消息处理完毕后,该函数才返回(SendMessage发送的消息为不进队消息)。PostMessage函数将消息放入与创建窗口的线程相 关联的消息队列后立即返回。除了这两个函数外,还有一个PostThreadMessage函数,用于向线程发送消息,对于线程消息,MSG结构体中的 hwnd成员为NULL。
关于线程,后面我们会有专门的章节进行介绍。
在完成上述步骤后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。一个Windows应用程序的主要代码部分就集中在窗口过程函数中。在MSDN中可以查到窗口过程函数的声明形式,如下所示:
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
窗口过程函数的名字可以随便取,如WinSunProc,但函数定义的形式必须和上述声明的形式相同。
提示:系统通过窗口过程函数的地址(指针)来调用窗口过程函数,而不是名字。
WindowProc函数的4个参数分别对应消息的窗口句柄、消息代码、消息代码的两个附加参数。一个程序可以有多个窗口,窗口过程函数的第1个参数hwnd就标识了接收消息的特定窗口。
在窗口过程函数内部使用switch/case语句来确定窗口过程接收的是什么消息,以及如何对这个消息进行处理。我们看下面的代码:
WinMain.cpp
……
1.LRESULT CALLBACK WinSunProc(
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 szChar[20];
12. sprintf(szChar,"char code is %d",wParam);
13. MessageBox(hwnd,szChar,"char",0);
14. break;
15. case WM_LBUTTONDOWN:
16. MessageBox(hwnd,"mouse clicked","message",0);
17. HDC hdc;
18. hdc=GetDC(hwnd);
19. TextOut(hdc,0,50,"程序员之家",strlen("程序员之家"));
20. ReleaseDC(hwnd,hdc);
21. break;
22. case WM_PAINT:
23. HDC hDC;
24. PAINTSTRUCT ps;
25. hDC=BeginPaint(hwnd,&ps);
26. TextOut(hDC,0,0,"",strlen(""));
27. EndPaint(hwnd,&ps);
28. break;
29. case WM_CLOSE:
30. if(IDYES==MessageBox(hwnd,"是否真的结束?","message",MB_YESNO))
31. {
32. DestroyWindow(hwnd);
33. }
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.}
10~14行代码:当用户在窗口中按下一个字符键,程序将得到一条WM_CHAR消息(通过调用TranslateMessage函数转换得到),在其wParam参数中含有字符的ASCII码值。
MessageBox函数(其用法,请读者查看MSDN,并结合本章程序来学习)弹出一个包含了显示信息的消息框,如果我们按下字母“a”键(注意大小写),程序将弹出如图1.3所示的消息框。
15~21 行代码:当用户在窗口中按下鼠标左键时,将产生WM_ LBUTTONDOWN消息。为了证实这一点,我们在WM_LBUTTONDOWN消息的响应代码中,调用MessageBox函数弹出一个提示信息,告 诉用户“点击了鼠标”。接下来,我们在窗口中(0,50)的位置处输出一行文字。要在窗口中输出文字或者显示图形,需要用到设备描述表(Device Context),简称DC。DC是一个包含设备(物理输出设备,如显示器,以及设备驱动程序)信息的结构体,在Windows平台下,所有的图形操作都 是利用DC来完成的。
关于DC,我们可以用一个形象的比喻来说明它的作用。现在有一个美术老师,他让他的学生画一幅森林的图像,有的学生 采用素描,有的学生采用水彩画,有的学生采用油画,每个学生所作的图都是森林,然而表现形式却各不相同。如果让我们来画图,老师指定了一种画法(例如用水 彩画),我们就要去学习它,然后才能按照要求画出图形。如果画法(工具)经常变换,我们就要花大量的时间和精力去学习和掌握它。在这里,画法就相当于计算 机中的图形设备及其驱动程序。我们要想作一幅图,就要掌握我们所用平台的图形设备和它的驱动程序,调用驱动程序的接口来完成图形的显示。不同图形设备的设 备驱动程序是不一样的,对于程序员来说,要掌握各种不同的驱动程序,工作量就太大了。因此,Windows就给我们提供了一个DC,让我们从学生的角色转 变为老师的角色,只要下命令去画森林这幅图,由DC去和设备驱动程序打交道,完成图形的绘制。至于图形的效果,就要由所使用的图形设备来决定了。对于老师 来说,只要画出的是森林图像就可以了。对于程序员来说,充当老师的角色,只需要获取DC(DC也是一种资源)的句柄,利用这个句柄去作图就可以了。
使用DC,程序不用为图形的显示与打印输出分别处理了。无论显示,还是打印,都是直接在DC上操作,然后由DC映射到这些物理设备上。
第17行代码:定义了一个类型为HDC的变量hdc。
第18行代码:用hdc保存GetDC函数返回的与特定窗口相关联的DC的句柄。为什么DC要和窗口相关联呢?想像一下,我们在作图时,需要有画布;利用计算机作图,窗口就相当于画布,因此,在获取DC的句柄时,总是和一个指定的窗口相关联。
第19行代码:TextOut函数利用得到的DC句柄在指定的位置(x坐标为0,y坐标为50)处输出一行文字。
第20行代码:在执行图形操作时,如果使用GetDC函数来得到DC的句柄,那么在完成图形操作后,必须调用ReleaseDC函数来释放DC所占用的资源,否则会引起内存泄漏。
第22~28行代码:对WM_PAINT消息进行处理。当窗口客户区的一部分或者全部变为“无效”时,系统会发送 WM_PAINT消息,通知应用程序重新绘制窗口。当窗口刚创建的时候,整个客户区都是无效的。因为这个时候程序还没有在窗口上绘制任何东西,当调用 UpdateWindow函数时,会发送WM_PAINT消息给窗口过程,对窗口进行刷新。当窗口从无到有、改变尺寸、最小化后再恢复、被其他窗口遮盖后 再显示时,窗口的客户区都将变为无效,此时系统会给应用程序发送WM_PAINT消息,通知应用程序重新绘制。
提示:窗口大小发生变化时是否发生重绘,取决于WNDCLASS结构体中style成员是否设置了CS_HREDRAW和CS_VREDRAW标志。
第25行,调用BeginPaint函数得到DC的句柄。BeginPaint函数的第1个参数是窗口的句柄,第二 个参数是PAINTSTRUCT结构体的指针,该结构体对象用于接收绘制的信息。在调用BeginPaint时,如果客户区的背景还没有被擦除,那么 BeginPaint会发送WM_ERASEBKGND消息给窗口,系统就会使用WNDCLASS结构体的hbrBackground成员指定的画刷来擦 除背景。
第26行,调用TextOut函数在(0,0)的位置输出一个网址“http: //”。当发生重绘时,窗口中的文字和图形都会被擦除。在擦除背景后,TextOut函数又一次执行,在窗口中再次绘制出 “”。这个过程对用户来说是透明的,用户并不知道程序执行的过程,给用户的感觉就是你在响应WM_PAINT 消息的代码中输出的文字或图形始终保持在窗口中。换句话说,如果我们想要让某个图形始终在窗口中显示,就应该将图形的绘制操作放到响应WM_PAINT消 息的代码中。
那么系统为什么不直接保存窗口中的图形数据,而要由应用程序不断地进行重绘呢?这主要是因为在图形环境中涉及的数据量太大,为了节省内存的使用,提高效率,而采用了重绘的方式。
在响应WM_PAINT消息的代码中,要得到窗口的DC,必须调用BeginPaint函数。BeginPaint 函数也只能在WM_PAINT消息的响应代码中使用,在其他地方,只能使用GetDC来得到DC的句柄。另外,BeginPaint函数得到的DC,必须 用EndPaint函数去释放。
29~34行代码:当用户单击窗口上的关闭按钮时,系统将给应用程序发送一条WM_CLOSE消息。在这段消息响应 代码中,我们首先弹出一个消息框,让用户确认是否结束。如果用户选择“否”,则什么也不做;如果用户选择“是”,则调用DestroyWindow函数销 毁窗口,DestroyWindow函数在销毁窗口后会向窗口过程发送WM_DESTROY消息。注意,此时窗口虽然销毁了,但应用程序并没有退出。有不 少初学者错误地在WM_DESTROY消息的响应代码中,提示用户是否退出,而此时窗口已经销毁了,即使用户选择不退出,也没有什么意义了。所以如果你要 控制程序是否退出,应该在WM_CLOSE消息的响应代码中完成。
对WM_CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给DefWindowProc函数(参见第39行),而DefWindowProc函数则调用DestroyWindow函数来响应这条WM_CLOSE消息。
35~37行代码:DestroyWindow函数在销毁窗口后,会给窗口过程发送WM_DESTROY消息,我们 在该消息的响应代码中调用PostQuitMessage函数(第36行)。PostQuitMessage函数向应用程序的消息队列中投递一条 WM_QUIT消息并返回。我们在1.4.3小节介绍过,GetMessage函数只有在收到WM_QUIT消息时才返回0,此时消息循环才结束,程序退 出。要想让程序正常退出,我们必须响应WM_DESTROY消息,并在消息响应代码中调用PostQuitMessage,向应用程序的消息队列中投递 WM_QUIT消息。传递给PostQuitMessage函数的参数值将作为WM_QUIT消息的wParam参数,这个值通常用做WinMain函数 的返回值。
38、39行代码:DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处 理。对于大多数的消息,应用程序都可以直接调用DefWindowProc函数进行处理。在编写窗口过程时,应该将DefWindowProc函数的调用 放到default语句中,并将该函数的返回值作为窗口过程函数的返回值。
读者可以试着将38、39行代码注释起来,运行一下,看看会有什么结果。
提示:运行之后,在NT4.0/Win2000下启动任务管理器,切换到进程标签,查看程序是否运行。