第3章
创建窗口与消息响应
本章介绍Windows中最基本的概念——窗口,以及围绕着窗口的消息响应。这是所有Windows应用程序的基础。
3.1 Windows应用程序的基本概念
3.1.1 窗口
毫无疑问,基于窗口的Win32应用程序是本书的主角,默认情况下不选择空项目,直接单击完成,应用程序向导会帮助我们完成一个最基本的Windows应用程序,如图2-4所示。
单击工具栏中的按钮,或者选择菜单“调试”→“启动调试”,或者直接按F5键就会编译、链接并且执行当前的应用程序,应该出现如图3-1所示的窗口。
图3-1 窗口的基本要素
如图3-1所示的这个窗口虽然没有任何内容,但是“麻雀虽小、五脏俱全”。下面介绍Windows最基本的元素“窗口”(Window,单数)所包含的基本元素。首先,窗口最外侧是边框;最上方的是标题栏;标题栏的左侧是图标,紧挨着是标题(在这里就是项目名称FirstWinApp);标题栏的右侧依次是“最小化”按钮、“最大化”按钮以及“关闭”按钮;标题栏的下方是菜单,准确地说是下拉菜单;中间很大的空白区域就是客户区,之后的大部分操作都是针对客户区的。
单击菜单“帮助”→“关于”,还会弹出一个有关版权信息的对话框。对话框也是窗口,它可以有标题栏也可以没有标题栏。装饰对话框的还有各式各样的按钮、单选钮、复选框、滚动条和文本框,这些都是窗口。或者更确切地说,这些都称为“子窗口”或“控件窗口”或“子窗口控件”。从操作系统的名字Windows就能够理解这款操作系统是由各种各样的窗口构成的。
3.1.2 入口函数
我们在学习C/C++编程语言时就知道,main函数是程序的主函数或入口函数,如果要使用Unicode,则要求主函数名是wmain。例如大多数C语言教科书中的第一个Hello World程序,往往是这样的:
main() { printf("Hello World!\n"); }
在编写Windows应用程序时,要求入口函数名是WinMain,对应Unicode的则是wWinMain。我们可以创建一个新的Win32应用程序,并在向导中选择“空项目”,如图3-2所示。
图3-2 选择“空项目”
在创建好项目后,在“源文件”的右键菜单中选择“添加”→“新建项”,如图3-3所示。
图3-3 添加新建项
在弹出的如图3-4所示的“添加新项”对话框中,选择“代码”→“C++文件”,然后在对话框下方填写任意文件名,例如main。最后单击“添加”按钮。
图3-4 “添加新项”对话框
在新添加的main.cpp中输入如下代码:
#include <windows.h> int APIENTRY WinMain(HINSTANCE hInstance,//当前应用程序的实例句柄 HINSTANCE hPrevInstance,//永远为0,已经不用了 LPSTR lpCmdLine,//命令行参数 int nCmdShow) //主窗口初始化时的显示方式 { MessageBox(NULL,L"Hello World!",L"My First Windows App",MB_OKCANCEL); return 0; }
运行结果如图3-5所示。
图3-5 窗口版本的Hello World
当由一个进程创建另一个进程(例如棋牌游戏大厅启动某一个具体游戏)时,会由第一个应用程序(棋牌游戏大厅)调用CreateProcess函数来执行第二个应用程序(斗地主),就可以通过CreateProcess的实参给第三、第四个参数传递数据。对于本书所涉及的内容,WinMain的四个参数都用不到。有兴趣的读者可以参考其他相关资料。
MessageBox的作用是弹出一个小的对话框向用户显示短信息,并将用户最终的选择返回给调用者。其函数原型如下:
int MessageBox( HWND hWnd,//父窗口句柄(后面还会详细介绍)这里为NULL,表示没有父窗口 LPCTSTR lpText,//将要显示的字符串 LPCTSTR lpCaption,//在对话框的标题栏显示的字符串 UINT uType //指定对话框的内容和行为 );
在上面的程序中,MessageBox的第四个参数uType使用了MB_OKCANCEL,表示对话框出现“确定”(OK)和“取消”(CANCEL)两个按钮。注意,MB_OKCANCEL实际上是预定义的一个字符常量,并非字符串。例如使用参数MB_YESNO|MB_ICONEXC LAMATION,则会弹出如图3-6所示的对话框。完整的uType的含义请参见MSDN。
图3-6 MessageBox对话框
3.1.3 消息和窗口函数
创建窗口后,就要对窗口的行为负责。例如,当用鼠标拖动窗口的边框时,窗口的大小会随之改变;当用户拖曳窗口的标题栏时,窗口会跟随鼠标移动;如果“最大化”按钮可用,当用户单击“最大化”按钮时,窗口会最大化。这也正是窗口界面的友好性。
但是,应用程序如何能够知道用户在窗口上的动作呢?以窗口最大化为例,是不是应用程序完成了这整个过程呢?答案:不是,是Windows本身而不是应用程序处理了窗口最大化的所有命令。但是,Windows会以消息的形式通知应用程序:“窗口最大化了”。在接收到最大化的消息后,应用程序会根据窗口最大化时的需要重新绘制窗口。
Windows是怎么向应用程序发送消息呢?这里的消息并非eMail或者QQ中的消息。一定要注意,对于程序员来讲,所谓发送消息就是调用某一个函数,并把消息的内容通过函数的参数进行传递。对于Windows应用程序,除了要求必须有一个主函数(WinMain)外,还必须有一个窗口函数。窗口函数的作用就是来处理各种消息。当Windows向应用程序发送消息时,它会调用程序中的窗口函数。窗口函数的原型如下:
LRESULT CALLBACK WindowProc( HWND hwnd,//窗口句柄 UINT uMsg,//消息类型 WPARAM wParam,//消息参数 LPARAM lParam //消息参数 );
其中第一个参数表示接收消息的窗口的句柄;第二个参数表示消息的类型,例如WM_ LBUTTONDOWN(鼠标左键按下)、WM_CLOSE(关闭窗口)、WM_KEYDOWN(键盘按下)、WM_TIMER(定时器消息),等等。第三、第四个参数针对不同的消息会有不同的含义,例如针对WM_MOUSEMOVE(鼠标移动)消息,两个参数就用来表示鼠标位置等信息,而针对WM_KEYUP(按键弹起)消息,两个参数就用来表示键码等信息。
一个Windows应用程序至少应该包含两个函数:主函数WinMain和窗口函数Window-Proc,具体如何使用将在3.2节中阐述。
细心的读者可能已经发现,在WinMain函数前有一个修饰APIENTRY,而在WindowProc前有一个修饰CALLBACK,这是什么含义呢?我们可以在系统头文件windef.h中发现它们的定义都是__stdcall(注意是连续两个下画线)。与__stdcall对应的还有__cdecl、__fastcall和__thiscall等函数调用方式。它们的区别主要在于参数传递上,例如是从左到右还是从右到左,退出时的方式等细节问题。建议初学者就不要深究了。大家还要记住,这两个函数都是由操作系统来调用的。程序员采用C/C++编码的各种函数之间的互相调用,则采用__cdecl(C语言的默认调用方式)。
3.1.4 进队消息与不进队消息
Windows给窗口发送消息的含义是Windows调用窗口过程。但是,WinMain也有一个消息循环,它调用GetMessage从消息队列中取出消息,并且调用DispatchMessage将消息发送给窗口函数。
Windows把消息分为“进队消息”和“不进队消息”。进队消息是由Windows放入程序的消息队列中的,在程序的消息循环中再次发送到窗口函数。不进队的消息在Windows调用窗口函数时直接发送给窗口函数。实际上,殊途同归,无论通过哪条途径,窗口函数都将获得窗口的所有消息——包括进队的和不进队的。
进队消息主要是指各种输入,例如各种按键消息、字符消息、鼠标消息、定时器消息以及刷新消息和退出消息。
在很多情况下,不进队消息来自于调用特定的API函数。例如,当WinMain调用Create Window时,Windows将创建窗口并给窗口函数发送一个WM_CREATE消息。当WinMain调用ShowWindow时,Windows将给窗口函数发送一个WM_SIZE和WM_SHOWWINDOWS消息。当WinMain调用UpdateWindow时,Windows将给窗口过程发送一个WM_PAINT消息。
这一过程显然很复杂,但幸运的是,其中大部分都是由Windows来解决的,不关程序员的事。从窗口函数的角度来看,各种消息都是以一种有序的、同步的方式进出的,窗口函数可以处理它们,也可以调用DefWindowProc进行默认的处理。在这里,“有序的、同步的”的含义是说,当窗口函数在处理一个消息时,不会被其他的消息所中断。
3.1.5 使用MSDN来学习窗口消息
Win32 API的编程,主要就是应用程序针对不同的窗口消息完成响应。针对不同的应用程序,消息就是那些消息,只是有不同的响应。对于初学者来讲,掌握应用程序需要处理的窗口消息是最重要的。当然,并不是说对所有的窗口消息都需要响应。而描述窗口消息的最权威的文档就是MSDN。
打开MSDN,在索引视图中输入一个消息,往往在索引结果中会出现两个选择,其中一个是针对MFC的,我们需要的是位于“Windows Management”的“WM_KEYDOWN Notifi cation()”,如图3-7所示。
图3-7 MSDN界面
在MSDN中,窗口消息的描述主要有以下几个部分:
(1)对该消息的一些描述。例如在什么情况下会收到这个消息。
(2)语法Syntax。对于窗口消息来讲,这一部分没任何区别。
(3)参数Parameters。这部分分别针对wParam和lParam进行了完整的描述。
(4)返回值Return Value。应用程序在响应某个窗口消息后,不同的返回值代表不同的含义。
(5)其他一些注意事项Remarks。很多窗口函数的这一部分也很关键,程序出错了很有可能是因为没有吃透这一部分。
(6)编程环境配置方面的内容。例如所需要的头文件、操作系统的最低版本等。好在窗口函数的头文件往往都包含在windows.h中了。
(7)一些相关的链接See Also。
3.2 创建窗口
了解了上一节所描述的窗口、消息、主函数、窗口函数的各种概念之后,下面将仔细地描述具体的每一个步骤。
首先介绍典型的Win32窗口应用程序结构,通常需要以下7个步骤:
(1)程序入口点(WinMain函数)。
(2)注册窗口类(RegisterClass/Ex)。
(3)创建窗口类(CreateWindow/Ex)。
(4)显示主窗口(Show Window)。
(5)更新主窗口(Update Window)。
(6)进入消息循环(GteMessage→TranlateMessage→DispatchMessage→对相应消息的处理)。
(7)程序出口点(WinMain返回)。
按照这个顺序,我们来设计一个窗口。首先,我们在VC下选择建立一个Win32应用程序,并在应用程序的配置中选中“空项目”,我们获得一个完全空白的项目,之后打开解决方案资源管理器,按照图3-2所示,在源代码中添加新项,选择C++文件,即后缀名为cpp的文件,之后输入相应的代码。源代码如下:
第1行:#include <Windows.h> 第2行:#include <tchar.h> 第3行://声明回调函数 第4行:LRESULT CALLBACK WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam); 第5行://窗口类名和窗口标题 第6行:const TCHAR szWindowClass[]=L"第一个窗口"; 第7行:const TCHAR szWindowTitle[]=L"主窗口标题"; 第8行://WinMain函数,入口点 第9行:int WINAPI _tWinMain (HINSTANCE hInstance,HINSTANCE hPreInstance,LPTSTR lpCmdLine,int nCmdShow) 第10行:{ 第11行://注册窗口类 第12行:WNDCLASSEX wcex={ 0 }; //窗口类结构体 第13行:wcex.cbSize=sizeof(WNDCLASSEX); 第14行:wcex.style=CS_HREDRAW|CS_VREDRAW; 第15行:wcex.lpfnWndProc=(WNDPROC)WndProc; 第16行:wcex.hInstance=hInstance; 第17行:wcex.hIcon=LoadIcon(NULL,IDI_APPLICATION);//使用系统默认的图标 第18行:wcex.hCursor=LoadCursor(NULL,IDC_ARROW);//使用系统默认的光标 第19行:wcex.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);//白色画刷 第20行:wcex.lpszClassName=szWindowClass; 第21行:RegisterClassEx(&wcex); 第22行://创建窗口 第23行:HWND hWnd=CreateWindow( 第24行:szWindowClass,//窗口类名 第25行:szWindowTitle,//窗口标题 第26行:WS_OVERLAPPEDWINDOW,//窗口风格 第27行:100,200,500,500,//左上角坐标以及宽度、高度 第28行:HWND_DESKTOP, 第29行:NULL, 第30行:hInstance, 第31行:NULL 第32行:); 第33行:if(!hWnd) return FALSE; //如果窗口创建失败则退出 第34行://显示并更新窗口 第35行:ShowWindow(hWnd,nCmdShow); 第36行:UpdateWindow(hWnd); 第37行://进入消息循环 第38行:MSG msg; 第39行:while(GetMessage(&msg,NULL,0,0)) 第40行:{ 第41行: TranslateMessage(&msg); 第42行: DispatchMessage(&msg); 第43行:} 第44行:return msg.wParam; 第45行:} 第46行://窗口函数,用于消息处理 第47行:LRESULT CALLBACK WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam) 第48行:{ 第49行: switch(msg) 第50行: { 第51行: case WM_DESTROY: 第52行: PostQuitMessage(0); 第53行: return 0; 第54行: default: 第55行: return DefWindowProc(hWnd,msg,wParam,lParam); 第56行: } 第57行:}
如果没有错误,应当在(100,200)的位置生成如图3-8所示的窗口,大小为500像素×500像素。
图3-8 第一个窗口程序
下面将对具体的代码进行详细讲解。
3.2.1 包含头文件
首先,我们包含了两个头文件,一个是windows.h文件,它是一切Win32窗口必须包含的头文件;同时为了兼容Unicode编码,也包含了tchar.h头文件,此时需要的主函数名应该是_tWinMain。
3.2.2 定义类名和标题字符串
我们声明了两个TCHAR类型的字符数组szWindowClass和szWindowTitle分别存储窗口的类名和窗口标题。设置const是因为它们是不需要去改变的常量,常量使用const定义或者字符串资源列表是个好习惯。若你更改这行为:
const TCHAR szWindowClass[]=L"我的游戏";
此时,窗口显示的最上面一栏将是“我的游戏”。
3.2.3 注册窗口
窗口创建前需要先注册窗口,我们需要使用结构体WNDCLASSEX来声明一个变量,其中包含了窗口的各种属性。当然,也可以使用WNDCLASS结构体,它和WNDCLASSEX类型的区别仅仅是EX是增强型窗口,它多两个参数而已。以后我们接触的很多类和函数也常有EX结尾和无EX结尾的,区别也都如此,仅仅是一个增强型和非增强型而已。值得注意的是在定义的同时赋初值为0。赋初值是一个好习惯,避免了我们忘记赋值带来的程序错误,因为无法确保该块内存原本存放了些什么。WNDCLASSEX定义如下:
typedef struct { UINT cbSize; //结构体的大小,通常等于sizeof(WNDCLASSEX) UINT style; //窗口类的风格 WNDPROC lpfnWndProc; //窗口函数名 int cbClsExtra; //窗口类占用的额外内存 int cbWndExtra; //窗口占用的额外内存 HINSTANCE hInstance; //应用程序实例句柄 HICON hIcon; //图标句柄 HCURSOR hCursor; //鼠标的光标句柄 HBRUSH hbrBackground; //背景画刷句柄 LPCTSTR lpszMenuName; //菜单的资源名称 LPCTSTR lpszClassName; //类名 HICON hIconSm; //小图标的句柄 } WNDCLASSEX,*PWNDCLASSEX;
其中,cbSize代表着wcex结构体变量的大小,我们就是通过这个参数来区别到底wcex是EX增强版还是普通版。对应wcex.cbSize=sizeof (WNDCLASSEX)。
style是类的风格,第14行写的是CS_HREDRAW|CS_VREDRAW,意味着当窗口宽度或高度发生改变时,窗口将根据窗口大小进行重新绘制。详细的解释请参见下一节。
lpfnWndProc用于指定窗口函数。请注意,窗口函数只需要符合WindowProc的函数原型并保持一致即可,窗口函数名可以任意。这里用的是WndProc。
cbClsExtra代表额外的类的内存,可以在其中存放窗口类所共有的数据,这个对于游戏编程并不重要,可以默认为0。cbWndExtra为额外的窗口的内存,可以选择其中存放的每个窗口所拥有的数据,依旧不重要,默认为0。
hInstance用于标志应用程序类实例句柄,已经由WinMain函数传递进来。
hIcon为窗口图标,也就是窗口标题栏左上角的图标。这里通过LoadIcon获取系统默认的应用程序图标。
hCursor为窗口的鼠标光标类型,这里通过LoadCursor获取系统默认的光标。我们在游戏窗口中很少使用微软公司默认的光标,而常常使用的自定的手状或剑状图标,我们可以在这里更改。
hbrBackGround是默认的窗口背景颜色。这里通过GetStockObject获取系统提供的白色画刷。
lpszMenuName是默认的菜单名。这里使用默认的初始值NULL,表示没有菜单。
lpszClassName是这个窗口类的名字,我们创建窗口时是依靠名字来指定窗口类的。
hIconSm是当窗口被最小化时,WIN任务栏显示的程序图标。这里使用默认的初始值NULL,表示没有。
对于第21行代码,在设置好了这些窗口属性之后,我们将它传参给RegisterClassEx来注册我们的窗口。
3.2.4 窗口类的风格
在程序中,我们使用CS_HREDRAW|CS_VREDRAW来表示窗口类的风格。其中前缀CS_代表着class style,CS_HREDRAW代表着当宽度发生改变(H代表水平方向horizontal)时进行重绘(Redraw),CS_VREDRAW代表着当高度发生改变(V代表垂直方向vertical)时进行重绘。按位或|则代表着两个同时有效。为什么会这样呢?在winuser.h中定义了全部可选样式,如下:
#define CS_VREDRAW 0x0001 #define CS_HREDRAW 0x0002 #define CS_DBLCLKS 0x0008 #define CS_OWNDC 0x0020 #define CS_CLASSDC 0x0040 #define CS_PARENTDC 0x0080 #define CS_NOCLOSE 0x0200 ……
在这里,预定义的符号常量实际上使用了不重复的数据位,所以在组合使用的同时不会发生混淆。
3.2.5 创建窗口
注册完毕后,调用CreateWindowEx来创建我们的窗口,其原型如下:
HWND CreateWindow ( LPCTSTR lpClassName,//创建的窗口的基础类名 LPCTSTR lpWindowName,//窗口的名称 DWORD dwStyle,//窗口的类型 int x,//该窗口左上角位置X坐标 int y,//该窗口左上角位置Y坐标 int nWidth,//窗口的宽度 int nHeight,//窗口的高度 HWND hWndParent,//父窗口句柄 HMENU hMenu,//菜单句柄 HINSTANCE hInstance,//应用程序句柄 LPVOID lpParam); //通过WM_CREATE消息的参数传递给窗口函数
在第23~32行,我们使用CreateWindow在屏幕坐标[100,200]处创建了一个大小为500×500的正方形窗口。第33行代码检查窗口是否创建成功。这也是一种良好编程习惯,可以增强程序健壮性。如果你不知道该设置多大的窗口尺寸,可以把nWidth和nHeight都设置为CW_USEDEFAULT,那么操作系统会使用默认的窗口尺寸。并且,x和y两个参数同样也可以使用CW_USERDEFAULT,表示操作系统使用默认的位置来创建这个窗口。在5.2.7节“改变窗口的位置与尺寸”中,我们会使用背景图片的尺寸来计算窗口的尺寸,保证客户区正好能够容纳一张图片,并且整个窗口在屏幕的中间。
3.2.6 窗口风格
除了窗口类以外,还有各种窗口风格供用户指定窗口的绘制及其行为。其中有3种最重要的风格创建了3种最基本的窗口类型:重叠窗口、弹出窗口和子窗口。
(1)重叠窗口(Overlapped Window):具有应用程序主窗口的全部特点。它的非客户区包括一个可伸缩的框架、菜单栏、标题栏和最小化、最大化按钮。
(2)弹出窗口(Popup Window):具有消息框或者对话框的全部特点。它的非客户区包括一个固定大小的框架和一个标题栏。
(3)子窗口(Child Window):具有类似按钮控件的全部特点。它没有非客户区,窗口的处理过程负责绘制窗口的每个部分。
如果我们想创建一个在游戏中常用的全屏窗口,应该如下调用CreateWindow:
HWND hWnd=CreateWindow( szWindowClass, szWindowTitle, WS_POPUP,//因为是固定大小,所以选择弹出窗口 0,0,//窗口的位置在屏幕的左上角 GetSystemMetrics(SM_CXSCREEN),//与屏幕等宽 GetSystemMetrics(SM_CYSCREEN),//与屏幕等高 NULL, NULL, hInstance, NULL );
其中,通过GetSystemMectrics函数获得了屏幕的宽度和高度。除了获取屏幕的尺寸属性之外,根据其参数的不同,该函数还可以获取其他系统属性,包括菜单栏的高度、工具栏的高度、窗口边框的高度等,现在不一一描述,详情请参考MSDN。
在3.2节中代码的第26行,我们使用WS_OVERLAPPEDWINDOW作为窗口风格。查看MSDN,我们可以知道这种风格实际上包含了WS_OVERLAPPED(可重叠)、WS_ CAPTION(标题栏)、WS_SYSMENU(系统菜单)、WS_THICKFRAME(粗边框)、WS_ MINIMIZEBOX(最小化按钮)以及WS_MAXIMIZEBOX(最大化按钮)等多种风格。这是因为在winuser.h中,我们也可以看到如下的代码:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED|\ WS_CAPTION|\ WS_SYSMENU|\ WS_THICKFRAME|\ WS_MINIMIZEBOX|\ WS_MAXIMIZEBOX)
如果我们希望仅仅是把该风格中的最大化按钮去掉,其他风格仍然保留,应该怎么做呢?根据3.2.4节所描述“预定义的符号常量实际上使用不重复的数据位”,应该这样运算:WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX。
3.2.7 显示窗口
当我们创建窗口成功后,我们的窗口并没有在屏幕上显示出来,仅仅是在内存中将窗口的一切数据全部准备好了而已。在第35行代码,需要调用下面的函数来显示窗口。
ShowWindow(hWnd,nCmdShow); //显示窗口
首先将窗口句柄hWnd作为实参进行传递,方便系统知道我们准备显示哪个窗口。nCmdShow则是说明窗口显示的方式,是系统传递给WinMain的参数。nCmdShow也可以取不同的值,例如SW_HIDE将隐藏创建的窗口,SW_MINIMIZE将最小化创建的窗口,详细情况请参考MSDN。
3.2.8 更新窗口
UpdateWindow(hWnd); //更新窗口
如果指定窗口的更新区域不为话,UpdateWindow将通过向该窗口发送WM_PAINT消息来更新其客户区。
3.2.9 消息循环
当显示完窗口后,应用程序已经准备好从用户接受键盘和鼠标输入了,必须加入消息循环,不断地对各种消息进行响应,否则该程序永远陷入无响应的状态。
Windows为每个窗口维护了一个消息队列,每当有输入发生时,Windows就把用户的输入翻译成消息放在消息队列中。利用GetMessage函数可以从消息队列中取出一个消息放在MSG类型的结构体中。MSG的定义以及GetMessage的原型如下:
typedef struct { HWND hwnd; //窗口句柄 UINT message; //消息类型 WPARAM wParam; //消息参数 LPARAM lParam; //消息参数 DWORD time; //获取消息的时间 POINT pt; //获取消息时光标的位置 } MSG,*PMSG; BOOL GetMessage( LPMSG lpMsg,//MSG结构体变量的指针 HWND hWnd,//窗口句柄,NULL意味着当前窗口 UINT wMsgFilterMin,//这两个消息用于消息过滤,通常设为0 UINT wMsgFilterMax );
如果消息队列中没有任何消息,则GetMessage会一直等下去,直到有消息进入消息队列为止。GetMessage函数从消息队列中取得的消息如果是WM_QUIT(退出消息),则返回0,否则返回非零值。通常我们会利用这个返回值结束消息循环。
TranslateMessage(&msg)用于把键盘输入转换成字符消息,例如把WM_KEYDOWN和WM_KEYUP转换成WM_CHAR。
DispatchMessage(&msg)把消息分发到对应窗口的窗口函数中。
有了上述这些知识,最常见到的消息循环的代码如第37~43行:
MSG msg; while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
如上所述,如果获取消息为WM_QUIT,则GetMessage返回0,因此while循环退出,并最终导致WinMain结束。主函数退出就意味着整个程序也就结束了。
3.2.10 窗口函数
WinMain只是两个最基本函数中的一个。在WinMain中,我们注册了窗口类、创建并显示了窗口,获取的消息已经通过DispatchMessage发送到窗口函数了。窗口函数应该怎么来处理这些消息呢?真正的工作才刚刚开始,应用程序的各种功能要在第46~57行的WndProc中完成。例如,如何响应用户的输入,如何把计算的结果显示在窗口的客户区中,都是在窗口函数中完成的。窗口函数的原型如下:
LRESULT CALLBACK WindowProc( HWND hwnd,//窗口句柄 UINT uMsg,//消息类型 WPARAM wParam,//消息参数 LPARAM lParam //消息参数 );
窗口函数接收到的所有消息都被标志为一个字符常量,这就是该函数的第二个参数uMsg。这些字符常量都在winuser.h中定义好了,表3-1所列的就是一些常见的Windows消息。
表3-1 常见的Windows消息
通常,Windows程序员都用一个switch case结构来判断窗口函数接收到了什么样的消息,以及如何处理这些消息。同时,Win32 API也提供了一个名为DefWindowProc的函数,它可以对各种消息进行默认处理。一般情况下,窗口函数的结构形式大致如下:
switch(uMsg) { case WM_DESTROY: //处理WM_DESTROY消息的代码; return 0; case WM_PAINT: //处理WM_PAINT消息的代码; return 0; case …… //处理该消息的代码; return 0; default: //对于不需要处理的消息,则调用默认的消息处理函数 return DefWindowProc(hWnd,uMsg,wParam,lParam); }
程序员不必亲自处理每一个消息,但是必须把每一个不处理的消息交给DefWindow Proc进行处理,同时要把它的返回值返回Windows,否则Windows就失去了与应用程序通信的途径,也就不能再控制窗口的行为了。
在Win 3.x中,WPARAM是16位的,而LPARAM是32位的,两者有明显的区别。因为地址通常是32位的,所以LPARAM被用来传递地址,这个习惯在Win32 API中仍然能够看到。在Win32 API中,WPARAM和LPARAM都是32位,所以没有什么本质的区别。Windows的消息必须参考MSDN才能知道具体的含义。如果是你的自定义消息,你随意使用这两个参数。但是习惯上,我们愿意使用LPARAM传递地址,而用WPARAM传递其他参数。
3.2.11 应用程序的退出
当用户关闭窗口时,窗口函数就会收到一个WM_DESTROY消息。正如第51~53行代码所示,窗口函数应该调用PostQuitMessage(0)向消息队列插入一个WM_QUIT消息。在3.2.9节已经提到,GetMessage如果从消息队列中取得的是WM_QUIT消息,它将返回0,从而导致消息循环结束,WinMan函数退出,整个应用程序退出。
一定要注意,WM_DESTROY是窗口函数必须处理的消息。DefWindowProc并没有我们所需要的功能。请你尝试着把第51~53行代码注释掉,然后执行程序,你会发现窗口依然消失了,似乎一切都很正常。但你会发现Visual C++的调试菜单依然表明程序仍在运行;打开任务管理器,也会发现程序仍在运行。这能够说明两个问题:
(1)尽管第51~53行代码基本上是每一个应用程序都必须实现的功能,但是作为默认的窗口函数,DefWindowsProc并没有实现这个功能。
(2)在Windows中,窗口和进程是有联系的两个对象。窗口关闭了,并不能说明进程也关闭了。
在3.2节这个程序中,只有通过单击窗口右上角的“关闭”按钮来关闭窗口并退出应用程序。能不能在结束应用程序之前弹出一个对话框来确认一下我们的操作呢?要做到这一点,首先要了解从单击“关闭”按钮开始一直到最后应用程序退出到底发生了什么。其实这是一个很“复杂”的过程,描述如下。
(1)单击窗口右上角的“关闭”按钮,系统向消息队列插入WM_CLOSE消息。
(2)窗口函数调用DefWindowProc处理WM_CLOSE消息:调用DestroyWindow函数。
(3)窗口关闭,并向消息队列插入WM_DESTROY消息。
(4)窗口函数处理WM_DESTROY消息:调用PostQuitMessage函数,向消息队列插入WM_QUIT消息。
(5)主函数的消息循环中的GetMessage获取WM_QUIT消息返回0,导致消息循环结束,进而WinMain函数结束,再进而整个进程结束。
我们可以看到这么一个貌似简单的过程实际上涉及了三个消息:WM_CLOSE代表着用户希望结束应用程序;WM_DESTROY代表着窗口的关闭;WM_QUIT代表着进程的结束。仔细想一想,还真得这么做。
怎样做才能在结束应用程序之前弹出一个对话框来确认我们的操作呢?我们看到,由于在窗口函数中并没有对WM_CLOSE消息进行处理,所以第(2)步中是调用Def Window Proc进行默认的处理。我们应该用自己的代码来代替DefWindowProc。修改WndProc如下:
//窗口函数,用于消息处理 LRESULT CALLBACK WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam) { int nSel=0; switch(msg) { case WM_CLOSE: nSel=MessageBox(hWnd,L"你真的要退出吗-",szWindowTitle,MB_YESNO| MB_ICONQUESTION); if (nSel == IDYES) DestroyWindow(hWnd); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProc(hWnd,msg,wParam,lParam); } }
3.3 关键在于应用
即使对Windows应用程序的框架进行了详细的说明,通常读者仍然可能对程序的结构和原理感到神秘。在之前学习C/C++以及数据结构等编程课程时,所有的代码都包含在main函数中;而在Windows应用程序中,WinMain只包含了注册窗口类、创建窗口、消息循环等所必需的代码,程序的所有动作都在窗口函数中发生。但是在后面的章节中,读者将发现,Windows应用程序所做的一切都是响应发送给窗口函数的消息。这是在概念上的主要难点之一。在继续更复杂的编程之前,这一点必须搞清楚。
我们可以使用一辆汽车从图纸到最终运行的过程来粗略地类比Windows应用程序的设计步骤。
(1)第12~20行,设计窗口类就好像是为汽车设计图纸。
(2)第21行,注册窗口类就好像是把图纸拿到主管部门去报批。
(3)第22~33行,创建窗口就好像是在生产车间里生产汽车。
(4)第35行,显示窗口就好像是到4S店进行展示。
(5)第36行,更新窗口就好像是买好汽车后对汽车进行装饰。
(6)消息循环就是不断对外界的各种输入(红绿灯、道路状况)进行响应(离合、油门、刹车、方向盘)。
其中(1)~(5)步是由汽车工程师完成的,而第(6)步是由司机完成的。游戏程序员不属于系统程序员,而属于应用程序的开发人员。类比于汽车,游戏程序员更像司机而非汽车工程师。因此,在后面的章节中,我们编程的重点应该是窗口函数。
3.4 习题
(1)Windows应用程序的入口点是哪个函数?
(2)创建窗口需要经过哪几个步骤?
(3)解释GetMessage、TranslateMessage和DispatchMessage的作用。
(4)简述Windows程序退出时的消息处理过程。
(5)窗口函数中的“return DefWindowProc(hWnd,msg,wParam,lParam);”能否省略?为什么?
(6)创建一个大小为200×200的窗口,要求位于整个屏幕的中间;当使用左键或右键单击客户区任意位置时,弹出一个对话框,询问是否退出。是,则退出应用程序,否则返回。提示:改变窗口的位置与尺寸可以参考5.2.7节;在窗口函数中响应WM_LBUTTONDOWN函数完成弹出对话框的功能,可以参考4.4.3节。
(7)至少完整地翻译MSDN中一个窗口消息的帮助文档。