二、显示区域鼠标消息
在前一章中您已经看到,Windows只把键盘消息发送给拥有输入焦点的窗口。鼠标消息与此不同:只要鼠标跨越窗口或者在某窗口中按下鼠标按键,那么窗口消息处理程序就会收到鼠标消息,而不管该窗口是否活动或者是否拥有输入焦点。Windows为鼠标定义了21种消息,不过,其中有11个消息和显示区域无关(下面称之为「非显示区域」消息),Windows程序经常忽略这些消息。
当鼠标移过窗口的显示区域时,窗口消息处理程序收到WM_MOUSEMOVE消息。当在窗口的显示区域中按下或者释放一个鼠标按键时,窗口消息处理程序会接收到下面这些消息:
表7-1 |
键 |
按下 |
释放 |
按下(双键) |
左 |
WM_LBUTTONDOWN |
WM_LBUTTONUP |
WM_LBUTTONDBLCLK |
中 |
WM_MBUTTONDOWN |
WM_MBUTTONUP |
WM_MBUTTONDBLCLK |
右 |
WM_RBUTTONDOWN |
WM_RBUTTONUP |
WM_RBUTTONDBLCLK |
只有对三键鼠标,窗口消息处理程序才会收到MBUTTON消息;只有对双键或者三键鼠标,才会接收到RBUTTON消息。只有当定义的窗口类别能接收DBLCLK(双击)消息,窗口消息处理程序才能接收到这些消息(请参见本章中「双击鼠标按键」一节)。
对于所有这些消息来说,其lParam值均含有鼠标的位置:低字组为x坐标,高字组为y坐标,这两个坐标是相对于窗口显示区域左上角的位置。您可以用LOWORD和HIWORD宏来提取这些值:
x = LOWORD (lParam) ; y = HIWORD (lParam) ;
wParam的值指示鼠标按键以及Shift和Ctrl键的状态。您可以使用表头文件WINUSER.H中定义的位屏蔽来测试wParam。MK前缀代表「鼠标按键」。
MK_LBUTTON |
按下左键 |
MK_MBUTTON |
按下中键 |
MK_RBUTTON |
按下右键 |
MK_SHIFT |
按下Shift键 |
MK_CONTROL |
按下Ctrl键 |
例如,如果收到了WM_LBUTTONDOWN消息,而且值
wparam & MK_SHIFT
是TRUE(非0),您就知道当左键按下时也按下了Shift键。
当您把鼠标移过窗口的显示区域时,Windows并不为鼠标的每个可能的图素位置都产生一个WM_MOUSEMOVE消息。您的程序接收到WM_MOUSEMOVE消息的次数,依赖于鼠标硬件,以及您的窗口消息处理程序在处理鼠标移动消息时的速度。换句话说,Windows不能用未处理的WM_MOUSEMOVE消息来填入消息队列。当您执行下面将描述的CONNECT程序时,您将会更了解WM_MOUSEMOVE消息处理的速率。
如果您在非活动窗口的显示区域中按下鼠标左键,Windows将把活动窗口改为在其中按下鼠标按键的窗口,然后把WM_LBUTTONDOWN消息送到该窗口消息处理程序。当窗口消息处理程序得到WM_LBUTTONDOWN消息时,您的程序就可以安全地假定该窗口是活动化的了。不过,您的窗口消息处理程序可能在未接收到WM_LBUTTONDOWN消息的情况下先接收到了WM_LBUTTONUP的消息。如果在一个窗口中按下鼠标按键,然后移动到使用者窗口释放它,就会出现这种情况。类似的情况,当鼠标按键在另一个窗口中被释放时,窗口消息处理程序只能接收到WM_LBUTTONDOWN消息,而没有相应的WM_LBUTTONUP消息。
这些规则有两个例外:
-
窗口消息处理程序可以「拦截鼠标」并且连续地接收鼠标消息,即使此时鼠标在该窗口显示区域之外。您将在本章的后面学习如何拦截鼠标。
-
如果正在显示一个系统模态消息框或者系统模态对话框,那么其它程序就不能接收鼠标消息。当系统模态消息框或者对话框活动时,禁止切换到其它窗口或者程序。一个显示系统模态消息框的例子,是当您关闭Windows时。
简单的鼠标处理:一个例子
程序7-1中所示的CONNECT程序能作一些简单的鼠标处理,使您对Windows如何向您的程序发送鼠标消息有一些体会。
CONNECT.C /*-------------------------------------------------------------------------- CONNECT.C -- Connect-the-Dots Mouse Demo Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define MAXPOINTS 1000 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Connect") ; 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 ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Connect-the-Points Mouse 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 ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static POINT pt[MAXPOINTS] ; static int iCount ; HDC hdc ; in i, j ; PAINTSTRUCT ps ; switch (message) { case WM_LBUTTONDOWN: iCount = 0 ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_MOUSEMOVE: if (wParam & MK_LBUTTON && iCount < 1000) { pt[iCount ].x = LOWORD (lParam) ; pt[iCount++].y = HIWORD (lParam) ; hdc = GetDC (hwnd) ; SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_LBUTTONUP: InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; for (i = 0 ; i < iCount - 1 ; i++) for (j = i + 1 ; j < iCount ; j++) { MoveToEx (hdc, pt[i].x, pt[i].y, NULL) ; LineTo (hdc, pt[j].x, pt[j].y) ; } ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
CONNECT处理三个鼠标消息:
-
WM_LBUTTONDOWNCONNECT 清除显示区域。
-
WM_MOUSEMOVE如果按下左键,那么CONNECT就在显示区域中的鼠标位置处绘制一个黑点,并保存该坐标。
- WM_LBUTTONUP CONNECT把显示区域中绘制的点与其它每个点连接起来。有时会产生一个漂亮的图形,有时则会是黑鸦鸦的一团糟(见图7-1)。
CONNECT的使用方法:把鼠标光标移动到显示区域中,按下左键,移动一下位置,释放左键。对几个构成曲线的点,CONNECT能处理得很好,方法是按住左键,快速移动鼠标,这样就可以绘制出该曲线图案。
CONNECT使用了三个简单的图形设备接口(GDI)函数,我在第五章讨论过这些函数。当鼠标左键按下时,SetPixel为每个WM_MOUSEMOVE消息绘制一个黑图素(对于高分辨率的显示器,图素几乎看不见)。画直线需要MoveToEx和LineTo函数。
如果您在释放鼠标按键之前把鼠标光标移到显示区域之外,那么CONNECT就不会连接这些点,因为它没有收到WM_LBUTTONUP消息。如果您把鼠标移回显示区域内并按下左键,那么CONNECT将清除显示区域。如果想在显示区域外释放左键后还继续进行画图,那么可以在显示区域外按下鼠标再移回显示区域中。
CONNECT最多可以保存1000个点。设点数为P,则CONNECT画的线数就等于P × (P - 1) / 2。如果有1000个点,则要绘制50万条直线,大约需要几分钟才能画完(时间的长短取决于您的硬设备)。由于Windows 98是一种优先权式多任务环境,因此您可以在这一段时间切换到别的程序中。但是,当程序正在忙的时候,您将无法对CONNECT程序做任何事(诸如移动或者缩放等)。在第二十章中,我们将讨论解决这一问题的方法。
因为CONNECT可能会花一些时间来绘制直线,因此在处理WM_PAINT消息时它将切换到沙漏光标,然后再恢复原状。这要求使用两个现有光标来呼叫SetCursor。CONNECT还呼叫两次ShowCursor,一次用TRUE参数,另一次用FALSE参数。我将在本章的后面,「使用键盘仿真鼠标」一节中更详细地讨论这些呼叫。
有时,我们使用「跟踪」这个词代表程序处理鼠标移动的方法。但是,跟踪并不意味着,程序在窗口消息处理程序中的某个循环里,不断跟随鼠标在显示器上的运动。实际上,窗口消息处理程序处理每条鼠标消息,然后迅速退出。