SendMessage() 函数¶
发送按键的替代方法依赖 SendMessage() Windows API 函数。这个函数通过直接把数据发布到目标窗口的消息队列,允许你向任何窗口发送输入,即使它被最小化或隐藏。这个优势使它成为游戏黑客的首选方法,因为它让用户可以在机器人后台玩游戏时做其他事情。SendMessage() 原型如下:
第一个参数 window 是输入被发送到的窗口句柄。第二个参数 message 是发送的输入类型;对于键盘输入,这个参数是 WM_KEYUP、WM_KEYDOWN 或 WM_CHAR。第三个参数 wparam 应该是键码。最后一个参数 lparam 在消息是 WM_KEYDOWN 时应该是 0,否则是 1。
使用 SendMessage() 函数之前,你必须获得目标进程主窗口的句柄。给定窗口标题,你可以用 FindWindow() Windows API 函数获得句柄,如下所示:
有了有效的窗口句柄,调用 SendMessage() 看起来像这样:
第一次调用按下 F1 键,第二次调用释放它。但请记住,这一系列调用只适用于不输入文本的键,比如 F1、Insert 或 Tab。要让机器人按下输入文本的键,你必须在按下和释放消息之间发送 WM_CHAR 消息。例如,要输入 W,你会做类似的事情:
DWORD key = (DWORD)'W';
SendMessageA(window, WM_KEYDOWN, key, 0);
SendMessageA(window, WM_CHAR, key, 1);
SendMessageA(window, WM_KEYUP, key, 1);
这会创建一个 key 变量,方便更改要按的字母键。然后它遵循 F1 示例使用的相同步骤,只是在中间加了一条 WM_CHAR 消息。
注 你实际上可以只发送 WM_CHAR 消息并获得结果,但发送全部三条消息是最佳实践。游戏开发者可以通过修补游戏忽略不跟在 WM_KEYDOWN 后面的 WM_CHAR 消息来轻松关闭机器人,他们甚至可以用它作为检测你的机器人并封禁你的方法。
正如我用 SendInput() 技术展示的,你可以围绕这个功能创建包装器,让你的机器人代码更容易处理。包装器看起来像这样:
void sendKeyWithSendMessage(HWND window, WORD key,
char letter)
{
SendMessageA(window, WM_KEYDOWN, key, 0);
if (letter != 0)
SendMessageA(window, WM_CHAR, letter, 1);
SendMessageA(window, WM_KEYUP, key, 1);
}
与清单 10-3 不同,这个包装器实际同时发送按下和释放。这是因为 SendMessage() 不能用来发送带修饰键的按键,所以永远不需要在两次调用之间插入代码。
注 不过,游戏检查修饰键是否按下的方式有几种。你可能可以通过调用 SendMessage() 函数向某些游戏发送修饰键,但这取决于这些游戏如何检测修饰键。
你可以像使用清单 10-3 中的包装器一样使用这个包装器。例如,这段代码发送 F1 后跟 W:
这个示例(就像我目前展示的所有 SendMessage() 代码一样)只是完成任务。它可以输入文本,但并不完全发送正确的消息。
如果你想用 SendMessage() 函数发送 100% 有效的消息,有很多小细节必须做对。例如,lparam 的前 16 位应该存储键因按住而自动重复的次数。接下来的 8 位应该存储扫描码,一个特定于每个键盘制造商的键标识符。下一位(第 24 位)只有在按钮位于键盘的扩展部分(比如数字键盘)时才应该设置。接下来的 4 位没有文档,下一位只有在消息产生时 alt 键被按下时才应该设置。最后 2 位是先前状态标志和转换状态标志。先前状态标志只有在键之前被按下时才设置,转换状态只有在键之前处于与其当前位置相反的状态时才设置(即键现在抬起且之前按下,或反之)。
谢天谢地,普通游戏不会考虑这些值中的大多数。就此而言,普通软件也不在乎它们。如果你必须用正确的数据填充所有这些值才能让机器人工作,那你走错了方向。执行动作的方式还有很多,大多数比尝试模拟操作系统内核级键盘输入处理程序/调度器的确切行为更简单。事实上,已经有一个函数做这件事了,我已经讨论过它:SendInput() 函数。
你也可以用 SendInput() 和 SendMessage() 函数控制鼠标,但我强烈建议避免。你发送的任何鼠标命令都会影响(并受)玩家发送的任何合法鼠标移动、鼠标点击或按键影响。键盘输入也是如此,但复杂情况罕见得多。