鼠标 |
|
鼠标是有一个或多个键的定位设备。虽然也可以使用诸如触摸画面和光笔之类的输入设备,但是只有鼠标以及常用在膝上型计算机上的轨迹球等才是渗透了PC市场的唯一输入设备。
情况并非总是如此。当然,Windows的早期开发人员认为他们不应该要求使用者为了执行其产品而必须买只鼠标。因此,他们将鼠标作为一种选择性的附加设备,而为Windows中的所有操作以及applet提供一种键盘接口(例如,查看Windows小算盘程序的在线说明信息,可以看到每个按钮都提供了一个同等功效的键盘操作方式)。第三方软件开发人员使用键盘接口来提供与鼠标操作相同的功能,这本书以前的版本也是这么做的。
理论上来说,现在的Windows需要鼠标。至少,一些消息框是这样讲的。当然,您也可以拔下鼠标,而且Windows仍然可以执行良好(只有消息框会提示您没有连接鼠标)。试图不用鼠标来使用Windows就像用脚趾来弹钢琴一样(至少在最初的一段时间里是这样),但您依然可以这样做。正因为如此,我还是喜欢为鼠标功能提供键盘操作。打字员尤其喜欢让他们的手保持在键盘上,并且我认为每个人都有在杂乱的桌上找不到鼠标,或者鼠标移动不灵敏的经验。使用键盘通常不需要花费更多的精力和努力,并且为喜欢使用键盘的人提供更多的功能。
我们通常认为,键盘便于输入和操作文字数据,而鼠标则便于画图和操作图形对象。实际上,本章大多数的范例程序都画了一些图形,并且用到了我们在第五章所学到的知识。
Windows 98能支持单键、双键或者三键鼠标,也可以使用摇杆或者光笔来仿真单键鼠标。早期,由于许多使用者都有单键鼠标,所以Windows应用程序总是避免使用双键或三键鼠标。不过,由于双键鼠标已经成为事实上的标准,因此不使用第二个键的传统已经不再合理了。当然,第二个鼠标按键是用于启动一个「快捷菜单」,亦即出现在普通菜单列之外的窗口中菜单,或者用于特殊的拖曳操作(拖曳将在后面加以解释)。然而,程序不能依赖双键鼠标。
理论上,您可以用我们的老朋友GetSystemMetrics函数来确认鼠标是否存在:
fMouse = GetSystemMetrics (SM_MOUSEPRESENT) ;
如果已经安装了鼠标,fMouse将传回TRUE(非0);如果没有安装,则传回0。然而,在Windows 98中,不论鼠标是否安装,此函数都将传回TRUE 。在Microsoft Windows NT中,它可以正常工作。
要确定所安装鼠标其上按键的个数,可使用
cButtons = GetSystemMetrics (SM_CMOUSEBUTTONS) ;
如果没有安装鼠标,那么函数将传回0。然而,在Windows 98下,如果没有安装鼠标,此函数将传回2。
习惯用左手的使用者可以使用Windows的「控制台」来切换鼠标按键。虽然应用程序可以通过在GetSystemMetrics中使用SM_SWAPBUTTON参数来确定是否进行了这种切换,但通常没有这个必要。由食指触发的键被认为是左键,即使事实上是位于鼠标的右边。不过,在一个教育训练程序中,您可能想在屏幕上画一个鼠标,在这种情况下,您可能想知道鼠标按键是否被切换过了。
您可以在「控制台」中设定鼠标的其它参数,例如双击速度。从Windows应用程序,通过使用SystemParametersInfo函数可以设定或获得此项信息。
一些简单的定义
当Windows使用者移动鼠标时,Windows在显示器上移动一个称为「鼠标光标」的小位图。鼠标光标有一个指向显示器上精确位置的单图素「热点」。当我提到鼠标光标在屏幕上的位置时,指的是热点的位置。
Windows支持几种预先定义的鼠标光标,程序可以使用这些光标。最常见的是称为IDC_ARROW的斜箭头(在WINUSER.H中定义)。热点在箭头的顶端。IDC_CROSS光标(在本章后面的BLOKOUT程序中有用到)的热点在十字交叉线的中心。IDC_WAIT光标是一个沙漏,通常用于指示程序正在执行。程序写作者也可以设计自己的光标。我们将在第十章学习设计方法。在定义窗口类别结构时指定特定窗口的内定光标,例如:
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
下面是一些描述鼠标按键动作的术语:
- Clicking按下并放开一个鼠标按键。
- Double-clicking快速按下并放开鼠标按键两次。
- Dragging按住鼠标按键并移动鼠标。
对三键鼠标来说,三个键分别称为左键、中键、右键。在Windows表头文件中定义的与鼠标有关的标识符使用缩写LBUTTON、MBUTTON和RBUTTON。双键鼠标只有左键与右键,单键鼠标只有一个左键。
鼠标(Mouse)的复数
现在,为了展现我的勇气,我将面对输入设备最难辩的争论话题:什么是「mouse」的复数。虽然每个人都知道多只啮齿动物称为mice,似乎没有人对该如何称呼多个输入设备有最后的答案。不管「mice」或「mouse」听起来都不对劲。我惯常参考的《American Heritage Dictionary of the English Language》第三版则只字未提。
《Wired style:Principles of English Usage in the Digital Age》(HardWired, 1996)指出「mouse」比较好,以避免与啮齿动物搞混。在1964发明鼠标的Doug Engelbart对此争议也帮不上忙。我曾经问过他mouse的复数是什么,他说我不知道。
最后,高权威的Microsoft Manual of Style for Technical Publications告诉我们「避免使用复数mice。假如你必须提到多只mouse,使用mouse devices」。这听起来像是在逃避问题,但当一切听起来都不对劲时,它确实是个明智的忠告了。事实上,大部分需要mouse复数的句子都能重新修改来避开。例如,试着说"People use the almost as much as keyboard",而不是"Pople use mice almost as much as keyboards"。
在前一章中您已经看到,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)。
图7-1 CONNECT的屏幕显示 |
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参数。我将在本章的后面,「使用键盘仿真鼠标」一节中更详细地讨论这些呼叫。
有时,我们使用「跟踪」这个词代表程序处理鼠标移动的方法。但是,跟踪并不意味着,程序在窗口消息处理程序中的某个循环里,不断跟随鼠标在显示器上的运动。实际上,窗口消息处理程序处理每条鼠标消息,然后迅速退出。
处理Shift键
当CONNECT接收到一个WM_MOUSEMOVE消息时,它把wParam和MK_LBUTTON进行位与(AND)运算,来确定是否按下了左键。wParam也可以用于确定Shift键的状态。例如,如果处理必须依赖于Shift和Ctrl键的状态,那么您可以使用如下所示的方法:
if (wParam & MK_SHIFT) { if (wParam & MK_CONTROL) { //按下了Shift和Ctrl键 } else { //按下了Shift键 } { else { if (wParam & MK_CONTROL) { //按下了Ctrl键 } else { //Shift和Ctrl键均未按下 } }
如果您想在程序中同时使用左右键,同时如果您还希望只有单键鼠标的使用者也能使用您的程序,那么您可以这样来写作程序:Shift与左键的组合使用等效于右键。在这种情况下,对鼠标按键的处理可以采用如下所示的方法:
caseWM_LBUTTONDOWN: if (!(wParam & MK_SHIFT)) { //处理左键 return 0 ; } // Fall through case WM_RBUTTONDOWN: //处理右键 return 0 ;
Windows函数GetKeyState(在第六章中介绍过)可以使用虚拟键码VK_LBUTTON、VK_RBUTTON、VK_MBUTTON、VK_SHIFT和VK_CONTROL来传回鼠标按键与Shift键的状态。如果GetKeyState传回负值,则说明已按下了鼠标按键或者Shift键。因为GetKeyState传回目前正在处理的鼠标按键或者Shift键的状态,所以全部状态信息与相应的消息都是同步的。但是,正如不能把GetKeyState用于尚未按下的键一样,您也不能为尚未按下的鼠标按键呼叫GetKeyState。请不要这样做:
while (GetKeyState (VK_LBUTTON) >= 0) ; // WRONG !!!
只有在您呼叫GetKeyState期间处理消息时,而左键已经按下,才会报告键已经按下的消息。
双击鼠标按键是指在短时间内单击两次。要确定为双击,则这两次单击必须发生在其相距的实际位置十分接近的状况下(内定范围是一个平均系统字体字符的宽,半个字符的高),并且发生在指定的时间间隔(称为「双击速度」)内。您可以在「控制台」中改变时间间隔。
如果希望您的窗口消息处理程序能够收到双按键的鼠标消息,那么在呼叫RegisterClass初始化窗口类别结构时,必须在窗口风格中包含CS_DBLCLKS标识符:
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;
如果在窗口风格中未包含CS_DBLCLKS,而使用者在短时间内双击了鼠标按键,那么窗口消息处理程序会接收到下面这些消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP
窗口消息处理程序可能在这些键的消息之前还收到了其它消息。如果您想实作自己的双击处理,那么您可以使用Windows函数GetMessageTime取得WM_LBUTTONDOWN消息之间的相对时间。第八章将更详细地讨论这个函数。
如果您的窗口类别风格中包含了CS_DBLCLKS,那么双击时窗口消息处理程序将收到如下消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
WM_LBUTTONDBLCLK消息简单地替换了第二个WM_LBUTTONDOWN消息。
如果双击中的第一次键操作完成单击的功能,那么双击这一消息是很容易处理的。第二次按键(WM_LBUTTONDBLCLK消息)则完成第一次按键以外的事情。例如,看看Windows Explorer中是如何用鼠标来操作文件列表的。按一次键将选中文件,Windows Explorer用反白显示列指出被选择文件的位置。双击则实作两个功能:第一次是单击那个选中文件;第二次则指向Windows Explorer以打开该文件。执行方式相当简单,如果双击中的第一次按键不执行单击功能,那么鼠标处理方式会变得非常复杂。
在窗口的显示区域内移动或按下鼠标按键时,将产生10种消息。如果鼠标在窗口的显示区域之外但还在窗口内,Windows就给窗口消息处理程序发送一条「非显示区域」鼠标消息。窗口非显示区域包括标题列、菜单和窗口滚动条。
通常,您不需要处理非显示区域鼠标消息,而是将这些消息传给DefWindowProc,从而使Windows执行系统功能。就这方面来说,非显示区域鼠标消息类似于系统键盘消息WM_SYSKEYDOWN、WM_SYSKEYUP和WM_SYSCHAR。
非显示区域鼠标消息几乎完全与显示区域鼠标消息相对应。消息中含有字母「NC」以表示是非显示区域消息。如果鼠标在窗口的非显示区域中移动,那么窗口消息处理程序会接收到WM_NCMOUSEMOVE消息。鼠标按键产生如表7-2所示的消息。
表7-2 |
键 |
按下 |
释放 |
按下(双击) |
左 |
WM_NCLBUTTONDOWN |
WM_NCLBUTTONUP |
WM_NCLBUTTONDBLCLK |
中 |
WM_NCMBUTTONDOWN |
WM_NCMBUTTONUP |
WM_NCMBUTTONDBLCLK |
右 |
WM_NCRBUTTONDOWN |
WM_NCRBUTTONUP |
WM_NCRBUTTONDBLCLK |
对非显示区域鼠标消息,wParam和lParam参数与显示区域鼠标消息的wParam和lParam参数不同。wParam参数指明移动或者按鼠标按键的非显示区域。它设定为WINUSER.H中定义的以HT开头的标识符之一(HT表示「命中测试」)。
lParam参数的低位word为x坐标,高位word为y坐标,但是,它们是屏幕坐标,而不是像显示区域鼠标消息那样指的是显示区域坐标。对屏幕坐标,显示器左上角的x和y的值为0。当往右移时x的值增加,往下移时y的值增加(见图7-2)。
您可以用两个Windows函数将屏幕坐标转换为显示区域坐标或者反之:
ScreenToClient (hwnd, &pt) ; ClientToScreen (hwnd, &pt) ;
这里pt是POINT结构。这两个函数转换了保存在结构中的值,而且没有保留以前的值。注意,如果屏幕坐标点在窗口显示区域的上面或者左边,显示区域坐标x或y值就是负值。
图7-2 屏幕坐标与客户显示区域坐标 |
命中测试消息
如果您数一下,就可以知道我们已经介绍了21个鼠标消息中的20个,最后一个消息是WM_NCHITTEST,它代表「非显示区域命中测试」。此消息优先于所有其它的显示区域和非显示区域鼠标消息。lParam参数含有鼠标位置的x和y屏幕坐标,wParam参数没有用。
Windows应用程序通常把这个消息传送给DefWindowProc,然后Windows用WM_NCHITTEST消息产生与鼠标位置相关的所有其它鼠标消息。对于非显示区域鼠标消息,在处理WM_NCHITTEST时,从DefWindowProc传回的值将成为鼠标消息中的wParam参数,这个值可以是任意非显示区域鼠标消息的wParam值再加上以下内容:
HTCLIENT HTNOWHERE HTTRANSPARENT HTERROR |
显示区域 不在窗口中 窗口由另一个窗口覆盖 使DefWindowProc产生警示用的哔声 |
如果DefWindowProc在其处理WM_NCHITTEST消息后传回HTCLIENT,那么Windows将把屏幕坐标转换为显示区域坐标并产生显示区域鼠标消息。
如果您还记得我们如何通过拦截WM_SYSKEYDOWN消息来停用所有的系统键盘功能,那么您可能会想我们可否通过拦截鼠标消息完成类似的事情。完全可以!只要您在窗口消息处理程序中包含以下几条叙述:
case WM_NCHITTEST: return (LRESULT) HTNOWHERE ;
就可以有效地禁用您窗口中的所有显示区域和非显示区域鼠标消息。这样一来,当鼠标在您的窗口(包括系统菜单图标、缩放按钮以及关闭按钮)中时,鼠标按键将会失效。
从消息产生消息
Windows用WM_NCHITTEST消息产生所有其它鼠标消息,这种由消息引出其它消息的想法在Windows中是很普遍的。让我们来举个例子。您知道,如果您在一个Windows程序的系统菜单图标上双击一下,那么程序将会终止。双击产生一系列的WM_NCHITTEST消息。由于鼠标定位在系统菜单图标上,因此DefWindowProc将传回HTSYSMENU的值,并且Windows把wParam等于HTSYSMENU的WM_NCLBUTTONDBLCLK消息放在消息队列中。
窗口消息处理程序通常把鼠标消息传递给DefWindowProc,当DefWindowProc接收到wParam参数等于HTSYSMENU的WM_NCLBUTTONDBLCLK消息时,它就把wParam参数等于SC_CLOSE的WM_SYSCOMMAND消息放入消息队列中(这个WM_SYSCOMMAND消息是在使用者从系统菜单中选择「Close」时产生的)。同样地,窗口消息处理程序也把这个消息传给DefWindowProc。DefWindowProc通过给窗口消息处理程序发送WM_CLOSE消息来处理该消息。
如果一个程序在终止之前要求来自使用者的确认,那么窗口消息处理程序就需要拦截WM_CLOSE,否则,DefWindowProc呼叫DestroyWindow函数来处理WM_CLOSE。除了其它处理,DestroyWindow还给窗口消息处理程序发送一个WM_DESTROY消息。窗口消息处理程序通常用下列程序代码来处理WM_DESTROY消息:
caseWM_DESTROY: PostQuitMessage (0) ; return 0 ;
PostQuitMessage使得Windows把WM_QUIT消息放入消息队列中,此消息永远不会到达窗口消息处理程序,因为它使GetMessage传回0,并终止消息循环,从而也终止了程序。
我在前面讨论了Windows Explorer如何响应鼠标的单击和双击。显然,程序(或者更精确的说,如同Windows Explorer般使用list view control)必须确定使用者鼠标所指向的是哪一个文件。
这叫做「命中测试」。正如DefWindowProc在处理WM_NCHITTEST消息时做一些命中测试一样,窗口消息处理程序经常必须在显示区域中进行一些命中测试。一般来说,命中测试中会使用x和y坐标值,它们由传到窗口消息处理程序的鼠标消息的lParam参数给出。
一个假想的例子
有这样一个例子。假设您的程序需要显示几列按字母排列的文件。通常,您可以使用list view control,他会帮您由于要做全部的命中测试工作。但我们假设您由于某种原因而不能使用,这时就需要自己来做了。让我们假定文件名保存在称为szFileNames的已排序字符串指针数组中。
让我们也假定文件列表开始于显示区域的顶端,显示区域为cxClient图素宽,cyClient图素高,每列为cxColWidth图素宽,每个字符高度为cyChar图素高。那么每栏可填入的文件数就是:
iNumInCol = cyClient / cyChar ;
接收到一个鼠标单击消息后,您就能从lParam获得cxMouse和cyMouse坐标。然后可以用下面的公式来计算使用者所指的是哪一列的文件名:
iColumn = cxMouse / cxColWidth ;
相对于列顶端的文件名位置为:
iFromTop = cyMouse / cyChar ;
现在您就可以计算szFileNames数组的下标:
iIndex = iColumn * iNumInCol + iFromTop ;
如果iIndex超过了数组中的文件数,则表示使用者是在显示器的空白区域内按鼠标按键。
在许多情况下,命中测试要比本例更加复杂。在显示一幅包含许多小图形的图像时,您必须决定要显示的每个小图形的坐标。在命中计算中,您必须从坐标找到对象。但这将在使用不确定字体大小的字处理程序中变得非常凌乱,因为您必须找到字符在字符串中的位置。
范例程序
程序7-2所示的CHECKER1程序展示了一些简单的命中测试,此程序把显示区域分为5×5的25个矩形。如果您在某个矩形中按下鼠标按键,那么在该矩形中将出现一个「X」。如果您再按一次,那么「X」将被删除。
CHECKER1.C /*------------------------------------------------------------------------- CHECKER1.C -- Mouse Hit-Test Demo Program No. 1 (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker1") ; 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 ("Checker1 Mouse Hit-Test 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,LPARAMlParam) { static BOOL fState[DIVISIONS][DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) / cyBlock ; if (x < DIVISIONS && y < DIVISIONS) { fState [x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; } else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) { Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) { MoveToEx (hdc, x * cxBlock, y * cyBlock, NULL) ; LineTo(hdc, (x+1) * cxBlock, (y+1) * cyBlock) ; MoveToEx (hdc, x * cxBlock, (y+1) * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock, y * cyBlock) ; } } EndPaint (hwnd,&ps); return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图7-3是CHECKER1的显示。程序画的25个矩形的宽度和高度均相同。这些宽度和高度保存在cxBlock和cyBlock中,当显示区域大小发生改变时,将重新对这些值进行计算。WM_LBUTTONDOWN处理过程使用鼠标坐标来确定在哪个矩形中按下了键,它在fState数组中标志目前矩形的状态,并使该矩形区域失效,从而产生WM_PAINT消息。
图7-3 CHECKER1的屏幕显示 |
如果显示区域的宽度和高度不能被5整除,那么在显示区域的左边和下边将有一小条区域不能被矩形所覆盖。对于错误情况,CHECKER1通过呼叫MessageBeep响应此区域中的鼠标按键操作。
当CHECKER1收到WM_PAINT消息时,它通过GDI的Rectangle函数来重新绘制显示区域。如果设定了fState值,那么CHECKER1将使用MoveToEx和LineTo函数来绘制两条直线。在处理WM_PAINT期间,CHECKER1在重新绘制之前并不检查每个矩形区域的有效性,尽管它可以这样做。检查有效性的一种方法是在循环中为每个矩形块建立RECT结构(使用与WM_LBUTTONDOWN处理程序中相同的公式),并使用IntersectRect函数检查它是否与无效矩形(ps.rcPaint)相交。
CHECKER1只能在装有鼠标情况下才可执行。下面我们在程序中加入键盘接口,就如同第六章中对SYSMETS程序所做的那样。不过,即使在一个使用鼠标光标作为指向用途的程序中加入键盘接口,我们还是必须处理鼠标光标的移动和显示问题。
即使没有安装鼠标,Windows仍然可以显示一个鼠标光标。Windows为这个光标保存了一个「显示计数」。如果安装了鼠标,显示计数会被初始化为0;否则,显示计数会被初始化为-1。只有在显示计数非负时才显示鼠标光标。要增加显示计数,您可以呼叫:
ShowCursor (TRUE) ;
要减少显示计数,可以呼叫:
ShowCursor (FALSE) ;
您在使用ShowCursor之前,不需要确定是否安装了鼠标。如果您想显示鼠标光标,而不管鼠标存在与否,那么只需呼叫ShowCursor来增加显示计数。增加一次显示计数之后,如果没有安装鼠标则减少它以隐藏光标,如果安装了鼠标,则保留其显示。
即使没有安装鼠标,Windows也保留了鼠标目前的位置。如果没有安装鼠标,而您又显示鼠标光标,光标就可能出现在显示器的任意位置,直到您确实移动了它。要获得光标的位置,可以呼叫:
GetCursorPos (&pt) ;
其中pt是POINT结构。函数使用鼠标的x和y坐标来填入POINT字段。要设定光标位置,可以使用:
SetCursorPos (x, y) ;
在这两种情况下,x和y都是屏幕坐标,而不是显示区域坐标(这是很明显的,因为这些函数没有要求hwnd参数)。前面已经提到过,呼叫ScreenToClient和ClientToScreen就能做到屏幕坐标与客户坐标的相互转换。
如果您在处理鼠标消息并转换显示区域坐标时呼叫GetCursorPos ,这些坐标可能与鼠标消息的lParam参数中的坐标稍微有些不同。从GetCursorPos传回的坐标表示鼠标目前的位置。lParam中的坐标则是产生消息时鼠标的位置。
您或许想写一个键盘处理程序:使用键盘方向键来移动鼠标光标,使用Spacebar和Enter键来仿真鼠标按键。您肯定不希望每次按键只是将鼠标光标移动一个图素,如果这样做,当要把鼠标光标从显示器的一边移动到另一边时,会使用者在很长一段时间内都要按住同一个方向键。
如果您需要实作鼠标光标的键盘接口,并保持光标的精确定位能力,那么您可以采用下面的方式来处理按键消息:当按下方向键时,一开始鼠标光标移动较慢,但随后会加快。您也许还记得WM_KEYDOWN消息中的lParam参数标志着按键消息是否是重复活动的结果,这就是此参数的一个重要应用。
在CHECKER中加入键盘接口
程序7-3所示的CHECKER2程序,除了包括键盘接口外,和CHECKER1是一样的,您可以使用左、右、上和下方向键在25个矩形之间移动光标。Home键把光标移动到矩形的左上角, End键把光标移动到矩形的右下角。Spacebar和Enter键都能切换X标记。
CHECKER2.C /*---------------------------------------------------------------------------- CHECKER2.C -- Mouse Hit-Test Demo Program No. 2 (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker2") ; 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 ("Checker2 Mouse Hit-Test 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 BOOL fState[DIVISIONS][DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; POINT point ; RECT rect ; switch (message) { case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_SETFOCUS : ShowCursor (TRUE) ; return 0 ; case WM_KILLFOCUS : ShowCursor (FALSE) ; return 0 ; case WM_KEYDOWN : GetCursorPos (&point) ; ScreenToClient (hwnd, &point) ; x = max (0, min (DIVISIONS - 1, point.x / cxBlock)) ; y = max (0, min (DIVISIONS - 1, point.y / cyBlock)) ; switch (wParam) { case VK_UP : y-- ; break ; case VK_DOWN : y++ ; break ; case VK_LEFT : x-- ; break ; case VK_RIGHT : x++ ; break ; case VK_HOME : x = y = 0 ; break ; case VK_END : x = y = DIVISIONS - 1 ; break ; case VK_RETURN : case VK_SPACE : SendMessage (hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG (x * cxBlock, y * cyBlock)) ; break ; } x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; point.x = x * cxBlock + cxBlock / 2 ; point.y = y * cyBlock + cyBlock / 2 ; ClientToScreen (hwnd, &point) ; SetCursorPos (point.x, point.y) ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) / cyBlock ; if (x < DIVISIONS && y < DIVISIONS) { fState[x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; } else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) { Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) { MoveToEx (hdc, x *cxBlock, y *cyBlock, NULL) ; LineTo (hdc, (x+1)*cxBlock, (y+1)*cyBlock) ; MoveToEx (hdc, x *cxBlock, (y+1)*cyBlock, NULL) ; LineTo (hdc, (x+1)*cxBlock, y *cyBlock) ; } } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
CHECKER2中的WM_KEYDOWN的处理方式决定光标的位置(用GetCursorPos),把屏幕坐标转换为显示区域坐标(用ScreenToClient),并用矩形方块的宽度和高度来除这个坐标。这会产生指示矩形位置的x和y值(5×5数组)。当按下一个键时,鼠标光标可能在或不在显示区域中,所以x和y必须经过min和max宏处理以保证它们的范围是0到4之间。
对方向键,CHECKER2近似地增加或减少x和y。如果是Enter键或Spacebar键,那么CHECKER2使用SendMessage把WM_LBUTTONDOWN消息发送给它自身。这种技术类似于在第六章SYSMETS程序中把键盘接口加到窗口滚动条时所使用的方法。WM_KEYDOWN的处理方式是通过计算指向矩形中心的显示区域坐标,再用ClientToScreen转换成屏幕坐标,然后用SetCursorPos设定光标位置来实作的。
将子窗口用于命中测试
有些程序(例如,Windows的「画图」程序),把显示区域划分为几个小的逻辑区域。「画图」程序在其左边有一个由图示组成的工具菜单区,在底部有颜色菜单区。在这两个区做命中测试的时候,「画图」必须在使用者选中菜单项之前记住菜单的位置。
不过,也可能不需要这么做。实际上,画风经由使用子窗口简化了菜单的绘制和命中测试。子窗口把整个矩形区域划分为几个更小的矩形区,每个子窗口有自己的窗口句柄、窗口消息处理程序和显示区域,每个窗口消息处理程序接收只适用于它的子窗口的鼠标消息。鼠标消息中的lParam参数含有相当于该子窗口显示区域左上角的坐标,而不是其父窗口(那是「画图」的主应用程序窗口)显示区域左上角的坐标。
以这种方式使用子窗口有助于程序的结构化和模块化。如果子窗口使用不同的窗口类别,那么每个子窗口都有它自己的窗口消息处理程序。不同的窗口也可以定义不同的背景颜色和不同的内定光标。在第九章中,我将看到「子窗口控件」-滚动条、按钮和编辑方块等预先定义的子窗口。现在,我们说明在CHECKER程序中是如何使用子窗口的。
CHECKER中的子窗口
程序7-4所示的CHECKER3程序,这一版本建立了25个处理鼠标单击的子窗口。它没有键盘接口,但是可以按本章后面的CHECKER4程序范例的方法添加。
CHECKER3.C /*--------------------------------------------------------------------------- CHECKER3.C -- Mouse Hit-Test Demo Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szChildClass[] = TEXT ("Checker3_Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker3") ; 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 ; } wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; hwnd = CreateWindow (szAppName, TEXT ("Checker3 Mouse Hit-Test 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 HWND hwndChild[DIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; switch (message) { case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y << 8 | x), (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow ( hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : MessageBeep (0) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // on/off flag return 0 ; case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; if (GetWindowLong (hwnd, 0)) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; } EndPaint (hwnd, &ps) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
CHECKER3有两个窗口消息处理程序WndProc和ChildWndProc。WndProc还是主(或父)窗口的窗口消息处理程序。ChildWndProc是针对25个子窗口的窗口消息处理程序。这两个窗口消息处理程序都必须定义为CALLBACK函数。
因为窗口消息处理程序与特定的窗口类别结构相关联,该窗口类别结构由Windows呼叫RegisterClass函数来注册,CHECKER3需要两个窗口类别。第一个窗口类别用于主窗口,名为「Checker3」。第二个窗口类别名为「Checker3_Child」。当然,您不必选择像这样有意义的名字。
CHECKER3在WinMain函数中注册了这两个窗口类别。注册完常规的窗口类别之后,CHECKER3只是简单地重新使用wndclass结构中的大多数的字段来注册Checker3_Child类别。无论如何,有四个字段根据子窗口类别而设定为不同的值:
- pfnWndProc字段设定为ChildWndProc,子窗口类别的窗口消息处理程序。
- cbWndExtra字段设定为4字节,或者更确切地用sizeof
(long)。该字段告诉Windows在其为依据此窗口类别的窗口保留的内部结构中,预留了4字节额外的空间。您能使用此空间来保存每个窗口的可能有所不同的信息。
- 因为像CHECKER3中的子窗口不需要图标,所以hIcon字段设定为NULL 。
- pszClassName字段设定为「Checker3_Child」,是类别的名称。
通常,在WinMain中,CreateWindow呼叫建立依据Checker3类别的主窗口。然而,当WndProc收到WM_CREATE消息后,它呼叫CreateWindow 25次以建立25个Checker3_Child类别的子窗口。表7-3是在WinMain中CreateWindow呼叫的参数,与在建立25个子窗口的WndProc中CreateWindow呼叫的参数间的比较。
表7-3 |
参数 |
主窗口 |
子窗口 |
窗口类别 |
「Checker3」 |
「Checker3_Child」 |
窗口标题 |
「Checker3...」 |
NULL |
窗口样式 |
WS_OVERLAPPEDWINDOW |
WS_CHILDWINDOW | WS_VISIBLE |
水平位置 |
CW_USEDEFAULT |
0 |
垂直位置 |
CW_USEDEFAULT |
0 |
宽度 |
CW_USEDEFAULT |
0 |
高度 |
CW_USEDEFAULT |
0 |
父窗口句柄 |
NULL |
hwnd |
菜单句柄/子ID |
NULL |
(HMENU) (y << 8 | x) |
执行实体句柄 |
hInstance |
(HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) |
额外参数 |
NULL |
NULL |
一般情况下,子窗口要求有关位置和大小的参数,但是在CHECKER3中的子窗口由WndProc确定位置和大小。对于主窗口,因为它本身就是父窗口,所以它的父窗口句柄是NULL。当使用CreateWindow呼叫来建立一个子窗口时,就需要父窗口句柄了。
主窗口没有菜单,因此参数是NULL。对于子窗口,相同位置的参数称为子ID(或子窗口ID)。这是唯一代表子窗口的数字。像我们在第十一章将看到的一样,在处理对话框的子窗口控件时,子ID显得更为重要。对于CHECKER3来说,我只是简单地将子ID设定为一个数值,该数值是每个子窗口在5×5的主窗口中的x和y位置的组合。
CreateWindow函数需要一个执行实体句柄。在WinMain中,执行实体句柄可以很容易地取得,因为它是WinMain的一个参数。在建立子窗口时, CHECKER3必须用GetWindowLong来从Windows为窗口保留的结构中取得hInstance值(相对于GetWindowLong,我也能将hInstance的值保存到整体变量,并直接使用它)。
每一个子窗口都在hwndChild数组中保存了不同的窗口句柄。当WndProc接收到一个WM_SIZE消息后,它将为这25个子窗口呼叫MoveWindow。MoveWindow的参数表示子窗口左上角相对于父窗口显示区域的坐标、子窗口的宽度和高度以及子窗口是否需要重画。
现在让我们看一下ChildWndProc。此窗口消息处理程序为所有这25个子窗口处理消息。ChildWndProc的hwnd参数是子窗口接收消息的句柄。当ChildWndProc处理WM_CREATE消息时(因为有25个子窗口,所以要发生25次),它用SetWindowWord在窗口结构保留的额外区域中储存一个0值(通过在定义窗口类别时使用的cbWndExtra来保留的空间)。ChildWndProc用此值来恢复目前矩形的状态(有X或没有X)。在子窗口中单击时,WM_LBUTTONDOWN处理例程简单地修改这个整数值(从0到1,或从1到0),并使整个子窗口无效。此区域是被单击的矩形。WM_PAINT的处理很简单,因为它所绘制的矩形与显示区域一样大。
因为CHECKER3的C原始码文件和.EXE文件比CHECKER1的大(更不用说程序的说明了),我不会试着告诉你说CHECKER3比CHECKER1更简单。但请注意,我们没有做任何的鼠标命中测试!我们所要的,就是知道CHECKER3中是否有个子窗口得到了命中窗口的WM_LBUTTONDOWN消息。
子窗口和键盘
为CHECKER3添加键盘接口就像CHECKER系列构想中的最后一步。但在这样做的时候,可能有更适当的做法。在CHECKER2中,鼠标光标的位置决定按下Spacebar键时哪个区域将获得标记符号。当我们处理子窗口时,我们能从对话框功能中获得提示。在对话框中,带有闪烁的插入符号或点划的矩形的子窗口表示它有输入焦点(当然也可以用键盘进行定位)。
我们不需要把Windows内部已有的对话框处理方式重新写过,我只是要告诉您大致上应该如何在应用程序中仿真对话框。研究过程中,您会发现这样一件事:父窗口和子窗口可能要共享同键盘消息处理。按下Spacebar键和Enter键时,子窗口将锁定复选标记。按下方向键时,父窗口将在子窗口之间移动输入焦点。实际上,当您在子窗口上单击时,情况会有些复杂,这时是父窗口而不是子窗口获得输入焦点。
CHECKER4.C如程序7-5所示。
CHECKER4.C /*--------------------------------------------------------------------------- CHECKER4.C -- Mouse Hit-Test Demo Program No. 4 (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; int idFocus = 0 ; TCHAR szChildClass[] = TEXT ("Checker4_Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker4") ; 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 ; } wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; hwnd = CreateWindow (szAppName, TEXT ("Checker4 Mouse Hit-Test 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 HWND hwndChild[DIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; switch (message) { case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y << 8 | x), HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow ( hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : MessageBeep (0) ; return 0 ; // On set-focus message, set focus to child window case WM_SETFOCUS: SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; // On key-down message, possibly change the focus window case WM_KEYDOWN: x = idFocus & 0xFF ; y = idFocus >> 8 ; switch (wParam) { case VK_UP: y-- ; break ; case VK_DOWN: y++ ; break ; case VK_LEFT: x-- ; break ; case VK_RIGHT: x++ ; break ; case VK_HOME: x = y = 0 ; break ; case VK_END: x = y = DIVISIONS - 1 ; break ; default: return 0 ; } x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; idFocus = y << 8 | x ; SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // on/off flag return 0 ; case WM_KEYDOWN: // Send most key presses to the parent window if (wParam != VK_RETURN && wParam != VK_SPACE) { SendMessage (GetParent (hwnd), message, wParam, lParam) ; return 0 ; } // For Return and Space, fall through to toggle the square case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; SetFocus (hwnd) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; // For focus messages, invalidate the window for repaint case WM_SETFOCUS: idFocus = GetWindowLong (hwnd, GWL_ID) ; // Fall through case WM_KILLFOCUS: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; // Draw the "x" mark if (GetWindowLong (hwnd, 0)) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; } // Draw the "focus" rectangle if (hwnd == GetFocus ()) { rect.left += rect.right / 10 ; rect.right -= rect.left ; rect.top += rect.bottom / 10 ; rect.bottom -= rect.top ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; SelectObject (hdc, CreatePen (PS_DASH, 0, 0)) ; Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ; DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ; } EndPaint (hwnd, &ps) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
您应该能回忆起每一个子窗口有唯一的子窗口ID,该ID在呼叫CreateWindow建立窗口时定义。在CHECKER3中,此ID是矩形的x和y位置的组合。一个程序可以通过下面的呼叫来获得一个特定子窗口的子窗口ID:
idChild = GetWindowLong (hwndChild, GWL_ID) ;
下面的函数也有同样的功能:
idChild = GetDlgCtrlID (hwndChild) ;
正如函数名称所表示的,它主要用于对话框和控制窗口。如果您知道父窗口的句柄和子窗口ID,此函数也可以获得子窗口的句柄:
hwndChild = GetDlgItem (hwndParent, idChild) ;
在CHECKER4中,整体变量idFocus用于保存目前输入焦点窗口的子窗口ID。我在前面说过,当您在子窗口上面单击鼠标时,它们不会自动获得输入焦点。因此,CHECKER4中的父窗口将通过呼叫下面的函数来处理WM_SETFOCUS消息:
SetFocus (GetDlgItem (hwnd, idFocus)) ;
这样设定一个子窗口为输入焦点。
ChildWndProc处理WM_SETFOCUS和WM_KILLFOCUS消息。对于WM_SETFOCUS,它将保存在整体变量idFocus中接收输入焦点的子窗口ID。对于这两种消息,窗口是无效的,并产生一个WM_PAINT消息。如果WM_PAINT消息画出了有输入焦点的子窗口,则它将用PS_DASH画笔的风格画一个矩形以表示此窗口有输入焦点。
ChildWndProc也处理WM_KEYDOWN消息。对于除了Spacebar和Enter键以外的其它消息,WM_KEYDOWN都将给父窗口发送消息。另外,窗口消息处理程序也处理类似WM_LBUTTONDOWN消息的消息。
处理方向移动键是父窗口的事情。在风格相似的CHECKER2中,此程序可获得有输入焦点的子窗口的x和y坐标,并根据按下的特定方向键来改变它们。然后通过呼叫SetFocus将输入焦点设定给新的子窗口。
一个窗口消息处理程序通常只在鼠标光标位于窗口的显示区域,或非显示区域上时才接收鼠标消息。一个程序也可能需要在鼠标位于窗口外时接收鼠标消息。如果是这样,程序可以自行「拦截」鼠标。别害怕,这么做没什么大不了的。
设计矩形
为了说明拦截鼠标的必要性,请让我们看一下BLOKOUT1程序(如程序7-6所示)。此程序看起来达到了一定的功能,但它却有十分严重的缺陷。
BLOKOUT1.C /*---------------------------------------------------------------------------- BLOKOUT1.C -- Mouse Button Demo Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #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 ("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') // i.e., Escape { 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-4显示了一个已经画完的矩形和另一个正在画的矩形。
图7-4 BLOKOUT1的屏幕显示 |
那么,问题在哪里呢?
请试一试下面的操作:在BLOKOUT1的显示区域按下鼠标的左键,然后将光标移出窗口。程序将停止接收WM_MOUSEMOVE消息。现在释放按钮,BLOKOUT1将不再获得WM_BUTTONUP消息,因为光标在显示区域以外。然后将光标移回BLOKOUT1的显示区域,窗口消息处理程序仍然认为按钮处于按下状态。
这样做并不好,因为程序不知道发生了什么事情。
拦截的解决方案
BLOKOUT1显示了一些常见的程序功能,但它的程序代码显然有缺陷。这种问题就是要使用鼠标拦截来对付。如果使用者正在拖曳鼠标,那么当鼠标短时间内被拖出窗口时应该没有什么大问题,程序应该仍然控制着鼠标。
拦截鼠标要比放置一个老鼠夹子容易一些,您只要呼叫:
SetCapture (hwnd) ;
在这个函数呼叫之后,Windows将所有鼠标消息发给窗口句柄为hwnd的窗口消息处理程序。之后收到鼠标消息都是以显示区域消息的型态出现,即使鼠标正在窗口的非显示区域。lParam参数将指示鼠标在显示区域坐标中的位置。不过,当鼠标位于显示区域的左边或者上方时,这些x和y坐标可以是负的。当您想释放鼠标时,呼叫:
ReleaseCapture () ;
从而使处理恢复正常。
在32位的Windows中,鼠标拦截要比在以前的Windows版本中有多一些限制。特别是,如果鼠标被拦截,而鼠标按键目前并未被按下,并且鼠标光标移到了另一个窗口上,那么将不是由拦截鼠标的那个窗口,而是由光标下面的窗口来接收鼠标消息。对于防止一个程序在拦截鼠标之后不释放它而引起整个系统的混乱,这是必要的。
换句话说,只有当鼠标按键在您的显示区域中被按下时才拦截鼠标;当鼠标按键被释放时,才释放鼠标拦截。
BLOKOUT2程序
展示鼠标拦截的BLOKOUT2程序如程序7-7所示。
BLOKOUT2.C /*---------------------------------------------------------------------------- BLOKOUT2.C -- Mouse Button & Capture Demo Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #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 ("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') // i.e., Escape { 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程序和BLOKOUT1程序一样,只是多了三行新程序代码:在WM_LBUTTONDOWN消息处理期间呼叫SetCapture,而在WM_LBUTTONDOWN和WM_CHAR消息处理期间呼叫ReleaseCapture。检查画出窗口:使窗口小于屏幕大小,开始在显示区域画出一块矩形,然后将鼠标光标移出显示区域的右边或下边,最后释放鼠标按键。程序将获得整个矩形的坐标。但是需要扩大窗口才能看清楚它。
拦截鼠标并非只适用于那些古怪的应用程序。如果您需要鼠标按键在显示区域按下时都能够追踪WM_MOUSEMOVE消息,并直到鼠标按键被释放为止,那么您就应该拦截鼠标。这样将简化您的程序,同时又符合使用者的期望。
与传统的鼠标相比,Microsoft IntelliMouse的特点是在两个键之间多了一个小滑轮。您可以按下这个滑轮,这时它的功能相当于鼠标按键的中键;或者您也可以用食指来转动它,这会产生一条特殊的消息,叫做WM_MOUSEWHEEL。使用鼠标滑轮的程序通过滚动或放大文件来响应此消息。它最初听起来像一个不必要的隐藏机关,但我必须承认,我很快就习惯于使用鼠标滑轮来滚动Microsoft Word和Microsoft Internet Explorer了。
我不想讨论鼠标滑轮的所有使用方法。实际上,我只是想告诉您如何在现有的程序(例如程序SYSMETS4)中添加鼠标滑轮处理程序,以便在显示区域中卷动数据。最终的SYSMETS程序如程序7-8所示。
SYSMETS.C /*---------------------------------------------------------------------------- SYSMETS.C -- Final System Metrics Display Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets") ; 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 ("Get System Metrics"), WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, 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 int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; static int iDeltaPerLine, iAccumDelta ; // for mouse wheel logic HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; ULONG ulScrollLines ; // for mouse wheel logic switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; // Fall through for mouse wheel information case WM_SETTINGCHANGE: SystemParametersInfo (SPI_GETWHEELSCROLLLINES, 0, &ulScrollLines, 0) ; // ulScrollLines usually equals 3 or 0 (for no scrolling) // WHEEL_DELTA equals 120, so iDeltaPerLine will be 40 if (ulScrollLines) iDeltaPerLine = WHEEL_DELTA / ulScrollLines ; else iDeltaPerLine = 0 ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; // Set horizontal scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on iVertPos = si.nPos ; switch (LOWORD (wParam)) { case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos -= 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) { case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; } return 0 ; case WM_KEYDOWN : switch (wParam) { case VK_HOME : SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ; break ; case VK_END : SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ; break ; case VK_PRIOR : SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0) ; break ; case VK_NEXT : SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0) ; break ; case VK_UP : SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ; break ; case VK_DOWN : SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ; break ; case VK_LEFT : SendMessage (hwnd, WM_HSCROLL, SB_PAGEUP, 0) ; break ; case VK_RIGHT : SendMessage (hwnd, WM_HSCROLL, SB_PAGEDOWN, 0) ; break ; } return 0 ; case WM_MOUSEWHEEL: if (iDeltaPerLine == 0) break ; iAccumDelta += (short) HIWORD (wParam) ; // 120 or -120 while (iAccumDelta >= iDeltaPerLine) { SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ; iAccumDelta -= iDeltaPerLine ; } while (iAccumDelta <= -iDeltaPerLine) { SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ; iAccumDelta += iDeltaPerLine ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) { x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut ( hdc, x, y, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut ( hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut ( hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
转动滑轮会导致Windows在有输入焦点的窗口(不是鼠标光标下面的窗口)产生WM_MOUSEWHEEL消息。与平常一样,lParam将获得鼠标的位置,当然坐标是相对于屏幕左上角的,而不是显示区域的。另外,wParam的低字组包含一系列的旗标,用于表示鼠标按键、Shift与Ctrl键的状态。
新的信息保存在wParam的高字组。其中有一个「delta」值,该值目前可以是120或-120,这取决于滑轮的向前转动(也就是说,向鼠标的前面,即带有按钮与电缆的一端)还是向后转动。值120或-120表示文件将分别向上或向下卷动三行。这里的构想是,以后版本的鼠标滑轮能有比现在的鼠标产生更精确的移动速度信息,并且用delta值,例如40和-40,来产生WM_MOUSEWHEEL消息。这些值能使文件只向上或向下卷动一行。
为使程序能在一般化环境执行,SYSMETS将在WM_CREATE和WM_SETTINGCHANGE消息处理时,以SPI_GETWHEELSCROLLLINES作为参数来呼叫SystemParametersInfo。此值说明WHEEL_DELTA的delta值将滚动多少行,WHEEL_DELTA在WINUSER.H中定义。WHEEL_DELTA等于120,并且,在内定情况下SystemParametersInfo传回3,因此与卷动一行相联系的delta值就是40。SYSMETS将此值保存在iDeltaPerLine。
在WM_MOUSEWHEEL消息处理期间,SYSMETS将delta值给静态变量iAccumDelta。然后,如果iAccumDelta大于或等于iDeltaPerLine(或者是小于或等于-iDeltaPerLin),SYSMETS用SB_LINEUP或SB_LINEDOWN值产生WM_VSCROLL消息。对于每一个WM_VSCROLL消息,iAccumDelta由iDeltaPerLine增加(或减少)。此代码允许delta值大于、小于或等于滚动一行所需要的delta值。
下面还有
还有一个引人注目的鼠标问题:建立自订鼠标光标。我将在第十章,与其它Windows资源一起讨论此问题。