首页 > 解决方案 > 如何在 Linux 中使用 C# 创建全局键盘挂钩

问题描述

我有一个在亚马逊上购买的无线 USB 遥控器,我想用它来触发我的程序中的操作。

在 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

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

标签: c#linuxkeyboard.net-5

解决方案


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

警告 - 这篇文章中会有很多代码

概括

出于我的意图和目的,我希望有一些代码可以在用户按下系统任意位置的键时发布事件。在开发这个时,我发现我也可以挂钩鼠标事件。

需要注意的是,这里的代码像 linux OS 并没有真正区分键盘按钮按下和鼠标按钮按下。对于 linux,它们都只是按钮。

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

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

设置权限

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

 sudo gpasswd -a $USER input

事件类型.cs

由于文件夹 /dev/input 本质上是 linux 操作系统的一堆输入/输出设备的事件总线,因此您可能想要使用多种事件类型。这是我能够组合在一起的枚举,以使破译事件类型更容易一些。

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
}

事件代码.cs

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

/// <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
}

鼠标轴.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; }
}

鼠标移动事件.cs

这是我使用进程鼠标移动更改更新的事件。

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

输入阅读器.cs

这是大部分工作发生的地方。在这里,我们有一个类,您可以在其中提供一个事件文件的路径,并在它进入时发布更新。执行此操作的示例文件是“/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;
    }
}

聚合输入读取器.cs

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

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

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();
    }
}

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


推荐阅读