非激活窗口中响应鼠标滚动之实践篇
基础知识
钩子(HOOK),是Windows消息处理机制的一个平台,应用程序可以通过设置钩子,监视指定窗口的某种消息,当消息到达后,在被目标窗口处理之前处理它,被监视的窗口可以是其他进程所创建的。
具体深入的知识,可参考这篇文章Windows Hook钩子技术全面总结
按照钩子拦截消息的范围,可分为
- 全局钩子,会拦截所有窗口线程消息
- 线程钩子,只会拦截特定线程消息
相关接口函数介绍:
HHOOK SetWindowsHookEx(
int idHook, //钩子类型,如 WH_MOUSE_LL, WH_MOUSE
HOOKPROC lpfn, //拦截回调函数
HINSTANCE hmod, // dll句柄
DWORD dwThreadId // 线程Id,当为0时表示全局钩子
);
该函数用于安装钩子。全局钩子要拦截其他线程消息,必须通过dll中的回调函数来触发,处理不当就造成系统崩溃,不建议新手使用,推荐使用线程钩子。
在该需求中,我们重点关注鼠标钩子,鼠标钩子以下两种:
- WM_MOUSE:普通鼠标钩子,可截获钩子所在模块的鼠标事件。
- WM_MOUSE_LL:低级鼠标钩子,可截获整个系统的鼠标事件。
在当前场景下,我们选用 WM_MOUSE
钩子即可。注意,WM_MOUSE_LL
钩子只能适用于全局钩子类型,若要监控指定线程,则在注入阶段会提示1429错误,错误信息为此挂接程序只可整体设置
。
LRESULT CALLBACK MouseProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
此函数定义了鼠标钩子回调函数的接口,
LRESULT CallNextHookEx(
HHOOK hhk,
int nCode,
WPARAM wParam,
LPARAM lParam
);
此函数用于将钩子信息在当前钩子链中传递给下一个钩子.
BOOL UnhookWindowsHookEx(
HHOOK hhk
);
此函数用于卸载钩子。
整体设计
该项功能的整体设计图如下:
该项功能的关键业务逻辑有两点:
- 注册鼠标钩子
- 截获到滚轮消息后进行消息转换
注册鼠标钩子
注册WM_MOUSE
鼠标钩子类型就够用了,这里注意一点,官方文档上针对WM_MOUSE
类型的消息的回调函数原型是:
LRESULT CALLBACK MouseProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
wParam代表鼠标消息标示符,例如,WM_MOUSEWHEEL
、WM_MOUSEMOVE
等。
lParam代表指向鼠标消息回调结构体类型MOUSEHOOKSTRUCT
,结构体声明如下:
typedef struct tagMOUSEHOOKSTRUCT {
POINT pt;
HWND hwnd;
UINT wHitTestCode;
ULONG_PTR dwExtraInfo;
} MOUSEHOOKSTRUCT, *LPMOUSEHOOKSTRUCT, *PMOUSEHOOKSTRUCT;
从上述结构体中,我们关心的是 pt(当前鼠标基于屏幕坐标系的位置坐标),还需要有一个鼠标滚轮滚动的距离,经过一番查找后,发现有两个地方有该参数:
- 低级别鼠标钩子(WM_MOUSE_LL)回调函数的lParam,类型为
MSLLHOOKSTRUCT
,声明如下:
typedef struct tagMSLLHOOKSTRUCT {
POINT pt;
DWORD mouseData;
DWORD flags;
DWORD time;
ULONG_PTR dwExtraInfo;
} MSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;
当截获到鼠标滚轮消息时,mouseData
的高位字节代表鼠标滚轮距离。但低级别钩子需要全局挂钩,不适合使用。
- 普通钩子消息回调扩展结构体
MOUSEHOOKSTRUCTEX
,MSDN上的解释:
This is an extension of the MOUSEHOOKSTRUCT structure that includes information
about wheel movement or the use of the X button.
typedef struct tagMOUSEHOOKSTRUCTEX {
DWORD mouseData;
} MOUSEHOOKSTRUCTEX, *LPMOUSEHOOKSTRUCTEX, *PMOUSEHOOKSTRUCTEX;
很显然,这个结构体是在普通消息结构体上有扩充,扩充的mouseData
字段,正好是我们所需要的。
截获到滚轮消息后进行消息转换
根据上文的分析,我们现在需要做的是转换钩子滚轮消息为普通滚轮消息。
根据MSDN文档可知,钩子滚轮消息的wParam就是鼠标消息标识符,如WM_MOUSE_WHEEL等。钩子滚轮消息的lParam进行如下转换:
MOUSEHOOKSTRUCTEX const &eventInfo = *(MOUSEHOOKSTRUCTEX*)lParam;
// eventInfo.mouseData <-- 鼠标滚轮距离
// eventInfo.pt <-- 鼠标当前坐标
可通过 GET_WHEEL_DELTA_WPARAM
宏从mouseData
成员中提取滚轮滚动距离,普通滚轮消息WM_MOUSEWHEEL
的wParam除了滚轮滚动距离之外,低位字节还包括虚拟按键消息。而钩子消息中是没有的,所以还补全虚拟按键消息,这一步需要用到 GetAsyncKeyState
函数。
最后一步,就是根据鼠标当前坐标,获得当前位置所在窗口句柄,Windows提供WindowFromPoint
函数来实现该功能。
到此,基于钩子实现非激活窗口中响应鼠标滚动的功能要点已经全部介绍完。
demo参考链接
重载预处理消息实现方案
在网上找到另一种方法,详见非激活窗口响应鼠标滚轮
核心思路是将当前应用收到的鼠标滚轮消息,在 PreTranslateMessage
中拦截,直接发给当前鼠标悬停位置下的窗口处理。这种解决方案很简洁,但可能带来的问题时,会将鼠标消息发送给非当前线程创建的窗口,可能会有问题,需要多加测试。
小结
以上给出两种实现思路,供大家参考。
心得:在这个世界上,如果做一种事情,只有一种方法,那岂不是太无聊了。