📄 7. 鼠标.txt
字号:
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」将被删除。
程序7-2 CHECKER1
CHECKER1.C
/*-------------------------------------------------------------------------
CHECKER1.C -- Mouse Hit-Test Demo Program No. 1
(c) Charles Petzold, 1998
--------------------------------------------------------------------------*/
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -