12.3 模拟鼠标录制回放

本节将向读者介绍如何使用键盘鼠标操控模拟技术,键盘鼠标操控模拟技术是一种非常实用的技术,可以自动化执行一些重复性的任务,提高工作效率,在Windows系统下,通过使用各种键盘鼠标控制函数实现动态捕捉和模拟特定功能的操作。

有时我们经常需要进行重复性的鼠标操作,例如繁琐的点击、拖拽。这些任务可能消耗大量时间和精力,为了解决这个问题,可自行设计并实现一个简单而强大的鼠标录制回放工具,旨在帮助用户轻松录制鼠标动作,通过借助鼠标录制回放工具,用户可以轻松实现自动化操作,从而解放双手。

首先我们需要创建一个Write_File函数,当用户每次调用该函数时都会向特定的文件内追加写入一条记录,此外还需要增加一个split函数,该函数用于将特定的一条记录根据特定的分隔符切割,保留分隔符后面的坐标信息。

// 切割字符串
int split(char dst[][32], char* str, const char* spl)
{
int n = 0;
char* result = NULL;
result = strtok(str, spl);
while (result != NULL)
{
strcpy(dst[n++], result);
result = strtok(NULL, spl);
}
return n;
}

// 每次写入一行
int Write_File(char* path, char* msg)
{
FILE* fp = fopen(path, "a+");
if (fp == NULL) return -1;

char ch, buffer[1024];

int index = 0;
while (msg[index] != '\0')
{
fputc(msg[index], fp);
index++;
}
fclose(fp);
return 1;
}

接着我们需要实现Recording()函数部分,该函数的左右是用于捕捉当前鼠标坐标与点击事件,函数中通过调用GetCursorPos()获取当前鼠标的屏幕坐标位置,这个函数参数传递非常简单,只需要传入一个POINT类型的结构体变量,其函数原型如下所示;

BOOL GetCursorPos(LPPOINT lpPoint);

参数:

  • lpPoint:指向 POINT 结构的指针,用于接收鼠标的屏幕坐标位置。

返回值:

  • 如果函数成功,返回值为非零,表示获取鼠标位置成功;
  • 如果函数失败,返回值为零,表示获取鼠标位置失败。

POINT 结构包含了两个成员变量 xy,分别表示鼠标在屏幕上的横坐标和纵坐标。

当有了当前鼠标坐标位置以后,接着就是需要获取到鼠标点击事件,鼠标点击可使用GetAsyncKeyState 获取指定虚拟键码对应的键盘键的状态,该函数原型如下所示;

SHORT GetAsyncKeyState(int vKey);

参数:

  • vKey:指定虚拟键码,它是一个整数,表示要获取的键的键码。

返回值:

  • 如果指定的虚拟键处于按下状态,返回值的最高位(符号位)为 1,其余位表示次数(持续时间)。如果指定的虚拟键处于释放状态或者参数无效,返回值为 0。

GetAsyncKeyState 函数允许检测键盘中某个虚拟键的状态,无论这个虚拟键是否处于焦点的窗口中。它适用于各种应用,通过VK_LBUTTON可用于检测鼠标左键是否被按下,通过VK_RBUTTON则可用于检测鼠标右键状态。

代码的主要功能如下:

  1. Recording 函数中,使用一个死循环不断检测鼠标的位置和按键状态。
  2. 使用 GetCursorPos 函数获取当前鼠标的位置,并将其保存在 xy 变量中。
  3. 使用 GetAsyncKeyState 函数检测鼠标左键和右键的状态,并将其保存在 lbuttonrbutton 变量中。
  4. 如果当前的鼠标位置或按键状态与之前保存的值不同,表示鼠标动作发生了变化,将当前的位置和按键状态记录下来。
  5. 将记录的鼠标动作信息以字符串的形式写入脚本文件,格式为 “X:位置,Y:位置,L:左键状态,R:右键状态”。
  6. 保存当前的鼠标位置和按键状态,用于下一次循环时比较是否发生了变化。
// 录制脚本
void Recording(char *script)
{
int static_x = 0, static_y = 0;
bool static_lbutton = 0, static_rbutton = 0;

while (1)
{
POINT Position;
GetCursorPos(&Position);

int x = Position.x;
int y = Position.y;
bool lbutton = GetAsyncKeyState(VK_LBUTTON);
bool rbutton = GetAsyncKeyState(VK_RBUTTON);

if (x != static_x || y != static_y || lbutton != static_lbutton || rbutton != static_rbutton)
{
char szBuf[1024] = { 0 };
std::cout << "X轴 = " << x << " Y轴 = " << y << " 鼠标左键 = " << lbutton << " 鼠标右键 = " << rbutton << std::endl;

sprintf(szBuf, "X:%d,Y:%d,L:%d,R:%d\n", x, y, lbutton, rbutton);
Write_File((char*)script, szBuf);

static_x = x;
static_y = y;
static_lbutton = lbutton;
static_rbutton = rbutton;
}
}
}

接着我们继续封装Play()回放功能,该功能的实现原理与录制保持一致,通过逐条读取传入文件中的参数,并调用SetCursorPos实现鼠标位置的移动操作,该函数与获取参数传递保持一致,这里我们需要注意mouse_event函数,该函数用于模拟鼠标的各种事件,如鼠标移动、鼠标按键的点击和释放等,其函数原型如下所示;

void mouse_event(DWORD dwFlags, DWORD dx, DWORD dy, DWORD dwData, ULONG_PTR dwExtraInfo);

其中dwFlags指定要模拟的鼠标事件类型和选项。可以是以下常量的组合;

  • MOUSEEVENTF_ABSOLUTE:指定鼠标位置是绝对坐标。如果不设置此标志,则坐标是相对于当前鼠标位置的增量。
  • MOUSEEVENTF_MOVE:模拟鼠标移动事件。
  • MOUSEEVENTF_LEFTDOWN:模拟鼠标左键按下事件。
  • MOUSEEVENTF_LEFTUP:模拟鼠标左键释放事件。
  • MOUSEEVENTF_RIGHTDOWN:模拟鼠标右键按下事件。
  • MOUSEEVENTF_RIGHTUP:模拟鼠标右键释放事件。

其他常量可根据需要自行查阅相关文档。

  • dx:鼠标事件发生时的横坐标(绝对坐标或增量坐标,根据 dwFlags 决定)。
  • dy:鼠标事件发生时的纵坐标(绝对坐标或增量坐标,根据 dwFlags 决定)。
  • dwData:鼠标事件的一些数据。对于滚轮事件,它表示滚动的数量。对于其他事件,通常设为 0。
  • dwExtraInfo:额外的信息。通常设为 0。

mouse_event 函数允许模拟鼠标事件,通过设置 dwFlags 参数来指定需要模拟的事件类型,设置 dxdy 参数来指定事件发生时的鼠标位置。通过调用这个函数,可以实现自动化鼠标操作,如模拟鼠标点击、拖动等。

如下代码段实现了鼠标动作脚本的回放功能,它从之前录制保存的脚本文件中读取鼠标动作信息,并按照脚本中记录的顺序模拟鼠标动作,实现鼠标动作的回放。

代码的主要功能如下:

  1. Play 函数中,打开之前保存的脚本文件,并使用 fgets 函数每次读取一行数据,保存在 buf 字符数组中。
  2. 使用 split 函数切割每行数据,将每行数据切割成以逗号分隔的四个字符串,并将这四个字符串转换为整数类型保存在 key_item 数组中。
  3. 根据 key_item 数组中的数据,判断是否需要进行鼠标点击动作,并调用 mouse_event 函数模拟鼠标点击。
  4. 调用 SetCursorPos 函数设置鼠标的位置,并使用 Sleep 函数模拟鼠标移动的延时,实现鼠标动作的回放。
  5. 循环执行以上步骤,直到脚本文件中的所有动作都被回放完毕。
// 回放脚本
void Play(char *script)
{
FILE* fp = fopen(script, "r");
char buf[1024];

while (feof(fp) == 0)
{
// 每次读入一行
memset(buf, 0, 1024);
fgets(buf, 1024, fp);

// 以逗号切割
char split_comma[4][32] = { 0 };
int comma_count = split(split_comma, buf, ",");

int key_item[4] = { 0 };

// std::cout << "长度: " << comma_count << std::endl;
for (int x = 0; x < comma_count; x++)
{
// 继续切割冒号
char split_colon[2][32] = { 0 };
split(split_colon, split_comma[x], ":");

// std::cout << "字典键 = " << split_colon[0] << " 字典值 = " << split_colon[1] << std::endl;
key_item[x] = atoi(split_colon[1]);
}

if (key_item[3] != 0)
{
mouse_event(MOUSEEVENTF_LEFTUP | MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
}
if (key_item[4] != 0)
{
mouse_event(MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
}

// 得到数据后开始回放
SetCursorPos(key_item[0], key_item[1]);
Sleep(70);
}
}

最后是主函数部分,我们通过RegisterHotKey函数注册两个全局热键,通过F1实现鼠标录制部分,通过F2则实现鼠标回放,最后通过GetMessage函数接收全局消息事件,当出现WM_HOTKEY消息则依次判断是否启用录制回放等功能,代码如下所示;

int main(int argc, char* argv[])
{
// 注册热键 F1 , F2
if (0 == RegisterHotKey(NULL, 1,0, VK_F1))
{
cout << GetLastError() << endl;
}
if (0 == RegisterHotKey(NULL, 2,0, VK_F2))
{
cout << GetLastError() << endl;
}
if (0 == RegisterHotKey(NULL, 3, 0, VK_F3))
{
cout << GetLastError() << endl;
}

// 消息循环
MSG msg = { 0 };

while (GetMessage(&msg, NULL, 0, 0))
{
switch (msg.message)
{
case WM_HOTKEY:
{
if (1 == msg.wParam)
{
std::cout << "录制脚本" << std::endl;
Recording((char *)"d://script.txt");
}

else if (2 == msg.wParam)
{
std::cout << "回放脚本" << std::endl;
Play((char *)"d://script.txt");
}

else if (3 == msg.wParam)
{
exit(0);
return 0;
}

break;
}
default:
break;
}
}
return 0;
}

读者可自行编译并运行这段代码,通过录制一段鼠标功能并回放,输出效果图如下所示;