摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P249
通常,只有当鼠标指针位于窗口的客户区或非客户区时,窗口过程才接收鼠标消息。而当鼠标处于窗口范围之外时,一个程序也可能需要接收鼠标消息。如果是这种情况,那么该程序可以“捕获”鼠标 。别担心,这只“老鼠”不会咬人。
7.5.1 设计一个矩形
为了理解为什么要捕获鼠标,让我们看一看 BLOKOUT1 程序。这个程序可能看上去功能完整,但它却存在十分严重的缺陷。
#include <windows.h> LRESULT CALLBACK WndProc ( HWND , UINT , WPARAM , LPARAM ) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ( "BlokOut1" ) ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH ) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!" ), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Mouse Button Demo" ), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBoxOutline ( HWND hwnd, POINT ptBeg, POINT ptEnd) { HDC hdc; hdc = GetDC(hwnd); SetROP2(hdc, R2_NOT); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y); ReleaseDC(hwnd, hdc); } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL fBlocking, fValidBox; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd; HDC hdc; PAINTSTRUCT ps; switch (message) { case WM_LBUTTONDOWN: ptBeg.x = ptEnd.x = LOWORD(lParam); ptBeg.y = ptEnd.y = HIWORD(lParam); DrawBoxOutline(hwnd, ptBeg, ptEnd); SetCursor(LoadCursor(NULL, IDC_CROSS)); fBlocking = TRUE; return 0; case WM_MOUSEMOVE: if (fBlocking) { SetCursor(LoadCursor(NULL, IDC_CROSS)); DrawBoxOutline(hwnd, ptBeg, ptEnd);; ptEnd.x = LOWORD(lParam); ptEnd.y = HIWORD(lParam); DrawBoxOutline(hwnd, ptBeg, ptEnd); } return 0; case WM_LBUTTONUP: if (fBlocking) { DrawBoxOutline(hwnd, ptBeg, ptEnd); ptBoxBeg = ptBeg; ptBoxEnd.x = LOWORD(lParam); ptBoxEnd.y = HIWORD(lParam); SetCursor(LoadCursor(NULL, IDC_ARROW)); fBlocking = FALSE; fValidBox = TRUE; InvalidateRect(hwnd, NULL, TRUE); } return 0; case WM_CHAR: if (fBlocking & (wParam == '\x1B' )) { DrawBoxOutline(hwnd, ptBeg, ptEnd); SetCursor(LoadCursor(NULL, IDC_ARROW)); fBlocking = FALSE; } return 0; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (fValidBox) { SelectObject(hdc, GetStockObject(BLACK_BRUSH)); Rectangle(hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y); } if (fBlocking) { SetROP2(hdc, R2_NOT); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y); } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
这个程序展示了 Windows 绘图程序中可能用到的一些东西。用户开始按下鼠标左键,标出矩形的一个顶点,然后拖动鼠标。程序将当前鼠标的位置看做矩形的对角点,并据此画出矩形的轮廓。当用户释放鼠标时,程序填充这个矩形。图 7-10 显示了一个已经画完的矩形和另一个正在画的矩形。
那么,哪里有问题呢?
试试这个操作:在 BLOKOUT1 的客户区内按下鼠标的左键,然后移动指针到窗口外。程序将停止接收 WM_MOUSEMOVE 消息。现在释放鼠标。由于鼠标落在客户区之外,所以 BLOKOUT1 不能获取 WM_BUTTONUP 消息。再将鼠标移回 BLOKOUT1 的客户区,窗口过程仍然会认为按钮处于按下的状态。
这很槽糕。程序现在不知道该如何运行了。
图 7-10 BLOKOUT1 的显示结果
7.5.2 捕获的解决方案
BLOKOUT1 程序提供了一些常见的程序功能,但是显然,代码存在缺陷。正是由于存在这类问题,所以要进行捕获鼠标的操作。
当用户拖动鼠标时,如果鼠标指针只是暂时离开窗口范围,这没什么要紧的。程序应该仍然控制着鼠标
。
捕获鼠标比给捕鼠器放鼠饵来抓老鼠简单得多。只需要调用
之后,Windows 会将所有鼠标消息发送给句柄为 hwnd 的窗口的窗口过程。鼠标消息总是以客户区消息的形式出现,即使鼠标位于窗口的非客户区。参数 lParam 仍然表示鼠标在客户区的位置。但是,当鼠标位于客户区的左方或上方时,这些 x 和 y 坐标会是负值。如果用户想释放鼠标了,则可以调用:
这时一切恢复正常。
在 32 位的 Windows 版本中,捕获鼠标的限制比 Windows 早期版本更多。具体来说,当鼠标被捕获,且当前没有按下鼠标按钮时,若鼠标指针移动经过另一个窗口,则将由指针下面的窗口接收鼠标消息,而不是捕获鼠标的窗口 。为了防止一个程序在捕获鼠标之后没有释放它而引起整个系统的混乱,这么做是十分必要的。
为了避免类似的问题,只有当鼠标在客户区内被按下时,程序才应该捕获鼠标。当释放按钮时,应该同时停止捕获 。
7.5.3 BLOKOUT2 程序
BLOKOUT2 程序展示了鼠标的捕获过程。
#include <windows.h> LRESULT CALLBACK WndProc ( HWND , UINT , WPARAM , LPARAM ) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ( "BlokOut2" ) ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH ) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!" ), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Mouse Button & Capture Demo" ), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBoxOutline ( HWND hwnd, POINT ptBeg, POINT ptEnd) { HDC hdc; hdc = GetDC(hwnd); SetROP2(hdc, R2_NOT); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y); ReleaseDC(hwnd, hdc); } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL fBlocking, fValidBox; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd; HDC hdc; PAINTSTRUCT ps; switch (message) { case WM_LBUTTONDOWN: ptBeg.x = ptEnd.x = LOWORD(lParam); ptBeg.y = ptEnd.y = HIWORD(lParam); DrawBoxOutline(hwnd, ptBeg, ptEnd); SetCapture(hwnd); SetCursor(LoadCursor(NULL, IDC_CROSS)); fBlocking = TRUE; return 0; case WM_MOUSEMOVE: if (fBlocking) { SetCursor(LoadCursor(NULL, IDC_CROSS)); DrawBoxOutline(hwnd, ptBeg, ptEnd);; ptEnd.x = LOWORD(lParam); ptEnd.y = HIWORD(lParam); DrawBoxOutline(hwnd, ptBeg, ptEnd); } return 0; case WM_LBUTTONUP: if (fBlocking) { DrawBoxOutline(hwnd, ptBeg, ptEnd); ptBoxBeg = ptBeg; ptBoxEnd.x = LOWORD(lParam); ptBoxEnd.y = HIWORD(lParam); ReleaseCapture(); SetCursor(LoadCursor(NULL, IDC_ARROW)); fBlocking = FALSE; fValidBox = TRUE; InvalidateRect(hwnd, NULL, TRUE); } return 0; case WM_CHAR: if (fBlocking & (wParam == '\x1B' )) { DrawBoxOutline(hwnd, ptBeg, ptEnd); ReleaseCapture(); SetCursor(LoadCursor(NULL, IDC_ARROW)); fBlocking = FALSE; } return 0; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (fValidBox) { SelectObject(hdc, GetStockObject(BLACK_BRUSH)); Rectangle(hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y); } if (fBlocking) { SetROP2(hdc, R2_NOT); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y); } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
BLOKOUT2 程序的内容与 BLOKOUT2 完全一致,只增加了三行新的代码:在处理 WM_LBUTTONDOWN 消息时调用 SetCapture,以及在处理 WM_LBUTTONUP 消息和处理 WM_CHAR 消息时调用 ReleaseCapture。检验下面的操作:调整窗口使其小于屏幕大小,开始在客户区内设计一个矩形,然后将鼠标移出客户区,向右或者向下,最后释放鼠标。
程序仍然会得到整个矩形的坐标。放大窗口就可以看到它
。
捕获鼠标并不只是用来处理一些古怪的应用 。当鼠标在客户区被按下后,在没有释放前,如果程序需要跟踪 WM_MOUSEMOVE 消息,就都应该进行鼠标捕获。这样程序会更简洁,且能达到用户的期望。