目前游戏开发中有两套输出消息的体系。一套就是我们本章要讲的Windows 消息处理,另一套就是在本书后面讲解DirectX时讲到的为游戏而生的Directlnput 消息处理API 。很多时候Directlnput 解决不了的问题,还得反过来找Windows消息处理帮忙呢。
1.1 Windows 键盘消息处理
首先我们对Windows 系统下键盘的基本概念及键盘消息的处理方式来一个简单的介绍。
1.1.1 虚拟键码与键盘消息
在Windows 中,所有键盘的按键都被定义为一组通用的“虚拟键码”,也就是说在Windows系统下所有按键都会被视为虚拟键(包含鼠标键在内),而每一个虚拟键都有其对应的一个虚拟键码。
Windows 系统是一个消息驱动的环境, 一旦我们在键盘上进行输入操作,那么系统便会接收到对应的键盘消息,下面我们再列出最常见的5 种键盘息。
当某一按键被接下时,伴随着这个操作所产生的是以虚拟键码类型传送的WM _KEYDOWN 与WM_KEYUP 消息。当程序接收到这些消息时。便可由虚拟键码的信息来得知是哪个按键被按下。
此外, WM_CHAR 则是当按下的按键为定义于ASCII 中的可打印字符时, 便发出此字符消息。
7.1.2 键盘消息处理
在Windows 中,我们把键盘消息和其他消息(比如窗口重绘消息WM_PAINT ) 一视同仁,同样是在消息处理函数中间来处理的。而按下按键事件一定会紧随着一个松开按键的事件, 因此WM_KEYDOWN 与WM_KEYUP 两种消息必须是成对发生的。但我们往往在程序中对WM_KEYDOWN 消息进行处理,而无视WM KEYUP 消息。
我们在之前讲窗口过程函数的时候提到过,窗口过程函数有两个参数与消息输出有关,它们就是——wParam 和lParam :
- LRESULT CALLBACK WindowProc(
- __in HWND hwnd,
- __in UINT uMsg,
- __in WPARAM wParam,
- __in LPARAM lParam
- );
当键盘消息触发时, wParam 的值为按下按键的虚拟键码, Windows 中所定义的虚拟键码是以“VK”开头的, lParam 则储存按键的相关状态信息, 因此,如果我们的程序要对键盘输入操作进行处理, 就可以用一个switch 语句来判断wParam 中的内容井进行理。那么消息处理函数的内容可以定义如下:-
-
-
- LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
- {
-
- switch( message )
- {
-
- case WM_KEYDOWN:
-
- switch (wParam)
- {
- case VK_ESCAPE:
- DestroyWindow(hwnd);
- PostQuitMessage( 0 );
- break;
- case VK_UP:
- break;
- case VK_DOWN:
- break;
- case VK_LEFT:
- break;
- case VK_RIGHT:
- break;
- }
- break;
-
- case WM_DESTROY:
- Game_CleanUp(hwnd);
- PostQuitMessage( 0 );
- break;
-
- default:
- return DefWindowProc( hwnd, message, wParam, lParam );
- }
-
- return 0;
- }
1.1.3 示例程序GDldemo10
这个示例让玩家以【↑ 】【↓ 】【← 】【→ 】键, 控制画面中李逍遥的上下左右的移动, 颇有些在玩仙剑奇侠传的感觉。
这里使用了李逍遥在4 个不同方向上走动的连续图案:

我们来看看代码。
程序代码片段一,全局变量声明:
-
-
-
- HDC g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;
- HBITMAP g_hSprite[4]={NULL},g_hBackGround=NULL;
- DWORD g_tPre=0,g_tNow=0;
- int g_iNum=0,g_iX=0,g_iY=0;
- int g_iDirection=0;
程序代码片段二, 窗口过程函数WndProc:-
-
-
- LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
- {
-
- switch( message )
- {
-
- case WM_KEYDOWN:
-
- switch (wParam)
- {
- case VK_ESCAPE:
- DestroyWindow(hwnd);
- PostQuitMessage( 0 );
- break;
- case VK_UP:
-
- g_iY -= 10;
- g_iDirection = 0;
- if(g_iY < 0)
- g_iY = 0;
- break;
- case VK_DOWN:
- g_iY = 10;
- g_iDirection = 1;
- if(g_iY > WINDOW_HEIGHT-135)
- g_iY = WINDOW_HEIGHT-135;
- break;
- case VK_LEFT:
- g_iX -= 10;
- g_iDirection = 2;
- if(g_iX < 0)
- g_iX = 0;
- break;
- case VK_RIGHT:
- g_iX = 10;
- g_iDirection = 3;
- if(g_iX > WINDOW_WIDTH-75)
- g_iX = WINDOW_WIDTH-75;
- break;
- }
- break;
-
- case WM_DESTROY:
- Game_CleanUp(hwnd);
- PostQuitMessage( 0 );
- break;
-
- default:
- return DefWindowProc( hwnd, message, wParam, lParam );
- }
-
- return 0;
- }
程序代码片段三, Game_Init()函数:-
-
-
- BOOL Game_Init( HWND hwnd )
- {
- HBITMAP bmp;
-
- g_hdc = GetDC(hwnd);
- g_mdc = CreateCompatibleDC(g_hdc);
- g_bufdc = CreateCompatibleDC(g_hdc);
- bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT);
-
-
- g_iX = 150;
- g_iY = 350;
- g_iDirection = 3;
- g_iNum = 0;
-
- SelectObject(g_mdc,bmp);
-
- g_hSprite[0] = (HBITMAP)LoadImage(NULL,L"go1.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
- g_hSprite[1] = (HBITMAP)LoadImage(NULL,L"go2.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
- g_hSprite[2] = (HBITMAP)LoadImage(NULL,L"go3.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
- g_hSprite[3] = (HBITMAP)LoadImage(NULL,L"go4.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
- g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
-
- Game_Paint(hwnd);
- return TRUE;
- }
上面这段代码和之前讲解的基本类似,就是初始化三缓冲环境,载入位图以及设定全局参数的初始值。程序代码片段四, Game_ Paint()函数:-
-
-
- VOID Game_Paint( HWND hwnd )
- {
-
- SelectObject(g_bufdc,g_hBackGround);
- BitBlt(g_mdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);
-
-
- SelectObject(g_bufdc,g_hSprite[g_iDirection]);
- BitBlt(g_mdc,g_iX,g_iY,60,108,g_bufdc,g_iNum*60,108,SRCAND);
- BitBlt(g_mdc,g_iX,g_iY,60,108,g_bufdc,g_iNum*60,0,SRCPAINT);
-
- BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);
-
- g_tPre = GetTickCount();
- g_iNum ;
- if(g_iNum == 8)
- g_iNum = 0;
-
- }
以上这段代码先贴背景图到g_mdc 中,然后根据目前的移动方向取出对应人物的连续走动图,并根据当前图号、宽度以及高度截取人物图中的单张人物一部分贴到g_mdc 中, 最后再将处理完成的g_mdc 中的图贴到g_hdc 中。来看一下运行截图:

1.2 Windows 鼠标消息处理
1.2.1 鼠标消息的处理方式
大家都知道, 目前市场上主流鼠标规格为两个按键加上一个滚轮。那么,我们先列出Windows中这种鼠标设备输入时的消息类型:
处理鼠标消息的方法与处理键盘消息的方法类似,同样是在消息处理函数中加入要处理的鼠标消息类型, 当鼠标消息发生时,输入的参数wParam 与lParam 就储存了鼠标状态的相关信息。
下面我们分别来展开讲解一下wParam 与lParam 参数以及滚轮消息。
1. IParam 参数
IParam 参数的值可分为高位字节与低位字节两个部分,其中高节部分储存的是鼠标光标所在的X 坐标值,低位字节部分存储的则是鼠标光标所在的Y 坐标值。
我们可以用下面两个宏来取得鼠标的坐标值:
- WORD LOWORD(
- DWORD dwValue
- );
-
- WORD HIWORD(
- DWORD dwValue
- );
2. wParam 参数
wParam 参数的值记录着鼠标按键及键盘CtrI 键与Shift键的状态信息,我们一般通过下面的这些定义在“ WINUSER . H ”中的测试标志与wParam 参数来检查上述按键的按下状态。

比如某个鼠标消息发生时, 要测试鼠标左键是否也被按下, 就把wPararn 拿着和某种消息& (逻辑与) 一下, 就像这样:
- if (wParam & MK_LBUTTON)
- {
-
- }
我们就是这样利用wParam 参数和测试标志来测试鼠标键是否被按下的。当按键被按下时, 条件式“ wParam &MK_LBUTTON ”所传回的结果就会为true 。当然, 若消息函数接收到“WM_LBUTTONDOWN ”消息,同样也可以知道鼠标键被按下而不必再去额外做这样的测试,这点大家要注意。
比如要测试鼠标左键与Shift 键的按下状态,那么程序我们就这样来写:
- if (wParam & MK_LBUTTON)
- {
- if (wParam & MK_CONTROL) //单击了鼠标左键,也按下了下Ctrl键
- {
-
- }
- else
- {
-
- }
- }
- else
- {
- if (wParam & MK_CONTROL)
- {
-
- }
- else
- {
-
- }
- )
通过上面这段代码可以清楚地看到,就是这样的if···el se 组合,加上wParam 参数与测试标志,可以测试鼠标键、Shift键和CtrI 键是否被按下。
3 . 滚轮消息
我们来看一下鼠标滚轮转动消息WM_MOUSEWHEEL。当鼠标滚轮转动消息发生时,lParam参数中的值同样是记录光标所在的位置的,而wParam 参数则分为高位字节与低位字节两部分,低位字节部分跟前面一样是储存鼠标键与Shift、CtrI 键的状态信息的,而高位字节部分的值会是“ 120”或"-120” 。“120 ” 表示鼠标滚轮向前转动,而“-120”则表示向后转动。
这里wParam 高位组值与低位组值所在的函数同样是HIWORD()与LOWORD() 。
- HIWORD (wParam) ;
- LOWORD (wParam);
7.2.2 鼠标相关常用函数讲解
对各种鼠标输入消息及鼠标状态信息的获取方法有了基本认识之后,下面我们来介绍一些游戏程序中用鼠标来做输出设备时常用到的、比较好用的函数,让大家在编写处理鼠标消息相关游戏或者程序时更加得心应手。
1 . 设定鼠标光标位置的函数
我们可以用SetCursorPos 函数来设定光标的位置。在MSDN 中我们查到SetCursorPos 函数的定义如下:
- BOOL SetCursorPos(
- __in int X,
- __in int Y
- );
我们设定的坐标是相对于屏幕左上角的屏幕坐标而言。实际上,我们经常需要将这个屏幕坐标转换为游戏窗口中的游戏窗口坐标。因此需要用到API 中的一个将窗口坐标转换到屏幕坐标的函数,即
ClientToScreen() 。我们来看一下这个函数:
- BOOL ClientToScreen(
- __in HWND hWnd,
- __inout LPPOINT lpPoint
- );
既然有将窗口坐标转换为屏幕坐标的函数ClientToScreen ,当然也有它的逆向转换函数一一将屏幕坐标转换为窗口坐标的ScreenToClient 函数。我们在MSDN 中查一下这个函数的原型和用法:
- BOOL ScreenToClient(
- __in HWND hWnd,
- LPPOINT lpPoint
- );
2 . 显示与隐醺鼠标光标的函数
显示和隐藏鼠标光标就一个函数,这个函数也就一个参数,这个参数取true 的话就是显示光标,取false 的话就是隐藏光标。它就是ShowCursor 函数:
- int ShowCursor(
- __in BOOL bShow
- );
3 . 获取窗口外鼠标消息的函数
为了确保程序可以正确地取得鼠标的输入消息,需要在必要的时候使用SetCapture 函数来指定一下窗口,以取得鼠标在窗口外所发出的消息到这个窗口中。SetCapture 函数在MSDN 中的解释翻译成中文如下:
该函数在属于当前线程的指定窗口里设置鼠标捕获。一旦窗口捕获了鼠标,所有鼠标输入都针对该窗口,无论鼠标是否在窗口的边界内。同一时刻只能有一个窗口捕获鼠标。如果鼠标光标在另一个线程创建的窗口上,只有当鼠标键按下时系统才将鼠标输入指向指定的窗口。
SetCapture 函数的定义是这样的:
- HWND SetCapture(
- __in HWND hWnd
- );
如果调用了上面的SetCapture()函数并输入要取得鼠标消息的窗口代号,那么便可取得鼠标在窗口外所发出的消息。这种方法也适用于多窗口的程序。与SetCapture() 函数相对应的函数为ReleaseCapture()函数,用于释放窗口取得窗口外鼠标消息的函数, 它的定义非常简单:
- BOOL ReleaseCapture(void);
这个函数可以这么理解: ReleaseCapture 函数从当前线程中的窗口释放鼠标捕获,并恢复通常的鼠标输入处理。捕获鼠标的窗口接收所有的鼠标输入(无论光标的位置在哪里〉,除非单击鼠标键时,光标热点在另一个线程的窗口中。
4. 限制鼠标光标移动区域的函数
Windows API 中提供的ClipCursor()函数可以用来设置限制鼠标光标的移动区域和解除鼠标光标移动区域的限制。
- BOOL ClipCursor(
- __in const RECT *lpRect
- );
唯一的一个参数, const RECT 类型的* lpRect , 指向阳CT 结构的指针,该结构包含限制矩形区域左上角和右下角的屏幕坐标,如果该指针为NULL (空), 则鼠标可以在屏幕的任何区域移动。
这里有一个RECT 移动区域矩形,我们在MSDN 中找出它的声明如下:
- typedef struct tagRECT {
- LONG left;
- LONG top;
- LONG right;
- LONG bottom;
- } RECT;
5 . 取得窗口外部区域及内部区域的函数
最后,我们再讲一下取得窗口外部区域及内部区域的API 函数。他们分别是GetWindowRect、和GetClientRect 。在MSDN 中他们的定义如下:
- BOOL GetWindowRect(
- __in HWND hWnd,
- __out LPRECT lpRect
- );
- BOOL GetClientRect(
- __in HWND hWnd,
- __out LPRECT lpRect
- );
这里需要注意的是, GetWindowRect()返回的坐标类型是屏幕坐标。GetClientRect()返回的坐标类型是窗口坐标。
由于限制鼠标光标移动区域的ClipCursor()函数中输入的矩形区域必须是屏幕坐标,因此如果取得的是窗口内部区域,那么还必须将窗口坐标转换为屏幕坐标的操作。下面我们用一段程序代码来说明将鼠标光标限制在窗口内部区域移动的过程:
-
- POINT lt,rb;
- RECT rect;
- GetClientRect(hwnd,&rect);
-
- lt.x = rect.left;
- lt.y = rect.top;
-
- rb.x = rect.right;
- rb.y = rect.bottom;
-
- ClientToScreen(hwnd,<);
- ClientToScreen(hwnd,&rb);
-
- rect.left = lt.x;
- rect.top = lt.y;
- rect.right = rb.x;
- rect.bottom = rb.y;
-
- ClipCursor(&rect);
上面这段代码中,我们先用GetClientRect 函数取得了窗口内部矩形区域到一个矩形结构体rect中,然后将取得的窗口内部矩形区域的左上角坐标和右下角坐标分别存到It 和rb 这两个POINT 类型的结构体中, 接着将It 和rb 的窗口坐标转换为屏幕坐标,最后用经过转换的这两个点重新赋给这个rect 矩形结构体, 这样rect 结构体涅架重生了,成为了表示屏幕坐标的矩形区域,这样再用
ClipCursor 函数以涅架后的矩形区域rect 为参数, 就把鼠标关闭的移动区域限制在窗口中了。
讲了这么多的Windows API 函数,这次给大家的游戏示例程序是“半个”卷轴式飞行射击类游戏。
在这个示例程序中,我们处理了鼠标移动消息WM_MOUSEMOVE ,使剑侠可以根据鼠标的移动方向跟着在窗口中移动,我们还处理了单击鼠标左键消息WM_LBUTTONDOWN 来让剑侠发射出类似子弹的“光剑”,并且我们设定了鼠标光标的初始位置,隐藏了鼠标光标,以及限制了鼠标光标移动的区域(背景贴图采用循环背景滚动,其实很简单,就是每次都把窗口右边多余的部分再贴到窗口坐标来〉。
好了,我们先看一下素材图:

程序代码片段一,全局变量声明:
-
-
-
- struct SwordBullets
- {
- int x,y;
- bool exist;
- };
-
-
-
-
- HDC g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;
- HBITMAP g_hSwordMan=NULL,g_hSwordBlade=NULL,g_hBackGround=NULL;
- DWORD g_tPre=0,g_tNow=0;
- int g_iX=0,g_iY=0,g_iXnow=0,g_iYnow=0;
- int g_iBGOffset=0,g_iBulletNum=0;
- SwordBullets Bullet[30];
以上代码就是做了一些全局结构体和变量的定义。
程序代码片段二, 窗口过程函数WndProc:
-
-
-
- LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
- {
- switch( message )
- {
-
- case WM_KEYDOWN:
-
- switch (wParam)
- {
- case VK_ESCAPE:
- DestroyWindow(hwnd);
- PostQuitMessage( 0 );
- break;
- }
-
- break;
-
- case WM_LBUTTONDOWN:
- for(int i=0;i<30;i )
- {
- if(!Bullet[i].exist)
- {
- Bullet[i].x = g_iXnow;
- Bullet[i].y = g_iYnow 30;
- Bullet[i].exist = true;
- g_iBulletNum ;
- break;
- }
- }
-
- case WM_MOUSEMOVE:
-
- g_iX = LOWORD(lParam);
- if(g_iX > WINDOW_WIDTH-317)
- g_iX = WINDOW_WIDTH-317;
- else if(g_iX < 0)
- g_iX = 0;
-
- g_iY = HIWORD(lParam);
- if(g_iY > WINDOW_HEIGHT-283)
- g_iY = WINDOW_HEIGHT-283;
- else if(g_iY < -200)
- g_iY = -200;
- break;
-
- case WM_DESTROY:
- Game_CleanUp(hwnd);
- PostQuitMessage( 0 );
- break;
-
- default:
- return DefWindowProc( hwnd, message, wParam, lParam );
- }
- return 0;
- }
程序代码片段三, Game_Init()函数:
-
-
-
- BOOL Game_Init( HWND hwnd )
- {
- HBITMAP bmp;
-
- g_hdc = GetDC(hwnd);
- g_mdc = CreateCompatibleDC(g_hdc);
- g_bufdc = CreateCompatibleDC(g_hdc);
- bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT);
-
-
- g_iX = 300;
- g_iY = 100;
- g_iXnow = 300;
- g_iYnow = 100;
-
- SelectObject(g_mdc,bmp);
-
- g_hSwordMan = (HBITMAP)LoadImage(NULL,L"swordman.bmp",IMAGE_BITMAP,317,283,LR_LOADFROMFILE);
- g_hSwordBlade = (HBITMAP)LoadImage(NULL,L"swordblade.bmp",IMAGE_BITMAP,100,26,LR_LOADFROMFILE);
- g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
-
-
- POINT pt,lt,rb;
- RECT rect;
-
- pt.x = 300;
- pt.y = 100;
- ClientToScreen(hwnd,&pt);
- SetCursorPos(pt.x,pt.y);
-
- ShowCursor(false);
-
-
- GetClientRect(hwnd,&rect);
-
- lt.x = rect.left;
- lt.y = rect.top;
-
- rb.x = rect.right;
- rb.y = rect.bottom;
-
- ClientToScreen(hwnd,<);
- ClientToScreen(hwnd,&rb);
-
- rect.left = lt.x;
- rect.top = lt.y;
- rect.right = rb.x;
- rect.bottom = rb.y;
-
- ClipCursor(&rect);
-
- Game_Paint(hwnd);
- return TRUE;
- }
其中的36~53 行就用了之前讲到的限定鼠标区域的一套组合代码,以后也一样,如果想在哪个程序中用到限定鼠标区域的功能,把这段代码拷过去就行了。
程序代码片段四, Game_ Paint()函数:
-
-
-
- VOID Game_Paint( HWND hwnd )
- {
-
- SelectObject(g_bufdc,g_hBackGround);
- BitBlt(g_mdc,0,0,g_iBGOffset,WINDOW_HEIGHT,g_bufdc,WINDOW_WIDTH-g_iBGOffset,0,SRCCOPY);
- BitBlt(g_mdc,g_iBGOffset,0,WINDOW_WIDTH-g_iBGOffset,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);
-
- wchar_t str[20] = {};
-
-
- if(g_iXnow < g_iX)
- {
- g_iXnow = 10;
- if(g_iXnow > g_iX)
- g_iXnow = g_iX;
- }
- else
- {
- g_iXnow -=10;
- if(g_iXnow < g_iX)
- g_iXnow = g_iX;
- }
-
- if(g_iYnow < g_iY)
- {
- g_iYnow = 10;
- if(g_iYnow > g_iY)
- g_iYnow = g_iY;
- }
- else
- {
- g_iYnow -= 10;
- if(g_iYnow < g_iY)
- g_iYnow = g_iY;
- }
-
-
- SelectObject(g_bufdc,g_hSwordMan);
- TransparentBlt(g_mdc,g_iXnow,g_iYnow,317,283,g_bufdc,0,0,317,283,RGB(0,0,0));
-
-
- SelectObject(g_bufdc,g_hSwordBlade);
- if(g_iBulletNum!=0)
- for(int i=0;i<30;i )
- if(Bullet[i].exist)
- {
-
- TransparentBlt(g_mdc,Bullet[i].x-70,Bullet[i].y 100,100,33,g_bufdc,0,0,100,26,RGB(0,0,0));
-
-
- Bullet[i].x -= 10;
- if(Bullet[i].x < 0)
- {
- g_iBulletNum--;
- Bullet[i].exist = false;
- }
- }
-
- HFONT hFont;
- hFont=CreateFont(20,0,0,0,0,0,0,0,GB2312_CHARSET,0,0,0,0,TEXT("微软雅黑"));
- SelectObject(g_mdc,hFont);
- SetBkMode(g_mdc, TRANSPARENT);
- SetTextColor(g_mdc,RGB(255,255,0));
-
-
- swprintf_s(str,L"鼠标X坐标为%d ",g_iX);
- TextOut(g_mdc,0,0,str,wcslen(str));
- swprintf_s(str,L"鼠标Y坐标为%d ",g_iY);
- TextOut(g_mdc,0,20,str,wcslen(str));
-
-
- BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);
-
- g_tPre = GetTickCount();
-
- g_iBGOffset = 5;
- if(g_iBGOffset==WINDOW_WIDTH)
- g_iBGOffset = 0;
- }
最后看一下运行截图吧:

我们单击鼠标,移动鼠标,就可以发出光剑子弹并控制剑侠在空中飞行。
本章我们一起探讨了Windows 游戏编程中的鼠标和键盘消息处理相关的知识,并带领大家一起学习了两个比较好玩而且有代表性的游戏小demo 。