首页 > 其他分享 >【XInput】手柄模拟鼠标运作之 .NET P/Invoke 和 UWP-API 方案

【XInput】手柄模拟鼠标运作之 .NET P/Invoke 和 UWP-API 方案

时间:2024-03-02 23:25:51浏览次数:33  
标签:XInput GAMEPAD Invoke UWP XINPUT API uint INPUT public

上一篇中,老周简单肤浅地介绍了 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 += onl oad;
     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,GAMEPAD,Invoke,UWP,XINPUT,API,uint,INPUT,public
From: https://www.cnblogs.com/tcjiaan/p/18048489

相关文章

  • 核心子方法5: invokeBeanFactoryPostProcessors(beanFactory)方法详解
    先总结: 该方法通过指定顺序,遍历调用各种实现了BeanDefinitionRegistryPostProcessor接口或BeanFactoryPostProcessor接口,的beanFactory后处理器注: BeanDefinitionRegistryPostProcessor接口继承了BeanFactoryPostProcessor接口调用顺序: 1.先调用已经提前放入Applicat......
  • Python Paramiko模块-exec_command() 和 invoke_shell() 两种操作方式
    前言Paramiko是Python语言的一个SSH客户端。可以远程连接Linux服务器,通过python对Linux进行操作,可以实现进行对远程服务器进行下载和上传文件操作。exec_command()操作importparamikossh=paramiko.SSHClient()ssh.set_missing_host_key_policy(paramiko.AutoAd......
  • Net 高级调试之十六:平台互用性及P/Invoke和内存泄漏调试
    一、简介今天是《Net高级调试》的第十六篇文章,也是这个系列的最后一篇文章了。既然是最后一篇文章,我需要在这里说明一下,我当前的这个系列,不是针对《Net高级调试》这本书来的,而是根据“一线码农”的视频做的这个系列。当然了,他的视频是根据《Net高级调试》这本书来的,内......
  • C#_Win32_PInvoke源码生成器
    介绍一个源代码生成器,用于向C#项目添加一组用户定义的Win32P/Invoke方法和相关的类型。链接地址:https://github.com/microsoft/CsWin32还在手动添加平台调用的代码或者增加无用的程序集?微软的官方解决方案来了!特色快速将P/Invoke方法和相关类型添加到您的C#项目......
  • winform 使用了invoke还是报错 线程间操作无效: 从不是创建控件“Form2”的线程访问它
    winform开发中,遇到“线程间操作无效:从不是创建控件“Form2”的线程访问它”,明明使用了网上说的this.invoke,怎么还是会报这个错误呢?代码如下,由于是测试configureAwait功能时发现的,所以带了它的一些使用 privateasyncvoidbutton7_Click(objectsender,EventArgse)//点......
  • 使用Swagger,在编写配置类时报错Caused by: java.lang.NullPointerException: Cannot i
    1.问题Causedby:java.lang.NullPointerException:Cannotinvoke"org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()"because"this.condition"isnull2.解决参考链接:解决Cannotinvoke"org.springframework......
  • SDL2 无法解析的外部符号 main,函数 "int __cdecl invoke_main(void)" (?invoke_main@@
    一、概述在使用VisualStudio+CMake集成SDL2的过程中。运行一个Demo示例出现了以下错误提示无法解析的外部符号main,函数"int__cdeclinvoke_main(void)"(?invoke_main@@YAHXZ) 二、解决办法上面问题的主要原因是程序找不到入口函数因为SDL中自己也定义了......
  • 推荐一个好工具:P/Invoke Interop Assistant
    在从托管代码里面调用非托管代码的时候,经常会翻阅MSDN找到需要调用的这个程序集里面的关于需要调用方法的签名,还要特别注意方法签名的写法。而且,有的时候我们找到一个非托管代码,对里面的调用方式还没有参考文档。这个boring的过程,以前我们可以利用MSDN,相关模块的参考文档,或者利用PI......
  • C++ invoke与function的区别
    C++invokeinvoke是C++17标准引入的一个函数模板,用来调用可调用对象(CallableObject,如函数指针、函数对象、成员函数指针等)并返回结果。invoke提供了统一的调用语法,无论可调用对象的类型是什么,都可以使用同一种方式进行调用。详见:https://en.cppreference.com/w/cpp/utility/fu......
  • C# UWP 项目debug发布无问题运行,Release模式闪退问题的解决
    近期解决了一个非常迷惑的一个问题,问题就在于,项目在本地环境运行时没有任何异常和闪退的现象,而且以Debug模式发布后,发布包安装后,运行也没有任何问题,但是当以Release模式发布并安装,就会出现闪退的问题。针对对系统日志的分析和错误的解决,我们一共找到两个原因导致这个问题的出现。......