开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第25天,点击查看活动详情
主要参考翻译自 How to Send Inputs using C#。正如原文所说,探索如何使用 C# 发送输入信息,可以用于自动化任务,或者仅仅是胡闹。
原文中给出了dome源文件,里面对 SendInput 的使用有个简单的封装,也都是本文介绍的内容。
SendInput win32 API
要使用的 SendInput 方法 位于 user32.dll
中。不使用 keybd_event 和 mouse_event 的原因在于它们已经过时了,在将来的Windows版本中可能不会正确的起作用。
DirectInput
是用来收集用户输入的API,这些输入来自输入设备(键盘keyboard
、鼠标mouse
等...)。依赖一些标志(后面会讨论到),可以发送虚拟扫描码(virtual scan codes
)或硬件扫描码(hardware scan codes
),虚拟扫描码可能被 DirectInput
忽略,即输入不会被执行。硬件扫描码更像手动按键。
SendInput
函数需要三个参数,input
的个数、发送的inputs的 INPUT 数组 和 INPUT
结构的大小。INPUT
结构(INPUT struct
)包含 一个表示输入类型的整数 和 一个将被传递的输入union(a union for the inputs)【C语言中的 Union 叫做 共用体 或 联合(体)】
关于 union 的更多介绍,可以参考此wiki。
在 C# 中可以使用 Struct 结构体 实现 Union 共用体。
input结构
首先实现 KEYBDINPUT、MOUSEINPUT 和 HARDWAREINPUT 输入结构。
KEYBDINPUT 结构
键盘输入的结构体。
使用 Sequential
布局类型的 StructLayout
强制成员按顺序存储,因为我们需要传递结构体到非托管代码。
We will use Sequential StructLayout to force the members to be in sequential order because we will pass the struct to unmanaged code.
-
wVk
是一个虚拟key code(virtual key code
); -
wScan
是我们要按下按键的扫描码(scan code
),此处有关于扫描码的更详细介绍。 -
dwFlags
是输入的标志(KeyUp
、ExtendKey
、Unicode
、ScanCode
). 请阅读 官方的remarks 了解标志(flags
)的更多信息。 -
time
是输入的时间戳,如果设置为0
,系统在后续会提供自己的时间戳。 -
dwExtraInfo
提供按键(keystroke
)的额外信息,此信息可以使用 GetMessageExtraInfo 函数获得。
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardInput
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
关于查看 scan code 扫描码(键盘键码)的详细信息
scan code
对应着键盘上的按键,因此相对来说比较重要,上面给出的 Keyboard scancodes 应该要特别注意。
MOUSEINPUT 结构
鼠标输入的结构体。
-
dx
是鼠标的绝对位置,或者从上次生成鼠标事件以来的运动量,取决于dwFlags
成员的值。绝对数据作为鼠标的x坐标被指定;相对数据作为移动的像素数值被指定。 -
dy
与dx
相同,只不过表示的是y轴。 -
如果
dwFlags
包含MOUSEEVENTF_WHEEL
或MOUSEEVENTF_HWHEEL
,mouseData
指定的是滚轮的移动量,正值表示滚轮向前旋转(远离用户的方向);负值表示滚轮向后旋转(朝向用户)。滚轮点击被定义为WHEEL_DELTA
值为120。 -
dwFlags
是位标志(bit flags
)的集合,指定鼠标移动和按钮点击的各个方面。后面可以查看下标志。 -
time
是输入的时间戳,如果设置为0
,系统在后续会提供自己的时间戳。 -
dwExtraInfo
是关联鼠标事件的额外值,可以使用GetMessageExtraInfo
函数获取。
[StructLayout(LayoutKind.Sequential)]
public struct MouseInput
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
HARDWAREINPUT 结构
硬件输入的结构体。
-
uMsg
是由输入硬件设备生成的消息 -
wParamL
是uMsg
的lParam
参数的低阶值。wParamH是
uMsg的
lParam` 参数的高阶值。
wParamL
is the low-order word of thelParam
parameter foruMsg
.wParamH
is the high-order word of thelParam
parameter foruMsg
.
[StructLayout(LayoutKind.Sequential)]
public struct HardwareInput
{
public uint uMsg;
public ushort wParamL;
public ushort wParamH;
}
InputUnion 输入共用体
InputUnion
是 INPUT
结构体中的 union 参数,它包含鼠标、键盘或硬件的输入数据。
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion
{
[FieldOffset(0)] public MouseInput mi;
[FieldOffset(0)] public KeyboardInput ki;
[FieldOffset(0)] public HardwareInput hi;
}
INPUT 结构体
INPUT
结构体被 SendInput
使用,存储输入事件的合成信息,比如按键、鼠标移动和鼠标点击。
-
type
是输入事件的类型。它指定来自 共用体union 的哪个输入结构体被使用。其取值为:MOUSEINPUT
KEYBDINPUT
HARDWAREINPUT
对于此,可以使用 InputType
枚举(enum
),后面将会看到。
public struct Input
{
public int type;
public InputUnion u;
}
Flags 标志
枚举(enumeration type (or enum type)
)是由基础整数数值类型(int、unit、byte等)对应的一组命名常量定义的值类型。默认关联枚举成员的常量值是int
类型,起始值为0,并按跟随的定义的文本顺序加1。
如果想要枚举类型表示选项的组合,可以为这些选项定义枚举成员,每个成员表示一个位(bit)字段的独立选择,也就是,这些枚举成员关联的值应该是2的幂。然后,可以使用位逻辑运算符 |
或 &
分别实现组合选择或交叉组合选择。
[Flags]
特性指示枚举被视为位字段(bit field
),即一组标志。
InputType
InputType
是一个简单的枚举,表示被INPUT
结构体使用的不同输入类型。
[Flags]
public enum InputType
{
Mouse = 0,
Keyboard = 1,
Hardware = 2
}
Key Event Flags 按键事件标志
KeyEventF
标志被KEYBDINPUT
结构体使用。
[Flags]
public enum KeyEventF
{
KeyDown = 0x0000,
ExtendedKey = 0x0001,
KeyUp = 0x0002,
Unicode = 0x0004,
Scancode = 0x0008
}
Mouse Event Flags 鼠标事件标志
MouseEventF
标志被MOUSEINPUT
结构体使用。
[Flags]
public enum MouseEventF
{
Absolute = 0x8000,
HWheel = 0x01000,
Move = 0x0001,
MoveNoCoalesce = 0x2000,
LeftDown = 0x0002,
LeftUp = 0x0004,
RightDown = 0x0008,
RightUp = 0x0010,
MiddleDown = 0x0020,
MiddleUp = 0x0040,
VirtualDesk = 0x4000,
Wheel = 0x0800,
XDown = 0x0080,
XUp = 0x0100
}
DLL Imports (dll导入)
DllImport
指示来自非托管动态链接库(dynamic linked library (DLL)
)暴露的方法。
Interoperability (互操作性)
互操作性使你可以保留和利用已存在的非托管代码的投入。
位于公共语言运行时(common language runtime (CLR)
)控制之下运行的代码叫作托管代码(managed code
);位于CLR之外运行的代码叫作非托管代码。COM、COM+、C++组件、ActiveX组件 和 微软 Windows API 就是非托管代码的例子。
.NET 通过平台调用服务提供了与非托管代码的互操作性,System.Runtime.InteropServices
命名空间、C++互操作性 和 COM互操作性(COM interop)。
PInvoke (平台调用)
平台调用(Platform invoke
)是一个服务,使得托管代码可以调用动态链接库(DLL)中实现的非托管函数,例如,Microsoft Windows API 中的函数。
导入 SendInput 函数
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, Input[] pInputs, int cbSize);
导入 GetMessageExtraInfo 函数
[DllImport("user32.dll")]
private static extern IntPtr GetMessageExtraInfo();
综合使用
Sending Keyboard Input 发送键盘输入
首先,创建一个 Input
数组 存储在 inputs
。对于每个input
,设置类型为 InputType.Keyboard
,并在KeyboardInput
对象中指定详细信息。
在第一个input中,设置扫描码(scancode
)为0x11
,设置 KeyDown
和 Scancode
标志。这意味着,我们使用希望使用的按键(此处为W
)并按下它。第二个input相同,但是使用 按下 按钮按下(button down
) 代替,用于释放按键。
inputs
设置后,使用 SendInput
函数发送它们。
The second input is the same, but instead of pressing the button down, it is released
Input[] inputs = new Input[]
{
new Input
{
type = (int)InputType.Keyboard,
u = new InputUnion
{
ki = new KeyboardInput
{
wVk = 0,
wScan = 0x11, // W
dwFlags = (uint)(KeyEventF.KeyDown | KeyEventF.Scancode),
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new Input
{
type = (int)InputType.Keyboard,
u = new InputUnion
{
ki = new KeyboardInput
{
wVk = 0,
wScan = 0x11, // W
dwFlags = (uint)(KeyEventF.KeyUp | KeyEventF.Scancode),
dwExtraInfo = GetMessageExtraInfo()
}
}
}
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(Input)));
通过间隔2秒发送两个inputs
组合,可以实现如下:
发送鼠标输入
对每个input
,设置类型为 InputType.Mouse
,并在 MouseInput
对象中指定详细信息。
dx
是鼠标在x轴将要移动的相对大小,dy
是鼠标在y轴将要移动的相对大小。此处,我们将移动鼠标向下100单位和向右100单位,然后 左击。第二个input
将释放LMB
。
inputs
设置后,使用 SendInput
函数发送它们。
Input[] inputs = new Input[]
{
new Input
{
type = (int) InputType.Mouse,
u = new InputUnion
{
mi = new MouseInput
{
dx = 100,
dy = 100,
dwFlags = (uint)(MouseEventF.Move | MouseEventF.LeftDown),
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new Input
{
type = (int) InputType.Mouse,
u = new InputUnion
{
mi = new MouseInput
{
dwFlags = (uint)MouseEventF.LeftUp,
dwExtraInfo = GetMessageExtraInfo()
}
}
}
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(Input)));
扩展:鼠标坐标
GetCursorPos 获取鼠标绝对坐标
使用 GetCursorPos 函数可以容易的获得当前鼠标的坐标,其返回值 bool
指示是否成功,并获取一个包含坐标的 POINT 结构体引用。
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
其使用如下:
GetCursorPos(out POINT point);
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
SetCursorPos 设置鼠标绝对坐标
使用 SetCursorPos 函数可以容易的设置当前鼠标的坐标,其返回值 bool
指示是否成功,并使用表示x坐标和y坐标的两个整数。
[DllImport("User32.dll")]
public static extern bool SetCursorPos(int x, int y);
其使用如下:
SetCursorPos(100, 100);