c# - 如何在 Linux 中使用 C# 创建全局键盘挂钩
问题描述
我有一个在亚马逊上购买的无线 USB 遥控器,我想用它来触发我的程序中的操作。
在 Linux 中连接时,遥控器显示为单独的键盘和鼠标。所以,我在 C# 中寻找一种方法来拦截来自遥控器的键盘事件并在我的应用程序中使用它们。
我考虑过的一些选择...
选项 1 - 读取 /dev/input/by-id 中的文件
在这个文件夹中,有一个名为“usb-SG.Ltd_SG_Control_Mic-if03-mouse”的文件,当我跟踪它时,它确实会产生一些信息。
这并不理想,原因有两个:
- 它需要提升权限才能访问数据
- 它不允许我的程序独占访问输入数据
选项 2 - 使用 HIDSharp
https://www.zer7.com/software/hidsharp
这是一个看起来像这样可以完成我正在寻找的库的库,但是文档非常稀疏。
解决方案
在这里回答我自己的问题,因为我必须为此做很多研究,我相信它会在以后对其他人有所帮助。我选择了选项 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();
}
}
感谢您坚持这一点。我希望它对你有用!
推荐阅读
- node.js - Node.js 微服务应该如何在 Rabbitmq 重启后存活下来?
- javascript - 跨域请求时,getJSON() 函数和 post() 函数有什么区别?
- ms-access - 当该列包含空值时格式化列抛出异常
- angular - “数字”类型的参数不可分配给“字符串”类型的参数。尝试将值作为 int 时
- python - 创建循环以从数据帧中动态选择行,然后将选定的行附加到另一个数据帧:df.query()
- python - kivy 中超过了最大递归深度,但仅在打包时才超出,而不是在使用 python 开发应用程序时
- json - 在 LinkExtractor 中抓取多个正则表达式似乎不起作用
- c# - C# : Common method/wrapper for methods with different definitions
- r - 移除 ggplot facet strip 标签周围的三边边框
- php - 联系表格 7 wpcf7_before_send_mail 挂钩