摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P223
第 6 章已经讲到,Windows 只把键盘消息发送到当前具有输入焦点的窗口。鼠标消息则不同:当鼠标经过窗口或在窗口内被单击,则即使该窗口是非活动窗口或不带输入焦点,窗口过程还是会收到鼠标消息。Windows 定义了 21 种鼠标消息。不过,其中 11 种消息与客户区无关,称为“非客户区消息”。Windows 应用程序经常忽略这类消息。
当鼠标移经窗口客户区时,窗口过程接收 WM_MOUSEMOVE 消息。在窗口客户区内按下或释放鼠标按钮时,窗口过程接收如下表所示的消息:
按 钮 | 按 下 | 释 放 | 第二次按下按钮 |
---|---|---|---|
左键 | WM_LBUTTONDOWN | WM_LBUTTONUP | WM_LBUTTONDBLCLK |
中键 | WM_MBOTTONDOWN | WM_MBUTTONUP | WM_MBUTTONDBLCLK |
右键 | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_RBUTTONDBLCLK |
窗口过程只对三键鼠标接收 MBUTTON 消息;只对双键鼠标接收 RBUTTON 消息。而只有当窗口类被定义成接收鼠标双击时,窗口过程才接收 DBLCLK(双击)消息。
对所有这些消息来说,参数 lParam 包含了鼠标的位置信息,其中低位字表示 x 坐标,高位字表示 y 坐标,它们都是相对于窗口客户区左上角的相对坐标。利用 LOWORD 宏和 HIWORD 宏,可以获取这些坐标值:
参数 wParam 表示鼠标按钮、Shift 键和 Ctrl 键的状态 。可以利用 WINUSER.H 头文件中定义的位掩码来测试参数 wParam。前缀 MK 代表“鼠标键”(mouse key)。
例如,当接收到 WM_LBUTTONDOWN 消息时,若
的值为 TRUE(非零),则表示按下左键的同时按下了 Shift 键。
鼠标移经窗口的客户区时,Windows 系统不会为鼠标经过的每个像素位置都产生 WM_MOUSEMOVE 消息。程序收到的 WM_MOUSEMOVE 消息个数取决于鼠标硬件和窗口过程处理鼠标移动消息的速度。换言之,如果消息队列里还有未处理的 WM_MOUSEMOVE 消息,Windows 就不会重复向消息队列中添加该消息。试验下面这个 CONNECT 程序,可以对 WM_MOUSEMOVE 消息的产生速度有一个全面的了解。
若在非活动窗口的客户区内按下鼠标左键,Windows 会将该窗口变为活动窗口,并向窗口过程发送 WM_LBUTTONDOWN 消息。当窗口过程接收到 WM_LBUTTONDOWN 消息时,程序就能够安全地保证该窗口是活动窗口。但是,在事先没有接收 WM_LBUTTONDOWN 消息的情况下,窗口过程仍然可以接收 WM_LBUTTONUP 消息。比如,当用户在其他窗口内按下鼠标,再移动到用户窗口,然后释放,此时就会发生这种情况。类似地,当移动鼠标到另一个窗口再释放时,前一个窗口过程在接收 WM_LBUTTONDOWN 消息后,就接收不到相应的 WM_LBUTTONUP 消息。
前面这些规则有两个例外:
- 即使鼠标位于窗口的客户区之外,窗口过程也有办法“捕获鼠标”,并且继续接收鼠标消息。本章会在后面讲述如何捕获鼠标。
- 若正在显示一个系统模式消息框或系统模式对话框,则其他任何程序都不能接收鼠标消息。当系统模式消息框或对话框处于活动状态时,它们会阻止系统切换到另一个窗口。例如,关闭 Windows 时弹出的消息框就是一个系统模式消息框。
7.2.1 简单的鼠标处理示例
为了使用户对 Windows 系统向程序发送鼠标消息的机制有一个全面的了解,CONNECT 程序进行了一些简单的鼠标处理。
CONNECT 程序处理以下三种鼠标消息。
- WM_LBUTTONDOWN CONNECT 程序情况客户区。
- WM_MOUSEMOVE 如果按下左键,CONNECT 程序就在客户区的鼠标位置上画一个黑点,并保存点的坐标。
- WM_LBUTTONUP CONNECT 程序将客户区内每个点都与其他点相连。显示结果有时会呈现出漂亮的设计图案,有时会变成浓密的一团。(如图 7-2 所示。)
CONNECT 程序的操作方法如下:将鼠标指针移到客户区,按下左键,略微移动鼠标,再释放左键。在按下左键时快速移动鼠标,就可以得到一条经过多个点的曲线。
CONNECT 程序利用了三个在第 5 章讨论过的 GDI 函数:SetPixel函数在按下左键时为每个 WM_MOUSEMOVE 消息绘制一个黑色像素点。(在高分辨率显示设备中,人眼几乎看不见一个单独的像素点。)绘制直线则需要用到 MoveToEx 函数和 LineTo 函数。
在用户释放左键时,如果鼠标指针已经移出客户区,CONNECT 程序就不会连接这些点,因为程序没有接收到 WM_LBUTTONUP 消息。此时如果再将鼠标移入客户区,并按下左键,CONNECT 程序就是清空客户区。如果想在客户区外释放鼠标,并继续设计图形,就可以在客户区外按下鼠标的左键,再将鼠标移入客户区。
CONNECT 程序最多能够保存 1000 个点。假设点的数目为 P,那么在 CONNECT 程序中,所画线条的数目等于 P * (P - 1) / 2。对 1000 个点来说,几乎需要画 500 000 条直线。取决于具体的硬件,这可能要耗费大约 1 分钟的时间。Windows 98 是一个抢占式多任务环境,因此在这段时间里,用户可以切换到其他程序。但是,当 CONNECT 程序处于忙碌状态时,用户不能对 CONNECT 程序做其他的任何操作(比如移动窗口或调整大小)。在第 20 章中,我们将会讨论如何处理类似的这种问题。
CONNECT 程序需要耗费一定的时候来绘制直线,因此,鼠标指针会变成沙漏形,并在处理 WM_PAINT 消息时回到原来的形状。这就需要两次调用 SetCursor 函数来切换两个备用指针。CONNECT 程序还调用了两次 ShowCursor 函数,其中第一次调用时参数为 TRUE,第二次调用时参数为 FLASE。
有时,“跟踪”(tracking) 一次常被用来指代程序处理鼠标移动的方式。然而,跟踪并不意味着程序的窗口过程要使用一个循环来不停地主动监视鼠标在显示设备上的运动。相反,窗口过程只会被动地处理每个到达的鼠标消息,然后迅速退出并将控制返还给 Windows 系统。
7.2.2 处理Shift键
当 CONNECT 程序接收到 WM_MOUSEMOVE 消息时,程序会对参数 wParam 和 MK_LBUTTON 进行位于(AND)运算,从而判断是否按下了左键。利用参数 wParam 还可以判断 Shift 键的状态。例如,当处理过程依赖于 Shift 键和 Ctrl 键的状态时,可能会用到类似下面的逻辑处理:
如果想在程序中同时使用鼠标的左键和右键,同时又想兼顾那些使用单键鼠标的用户,那么可以这样编写代码,使左键配合 Shift 键等效于右键。这时,鼠标的按钮处理可能如下所示:
利用虚拟键代码 VK_LBUTTON、VK_RBUTTON、BK_MBUTTON、VK_SHIFT 和 VK_CONTROL,Windows 的 GetKeyState 函数也能够返回鼠标按钮或 Shift 等键的状态。当 GetKeyState 函数 返回一个负值时 ,表示 已按下 了鼠标按钮或相应的 Shift 键或 Ctrl 键。GetKeyState 函数返回的是当前鼠标或键盘的状态,因此,状态信息与正被处理的消息是完全同步的。对于键盘中未被按下的键,不能使用 GetKeyState 函数;同样,在没有按下鼠标时,也不能使用 GetKeyState 函数。所以,请不要像下面这样做:
在消息的处理过程中,当调用 GetKeyState 函数时,只有在已经按下左键后,GetKeyState 函数才会报告左键被按下的状态。
7.2.3 鼠标双击
鼠标双击是指连续两次快速地单击。为了达到双击的效果,两次单击不仅要在物理位置上十分靠近(默认情况下,是一个平均系统字体字符宽度、半个字符高度的区域内)。还必须发生在特定的时间间隔之内。这个间隔称为“双击速度”。在控制面板中,可以改变这个时间间隔。
如果想让窗口过程接收鼠标双击消息,那么在调用 RegisterClass 初始化窗口类结构时,必须在窗口风格字段中包含标识符 CS_DBLCLKS:
如果窗口类型没有包含 CS_DBLCLKS,那么当用户连续两次快速单击左键时,窗口过程接收的消息顺序如下:
也许窗口过程还会在这些按钮消息中间接接收其他消息。用户若想定义自己的双击处理函数,可以利用 Windows 的 GetMessageTime 函数,得到两个 WM_LBUTTONDOWN 消息之间的间隔时间长度。
若窗口类的风格包含 CS_DBLCLKS,那么双击鼠标后,窗口过程会接收到如下消息:
如果双击的第一次单击与鼠标单击所执行的功能一致,那么处理双击消息就要容易得多。这是,第二次单击(WM_LBUTTONDBLCLK 消息)只需在地刺单击之后执行一些其他操作。例如,考察对 Windows 资源管理器文件列表的鼠标操作。鼠标单击选中文件,此时 Windows 资源管理器反相显示该文件。鼠标双击时执行下面两步:第一次单击会选中文件,正如鼠标单击;第二次单击指示 Windows 资源管理器打开文件。这是多么简洁的逻辑处理操作。如果双击中第一次单击与鼠标所执行的功能不一致,那么鼠标的处理逻辑就要复杂得多。