C#模拟键盘和鼠标输入事件,借助SendInput win32方法(替代 keybd_event 和 mouse_event)【译】

2,307 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第25天,点击查看活动详情

主要参考翻译自 How to Send Inputs using C#。正如原文所说,探索如何使用 C# 发送输入信息,可以用于自动化任务,或者仅仅是胡闹。

原文中给出了dome源文件,里面对 SendInput 的使用有个简单的封装,也都是本文介绍的内容。

SendInput win32 API

要使用的 SendInput 方法 位于 user32.dll 中。不使用 keybd_eventmouse_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结构

首先实现 KEYBDINPUTMOUSEINPUTHARDWAREINPUT 输入结构。

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 是输入的标志(KeyUpExtendKeyUnicodeScanCode). 请阅读 官方的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坐标被指定;相对数据作为移动的像素数值被指定。

  • dydx 相同,只不过表示的是y轴。

  • 如果 dwFlags 包含 MOUSEEVENTF_WHEELMOUSEEVENTF_HWHEELmouseData指定的是滚轮的移动量,正值表示滚轮向前旋转(远离用户的方向);负值表示滚轮向后旋转(朝向用户)。滚轮点击被定义为 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 是由输入硬件设备生成的消息

  • wParamLuMsglParam 参数的低阶值。wParamHuMsglParam` 参数的高阶值。

wParamL is the low-order word of the lParam parameter for uMsg. wParamH is the high-order word of the lParam parameter for uMsg.

[StructLayout(LayoutKind.Sequential)]
public struct HardwareInput
{
    public uint uMsg;
    public ushort wParamL;
    public ushort wParamH;
}

InputUnion 输入共用体

InputUnionINPUT结构体中的 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 的哪个输入结构体被使用。其取值为:

    1. MOUSEINPUT
    2. KEYBDINPUT
    3. 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,设置 KeyDownScancode 标志。这意味着,我们使用希望使用的按键(此处为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);