Repository navigation
Input Handling
Package Dapplo.Windows.Input. Full version: Keyboard and mouse.
KeyboardHook and MouseHook are static and run on their own thread. Your OnNext runs on that thread while the
keyboard of the whole system waits, and Windows removes a hook that is too slow. So only decide Handled there, and
ObserveOn for the real work. Listeners that never set Handled should use KeyboardEventsNonBlocking.
// The hook is installed with the first subscription and removed with the last
IDisposable subscription = KeyboardHook.KeyboardEvents
.Where(args => args.IsKeyDown)
.Subscribe(args => Console.WriteLine($"{args.Key} down, Ctrl: {args.IsControl}, Shift: {args.IsShift}"));
// ...
subscription.Dispose();// Decide Handled on the hook thread (quick!), then do the real work on the UI thread.
// Call this on the UI thread, so SynchronizationContext.Current is the one of the UI.
var subscription = KeyboardHook.KeyboardEvents
.Where(args => args.Key == VirtualKeyCode.PrintScreen)
// Swallow the key-down and the key-up, so Windows doesn't take its own screenshot
.Do(args => args.Handled = true)
.Where(args => args.IsKeyDown)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(args => TakeScreenshot());KeyCombinationHandler fires when exactly the keys of the combination are down, and swallows them (set
IsPassThrough = true to let them through). Control, Shift, Menu (Alt) and Win match the left and right key.
// Ctrl+Shift+S, Control and Shift match both the left and the right key.
// The handler marks the key events as handled, so other applications don't see the combination.
var subscription = KeyboardHook.KeyboardEvents
.Where(new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.Shift, VirtualKeyCode.KeyS))
.ObserveOn(SynchronizationContext.Current)
.Subscribe(args => SaveAll());A handler has state: use one instance per subscription, or the factory overload of Where:
// A handler keeps track of the pressed keys, so it may be used by one subscription only.
// When an observable is subscribed more than once, give Where a factory: every subscription gets its own handler.
IObservable<KeyboardHookEventArgs> saveHotkey = KeyboardHook.KeyboardEvents
.Where(() => new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.KeyS));
var first = saveHotkey.Subscribe(_ => Console.WriteLine("Save (1)"));
var second = saveHotkey.Subscribe(_ => Console.WriteLine("Save (2)"));// Ctrl+K followed by Ctrl+C (like Visual Studio), at most 2 seconds apart
var handler = new KeySequenceHandler(
new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.KeyK),
new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.KeyC))
{
Timeout = TimeSpan.FromSeconds(2)
};
var subscription = KeyboardHook.KeyboardEvents.Where(handler).Subscribe(_ => Console.WriteLine("Comment selection"));TriggerMode sets when a KeyCombinationHandler fires: KeyDown (default), FirstKeyUp (the first key of the
combination is released, the others can still be down) or AllKeysUp (the last key is released; another key pressed
in between cancels it). In the key-up modes the keys are passed on to the active application. AllKeysUp is the mode
for hotkeys which send input, see the
documentation.
// Ctrl+Alt+D types the date into the active application, when the user released all keys of the combination
var handler = new KeyCombinationHandler(VirtualKeyCode.Control, VirtualKeyCode.Menu, VirtualKeyCode.KeyD)
{
TriggerMode = TriggerMode.AllKeysUp
};
var subscription = KeyboardHook.KeyboardEvents
.Where(handler)
// Leave the hook thread, sending input from there would block the keyboard
.ObserveOn(TaskPoolScheduler.Default)
// No key of the combination is down anymore, so the text isn't combined with Ctrl or Alt
.Subscribe(_ => KeyboardInputGenerator.TypeText(DateTime.Now.ToString("yyyy-MM-dd")));KeyboardState.IsDown / IsAnyDown read the current (asynchronous) key state, IsDownForCurrentThread and
IsToggled the state of the calling thread's message queue:
// Right now, system wide (GetAsyncKeyState): use this in background threads and hooks
bool shiftDown = KeyboardState.IsDown(VirtualKeyCode.Shift);
bool anyModifier = KeyboardState.IsAnyDown(VirtualKeyCode.Shift, VirtualKeyCode.Control, VirtualKeyCode.Menu, VirtualKeyCode.Win);
// At the time of the keyboard message the UI thread is processing (GetKeyState): use this in a key event handler
bool ctrlWithThisKey = KeyboardState.IsDownForCurrentThread(VirtualKeyCode.Control);
// Toggle keys
bool capsLock = KeyboardState.IsToggled(VirtualKeyCode.Capital);Text sent as Unicode characters (TypeText, an IME, remote desktop) arrives in the hook as VirtualKeyCode.Packet
events (IsPacket, PacketCharacter); the handlers ignore them.
// Press and release keys one after the other: types "hi" (with the current keyboard layout)
KeyboardInputGenerator.KeyPresses(VirtualKeyCode.KeyH, VirtualKeyCode.KeyI);
// Ctrl+C: all keys down, then all keys up in reverse order
KeyboardInputGenerator.KeyCombinationPress(VirtualKeyCode.Control, VirtualKeyCode.KeyC);
// Hold Shift while pressing some keys
KeyboardInputGenerator.KeyDown(VirtualKeyCode.Shift);
KeyboardInputGenerator.KeyPresses(VirtualKeyCode.Right, VirtualKeyCode.Right);
KeyboardInputGenerator.KeyUp(VirtualKeyCode.Shift);
// Win+D shows the desktop, Win is sent as the left Windows key
KeyboardInputGenerator.KeyCombinationPress(VirtualKeyCode.Win, VirtualKeyCode.KeyD);TypeText types text as Unicode characters, independent of the keyboard layout. Line breaks become Enter, \t
becomes Tab, other control characters are skipped. Keys the user holds are combined with the input.
// Types the text as Unicode characters, independent of the keyboard layout.
// Line breaks become Enter, \t becomes Tab, other control characters are skipped.
var text = "Grüße aus Köln 👋\r\nPrice:\t42 €";
uint inserted = KeyboardInputGenerator.TypeText(text);
// Two events (down and up) per UTF-16 code unit, Enter and Tab, fewer means the input was blocked
if (inserted < KeyboardInput.ForText(text).Length)
{
Console.Error.WriteLine("The input was blocked, e.g. by an elevated application");
}// Coordinates are screen pixels, also on monitors left of or above the primary one
MouseInputGenerator.MoveMouse(new NativePoint(100, 200));
// Click at the current position, or at a location
MouseInputGenerator.MouseClick(MouseButtons.Left);
MouseInputGenerator.MouseClick(MouseButtons.Right, new NativePoint(300, 400));
// Drag: press, move, release
MouseInputGenerator.MouseDown(MouseButtons.Left, new NativePoint(100, 100));
MouseInputGenerator.MoveMouse(new NativePoint(400, 100));
MouseInputGenerator.MouseUp(MouseButtons.Left, new NativePoint(400, 100));
// Scroll one notch down
MouseInputGenerator.MoveMouseWheel(-120);// WindowsMessage tells what happened: WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_MOUSEWHEEL, ...
var clicks = MouseHook.MouseEvents
.Where(args => args.WindowsMessage == WindowsMessages.WM_LBUTTONDOWN)
.Subscribe(args => Console.WriteLine($"Left click at {args.Point}"));
// Wheel events have a WheelDelta, 120 is one notch (positive is away from the user)
var wheel = MouseHook.MouseEventsNonBlocking
.Where(args => args.WindowsMessage == WindowsMessages.WM_MOUSEWHEEL)
.Subscribe(args => Console.WriteLine($"Wheel {args.WheelDelta / 120} notches"));
// Mouse moves are frequent: sample them, and never do slow work in the hook
var moves = MouseHook.MouseEventsNonBlocking
.Where(args => args.WindowsMessage == WindowsMessages.WM_MOUSEMOVE)
.Sample(TimeSpan.FromMilliseconds(100))
.Subscribe(args => Console.WriteLine($"Mouse at {args.Point}"));Raw input, idle time and suppressing mouse events: see the documentation.