【XInput】手柄模拟鼠标运作之 .NET P/Invoke 和 UWP-API 方案
上一篇中,老周简单肤浅地介绍了 XInput API 的使用,并模拟了鼠标移动,左、右键单击和滚轮。本篇,咱们用 .NET 代码来完成相同的效果。
说起来也是倒霉,博文写了一半,电脑忽然断电了。不知道什么原因,可能是 UPS 电源出故障。重新开机进来一看,博文没有自动保存到草稿箱。我记得以前是有自动保存这功能的。很无奈,只好重写了。
在 dll 导入的时,容易出问题的是 INPUT 结构体,因为这货有 union 成员。不知各位还记不记得。
typedef struct tagINPUT {
DWORD type;
union
{
MOUSEINPUT mi;
KEYBDINPUT ki;
HARDWAREINPUT hi;
} DUMMYUNIONNAME;
} INPUT, *PINPUT, FAR* LPINPUT;
导入代码网上一搜一大把,然而,那些代码都是恐龙时代的,在 32 位平台上是没问题的,但在 64 位平台上会无法正常用的。伙伴们可能会说,如果不自定义各种属性,运行时不是自动处理的吗?对的,如果应用在字段成员上的各种特性(如 [StructLayout(LayoutKind.Sequential)])是会自动对齐字节的。
而 INPUT 结构体特别啊,在 type 后面的三个字段是共享内存的,所以,必须明确设置字节偏移。这个结构体在 32 位系统中是 4 字节对齐的,大小为 28;而在 64 位系统上是 8 字节对齐的,大小是 40 字节。type 字段占 4 字节,这个不变,但如果 8 字节对齐,那么,type 后面还要额外填充 4 个字节,即 mi、ki 等成员的偏移是从第 9 个字节开始的,索引是 8。如果你抄网上的代码,offset = 4,在 64 位系统上运行,是无效的。
解决这个核心问题,dll 导入就很顺利了。
public enum InputType : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2
} [Flags]
public enum MouseEventFlags : uint
{
MOUSEEVENTF_MOVE = 0x0001,
MOUSEEVENTF_LEFTDOWN = 0x0002,
MOUSEEVENTF_LEFTUP = 0x0004,
MOUSEEVENTF_RIGHTDOWN = 0x0008,
MOUSEEVENTF_RIGHTUP = 0x0010,
MOUSEEVENTF_ABSOLUTE = 0x8000
} [Flags]
public enum KeyboardEventFlags : uint
{
KEYEVENTF_KEYDOWN = 0x0000,
KEYEVENTF_EXTENDEDKEY = 0x0001,
KEYEVENTF_KEYUP = 0x0002,
KEYEVENTF_UNICODE = 0x0004,
KEYEVENTF_SCANCODE = 0x0008
}
这些在头文件中本来是宏定义的,我全定义为枚举,用起来方便几个档次。
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint MouseData;
public MouseEventFlags Flags;
public uint Time;
public nuint ExtraInfo;
} [StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT
{
public ushort Vk;
public ushort Scan;
public KeyboardEventFlags Flags;
public uint Time;
public nuint ExtraInfo;
}
以上两个结构体无需特殊处理,就按常规就行。但下面的 INPUT 结构体就要注意了。
public enum InputType : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2
} [StructLayout(LayoutKind.Explicit)]
public struct INPUT
{
[FieldOffset(0)]
public InputType Type;
[FieldOffset(8)]
public MOUSEINPUT mi;
[FieldOffset(8)]
public KEYBDINPUT ki;
}
StructLayoutAttribute 特性类在应用时,目标结构体的成员排列要设置为 Explicit。即由咱们手动指定各个成员的偏移字节。记住,在 64 位系统中,偏移量是 8(鉴于现在很多人都用 64 位了,所以我这里就不设置条件编译了,如果你要兼容,可以设定条件编译,32 位的偏移量是 4,64位的是 8)。
上面那一大堆东西弄好,SendInput 函数就可以导入了。
[DllImport("user32.dll")]
public static extern uint SendInput(
uint Inputs,
[MarshalAs(UnmanagedType.LPArray)] INPUT[] inputs,
int size);
然后是 XInput 的函数,这个就按常规方式导入即可(熟悉的配方,熟悉的味道)。
[Flags]
public enum GamePadButtons : ushort
{
XINPUT_GAMEPAD_DPAD_UP = 0x0001,
XINPUT_GAMEPAD_DPAD_DOWN = 0x0002,
XINPUT_GAMEPAD_DPAD_LEFT = 0x0004,
XINPUT_GAMEPAD_DPAD_RIGHT = 0x0008,
XINPUT_GAMEPAD_START = 0x0010,
XINPUT_GAMEPAD_BACK = 0x0020,
XINPUT_GAMEPAD_LEFT_THUMB = 0x0040,
XINPUT_GAMEPAD_RIGHT_THUMB = 0x0080,
XINPUT_GAMEPAD_LEFT_SHOULDER = 0x0100,
XINPUT_GAMEPAD_RIGHT_SHOULDER = 0x0200,
XINPUT_GAMEPAD_A = 0x1000,
XINPUT_GAMEPAD_B = 0x2000,
XINPUT_GAMEPAD_X = 0x4000,
XINPUT_GAMEPAD_Y = 0x8000
} [StructLayout(LayoutKind.Sequential)]
public struct XINPUT_GAMEPAD
{
public GamePadButtons Buttons;
public byte LeftTrigger;
public byte RightTrigger;
public short ThumbLX;
public short ThumbLY;
public short ThumbRX;
public short ThumbRY;
} [StructLayout(LayoutKind.Sequential)]
public struct XINPUT_STATE
{
public uint PacketNumber;
public XINPUT_GAMEPAD GamePad;
}
导入 XInputGetState 函数。
[DllImport("Xinput1_4.dll")]
public static extern uint XInputGetState(
uint UserIndex,
ref XINPUT_STATE State);
两个 API 咱们封装到一个类中。
static class WinApi
{
[DllImport("user32.dll")]
public static extern uint SendInput(
uint Inputs,
[MarshalAs(UnmanagedType.LPArray)] INPUT[] inputs,
int size); [DllImport("Xinput1_4.dll")]
public static extern uint XInputGetState(
uint UserIndex,
ref XINPUT_STATE State);
}
好了,API 已经导入,可以玩了。这一次老周只做了:
1、左边的摇杆负责控制鼠标移动;
2、A 键表示左键单击,B 键表示右键单击。
下面是示例代码:
internal class Program
{
// 记录序号,如果序号改变,才表示有新的数据
static uint SerialID = default; static void Main(string[] args)
{
while (true)
{
Thread.Sleep(80);
// 读取数据
XINPUT_STATE state = default;
if (WinApi.XInputGetState(0, ref state) != 0)
{
// 返回值不为0,表示不成功,跳过
continue;
}
// 比较一下序号,看是不是新的数据
if (SerialID == state.PacketNumber)
{
continue; // 数据是旧的,不处理
}
// 保存新的序号
SerialID = state.PacketNumber;
// 要发送的输入消息列表
List<INPUT> inputList = new();
// 计算鼠标移动量
int dx = state.GamePad.ThumbLX / 1000;
int dy = -state.GamePad.ThumbLY / 1000;
INPUT mouseMove = new();
mouseMove.Type = InputType.INPUT_MOUSE; // 消息类型是鼠标
// 设置鼠标事件标志
mouseMove.mi.Flags = MouseEventFlags.MOUSEEVENTF_MOVE;
// 设置移动量
mouseMove.mi.dx = dx;
mouseMove.mi.dy = dy;
inputList.Add(mouseMove); // 判断按键
if ((state.GamePad.Buttons & GamePadButtons.XINPUT_GAMEPAD_A) == GamePadButtons.XINPUT_GAMEPAD_A)
{
// 左键按下消息
INPUT lbpress = new INPUT();
lbpress.Type = InputType.INPUT_MOUSE;
lbpress.mi.Flags = MouseEventFlags.MOUSEEVENTF_LEFTDOWN;
inputList.Add(lbpress);
// 左键释放
INPUT lbrelease = new INPUT();
lbrelease.Type = InputType.INPUT_MOUSE;
lbrelease.mi.Flags = MouseEventFlags.MOUSEEVENTF_LEFTUP;
inputList.Add(lbrelease);
}
if ((state.GamePad.Buttons & GamePadButtons.XINPUT_GAMEPAD_B) == GamePadButtons.XINPUT_GAMEPAD_B)
{
// 右键按下
INPUT rbpress = new();
rbpress.Type = InputType.INPUT_MOUSE;
rbpress.mi.Flags = MouseEventFlags.MOUSEEVENTF_RIGHTDOWN;
inputList.Add(rbpress);
// 右键释放
INPUT rbrelease = new INPUT();
rbrelease.Type = InputType.INPUT_MOUSE;
rbrelease.mi.Flags = MouseEventFlags.MOUSEEVENTF_RIGHTUP;
inputList.Add(rbrelease);
}
// 发送消息
WinApi.SendInput((uint)inputList.Count, inputList.ToArray(), Marshal.SizeOf<INPUT>());
}
}
}
原理和上一篇中所述一样,先读取手柄数据,然后发送鼠标输入消息。
===================================================================================
微软其实有提供了新的 XInput API,即给 UWP 应用程序使用的,而实际上。.NET 应用项目是可以使用 UWP API 的。毕竟,Win 10/11 是内置了运行库的。
接下来,咱们就用 UWP 方案,这个不需要 Dll 导入,用起来方便多了。
1、像平常一样,创建 .NET 项目。WPF、WinForms 或 UWP App 都无所谓,但不建议控制台,有可能读不到数据。API 文档中说要求是可以 Focus 的窗口才能接收输入;
2、打开系统 CMD 窗口,或任意终端都行。执行 systeminfo

这里能看到 build 版本号,比如老周的是 Win 11,只要记住前两位数字就行了,即 10.0.22000.0。
3、回到开发环境,打开项目文件,找到这一行。
<TargetFramework>net8.0</TargetFramework>
默认是 net-<ver>,表明这个控制台应用是跨平台的,我们把它改为 Windows 特供的。
<TargetFramework>net8.0-Windows10.0.22000.0</TargetFramework>
保存,关闭文件。此时,你的项目可以用 UWP API 了。
注意:要模拟鼠标动作也是要导入 Win API 的,和前文一样,只是读手柄的API不同罢了。
下面的例子,老周就用一个 System.Threading.Timer 来每 100 ms 读取一次数据,并显示在窗口上。窗口的结构如下:

主要用到的是 Windows.Gaming.Input 命名空间下的 Gamepad 类,这个类的构造函数不是公共的,不能直接实例化,而是访问它的静态属性 Gamepads。这是一个集合,如果连接了多个手柄,里面会有多个元素。
我在窗口的 Load 事件处理中,开一个 Task 来获取。
_ = Task.Run(async () =>
{
while (gamePad == null)
{
gamePad = Gamepad.Gamepads.FirstOrDefault();
await Task.Delay(1000);
}
});
这里假设只连接了一个手柄,所以总是获取集合中的第一个元素。为什么要这样获取呢?因为当应用程序初始化时,访问 Gamepads 集合不一定能获取到手柄(有时候会有一两秒的延时),所以咱们要这样来获取。
本示例中,老周用来读数据的 Timer 是后台线程的。尽量不要用 System.Windows.Forms 下的 Timer,因为那个定时器用的是 UI 线程。在 UI 线程上读数据要把获取数据的一段代码放在 lock 里面,否则读到的全是 0,或者读到错的值。同理,WPF 也不用 DispatcherTimer,那个定时器也是在 UI 线程上运行的。
用非 UI 线程的定时器,在读取数据时可以不进行 lock。下面是定时器使用过程:
1、在窗口类中定义 Timer 为私有字段。
private Gamepad? gamePad;
private System.Threading.Timer timer;
gamepad 也是私有字段,待会儿用于引用 Gamepad 实例。
2、在窗口类的构造函数中,new 一个 Timer 实例,用 Change 方法禁用定时器。
public MyWindow()
{
InitializeComponent();
Load += OnLoad;
FormClosing += OnClosing;
timer = new System.Threading.Timer(OnTick);
timer.Change(Timeout.Infinite, Timeout.Infinite);
}
传给 Timer 构造函数的是一个回调委托,这里我绑定的是 OnTick 方法。委托类型接收一个 object 类型的参数,是用户自定义的状态数据,不使用的话可以忽略。这个 Timer 没有 Start、Stop 等方法,用 Change 方法设置超时为永不超时,这样就等于禁用定时器了。
实现 OnTick 方法,循环读取手柄数据,显示在窗口上。
private void OnTick(object? state)
{
if (gamePad == null) return; // 读数
GamepadReading data = gamePad.GetCurrentReading();
BeginInvoke(() =>
{
// 左摇杆
txtLeftX.Text = data.LeftThumbstickX.ToString("N4");
txtLeftY.Text = data.LeftThumbstickY.ToString("N4"); // 右摇杆
txtRightX.Text = data.RightThumbstickX.ToString("N4");
txtRightY.Text = data.RightThumbstickY.ToString("N4"); // 左右扳机键
txtLeftTrigger.Text = data.LeftTrigger.ToString("N2");
txtRightTrigger.Text = data.RightTrigger.ToString("N2"); // 检查按键
ckbX.Checked = (data.Buttons & GamepadButtons.X) == GamepadButtons.X;
ckbY.Checked = (data.Buttons & GamepadButtons.Y) == GamepadButtons.Y;
ckbStart.Checked = (data.Buttons & GamepadButtons.Menu) == GamepadButtons.Menu;
});
}
调用 GetCurrentReading 方法就可以获取实时读数了。返回的是 GamepadReading 结构体。注意它和 XInput API 的读数范围是不同的。
这个 UWP API 的读范围是 -1 到 1,如果摇杆在中间位置(默认位置),那么读数是 0。读出来的值是 -1 到 1 的小数(含-1 和 1)。
GamepadButtons 枚举定义的是手柄的按键,这个和 XInput API 差不多。
public enum GamepadButtons : uint
{
// 未按下任何键
None = 0u,
// 菜单键,老周的手柄上是 Start 键
Menu = 1u, // 这个不知道是什么
View = 2u, // A、B、X、Y 按键
A = 4u,
B = 8u,
X = 0x10u,
Y = 0x20u, // 手柄上的四个方向键
DPadUp = 0x40u,
DPadDown = 0x80u,
DPadLeft = 0x100u,
DPadRight = 0x200u, // 这两个是两个肩膀按键
LeftShoulder = 0x400u,
RightShoulder = 0x800u, // 下面两个指的是摇杆上的按键,摇杆除了可以摇,还可以按下去。
// 其实摇杆中间是一个轻触按钮
LeftThumbstick = 0x1000u,
RightThumbstick = 0x2000u, // 其他按键
}
一起来看看效果。

最后,共享点猛料给大伙伴。AOSP Android 14 原生系统,树莓派 4 / 5 镜像,都是最新版的。
链接:https://pan.baidu.com/s/1q9xnLh4n7pNBl62djxDNnQ?pwd=1981
提取码:1981
下载后解压出来,直接写入内存卡就行,就跟安装官方系统一样。
把卡插到 Pi 上,第一次运行要用 HDMI 口连显示器,如果显示器不能触控,顺便连上键盘鼠标。如果你有 DSI 接的触控显示屏,需要到 设置 - 系统 - Raspberry Pi 设置中打开 7 寸触控屏选项。不一定要官方的屏幕(很贵),某宝上随便弄的只要是 DSI 排线连接的,多数屏幕是可以用的。DSI 排线要在树莓派关机断电后再连接,不要热插拔。接了触控屏就不要再接 HDMI 口了。
由于是原生系统,时间服务器是不能用的,要自动更新网络时间,需要用 adb 改为国内的 NTP 服务器,方法可以百度,很多教程。
经老周测试,不管是4代还是5代,声音、触控、WiFi、蓝牙、HDMI 音/视频、GPIO 等功能都可正常使用。但是,自己连接到 i2c 上的 MPU6050(重力加速和陀螺仪)不能用。这个是在设置 - 系统 - Raspberry pi 设置中的传感选项中开启的,反正老周买的模块无法正常使用。
另外,把 GPIO 21 接低电平,可以触发电源按钮功能,就像手机上的电源键,可以长按关机/重启、唤醒锁屏等,有键盘的可以按 F5。
【XInput】手柄模拟鼠标运作之 .NET P/Invoke 和 UWP-API 方案的更多相关文章
- C#模拟鼠标键盘控制其他窗口(一)
编写程序模拟鼠标和键盘操作可以方便的实现你需要的功能,而不需要对方程序为你开放接口.比如,操作飞信定时发送短信等.我之前开发过飞信耗子,用的是对飞信协议进行抓包,然后分析协议,进而模拟协议的执行,开发 ...
- C# 模拟鼠标移动与点击
我们需要用到的mouse_event函数,位于user32.dll这个库文件里面,所以我们要先声明引用. [System.Runtime.InteropServices.DllImport(" ...
- C# 模拟鼠标(mouse_event)
想必有很多人在项目开发中可能遇见需要做模拟鼠标点击的小功能,很多人会在 百度过后采用mouse_event这个函数,不过我并不想讨论如何去使用mouse_event 函数怎么去使用,因为那没有多大意义 ...
- Selenium2学习-027-WebUI自动化实战实例-025-JavaScript 在 Selenium 自动化中的应用实例之三(页面滚屏,模拟鼠标拖动滚动条)
日常的 Web UI 自动化测试过程中,get 或 navigate 到指定的页面后,若想截图的元素或者指定区域范围不在浏览器的显示区域内,则通过截屏则无法获取相应的信息,反而浪费了无畏的图片服务器资 ...
- WEBBROWSER中模拟鼠标点击(SendMessage/PostMessage)
好久没有写文章,发一篇顶顶博客访问量.别人建议转一些比较好的代码也贴过来,但是我打算这里主要发自己原创的代码,所以么..流量该多少就多少吧... 回到主题,在webbrowser中点击某链接网上几乎都 ...
- Linux 模拟 鼠标 键盘 事件
/************************************************************************ * Linux 模拟 鼠标 键盘 事件 * 说明: ...
- selenium webdriver(4)---模拟鼠标键盘操作
webdriver提供Actions来模拟鼠标悬浮.拖拽和键盘输入等操作,详细代码见org.openqa.selenium.interactions.Actions.本文通过几个实例来说明Action ...
- python模拟鼠标键盘操作 GhostMouse tinytask 调用外部脚本或程序 autopy右键另存为
0.关键实现:程序窗口前置 python 通过js控制滚动条拉取全文 通过psutil获取pid窗口句柄,通过win32gui使程序窗口前置 通过pyauto实现右键菜单和另存为操作 1.参考 aut ...
- 可以用py库: pyautogui (自动测试模块,模拟鼠标、键盘动作)来代替pyuserinput
PyAutoGUI 是一个人性化的跨平台 GUI 自动测试模块 pyUserInput模块安装前需要安装pywin32和pyHook模块.(想要装的看https://www.cnblogs.com/m ...
- Java+selenium之WebDriver模拟鼠标键盘操作(六)
org.openqa.selenium.interactions.Actions类,主要定义了一些模拟用户的鼠标mouse,键盘keyboard操作.对于这些操作,使用 perform()方法进行执行 ...
随机推荐
- 基于Spring Cache实现Caffeine、jimDB多级缓存实战
作者: 京东零售 王震 背景 在早期参与涅槃氛围标签中台项目中,前台要求接口性能999要求50ms以下,通过设计Caffeine.ehcache堆外缓存.jimDB三级缓存,利用内存.堆外.jimDB ...
- web开发的模式的介绍与身份认证
web开发的模式的介绍 1.服务端渲染 2.前端端分离开发的web模式 服务端渲染优点与缺点 优点: 1.前端耗时少.因为服务器端负责动态生成HTML内容,浏览器只需要直接渲染页面即可.尤其是移动端更 ...
- 为游戏接入ios sdk的oc学习笔记
开发手机游戏,需要接入ios的sdk,截止2021年7月23日虽然swift已经推出一些年头,但对于大部分的渠道sdk,还是oc的代码. oc不仅仅用来开发ios,还是mac上的app开发语言 从新手 ...
- KPlayer无人直播
KPlayer文档 其实就看这个教程就可以了: KPlayer文档 启动阿里云或者腾讯云的服务器进行这个步骤 服务器的购买链接: 腾讯云618 夏日盛惠_腾讯云年中优惠活动-腾讯云 域名特惠活动_域名 ...
- Python 实现SynFlood洪水攻击
Syn-Flood攻击属于TCP攻击,Flood类攻击中最常见,危害最大的是Syn-Flood攻击,也是历史最悠久的攻击之一,该攻击属于半开放攻击,攻击实现原理就是通过发送大量半连接状态的数据包,从而 ...
- Java21 + SpringBoot3整合springdoc-openapi,自动生成在线接口文档,支持SpringSecurity和JWT认证方式
目录 前言 相关技术简介 OpenAPI Swagger Springfox springdoc swagger2与swagger3常用注解对比 实现步骤 引入maven依赖 修改配置文件 设置api ...
- Ubuntu 23.04 正式发布
Ubuntu 23.04 "Lunar Lobster" 是 Ubuntu 操作系统的最新短期支持版本,该版本将获得 9 个月的支持,直到 2024 年 1 月.如果你需要长期支持 ...
- P10114 [LMXOI Round 1] Size 题解
题目链接:[LMXOI Round 1] Size 挺有意思的诈骗题,其实这类题都喜欢批一个外壳,例如数据范围提示之类的.记得以前遇到的很多诈骗题,有一道 cf 的高分题,问的是区间出现次数的次数 \ ...
- 使用DoraCloud免费版搭建办公桌面云
DoraCloud是一款多平台的桌面虚拟化管理软件,支持Hyper-V.VMware.Proxmox.XenServer等多种虚拟化平台.DoraCloud在虚拟化平台上具有极大的灵活性,允许您的组织 ...
- macOS 上 常用的操作
首先 mac上 若使用的是windows的键盘,那么需要把ctrl 键,设置成 cmd键,因为mac上大多数操作都是 基于cmd键. 1.将ctrl键,修改为cmd键,这样后 复制.粘贴.剪切.全选等 ...