【XInput】手柄模擬滑鼠運作之 .NET P/Invoke 和 UWP-API 方案

来源:https://www.cnblogs.com/tcjiaan/p/18048489
-Advertisement-
Play Games

上一篇中,老周簡單膚淺地介紹了 XInput API 的使用,並模擬了滑鼠移動,左、右鍵單擊和滾輪。本篇,咱們用 .NET 代碼來完成相同的效果。 說起來也是倒霉,博文寫了一半,電腦忽然斷電了。不知道什麼原因,可能是 UPS 電源出故障。重新開機進來一看,博文沒有自動保存到草稿箱。我記得以前是有自動 ...


上一篇中,老周簡單膚淺地介紹了 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。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Pair和Triple類是commons-lang3庫提供的實用工具,可以方便地將多個值組合在一起,實現多個返回值的效果,提高了代碼的可讀性和可維護性,使得處理多值情景更加輕鬆。 ...
  • 拓展閱讀 maven 包管理平臺-01-maven 入門介紹 + Maven、Gradle、Ant、Ivy、Bazel 和 SBT 的詳細對比表格 maven 包管理平臺-02-windows 安裝配置 + mac 安裝配置 maven 包管理平臺-03-maven project maven 項目 ...
  • 在WPF開發中,Binding實現了數據在Source和Target之間的傳遞和流通,就像現實生活中的一條條道路,建立起了城鎮與城鎮之間的銜接,而數據校驗和類型轉換,就像高速公路之間的收費站和安檢站。那在WPF開發中,如何實現數據的校驗和類型轉換呢?本文以一個簡單的小例子,簡述在WPF開發中,實現數... ...
  • 概述:該通用單例泛型基類使用C#實現,線程安全,通過泛型參數和Lazy<T>實現簡化的單例模式。優點包括線程安全、泛型通用性、簡化實現、以及延遲載入的特性。 優點: 線程安全: 使用Lazy<T>確保了線程安全的延遲初始化,避免了在多線程環境下可能導致的競態條件問題。 泛型通用性: 通過泛型參數,該 ...
  • 概述:以上內容詳細介紹了在 C# 中實現不改變原 List 值的多層嵌套複製方法,包括使用 AutoMapper、Json.NET、以及對象序列化的步驟和示例。這些方法提供了靈活而高效的方式,可以根據項目需求選擇最適合的深度複製方式。 1. 使用 AutoMapper 進行多層嵌套複製 AutoMa ...
  • AvaloniaUI是一個強大的跨平臺.NET客戶端開發框架,讓開發者能夠針對Windows、Linux、macOS、Android和iOS等多個平臺構建應用程式。在構建複雜的應用程式時,模塊化和組件間的通信變得尤為重要。Prism框架提供了模塊化的開發方式,支持插件的熱拔插,而MediatR則是一... ...
  • 概述:JSON Web Token(JWT)是一種用於安全傳輸信息的標準。主要用於身份驗證和信息傳遞,通過頭部、載荷和簽名構成。在.NET Core中,可通過Microsoft.AspNetCore.Authentication.JwtBearer實現後臺服務,提供生成、刷新和驗證Token的介面。 ...
  • NuGet下載Flurl FlurlHttpClient類 public class FlurlHttpClient { private readonly FlurlClient client; public FlurlHttpClient(FlurlClient client) { this.cl ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...