本系列文章由zhmxy555編寫,轉載請注明出處。 http://blog.csdn.net/zhmxy555/article/details/7405479
作者:毛星云 郵箱: happylifemxy@qq.com 歡迎郵件交流編程心得
上一節我們講解了鍵盤消息處理相關的知識。鍵盤加鼠標作為目前人機交互方式依舊的主流,在講完鍵盤消息處理之后接著講鼠標消息處理,自然是理所當然的。
這一節主要介紹各種鼠標消息的處理方式以及一些相關函數的運用方法,然后用一個小實例來鞏固本節所學。
一,鼠標消息的處理方式
大家都知道,目前市場上主流鼠標規格為兩個按鍵加上一個滾輪。那么,我們先列出Windows中這種鼠標設備輸入時的消息:
WM_LBUTTONDBLCLK 雙擊鼠標左鍵消息
WM_LBUTTONDOWN 單擊鼠標左鍵消息
WM_LBUTTONUP 松開鼠標左鍵消息
WM_MBUTTONDBLCLK 雙擊鼠標中鍵(滾輪)消息
WM_MBUTTONDOWN 單擊鼠標中鍵(滾輪)消息
WM_MBUTTONUP 松開鼠標中鍵(滾輪)消息
WM_RBUTTONDBLCLK 雙擊鼠標右鍵消息
WM_RBUTTONDOWN 單擊鼠標右鍵消息
WM_RBUTTONUP 松開鼠標右鍵消息
WM_MOUSEMOVE 鼠標移動消息
WM_MOUSEWHEEL 鼠標滾輪轉動消息
處理鼠標消息的方法與處理鍵盤消息的方法類似,同樣是在消息處理函數中加入要處理的鼠標消息類型,當鼠標消息發生時,輸入的參數“wParam”與“lParam”則儲存了鼠標狀態的相關信息。
下面我們分別來展開講解一下“wParam”與“lParam”參數以及滾輪消息。
<l>Param參數
lParam參數的值可分為高位字節與低位字節兩個部分,其中高節部分儲存的是鼠標光標所在的X坐標值,低位字節部分存儲的則是鼠標光標所在的Y坐標值。
我們可以用下面兩個函數來取得鼠標的坐標值:
WORD LOWORD(lParam參數); //返回鼠標光標所在的X坐標值
WORD HIWORD(lParam參數); //返回鼠標光標所在的Y坐標值
這兩個兩個函數所返回的鼠標光標位置的坐標是相對于內部窗口左上點坐標的。
<2>wParam參數
"wParam"參數的值記錄著鼠標按鍵及鍵盤【Ctrl】鍵與【Shift】鍵的狀態信息,通過下面的這些定義在“WINUSER.H”中的測試標志與“wParam”參數來檢查上述按鍵的按下狀態。
MK_LBUTTON 按下鼠標右鍵
MK_MBUTTON 按下鼠標中(滾輪)鍵
MK_RBUTTON 按下鼠標右鍵
MK_SHIFT 按下【Shift】鍵
MK_CONTROL 按下【Ctrl】鍵
【例子1】例如某一鼠標消息發生時,要測試鼠標左鍵是否也被按下,程序代碼如下:
if(wParam & MK_LBUTTON) //這里應該是按位與&,之前我寫錯了,謝謝 a443475601 的指出, { //鼠標左鍵被按下 }這是利用wParam參數與測試標志來測試鼠標鍵是否被按下的方法。當按鍵被按下時,條件式“wParam && MK_LBUTTON”所傳回的結果會為“true”。當然,若消息函數接收到“WM_LBUTTONDOWN”消息,同樣也可以知道鼠標鍵被按下而不必再去額外做這樣的測試。
【例子2】如果要測試鼠標左鍵與【Shift】鍵的按下狀態,那么程序代碼如下:
If(wParam & MK_LBUTTON) { If(wParam & MK_SHIFT) { //單擊鼠標左鍵 //按下【Shift】鍵 } else { //單擊鼠標左鍵 //未按下【Shift】鍵 } } else { If(wParam & MK_SHIFT) { //未單擊鼠標左鍵 //按下【Shift】鍵 } else { //未單擊鼠標左鍵 //未按下【Shift】鍵 } }
我們通過這個例子可以清楚,如何利用“wParam”參數與測試標志來測試鼠標鍵及【Shift】鍵和【Ctrl】鍵是否被按下的方法。
<3>滾輪消息
這里我們要特別提一下鼠標滾輪轉動消息(WM_MOUSEWHEEL)。當鼠標滾輪轉動消息發生時,“lParam”參數中的值同樣是記錄光標所在的位置的,而“wParam”參數則分為高位字節與低位字節兩部分,低位字節部分跟前面一樣是儲存鼠標鍵與【Shift】【Ctrl】鍵的狀態信息的,而高位字節部分的值會是“120”或“-120”。“120”表示鼠標滾輪向前轉動,而“-120”則表示向后轉動。
這里“wParam”高位組值與低位組值所在的函數同樣是HIWORD( )與LOWORD( )。
HIWORD(wParam);//高位組,值為“120”或“-120”
LOWORD(wParam);//低位組,鼠標鍵及【Shift】和【Ctrl】鍵的狀態信息
二,相關函數的講解
對各種鼠標輸入消息及鼠標狀態信息的獲取方法有了基本認識之后,下面我們將介紹一些游戲程序中以鼠標來做輸出設備時常用到的函數。
1.獲取窗口外鼠標消息的函數
為了確保程序可以正確地取得鼠標的輸入消息,需要在必要的時候以下面的函數來設定窗口,以取得鼠標在窗口外所發出的消息。
HWND SetCapture(HWND hWnd) ; //設定獲取窗口外的鼠標消息
如果調用了上面的SetCapture( )函數,并輸入要取得鼠標消息的窗口代號,那么便可取得鼠標在窗口外所發出的消息。這種方法也適用于多窗口的程序,與SetCapture( )函數相對應的函數為ReleaseCapture( )函數,用于釋放窗口取得窗口外鼠標消息的函數。
BOOL ReleaseCapture(VOID); //釋放獲取窗口外的鼠標消息
2.設定鼠標光標位置的函數
BOOL SetCursorPos(int X坐標,int Y坐標); //設定鼠標光標位置
上面這個SetCursorPos()函數中所設定的坐標是相對于屏幕左上角的屏幕坐標而言。實際上,我們經常需要將這個屏幕坐標轉換為游戲窗口中的游戲窗口坐標。因此需要用到API中的一個將窗口坐標轉換到屏幕坐標的函數,即ClientToScreen()。
屏幕坐標和窗口坐標轉換的函數
BOOL ClientToScreen(HWND hWnd, //屏幕坐標轉換為窗口坐標
LPPOINT lpPoint屏幕點坐標);
同理,我們得到:
窗口坐標轉換為屏幕坐標的函數:
BOOL ScreenToClient( LPPOINT lpPoint窗口點坐標 ) //窗口坐標轉換為屏幕坐標
3.顯示與隱藏鼠標光標的函數
Int ShowCursor(BOOL true或flase); //隱藏及顯示鼠標光標
其中,true代表顯示光標,false代表隱藏光標。
4.限制鼠標光標移動區域的函數
Windows API中提供的ClipCursor()函數可以用來設置限制鼠標光標的移動區域和解除鼠標光標移動區域的限制。
BOOL ClipCursor(CONST RECT 移動區域矩形); //限制鼠標光標移動區域
BOOL ClipCursor(NOOL); //解除限制
這里有一個RECT移動區域矩形,我們在MSDN中找出它的聲明:
typedef struct tagRECT { LONG left; //矩形區域右上點X坐 LONG top; //矩形區域右上點Y坐標 LONG right; //矩形區域左上點X坐標 LONG bottom; //矩形區域左上點Y坐標 } RECT;
5.取得窗口外部區域及內部區域的API函數
我們還需知道取得窗口外部區域及內部區域的API函數。
BOOL GetWindowRect(HWND hWND,LPRECT 矩形結構);//取得窗口外部區域矩形
BOOL GetClientRect(HWND hWnd,LPRECT 矩形結構體); //取得窗口內部區域矩形
這里需要注意的是,GetWindowRect()返回的坐標類型是屏幕坐標。
GetClientRect()返回的坐標類型是窗口坐標。
由于限制鼠標光標移動區域的ClipCursor()函數中輸入的矩形區域必須是屏幕坐標,因此如果取得的是窗口內部區域,那么還必須將窗口坐標轉換為屏幕坐標的操作,下面我們以一段程序代碼來說明將鼠標光標限制在窗口內部區域移動的過程:
RECT rect; POINT lt,rb; GetClientRect(hWnd,&rect); //取得窗口內部矩形 //將矩形左上點坐標存入lt中 lt.x = rect.left; lt.y = rect.top; //將矩形右下坐標存入rb中 rb.x = rect.right; rb.y = rect.bottom; //將lt和rb的窗口坐標轉換為屏幕坐標 ClientToScreen(hWnd,<); ClientToScreen(hWnd,&rb); //以屏幕坐標重新設定矩形區域 rect.left = lt.x; rect.top = lt.y; rect.right = rb.x; rect.bottom = rb.y; //限制鼠標光標移動區域 ClipCursor(&rect);
三,在實例中融會貫通
講了這么多的windows API函數了,也早該到了我們的showtime了,依然,我們通過一個實例來把本節所講的內容融會貫通。
這個實例處理鼠標移動消息使飛機在窗口中移動,并且處理單擊鼠標左鍵消息來讓飛機發射子彈,而且設定了鼠標光標的位置,隱藏了鼠標光標,還有限制了鼠標光標移動的區域。(背景貼圖采用循環背景滾動,其實很簡單,就是每次都把窗口右邊多余的部分再貼到窗口坐標來,以后有機會我會作為一節筆記具體講)
同樣的,我們貼出詳細注釋好的代碼:
#include "stdafx.h" #include <stdio.h> //定義結構體 struct BULLET //bullet結構體代表飛機子彈 { int x,y; //子彈坐標 bool exist; //子彈是否存在 }; //全局變量聲明 HINSTANCE hInst; HBITMAP bg,ship,bullet; //存儲背景圖,飛機圖,子彈圖 HDC hdc,mdc,bufdc; HWND hWnd; DWORD tPre,tNow; int x,y,nowX,nowY; //x,y代表鼠標光標所在位置,nowX,nowY代表飛機坐標,也是貼圖的位置 int w=0,bcount; //w為滾動背景所要裁剪的區域寬度,bcount記錄飛機現有子彈數目 BULLET b[30]; //聲明一個“bullet”類型的數組,用來存儲飛機發出的子彈 //全局函數聲明 ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void MyPaint(HDC hdc); //****WinMain函數,程序入口點函數************************************** int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MSG msg; MyRegisterClass(hInstance); //初始化 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } //消息循環 GetMessage(&msg,NULL,NULL,NULL); //初始化msg while( msg.message!=WM_QUIT ) { if( PeekMessage( &msg, NULL, 0,0 ,PM_REMOVE) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } else { tNow = GetTickCount(); if(tNow-tPre >= 40) MyPaint(hdc); } } return msg.wParam; } //****設計一個窗口類,類似填空題,使用窗口結構體********************* ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = "canvas"; wcex.hIconSm = NULL; return RegisterClassEx(&wcex); } //****初始化函數************************************* // 1.設定飛機初始位置 // 2.設定鼠標光標位置及隱藏 // 3.限制鼠標光標移動區域 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HBITMAP bmp; POINT pt,lt,rb; RECT rect; hInst = hInstance; hWnd = CreateWindow("canvas", "繪圖窗口" , WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) { return FALSE; } MoveWindow(hWnd,10,10,640,480,true); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); hdc = GetDC(hWnd); mdc = CreateCompatibleDC(hdc); bufdc = CreateCompatibleDC(hdc); bmp = CreateCompatibleBitmap(hdc,640,480); SelectObject(mdc,bmp); bg = (HBITMAP)LoadImage(NULL,"bg.bmp",IMAGE_BITMAP,648,480,LR_LOADFROMFILE); ship = (HBITMAP)LoadImage(NULL,"ship.bmp",IMAGE_BITMAP,100,148,LR_LOADFROMFILE); bullet = (HBITMAP)LoadImage(NULL,"bullet.bmp",IMAGE_BITMAP,10,20,LR_LOADFROMFILE); //設定鼠標光標的x,y值,并設定飛機貼圖坐標的“nowX”和“nowY”的值為(300,300) x = 300; y = 300; nowX = 300; nowY = 300; //設定光標位置 pt.x = 300; pt.y = 300; ClientToScreen(hWnd,&pt); SetCursorPos(pt.x,pt.y); ShowCursor(false); //隱藏鼠標光標 //限制鼠標光標移動區域 GetClientRect(hWnd,&rect); //取得窗口內部矩形 //將矩形左上點坐標存入lt中 lt.x = rect.left; lt.y = rect.top; //將矩形右下坐標存入rb中 rb.x = rect.right; rb.y = rect.bottom; //將lt和rb的窗口坐標轉換為屏幕坐標 ClientToScreen(hWnd,<); ClientToScreen(hWnd,&rb); //以屏幕坐標重新設定矩形區域 rect.left = lt.x; rect.top = lt.y; rect.right = rb.x; rect.bottom = rb.y; //限制鼠標光標移動區域 ClipCursor(&rect); MyPaint(hdc); return TRUE; } //****自定義繪圖函數********************************* // 1.設定飛機坐標并進行貼圖 // 2.設定所有子彈坐標并進行貼圖 // 3.顯示真正的鼠標光標所在坐標 void MyPaint(HDC hdc) { char str[20] = ""; int i; //貼上背景圖 SelectObject(bufdc,bg); BitBlt(mdc,0,0,w,480,bufdc,640-w,0,SRCCOPY); BitBlt(mdc,w,0,640-w,480,bufdc,0,0,SRCCOPY); //計算飛機的貼圖坐標,設定每次進行飛機貼圖時,其貼圖坐標(nowX,nowY)會以10個單位慢慢向鼠標光標所在的目的點(x,y)接近,直到兩個坐標相同為止 if(nowX < x) { nowX += 10; if(nowX > x) nowX = x; } else { nowX -=10; if(nowX < x) nowX = x; } if(nowY < y) { nowY += 10; if(nowY > y) nowY = y; } else { nowY -= 10; if(nowY < y) nowY = y; } //貼上飛機圖 SelectObject(bufdc,ship); BitBlt(mdc,nowX,nowY,100,74,bufdc,0,74,SRCAND); BitBlt(mdc,nowX,nowY,100,74,bufdc,0,0,SRCPAINT); //子彈的貼圖,先判斷子彈數目“bcount”的值是否為“0”。若不為0,則對子彈數組中各個還存在的子彈按照其所在的坐標(b[i].x,b[i].y)循環進行貼圖操作 SelectObject(bufdc,bullet); if(bcount!=0) for(i=0;i<30;i++) if(b[i].exist) { //貼上子彈圖 BitBlt(mdc,b[i].x,b[i].y,10,10,bufdc,0,10,SRCAND); BitBlt(mdc,b[i].x,b[i].y,10,10,bufdc,0,0,SRCPAINT); //設置下一個子彈的坐標。子彈是又右向左發射的,因此,每次其X軸上的坐標值遞減10個單位,這樣貼圖會產生往左移動的效果。而如果子彈下次的坐標已超出窗口的可見范圍(h[i].x<0),那么子彈設為不存在,并將子彈總數bcount變量值減1. b[i].x -= 10; if(b[i].x < 0) { bcount--; b[i].exist = false; } } //顯示鼠標坐標 sprintf(str,"鼠標X坐標為%d ",x); TextOut(mdc,0,0,str,strlen(str)); sprintf(str,"鼠標Y坐標為%d ",y); TextOut(mdc,0,20,str,strlen(str)); BitBlt(hdc,0,0,640,480,mdc,0,0,SRCCOPY); tPre = GetTickCount(); w += 10; if(w==640) w = 0; } //****消息處理函數*********************************** // 1.處理WM_LBUTTONDOWN消息發射子彈 // 2.處理WM_MOUSEMOVE消息設定飛機貼圖坐標 // 3.在窗口結束時恢復鼠標移動區域 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int i; switch (message) { case WM_KEYDOWN: //按鍵按下消息 if(wParam==VK_ESCAPE) //按下【Esc】鍵 PostQuitMessage(0); break; case WM_LBUTTONDOWN: //單擊鼠標左鍵消息 for(i=0;i<30;i++) { if(!b[i].exist) { b[i].x = nowX; //子彈x坐標 b[i].y = nowY + 30; //子彈y坐標 b[i].exist = true; bcount++; //累加子彈數目 break; } } case WM_MOUSEMOVE: x = LOWORD(lParam); //取得鼠標X坐標 if(x > 530) //設置臨界坐標 x = 530; else if(x < 0) x = 0; y = HIWORD(lParam); //取得鼠標y坐標 if(y > 380) y = 380; else if(y < 0) y = 0; break; case WM_DESTROY: //窗口結束消息 ClipCursor(NULL); //恢復鼠標移動區域 DeleteDC(mdc); DeleteDC(bufdc); DeleteObject(bg); DeleteObject(bullet); DeleteObject(ship); ReleaseDC(hWnd,hdc); PostQuitMessage(0); break; default: //其他消息 return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }下面是這個例子的效果圖:
我們移動鼠標,小飛機會跟著鼠標的移動而移動;點擊鼠標,小飛機就會發射出綠色的子彈來;按下Esc鍵,這個小游戲就退出了。
當然,這個小游戲還有些的不足,比如小飛機對鼠標的跟隨有小小的延遲,也沒有進行WndProc函數中case分支的拆分(在這里感謝yao050421103的提醒)。
關于這個小游戲的改進,是以后我們需要去做的。比如后面我們會找機會用DirectX中的DirectInput函數(也是處理輸出消息的函數)來寫一下這個小游戲的升級版,無論是畫面上還是實現效果上都將進行升級。
在之前的筆記里有朋友(感謝pxg789的提醒)提到,用MFC和ATL共享的新類CImage進行貼圖會更加簡單和先進,在這里說明一下,在后面的筆記里面會專門花一節來講CImage類,目前和之前的筆記還是采用傳統的GDI進行貼圖操作。
筆記十三到這里就結束了。
本節筆記的源代碼請點擊這里下載: 【Visual C++】Code_Note_13
感謝一直支持【Visual C++】游戲開發筆記系列專欄的朋友們,也請大家繼續關注我的專欄,我一有時間就會把自己的學習心得,覺得比較好的知識點寫出來和大家一起分享。
精通游戲開發的路還很長很長,非常希望能和大家一起交流,共同學習,共同進步。
大家看過后覺得值得一看的話,可以頂一下這篇文章,讓更多的朋友有機會看到它。
如果文章中有什么疏漏的地方,也請大家指正。也希望大家可以多留言來和我探討編程相關的問題。
最后,謝謝你們一直的支持~~~
The end.