如何在 Linux 中使用 C# 创建全局键盘挂钩

How to Create Global Keyboard Hook with C# in Linux

我有一个无线 USB 遥控器 that I purchased on amazon,我想用它来触发程序中的操作。

在 Linux 中连接时,遥控器显示为单独的键盘和鼠标。所以,我正在寻找一种在 c# 中拦截远程键盘事件并在我的应用程序中使用它们的方法。

我考虑过的一些选项...

选项 1 - 读取 /dev/input/by-id

中的文件

在此文件夹中,有一个名为“usb-SG.Ltd_SG_Control_Mic-if03-mouse”的文件,当我跟踪它时,它确实会产生一些信息。

这并不理想,原因有二:

  1. 需要提升权限才能访问数据
  2. 它不允许我的程序独占访问输入数据

选项 2 - 使用 HIDSharp

https://www.zer7.com/software/hidsharp

这是一个看起来可以完成我正在寻找的东西的库,但文档非常稀疏。

在这里回答我自己的问题,因为我必须为此做大量研究,我相信它会在以后帮助其他人。我选择了选项 1,因为它似乎最容易实现。

警告 - 此 post

中将包含大量代码

总结

出于我的意图和目的,我想要一些代码,只要用户在系统上的任何地方按下一个键,它就会发布一个事件。在开发这个的时候,我发现我也可以连接到鼠标事件。

需要注意的是,此处的代码(例如 linux OS)并没有真正区分键盘按钮按下和鼠标按钮按下。对于 linux,它们都只是按钮。

了解您实际上可以扩展此代码以与游戏手柄和特殊输入外围设备等其他项目一起使用,如果您愿意的话。

其他陷阱 - 如问题中所述,此代码不会阻止设备输入到其他程序。如果您想覆盖电源按钮或音量按钮等默认功能,这可能会有问题。

设置权限

为了运行此代码,运行此程序的用户必须在输入用户组中,否则会抛出异常。 运行 此代码用于将当前用户添加到该组。

 sudo gpasswd -a $USER input

EventType.cs

由于文件夹 /dev/input 本质上是 linux OS 的一堆 input/output 设备的事件总线,因此您可以使用多种事件类型可能要消费。这是我能够放在一起的枚举,可以更轻松地解读事件类型。

public enum EventType
{
    /// <summary>
    /// Used as markers to separate events. Events may be separated in time or in space, such as with the multitouch protocol.
    /// </summary>
    EV_SYN,

    /// <summary>
    /// Used to describe state changes of keyboards, buttons, or other key-like devices.
    /// </summary>
    EV_KEY,

    /// <summary>
    /// Used to describe relative axis value changes, e.g. moving the mouse 5 units to the left.
    /// </summary>
    EV_REL,

    /// <summary>
    /// Used to describe absolute axis value changes, e.g. describing the coordinates of a touch on a touchscreen.
    /// </summary>
    EV_ABS,

    /// <summary>
    /// Used to describe miscellaneous input data that do not fit into other types.
    /// </summary>
    EV_MSC,

    /// <summary>
    /// Used to describe binary state input switches.
    /// </summary>
    EV_SW,

    /// <summary>
    /// Used to turn LEDs on devices on and off.
    /// </summary>
    EV_LED,

    /// <summary>
    /// Used to output sound to devices.
    /// </summary>
    EV_SND,

    /// <summary>
    /// Used for autorepeating devices.
    /// </summary>
    EV_REP,

    /// <summary>
    /// Used to send force feedback commands to an input device.
    /// </summary>
    EV_FF,

    /// <summary>
    /// A special type for power button and switch input.
    /// </summary>
    EV_PWR,

    /// <summary>
    /// Used to receive force feedback device status.
    /// </summary>
    EV_FF_STATUS,
}

KeyState.cs

与许多其他事件处理系统一样,每次用户按下一个键时都会发生多个事件。按下键一次,向上按下键一次,如果用户决定按住键一次。

public enum KeyState
{
    KeyUp,
    KeyDown,
    KeyHold
}

EventCode.cs

每个不同的按钮都与一个事件代码相关联。无论是键盘上的按钮还是鼠标上的按钮,您都可以在这里找到它。他是一个枚举助手 class,可以更轻松地破译这些代码。

/// <summary>
/// Mapping for this can be found here: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
/// </summary>
public enum EventCode
{
    Reserved = 0,
    Esc = 1,
    Num1 = 2,
    Num2 = 3,
    Num3 = 4,
    Num4 = 5,
    Num5 = 6,
    Num6 = 7,
    Num7 = 8,
    Num8 = 9,
    Num9 = 10,
    Num0 = 11,
    Minus = 12,
    Equal = 13,
    Backspace = 14,
    Tab = 15,
    Q = 16,
    W = 17,
    E = 18,
    R = 19,
    T = 20,
    Y = 21,
    U = 22,
    I = 23,
    O = 24,
    P = 25,
    LeftBrace = 26,
    RightBrace = 27,
    Enter = 28,
    LeftCtrl = 29,
    A = 30,
    S = 31,
    D = 32,
    F = 33,
    G = 34,
    H = 35,
    J = 36,
    K = 37,
    L = 38,
    Semicolon = 39,
    Apostrophe = 40,
    Grave = 41,
    LeftShift = 42,
    Backslash = 43,
    Z = 44,
    X = 45,
    C = 46,
    V = 47,
    B = 48,
    N = 49,
    M = 50,
    Comma = 51,
    Dot = 52,
    Slash = 53,
    RightShift = 54,
    KpAsterisk = 55,
    LeftAlt = 56,
    Space = 57,
    Capslock = 58,
    F1 = 59,
    Pf2 = 60,
    F3 = 61,
    F4 = 62,
    F5 = 63,
    F6 = 64,
    F7 = 65,
    F8 = 66,
    Pf9 = 67,
    F10 = 68,
    Numlock = 69,
    ScrollLock = 70,
    Kp7 = 71,
    Kp8 = 72,
    Kp9 = 73,
    PkpMinus = 74,
    Kp4 = 75,
    Kp5 = 76,
    Kp6 = 77,
    KpPlus = 78,
    Kp1 = 79,
    Kp2 = 80,
    Kp3 = 81,
    Kp0 = 82,
    KpDot = 83,

    Zenkakuhankaku = 85,
    //102ND = 86,
    F11 = 87,
    F12 = 88,
    Ro = 89,
    Katakana = 90,
    Hiragana = 91,
    Henkan = 92,
    Katakanahiragana = 93,
    Muhenkan = 94,
    KpJpComma = 95,
    KpEnter = 96,
    RightCtrl = 97,
    KpSlash = 98,
    SysRq = 99,
    RightAlt = 100,
    LineFeed = 101,
    Home = 102,
    Up = 103,
    Pageup = 104,
    Left = 105,
    Right = 106,
    End = 107,
    Down = 108,
    Pagedown = 109,
    Insert = 110,
    Delete = 111,
    Macro = 112,
    Mute = 113,
    VolumeDown = 114,
    VolumeUp = 115,
    Power = 116, // SC System Power Down
    KpEqual = 117,
    KpPlusMinus = 118,
    Pause = 119,
    Scale = 120, // AL Compiz Scale (Expose)

    KpComma = 121,
    Hangeul = 122,
    Hanja = 123,
    Yen = 124,
    LeftMeta = 125,
    RightMeta = 126,
    Compose = 127,

    Stop = 128, // AC Stop
    Again = 129,
    Props = 130, // AC Properties
    Undo = 131, // AC Undo
    Front = 132,
    Copy = 133, // AC Copy
    Open = 134, // AC Open
    Paste = 135, // AC Paste
    Find = 136, // AC Search
    Cut = 137, // AC Cut
    Help = 138, // AL Integrated Help Center
    Menu = 139, // Menu (show menu)
    Calc = 140, // AL Calculator
    Setup = 141,
    Sleep = 142, // SC System Sleep
    Wakeup = 143, // System Wake Up
    File = 144, // AL Local Machine Browser
    Sendfile = 145,
    DeleteFile = 146,
    Xfer = 147,
    Prog1 = 148,
    Prog2 = 149,
    Www = 150, // AL Internet Browser
    MsDos = 151,
    Coffee = 152, // AL Terminal Lock/Screensaver
    RotateDisplay = 153, // Display orientation for e.g. tablets
    CycleWindows = 154,
    Mail = 155,
    Bookmarks = 156, // AC Bookmarks
    Computer = 157,
    Back = 158, // AC Back
    Forward = 159, // AC Forward
    CloseCd = 160,
    EjectCd = 161,
    EjectCloseCd = 162,
    NextSong = 163,
    PlayPause = 164,
    PreviousSong = 165,
    StopCd = 166,
    Record = 167,
    Rewind = 168,
    Phone = 169, // Media Select Telephone
    Iso = 170,
    Config = 171, // AL Consumer Control Configuration
    Homepage = 172, // AC Home
    Refresh = 173, // AC Refresh
    Exit = 174, // AC Exit
    Move = 175,
    Edit = 176,
    ScrollUp = 177,
    ScrollDown = 178,
    KpLeftParen = 179,
    KpRightParen = 180,
    New = 181, // AC New
    Redo = 182, // AC Redo/Repeat
    
    F13 = 183,
    F14 = 184,
    F15 = 185,
    F16 = 186,
    F17 = 187,
    F18 = 188,
    F19 = 189,
    F20 = 190,
    F21 = 191,
    F22 = 192,
    F23 = 193,
    F24 = 194,
    
    PlayCd = 200,
    PauseCd = 201,
    Prog3 = 202,
    Prog4 = 203,
    Dashboard = 204,    // AL Dashboard
    Suspend = 205,
    Close = 206,    // AC Close
    Play = 207,
    FastForward = 208,
    BassBoost = 209,
    Print = 210,    // AC Print
    Hp = 211,
    Camera = 212,
    Sound = 213,
    Question = 214,
    Email = 215,
    Chat = 216,
    Search = 217,
    Connect = 218,
    Finance = 219,  // AL Checkbook/Finance
    Sport = 220,
    Shop = 221,
    AltErase = 222,
    Cancel = 223,   // AC Cancel
    BrightnessDown = 224,
    BrightnessUp = 225,
    Media = 226,
    
    SwitchVideoMode = 227,  // Cycle between available video outputs (Monitor/LCD/TV-out/etc)
    KbdIllumToggle = 228,
    KbdIllumDown = 229,
    KbdIllumUp = 230,

    Send = 231, // AC Send
    Reply = 232,    // AC Reply
    ForwardMail = 233,  // AC Forward Msg
    Save = 234, // AC Save
    Documents = 235,

    Battery = 236,

    Bluetooth = 237,
    Wlan = 238,
    Uwb = 239,

    Unknown = 240,

    VideoNext = 241,    // drive next video source
    VideoPrev = 242,    // drive previous video source
    BrightnessCycle = 243,  // brightness up, after max is min
    BrightnessAuto = 244,   // Set Auto Brightness: manual brightness control is off, rely on ambient
    DisplayOff = 245,   // display device to off state

    Wwan = 246, // Wireless WAN (LTE, UMTS, GSM, etc.)
    RfKill = 247,   // Key that controls all radios

    MicMute = 248,  // Mute / unmute the microphone
    LeftMouse = 272,
    RightMouse = 273,
    MiddleMouse = 274,
    MouseBack = 275,
    MouseForward = 276,
    
    ToolFinger = 325,
    ToolQuintTap = 328,
    Touch = 330,
    ToolDoubleTap = 333, 
    ToolTripleTap = 334, 
    ToolQuadTap = 335,
    Mic = 582
}

MouseAxis.cs

鼠标移动表示为移动量和与该变化相关联的轴。 0代表X轴运动,1代表Y轴运动。

public enum MouseAxis
{
    X,
    Y
}

KeypressEvent.cs

这是我用来处理按键事件的事件。

public class KeyPressEvent : EventArgs
{
    public KeyPressEvent(EventCode code, KeyState state)
    {
        Code = code;
        State = state;
    }

    public EventCode Code { get; }
    
    public KeyState State { get; }
}

MouseMoveEvent.cs

这里是我使用process mouse movement change updates的事件。

public class MouseMoveEvent : EventArgs
{
    public MouseMoveEvent(MouseAxis axis, int amount)
    {
        Axis = axis;
        Amount = amount;
    }
    
    public MouseAxis Axis { get; }
    
    public int Amount { get; set; }
}

InputReader.cs

这是大部分工作发生的地方。这里我们有一个 class,您可以在其中提供其中一个事件文件的路径,它会在它进入时发布更新。执行此操作的示例文件是“/dev/input/event0”。

需要更多的研究来支持更多的事件类型,但我只对键盘和鼠标输入感兴趣,所以它符合我的目的。我还选择删除每个按钮事件中包含的时间戳,但如果您有兴趣,可以在缓冲区的前 16 位中找到它。

public class InputReader : IDisposable
{
    public delegate void RaiseKeyPress(KeyPressEvent e);

    public delegate void RaiseMouseMove(MouseMoveEvent e);

    public event RaiseKeyPress OnKeyPress;
    public event RaiseMouseMove OnMouseMove;

    private const int BufferLength = 24;
    
    private readonly byte[] _buffer = new byte[BufferLength];
    
    private FileStream _stream;
    private bool _disposing;

    public InputReader(string path)
    {
        _stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

        Task.Run(Run);
    }

    private void Run()
    {
        while (true)
        {
            if (_disposing)
                break;

            _stream.Read(_buffer, 0, BufferLength);

            var type = BitConverter.ToInt16(new[] {_buffer[16], _buffer[17]}, 0);
            var code = BitConverter.ToInt16(new[] {_buffer[18], _buffer[19]}, 0);
            var value = BitConverter.ToInt32(new[] {_buffer[20], _buffer[21], _buffer[22], _buffer[23]}, 0);

            var eventType = (EventType) type;

            switch (eventType)
            {
                case EventType.EV_KEY:
                    HandleKeyPressEvent(code, value);
                    break;
                case EventType.EV_REL:
                    var axis = (MouseAxis) code;
                    var e = new MouseMoveEvent(axis, value);
                    OnMouseMove?.Invoke(e);
                    break;
            }
        }
    }

    private void HandleKeyPressEvent(short code, int value)
    {
        var c = (EventCode) code;
        var s = (KeyState) value;
        var e = new KeyPressEvent(c, s);
        OnKeyPress?.Invoke(e);
    }

    public void Dispose()
    {
        _disposing = true;
        _stream.Dispose();
        _stream = null;
    }
}

汇总InputReader.cs

因为我希望处理来自系统上任何位置的每个设备的输入,所以我将这个 classes 放在一起以聚合来自“/dev/input”中所有文件的输入事件"文件夹。

已知问题 - 如果 USB 设备在 运行ning 期间被移除,此代码将引发异常。我确实打算在我自己的应用程序实现中修复它,但我现在没有时间处理它。

public class AggregateInputReader : IDisposable
{
    private List<InputReader> _readers = new();
    
    public event InputReader.RaiseKeyPress OnKeyPress;

    public AggregateInputReader()
    {
        var files = Directory.GetFiles("/dev/input/", "event*");

        foreach (var file in files)
        {
            var reader = new InputReader(file);
            
            reader.OnKeyPress += ReaderOnOnKeyPress;

            _readers.Add(reader);
        }
    }

    private void ReaderOnOnKeyPress(KeyPressEvent e)
    {
        OnKeyPress?.Invoke(e);
    }

    public void Dispose()
    {
        foreach (var d in _readers)
        {
            d.OnKeyPress -= ReaderOnOnKeyPress;
            d.Dispose();
        }

        _readers = null;
    }
}

用法示例

这现在可以用两行代码完成,这不错。

public class Program
{
    public static void Main(string[] args)
    {
        using var aggHandler = new AggregateInputReader();

        aggHandler.OnKeyPress += (e) => { System.Console.WriteLine($"Code:{e.Code} State:{e.State}"); };

        System.Console.ReadLine();
    }
}

感谢您坚持这样做。我希望它对你有用!