Windows编程 第十回 鼠标的秘密
你平时使用的鼠标中藏着一个鲜为人知秘密,它与鼠标名字的起源有关,这回我就给大家揭开这个秘密。请看图:
哈哈,纯粹娱乐一下。不过这回我们确实要探一探鼠标在Windows编程中的秘密。
鼠标估计大家天天都在用,我觉得没有必要再给大家“扫盲”了,如果非要讲点什么的话,那我就提一下关于鼠标操作的常用术语:单击、双击和拖拽,相信大家都很清楚。还是同讲键盘一样,讲鼠标还是从与之相关的消息入手吧。
上一回我们了解到Windows只把键盘消息发送给拥有输入焦点的窗口。而鼠标消息与此不同:只要在某窗口按下鼠标,那么该窗口的窗口过程就会收到鼠标消息,而不管该窗口之前是否活动或者是否拥有输入焦点。
好了,大家了解了一下窗口对于键盘消息与鼠标消息接收的不同之后,我们就来学习一下关于鼠标的22个消息。(好像有点多哦……)
客户区鼠标消息
由上一回我们得知Windows只把键盘消息发送给拥有输入焦点的窗口,而鼠标消息与此不同:只要鼠标跨越窗口或者在某窗口下按下鼠标键,那么窗口过程就会收到鼠标消息,不管该窗口是否活动或者是否拥有输入焦点。
当在窗口的客户区中按下或者释放一个鼠标按键时,窗口过程会接收到下面这些消息:
键 |
按下 |
释放 |
按下(双键) |
左 |
WM_LBUTTONDOWN |
WM_LBUTTONUP |
WM_LBUTTONDBLCLK |
中 |
WM_MBUTTONDOWN |
WM_MBUTTONUP |
WM_MBUTTONDBLCLK |
右 |
WM_RBUTTONDOWN |
WM_RBUTTONUP |
WM_RBUTTONDBLCLK |
对于三键鼠标,窗口过程才会收到MBUTTON消息(当然现在这种鼠标很不常见了,常见的是两键一轮的鼠标,你会发现鼠标轮除了滚动还可以点击,点击鼠标轮就会产生三键鼠标点击中键的效果,同样会产生MBUTTON的消息)。
仅当定义的窗口类能接收DBLCLK(双击)消息之后,窗口过程才能接受这类消息。既然说到此我们就先来看一下关于鼠标双击的消息。鼠标双击是指在短时间内单击鼠标键两次。要确定为双击,这两次单击必须发生在其相互的物理位置十分接近的状况下,并且发生在指定的时间间隔内。你可以在“控制面板”中改变时间间隔。
一般窗口过程是接收不到双击键的鼠标消息的,如果希望窗口过程能够收到这种消息,那么在调用RegisterClass初始化窗口类结构时,必须在窗口风格中包含CS_DBLCLKS标识符:
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
如果在窗口风格中未包含CS_DBLCLKS,而用户在短时间内双击了鼠标键,那么窗口过程会接收到下面这些消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP
如果你的窗口类别风格中包含了CS_DBLCLKS,那么双击鼠标键时窗口过程将收到如下消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
WM_LBUTTONDBLCLK消息简单地替换了第二个WM_LBUTTONDOWN消息。
双击中的第一次单击操作完成单击的功能,第二次单击操作(WM_LBUTTONDBLCLK消息)则完成第一次按键以外的事情。看一个双击的“慢动作回放”:例如,看看“Windows 资源管理器”中是如何用鼠标来操作文件列表的。第一次单击操作将选中文件,“Windows资源管理器”用反白显示出被选择的文件。第二次单击操作则指示“Windows 资源管理器”打开该文件。
别怪我啰嗦,再举一个单击的例子吧。如果你在非活动窗口的客户区中按下鼠标左键,Windows将把活动窗口改为在其中按下鼠标按键的这个窗口,然后把WM_LBUTTONDOWN消息送到该窗口的窗口过程。当释放鼠标左键时,则Windows再把WM_LBUTTONUP消息送到该窗口的窗口过程。常规情况下,BUTTONUP与BUTTONDOWN消息会成对出现在一个窗口中,但也会有例外。如果鼠标按键在另一个窗口中被释放,则这个窗口的窗口过程只能接收到WM_LBUTTONDOWN消息,而没有相应的WM_LBUTTONUP消息。同理,另一个窗口的窗口过程在未接收到WM_LBUTTONDOWN消息的情况下却先接收到了WM_LBUTTONUP消息。当然这个例子有点“极端”,但确实存在这种情况。
我们再来认识一个鼠标消息WM_MOUSEMOVE。当鼠标移过窗口的客户区,窗口过程就会收到一系列此消息。当然在你把鼠标移过客户区时,Windows并不为鼠标经过的每个可能的像素位置都产生一条WM_MOUSEMOVE消息。你的程序接收到WM_MOUSEMOVE消息的次数依赖于鼠标硬件以及你的窗口过程处理鼠标移动消息的速度。
对于上面这10个消息来说,其lParam值均含有鼠标的位置:低位字为x坐标,高位字为y坐标,这两个坐标是相对于窗口客户区左上角的位置。您可以用LOWORD和HIWORD宏来提取这些值:
x = LOWORD (lParam) ;
y = HIWORD (lParam) ;
wParam的值指示鼠标按键以及Shift和Ctrl键的状态。你可以使用头文件WINUSER.H中定义的与运算来测试wParam。(如果忘了lParam和wParam,就去复习一下第四回中消息结构体的讲解)
MK_LBUTTON |
按下左键 |
MK_MBUTTON |
按下中键 |
MK_RBUTTON |
按下右键 |
MK_SHIFT |
按下Shift键 |
MK_CONTROL |
按下Ctrl键 |
MK前缀代表“鼠标按键”。
例如,如果收到了WM_LBUTTONDOWN消息,而且值
wparam &MK_SHIFT
是TRUE(非0),你就知道当左键按下时也按下了Shift键。
再来看一个例子,如果在程序中一种功能的实现依赖于鼠标单击和Shift、Ctrl键的组合,可以使用如下方法:
if (wParam & MK_SHIFT)
{
if (wParam & MK_CONTROL)
{
//按下了Shift和Ctrl键
}
else
{
//只按下了Shift键
}
}
else
{
if (wParam & MK_CONTROL)
{
//只按下了Ctrl键
}
else
{
//Shift和Ctrl键均未按下
}
}
在窗口的客户区内移动或按下鼠标按键时,将产生前面讲的10种消息。如果鼠标在窗口的客户区之外但还在窗口内,Windows就给窗口过程发送一条“非客户区”鼠标消息。窗口非客户区包括标题栏、菜单和窗口滚动条。
通常,我们不需要处理非客户区鼠标消息,而是将这些消息传给DefWindowProc,从而使Windows执行系统功能。就这方面来说,非客户区鼠标消息类似于系统键盘消息WM_SYSKEYDOWN、WM_SYSKEYUP和WM_SYSCHAR。
非客户区鼠标消息几乎完全与显示区域鼠标消息相对应。消息中含有字母“NC”以表示是非客户区消息。
如果鼠标在窗口的非客户区中移动,那么窗口过程会接收到WM_NCMOUSEMOVE消息。
鼠标按键会产生如下表所示的消息。
键 |
按下 |
释放 |
按下(双击) |
左 |
WM_NCLBUTTONDOWN |
WM_NCLBUTTONUP |
WM_NCLBUTTONDBLCLK |
中 |
WM_NCMBUTTONDOWN |
WM_NCMBUTTONUP |
WM_NCMBUTTONDBLCLK |
右 |
WM_NCRBUTTONDOWN |
WM_NCRBUTTONUP |
WM_NCRBUTTONDBLCLK |
对非客户区的10个鼠标消息,wParam和lParam参数与客户区的10个鼠标消息的wParam和lParam参数有一定的差别。
wParam参数指明移动或者按鼠标按键的非客户区位置。它设定为以HT开头的标识符之一(HT表示 “命中测试”)。
lParam参数的低位字为x坐标,高位字为y坐标,但是,它们都是屏幕坐标,而不是像客户区鼠标消息那样指的是客户区坐标。①
命中测试消息
这个消息是WM_NCHITTEST,它代表“非客户区命中测试”。从它的名字来看它应该在“非客户区鼠标消息”中讲才对呀,既然我把它单拿出来,自然是有道理的。它的作用比较“特殊”,而且要讲的内容比较多。
当光标移动或鼠标按下、释放时,Windows系统就会发送此消息,并且此消息优先于其它的客户区和非客户区鼠标消息。这里的“优先于”怎么讲?就是Windows用WM_NCHITTEST消息产生所有其它的鼠标消息,这种由消息引出其它消息的思想在Windows中是很普遍的。(想一想我们在第四回是不是曾经遇到过?)Windows用这个消息来做什么? “HITTEST”就是“命中测试”的意思,WM_NCHITTEST消息用来获取鼠标当前命中的位置。WM_NCHITTEST的消息响应函数会根据鼠标当前的坐标来判断鼠标命中了窗口的哪个部位,消息响应函数的返回值指出了部位。再补充一点,此消息的lParam 参数含有鼠标位置的x和y屏幕坐标,wParam参数没有用。
为了便于理解,我先描述一下Windows对鼠标键按下的响应流程:
1. 确定鼠标键点击的是哪个窗口。Windows会用表记录当前屏幕上各个窗口的区域坐标,当鼠标驱动程序通知Windows鼠标键按下了,Windows根据鼠标的坐标确定它点击的是哪个窗口。
2. 确定鼠标键点击的是窗口的哪个部位。Windows会向鼠标键点击的窗口发送WM_NCHITTEST消息,来询问鼠标键点击的是窗口的哪个部位。窗口程序通常把这个消息传送给DefWindowProc默认处理,然后Windows用WM_NCHITTEST消息产生与鼠标位置相关的所有其它鼠标消息。具体地说,就是在处理WM_NCHITTEST消息时,从DefWindowProc返回的值将成为其它鼠标消息中的wParam参数。一般来说,WM_NCHITTEST消息是系统来处理的,我们用户一般不用去主动去处理它。
3. 根据鼠标键点击的部位给窗口发送相应的消息。例如:如果WM_NCHITTEST的消息响应函数的返回值是HTCLIENT,表示鼠标点击的是客户区,同时Windows将把屏幕坐标转换为客户区坐标并产生相应的客户区鼠标消息,在这里就是Windows向窗口发送WM_LBUTTONDOWN消息;如果WM_NCHITTEST的消息响应函数的返回值不是HTCLIENT(比如说是HTCAPTION),即鼠标点击的是非客户区,Windows就会向窗口发送wParam等于HTCAPTION的WM_NCLBUTTONDOWN消息。
其返回值有很多,现在简单列举一部分吧,仅供参考。
HTNOWHERE -不在窗口中
HTCLIENT - 客户区
HTCAPTION - 标题
HTSYSMENU - 系统菜单
HTMENU - 菜单
HTHSCROLL - 水平滚动条
HTVSCROLL - 垂直滚动条
HTMINBUTTON - 最小化按钮
HTMAXBUTTON - 最大化按钮
HTLEFT - 左边界
HTRIG - 右边界
HTTOP - 上边界
HTTOPLEFT - 左上角
HTTOPRIG - 右上角
HTBOTTOM - 下边界
HTBOTTOMLEFT - 左下角
HTBOTTOMRIG- 右下角
HTCLOSE- 关闭按钮
再来看个实际例子吧,方便大家理解一下上面的内容。
大家想一下我们如何来屏蔽鼠标键的操作,让其失效?我们上面讲过“Windows用WM_NCHITTEST消息产生所有其它的鼠标消息”,我们可以在窗口过程中加入以下语句:
case WM_NCHITTEST:
return (LRESULT)HTNOWHERE;//由于窗口过程返回值是LRESULT类型的,这里
//行了强制类型转换
我们把第二回的代码模板中“caseWM_CREATE”语段和“case WM_PAINT”语段(行59-68)删掉,加入上面的语段,运行一下。是不是发现鼠标对窗口(包括客户区和非客户区)所有的点击、拖动操作都失效了,因为我们截获了WM_NCHITTEST消息,返回HTNOWHERE ,“欺骗”操作系统这时鼠标“不在”窗口中的,虽然鼠标确实在窗口中。再来讲一个“欺骗”的例子吧。一般我们鼠标单击一个窗口标题栏的时才可拖动窗口移动,如果我们要想实现鼠标只要单击窗口任一个位置就可以拖动窗口的功能,这该怎么办呢?照葫芦画瓢即可:
case WM_NCHITTEST:
return (LRESULT)HTCAPTION;
这样只要你在窗口中任意位置单击鼠标,系统就会由WM_NCHITTEST消息引发wParam 值标志标题栏的WM_NCLBUTTONDOWN消息,这时当前窗口将处于 “拖拽状态”(Windows内部记录了每个窗口的状态信息)。由于标识了“拖拽状态”,则从此刻起到鼠标键放开之前,你的鼠标移动状况完全由Windows跟踪。它根据鼠标的移动,使得窗口作“同步”移动。
注意,这个过程中,窗口不会收到WM_NCMOUSEMOVE消息,因为窗口和鼠标是“同步”移动的,你的鼠标相对于窗口是静止的。
注:但问题同时也出现了, 我想在右击这个窗体客户区的时候弹出一个菜单,当我完成 WM_RBUTTONDOWN 这个消息的时候,发现窗体收不到这个消息, 将WM_NCHITTEST消息的实现去掉就可以了,看了一原因是:
因为你在WM_NCHITTEST中处理了鼠标消息,把它定位成HTCAPTION,也就是鼠标在标题栏上,而标题栏属于非客户区(NC);非客户区的事件消息都是以WM_NC开头的。也就是说,当你的WM_NCHITTEST返回HTCAPTION时,原来可以用WM_LBUTTONDOWN处理的消息,你只能用WM_NCLBUTTONDOWN来处理。
解决方法:同时处理WM_NCHITTEST和WM_NCRBUTTONDOWN,而不处理WM_RBUTTONDOWN。
最后一个鼠标消息
我们平时常用的鼠标轮来移动滚动条,当鼠标轮转动时就会产生WM_MOUSEWHEEL消息。关于这个消息我将在以后和滚动条一起讲。
①对屏幕坐标,显示器左上角的x和y的值为0。当往右移时x的值增加,往下移时y的值增加。
你可以用两个Windows函数将屏幕坐标转换为客户区坐标或者反之:
ScreenToClient (hwnd, &pt) ;
ClientToScreen (hwnd, &pt) ;
这里pt是POINT结构。这两个函数转换了保存在结构中的值,而且没有保留以前的值。注意,如果屏幕坐标点在窗口客户区的上面或者左边,客户区坐标x或y值就是负值。