笔記-使用C++编写Windows脚本

  1. 1. 思路分析
  2. 2. 程序实现(API模拟)
    1. 2.1. API调用
    2. 2.2. 函数编写
      1. 2.2.1. 滑鼠模拟
      2. 2.2.2. 键盘模拟
    3. 2.3. 其他內容
      1. 2.3.1. 获取系统缩放比例
      2. 2.3.2. 获得特定点的RGB值
  3. 3. 引用
    1. 3.1. 參考列表

本文为本人使用C++编写Windows脚本的个人笔记,如有错误敬请指正


思路分析

编写脚本的目的是通过程序协助我们完成复杂而重复的任务,即通过程序代替使用者进行输入。为此我们需要先分析用户输入计算机的信息和相应的设备。

一般而言,输入计算机的信息可分类为字符、图像图形、模拟量、语音。而用户最常用于计算机交互的是键盘和滑鼠两类设备,它们分别对应于字符和图形信号。

由此,编写脚本至少需要实现程序模拟字符输入和图形输入。至于模拟量和语音则暂不作提及。

附注:键盘作为输入设备可追溯至19世纪的打字机,在计算机领域则伴随着DOS等命令行交互系统出现而成为标准交互设备。同理,滑鼠伴随着windows等图形交互系统的出现而成为了最常见的交互设备。

程序实现(API模拟)

API调用

为避免重复造轮子,我们可以使用各种C++库达到目的並简化我们的程序。

对Windows系统用户而言,微软为方便开发者使用各种系统功能封装了Windows API(Application Programming Interface),可在编程时通过引用头文件<windows.h>,使用各种Windows系统已经实现的功能。

附注:<Windows.h>包含的头文件主要包含了下列头文件。內容參考自:Windows.h(百度百科)

1
2
3
4
5
<Windef.h>	//定义基本数据类型
<Winnt.h> //定义了Unicode类型
<Winbase.h> //內核函数
<Winuser.h> //用户介面函数
<Wingdi.h> //图形设备接口

附注:对Linux用户,Linux标准库的头文件为"<unistd.h>"

函数编写

本文所提及的模拟方式已擁有新替代方案,见微软-SendInput

滑鼠模拟

通过系统API可以很方便地实现一些事件,我们先从滑鼠点击开始。

滑鼠点击可通过mouse_event()函数实现,下面是它的一些参数。

1
2
3
4
5
6
7
8
/*鼠标事件API*/
void mouse_event(
DWORD dwFlags, //标志位集,见下表。
DWORD dx, //表示事件相对当前鼠标位置的x轴(横轴)偏移量
DWORD dy, //表示事件相对当前鼠标位置的y轴(垂轴)偏移量
DWORD dwData, //表示鼠标滚轮的运动,正值为滚轮向前,负值为向后。滚动一圈的值為120
ULONG_PTR dwExtraInfo //与鼠标事件相关的附加值,32位整型
)

附注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*滑鼠左键*/
DWORD MOUSEEVENTF_LEFTDOWN //按下滑鼠左键
DWORD MOUSEEVENTF_LEFTUP //抬起滑鼠左键
/*滑鼠右键*/
DWORD MOUSEEVENTF_RIGHTDOWN //按下滑鼠右键
DWORD MOUSEEVENTF_RIGHTUP //抬起滑鼠右键
/*滑鼠中键*/
DWORD MOUSEEVENTF_MIDDLEDOWN //按下滑鼠中键
DWORD MOUSEEVENTF_MIDDLEUP //抬起滑鼠中键
DWORD MOUSEEVENTF_WHEEL //滚动滚轮,数值由dwData控制

DWORD MOUSEEVENTF_MOVE //移动滑鼠,使用dx和dy表示移动的值

DWORD MOUSEEVENTF_ABSOLUTE //使用绝对位置
//dx和dy的表示区间为(0-65535),其中左上角为(0,0),右下角为(65535,65535)。
//可通过(像素绝对值/显示器分办率)*65535转换为对应坐标,注意数据类型转换。
1
2
//例:移动鼠标至绝对位置点(100,100)
mouse_event(MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE,100,100,0,0)

理解了mouse_event()函數的功能后,我們可以通过它实现各种滑鼠事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*左键点击*/
void LeftClick()
{
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
Sleep(50);//加入间隔,避免不希望的多次点击
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
}
/*右键点击*/
void RightClick()
{
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
Sleep(50);
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
}
/*移动到屏幕某点*/
void MoveToPoint(int iXPos, int iYPos)
{
//获得屏幕大小,此方法仅能得到主屏幕大小,其他方法见附注
int iScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int iScreenHigh = GetSystemMetrics(SM_CYSCREEN);
//参数转换
int iXPara = ((float)iXPos / iScreenWidth) * 65535;
int iYPara = ((float)iYPos / iScreenHigh) * 65535;

mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, iXPara, iYPara, 0, 0);
}
/*按下并拖动鼠标*/
void MouseDrag(int iXPos, int iYPos)
{
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
Sleep(50);
MoveToPoint(iXPos, iYPos);
Sleep(50);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
}

附注:GetSystemMetrics(SM_CXSCREEN)函數返回值為当前主屏的逻輯監视器大小,值為(顯示器分辨率/系統縮放率)

应用:按圆形轨迹顺时针拖动鼠标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<Windows.h>
#include<math.h>
/**
* @brief 按圆形轨迹顺时针拖动鼠标(同时按下右键)
* @param iXPos:圆心横轴坐标
* @param iYPos:圆心垂轴坐标
* @param Angle:鼠标转动的角度,范围为(0-180)
* @param Anti:是否逆向拖动,当为true时以逆时针拖动
* @retval 无
*/
void CircleDrag(int iXPos, int iYPos,int iAngle,bool Anti)
{
//初始化
int x, y;
float theta;
float R = 100; //圆的半径(像素点)
POINT ParaPoint;

for (int i = 0; i < 100 * (iAngle / 90.0) + 1; i++)
{
//计算圆形轨迹
x = R * (1 - i / 100.0);
theta = acos(x / R);
y = R * sin(theta);

//参数转换
ParaPoint.x = x+ iXPos;
if (Anti == true) y = -y;
ParaPoint.y = y+ iYPos;
ParaPoint = ParaChange(ParaPoint);

//拖动鼠标
mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, ParaPoint.x, ParaPoint.y, 0, 0);
if (i == 0) mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
Sleep(10);
}
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
return;
}
/**
* @brief 参数转换函数,按屏幕大小映射(x,y)坐标至16位值域
* @param ParaPoint:以像素值域表示的(x,y)坐标
* @retval ParaPoint:以16位值域表示的(x,y)坐标
*/
POINT ParaChange(POINT ParaPoint)
{
//获得屏幕大小,此方法仅能得到主屏幕的虛拟屏幕大小,其他方法见附注
int iScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int iScreenHigh = GetSystemMetrics(SM_CYSCREEN);

//参数转换
ParaPoint.x = ((float)ParaPoint.x / iScreenWidth) * 65535;
ParaPoint.y = ((float)ParaPoint.y / iScreenHigh) * 65535;

return ParaPoint;
}

附注:windows屏幕分辨率及系统缩放获取方法-CSDN

另外,可以通过函数GetCursorPos()得到鼠标当前的坐标位置(以分办率表示),它需要一个点类的地址来记录并输出数据。參考:GetCursorPos function

1
2
3
4
5
6
7
8
9
//获得滑鼠坐标
BOOL GetCursorPos(
LPPOINT lpPoint
);
//点类结构
typedef struct tagPOINT {
LONG x;
LONG y;
} POINT, *PPOINT;

键盘模拟

另一个需要实现的是按下键盘,可以通过keybd_event()函数实现,下面是它的一些参数。

1
2
3
4
5
6
void keybd_event(
BYTE bVk, //虚拟键值,见附注虚拟键值表。
BYTE bScan, //键的硬件扫描码,一般为0,详请见附注。
DWORD dwFlags, //标志位,设置为0表示键被按下,为KEYEVENTF_KEYUP时键被释放。
DWORD dwExtralnfo //与按键事件相关的附加值,32位整型。
);

理解keybd_event()函數后,我們便可以通过它实现按鍵輸入。

1
2
3
4
5
6
7
//按下A鍵
void KeyPress(char key)
{
keybd_event(key, 0, 0, 0);
Sleep(50);
keybd_event(key, 0, KEYEVENTF_KEYUP, 0);
}

此外,可通过GetAsyncKeyState()获得按键状态。

1
2
3
4
//获取按键状态,被按下时追回1,否则返回0
SHORT GetAsyncKeyState(
int vKey //虚拟键值,见附注虚拟键值表。
);

附注:
  网上查找到的扫描码表不适用于keybd_event,bvk的值需按微软的虚拟键值表查找
  本人使用VS2019作为编译器时未发现bScan和KEYEVENTF_EXTENDEDKEY参数的作用,在调试时加入bScan和KEYEVENTF_EXTENDEDKEY后输出值相比其为0时未发现任何变化,望指教。

扩展阅读:
一.关于bScan
  bScan为键扫描码,这种通过扫描码转换为字符的方式在IBM时代能有效地降低成本,后来的软件为保持兼容仍然保留着这些传统的扫描码。(详见:ScanCode

二.关于dwFlags
  随着键盘的发展,不断增加的按键数量已超出了扫描码的容纳范围(扫描码范围00-7F),由此诞生了扩充扫描码(00-E0 7F),为了兼容原有的扫描码系统,扩充扫描码增加了E0作为标识符,由此可通过'E0'表示特殊的按键,例如'win键'便是'E0-1F'对应的'1F'则是字符'S'按键释放的扫描码。对于扩充扫描码集內的按键,需要在dwFlags加入标识符"KEYEVENTF_EXTENDEDKEY",该标识符在'1F'前加入了前缀'E0',使扫描码变为'E0 1F',从而使程序正确模拟'win'按键。
  范围至0x7F的原因是按键释放需要独立的扫描码,做法是在扫描码前并上0x80

參考: 虛拟鍵值(Virtual-Key)Keyboard-internal scancodes

其他內容

获取系统缩放比例

对于更复杂的脚本,我们需要根据程序的状态给予不同的动作,以下列出一些相关信息的获取方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//获取系统DPI比例
float SystemDPIScale()
{
HWND hWnd = GetDesktopWindow();
HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);//
//获得逻辑监视器大小
MONITORINFOEX LogicalMonitor;
LogicalMonitor.cbSize = sizeof(LogicalMonitor);
GetMonitorInfo(hMonitor, &LogicalMonitor);
int iXLogical = (LogicalMonitor.rcMonitor.right - LogicalMonitor.rcMonitor.left);
//获得物理显示器大小
DEVMODE PhysicalMonitor;
PhysicalMonitor.dmSize = sizeof(PhysicalMonitor);
PhysicalMonitor.dmDriverExtra = 0;
EnumDisplaySettings(LogicalMonitor.szDevice, ENUM_CURRENT_SETTINGS, &PhysicalMonitor);
int iXPhysical = PhysicalMonitor.dmPelsWidth;
//返回DPI比例
return ((double)iXPhysical / iXLogical);
}

以下給出上述程序中一些关鍵字的解析

HWND类(窗口句柄)

句柄指系统内数据结构(对象)的标识。概念上可理解为系统的智能指针(但不直接指向对象的内存地址),实际上它指向了一个系统定义的结构体,该结构体用于存放函数指针及其他信息。详见:深入了解Windows句柄—CSDN

在Windows系统中,每个窗口对应了唯一的窗口句柄,通过窗口句柄,用户可以使用Windows API获取窗口信息,如包括窗口,图标,字体,内存块等,或是修改窗口的状态。

GetDesktopWindow函数

Windows API函数,返回值桌面的窗口句柄。桌面指覆盖整个屏幕的窗口,所有程序窗都被绘制于其上。

HMONITOR类(监视器句柄)

每台监视器对应的唯一句柄,用户可通过监视器句柄在系统API中进行相关的操作。

MonitorFromWindow函数

Windows API函数,通过输入的窗口句柄获得对应的监视器句柄。

1
2
3
4
5
6
7
8
HMONITOR MonitorFromWindow(
HWND hwnd, //窗口句柄
DWORD dwFlags //标志位,详情在下方给出。
);
//标志位(dwFlags)的值
DWORD MONITOR_DEFAULTTONEAREST //返回离窗口最近的监视器句柄(即当前窗口所在的监视器的句柄)
DWORD MONITOR_DEFAULTTONULL //返回NULL值
DWORD MONITOR_DEFAULTTOPRIMARY //返回主监视器的句柄

MONITORINFOEX(监视器信息结构体)

MONITORINFOEX结构在MONITORINFO结构上增加了监视器名称字符串。以下是兩个结构体的内容。

1
2
3
typedef struct tagMONITORINFOEXA : tagMONITORINFO {
CHAR szDevice[CCHDEVICENAME]; //监视器在系统下的名称字符串
} MONITORINFOEXA, *LPMONITORINFOEXA;
1
2
3
4
5
6
typedef struct tagMONITORINFO {
DWORD cbSize; //MONITORINFOEX的结构体大小(bytes),用于使程序分辨结构体类型
RECT rcMonitor; //RECT结构体,保存了显示器的矩型坐标(以虚拟屏幕坐标表示)
RECT rcWork; //RECT结构体,保存了工作区域的矩型坐标(以虚拟屏幕坐标表示)
DWORD dwFlags; //标志位,当它的值为MONITORINFOF_PRIMARY时表明为主监视器
} MONITORINFO, *LPMONITORINFO;

附注:虚拟屏幕—微软

GetMonitorInfo函数

Windows API函数,用于检索监视器的信息,返回值为Bool型变量,成功获取数据时返回1,失败时返回0。

1
2
3
4
BOOL GetMonitorInfo(
HMONITOR hMonitor, //目标监视器的句柄
LPMONITORINFO lpmi //指向MONITORINFOEX的指针,检索的信息返回至指向的结构体中
);//附注:调用前需初始化MONITORINFOEX.cbSize的值

DEVMODE(设备信息结构体)

该结构体用于包含打印机或显示器的初始化和环境信息,以下只说明本文中应用的几个。详见 :DEVMODE—百度百科

1
2
3
4
5
typedef struct _devicemodeA {
WORD dmSize //DEVMODE的结构体大小(bytes),用于使程序分辨结构体类型
WORD dmDriverExtra //指定了有关设备的私有数据的大小(bytes),当不使用device-specific information时,值为零
...
}DEVMODEA, *PDEVMODEA, *NPDEVMODEA, *LPDEVMODEA;

EnumDisplaySettings函数

Windows API函数,用于检索某一图形模式下显示器的资讯,成功获取数据时返回1,失败时返回0

1
2
3
4
5
6
7
8
9
BOOL EnumDisplaySettingsA(
LPCSTR lpszDeviceName, //句柄,用于指定函数获取数据的对象,通常为DISPLAY_DEVICE.DeviceName
DWORD iModeNum, //检索的显示设定类型,可分别获取显示器当前或者出厂的设定,值见附注
DEVMODEA *lpDevMode //指向DevMode的指针,检索的信息返回至指向的结构体中
);
//附注一:调用前需对结构体DEVMODE的dmSize和dmDriverExtra赋值进行初始化
//附注二:显示设定包括分办率,扫描频率、显示器类型等等。
ENUM_CURRENT_SETTINGS //指当前的显示设定。
ENUM_REGISTRY_SETTINGS //则指设备储存的出厂显示设定。

获得特定点的RGB值

在程序中按键与背景间对比度高或存在显著的亮度差异时,可通过亮度或RGB值判别按键存在性,并以此编写程序逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//自定義結构体用於存放數据
typedef struct
{
int Red;
int Green;
int Blue;
}RGB_Color;
//获得某点的RGB值
RGB_Color GetPixelColor(int iXPos,int iYPos)
{
//初始化
static RGB_Color PixelColor;
HWND hWnd = GetDesktopWindow();
HDC hdc = GetDC(hWnd);
//獲得像素点RGB值
COLORREF cPixel = GetPixel(hdc, iXPos, iYPos);
if (cPixel != CLR_INVALID)
{
PixelColor.Red = GetRValue(cPixel);
PixelColor.Green = GetGValue(cPixel);
PixelColor.Blue = GetBValue(cPixel);
}
return PixelColor;
}

HDC(设备场景句柄)

Handle of Device Context的简写,一般用于处理图像相关对像的句柄,在调用GetDC()时用于储存返回的图形对象检索表。

GetDC函数

Windows API函数,它为所输入的窗口句柄对应的窗口建立图形对象检索表并返回HDC句柄至用户。

COLORREF类型

十六进制颜色储存类型,值域为(0x00000000~0x00FFFFFFF),bit0和bit1储存红色的代码值,bit2和bit3储存绿色代码值,bit4-bit5储存蓝色代码值。bit6和bit7为零。COLORREF类可通过函数GetRValue,GetBValue,GetGValue转换为对应的十进制颜色代码值。

GetPixel函数

Windows API函数,可以通过它获取窗口中某一坐标点的颜色数据,返回值为一个COLORREF类型变量。

1
2
3
4
5
COLORREF GetPixel(
HDC hdc, //Device Context句柄,函数通过句柄检索窗口的图形数据
int x, //横轴坐标
int y //垂轴坐标
);

附注:可通过以下公式转换RGB至灰阶值:Gray = Red*0.299 + Green*0.587 + Blue*0.114

引用

參考列表

键盘鼠标模拟方式原理及实现-CSDN:

https://blog.csdn.net/THMAIL/article/details/113812698

Windows.h—百度百科:

https://baike.baidu.com/item/windows.h/2438991

mouse_event—百度百科:

https://baike.baidu.com/item/mouse_event/6374455

C++模拟鼠标点击和键盘输入的操作:

https://blog.csdn.net/Ikaros_521/article/details/104532192

windows屏幕分辨率及系统缩放获取方法-CSDN:

https://blog.csdn.net/siyacaodeai/article/details/112971964

用C++获取屏幕上某点的颜色:

https://blog.csdn.net/weixin_30412577/article/details/99901948

附注:其他引用已在对应区域以超连结给出