diff --git a/Pilz.Input/KeyboardListener.cs b/Pilz.Input/KeyboardListener.cs new file mode 100644 index 0000000..456dcfc --- /dev/null +++ b/Pilz.Input/KeyboardListener.cs @@ -0,0 +1,431 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Windows.Input; +using System.Windows.Threading; +using System.Collections.Generic; + +namespace Pilz.Input +{ + /// + /// Listens keyboard globally. + /// + /// Uses WH_KEYBOARD_LL. + /// + public class KeyboardListener : IDisposable + { + /// + /// Creates global keyboard listener. + /// + public KeyboardListener() + { + // Dispatcher thread handling the KeyDown/KeyUp events. + this.dispatcher = Dispatcher.CurrentDispatcher; + + // We have to store the LowLevelKeyboardProc, so that it is not garbage collected runtime + hookedLowLevelKeyboardProc = (InterceptKeys.LowLevelKeyboardProc)LowLevelKeyboardProc; + + // Set the hook + hookId = InterceptKeys.SetHook(hookedLowLevelKeyboardProc); + + // Assign the asynchronous callback event + hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync); + } + + private Dispatcher dispatcher; + + /// + /// Destroys global keyboard listener. + /// + ~KeyboardListener() + { + Dispose(); + } + + /// + /// Fired when any of the keys is pressed down. + /// + public event RawKeyEventHandler KeyDown; + + /// + /// Fired when any of the keys is released. + /// + public event RawKeyEventHandler KeyUp; + + #region Inner workings + + /// + /// Hook ID + /// + private IntPtr hookId = IntPtr.Zero; + + /// + /// Asynchronous callback hook. + /// + /// Character + /// Keyboard event + /// VKCode + private delegate void KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character); + + /// + /// Actual callback hook. + /// + /// Calls asynchronously the asyncCallback. + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam) + { + string chars = ""; + + if (nCode >= 0) + if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN || + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP || + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYDOWN || + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYUP) + { + // Captures the character(s) pressed only on WM_KEYDOWN + chars = InterceptKeys.VKCodeToString((uint)Marshal.ReadInt32(lParam), + (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN || + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYDOWN)); + + hookedKeyboardCallbackAsync.BeginInvoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), chars, null, null); + } + + return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); + } + + /// + /// Event to be invoked asynchronously (BeginInvoke) each time key is pressed. + /// + private KeyboardCallbackAsync hookedKeyboardCallbackAsync; + + /// + /// Contains the hooked callback in runtime. + /// + private InterceptKeys.LowLevelKeyboardProc hookedLowLevelKeyboardProc; + + /// + /// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events. + /// + /// Keyboard event + /// VKCode + /// Character as string. + void KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character) + { + switch (keyEvent) + { + // KeyDown events + case InterceptKeys.KeyEvent.WM_KEYDOWN: + if (KeyDown != null) + dispatcher.BeginInvoke(new RawKeyEventHandler(KeyDown), this, new RawKeyEventArgs(vkCode, false, character)); + break; + case InterceptKeys.KeyEvent.WM_SYSKEYDOWN: + if (KeyDown != null) + dispatcher.BeginInvoke(new RawKeyEventHandler(KeyDown), this, new RawKeyEventArgs(vkCode, true, character)); + break; + + // KeyUp events + case InterceptKeys.KeyEvent.WM_KEYUP: + if (KeyUp != null) + dispatcher.BeginInvoke(new RawKeyEventHandler(KeyUp), this, new RawKeyEventArgs(vkCode, false, character)); + break; + case InterceptKeys.KeyEvent.WM_SYSKEYUP: + if (KeyUp != null) + dispatcher.BeginInvoke(new RawKeyEventHandler(KeyUp), this, new RawKeyEventArgs(vkCode, true, character)); + break; + + default: + break; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Disposes the hook. + /// This call is required as it calls the UnhookWindowsHookEx. + /// + public void Dispose() + { + InterceptKeys.UnhookWindowsHookEx(hookId); + } + + #endregion + } + + /// + /// Raw KeyEvent arguments. + /// + public class RawKeyEventArgs : EventArgs + { + /// + /// VKCode of the key. + /// + public int VKCode; + + /// + /// WPF Key of the key. + /// + public Key Key; + + /// + /// Is the hitted key system key. + /// + public bool IsSysKey; + + /// + /// Convert to string. + /// + /// Returns string representation of this key, if not possible empty string is returned. + public override string ToString() + { + return Character; + } + + /// + /// Unicode character of key pressed. + /// + public string Character; + + /// + /// Create raw keyevent arguments. + /// + /// + /// + /// Character + public RawKeyEventArgs(int VKCode, bool isSysKey, string Character) + { + this.VKCode = VKCode; + this.IsSysKey = isSysKey; + this.Character = Character; + this.Key = System.Windows.Input.KeyInterop.KeyFromVirtualKey(VKCode); + } + + } + + /// + /// Raw keyevent handler. + /// + /// sender + /// raw keyevent arguments + public delegate void RawKeyEventHandler(object sender, RawKeyEventArgs args); + + #region WINAPI Helper class + /// + /// Winapi Key interception helper class. + /// + internal static class InterceptKeys + { + public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); + public static int WH_KEYBOARD_LL = 13; + + /// + /// Key event + /// + public enum KeyEvent : int + { + /// + /// Key down + /// + WM_KEYDOWN = 256, + + /// + /// Key up + /// + WM_KEYUP = 257, + + /// + /// System key up + /// + WM_SYSKEYUP = 261, + + /// + /// System key down + /// + WM_SYSKEYDOWN = 260 + } + + public static IntPtr SetHook(LowLevelKeyboardProc proc) + { + using (Process curProcess = Process.GetCurrentProcess()) + using (ProcessModule curModule = curProcess.MainModule) + { + return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); + } + } + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + #region Convert VKCode to string + // Note: Sometimes single VKCode represents multiple chars, thus string. + // E.g. typing "^1" (notice that when pressing 1 the both characters appear, + // because of this behavior, "^" is called dead key) + + [DllImport("user32.dll")] + private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl); + + [DllImport("user32.dll")] + private static extern bool GetKeyboardState(byte[] lpKeyState); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + private static extern IntPtr GetKeyboardLayout(uint dwLayout); + + [DllImport("User32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("User32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + private static uint lastVKCode = 0; + private static uint lastScanCode = 0; + private static byte[] lastKeyState = new byte[255]; + private static bool lastIsDead = false; + + /// + /// Convert VKCode to Unicode. + /// isKeyDown is required for because of keyboard state inconsistencies! + /// + /// VKCode + /// Is the key down event? + /// String representing single unicode character. + public static string VKCodeToString(uint VKCode, bool isKeyDown) + { + // ToUnicodeEx needs StringBuilder, it populates that during execution. + System.Text.StringBuilder sbString = new System.Text.StringBuilder(5); + + byte[] bKeyState = new byte[255]; + bool bKeyStateStatus; + bool isDead = false; + + // Gets the current windows window handle, threadID, processID + IntPtr currentHWnd = GetForegroundWindow(); + uint currentProcessID; + uint currentWindowThreadID = GetWindowThreadProcessId(currentHWnd, out currentProcessID); + + // This programs Thread ID + uint thisProgramThreadId = GetCurrentThreadId(); + + // Attach to active thread so we can get that keyboard state + if (AttachThreadInput(thisProgramThreadId, currentWindowThreadID, true)) + { + // Current state of the modifiers in keyboard + bKeyStateStatus = GetKeyboardState(bKeyState); + + // Detach + AttachThreadInput(thisProgramThreadId, currentWindowThreadID, false); + } + else + { + // Could not attach, perhaps it is this process? + bKeyStateStatus = GetKeyboardState(bKeyState); + } + + // On failure we return empty string. + if (!bKeyStateStatus) + return ""; + + // Gets the layout of keyboard + IntPtr HKL = GetKeyboardLayout(currentWindowThreadID); + + // Maps the virtual keycode + uint lScanCode = MapVirtualKeyEx(VKCode, 0, HKL); + + // Keyboard state goes inconsistent if this is not in place. In other words, we need to call above commands in UP events also. + if (!isKeyDown) + return ""; + + // Converts the VKCode to unicode + int relevantKeyCountInBuffer = ToUnicodeEx(VKCode, lScanCode, bKeyState, sbString, sbString.Capacity, (uint)0, HKL); + + string ret = ""; + + switch (relevantKeyCountInBuffer) + { + // Dead keys (^,`...) + case -1: + isDead = true; + + // We must clear the buffer because ToUnicodeEx messed it up, see below. + ClearKeyboardBuffer(VKCode, lScanCode, HKL); + break; + + case 0: + break; + + // Single character in buffer + case 1: + ret = sbString[0].ToString(); + break; + + // Two or more (only two of them is relevant) + case 2: + default: + ret = sbString.ToString().Substring(0, 2); + break; + } + + // We inject the last dead key back, since ToUnicodeEx removed it. + // More about this peculiar behavior see e.g: + // http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_23453780.html + // http://blogs.msdn.com/michkap/archive/2005/01/19/355870.aspx + // http://blogs.msdn.com/michkap/archive/2007/10/27/5717859.aspx + if (lastVKCode != 0 && lastIsDead) + { + System.Text.StringBuilder sbTemp = new System.Text.StringBuilder(5); + ToUnicodeEx(lastVKCode, lastScanCode, lastKeyState, sbTemp, sbTemp.Capacity, (uint)0, HKL); + lastVKCode = 0; + + return ret; + } + + // Save these + lastScanCode = lScanCode; + lastVKCode = VKCode; + lastIsDead = isDead; + lastKeyState = (byte[])bKeyState.Clone(); + + return ret; + } + + private static void ClearKeyboardBuffer(uint vk, uint sc, IntPtr hkl) + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(10); + + int rc; + do + { + byte[] lpKeyStateNull = new Byte[255]; + rc = ToUnicodeEx(vk, sc, lpKeyStateNull, sb, sb.Capacity, 0, hkl); + } while (rc < 0); + } + #endregion + } + #endregion +} \ No newline at end of file diff --git a/Pilz.Input/Pilz.Input.csproj b/Pilz.Input/Pilz.Input.csproj new file mode 100644 index 0000000..12ccc21 --- /dev/null +++ b/Pilz.Input/Pilz.Input.csproj @@ -0,0 +1,11 @@ + + + + net48 + + + + + + + diff --git a/Pilz.sln b/Pilz.sln index 6271f04..746f885 100644 --- a/Pilz.sln +++ b/Pilz.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.329 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 MinimumVisualStudioVersion = 10.0.40219.1 Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Pilz", "Pilz\Pilz.vbproj", "{277D2B83-7613-4C49-9CAB-E080195A6E0C}" EndProject @@ -31,6 +31,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Pilz.Networking", "Pilz.Net EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.Cryptography", "Pilz.Cryptography\Pilz.Cryptography.csproj", "{3F5988E6-439E-4A9D-B2C6-47EFFB161AC6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pilz.Input", "Pilz.Input\Pilz.Input.csproj", "{6F52431D-5D7D-4A6F-AF88-29575F4196B3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +153,14 @@ Global {3F5988E6-439E-4A9D-B2C6-47EFFB161AC6}.Release|Any CPU.Build.0 = Release|Any CPU {3F5988E6-439E-4A9D-B2C6-47EFFB161AC6}.Release|x86.ActiveCfg = Release|Any CPU {3F5988E6-439E-4A9D-B2C6-47EFFB161AC6}.Release|x86.Build.0 = Release|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Debug|x86.Build.0 = Debug|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Release|Any CPU.Build.0 = Release|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Release|x86.ActiveCfg = Release|Any CPU + {6F52431D-5D7D-4A6F-AF88-29575F4196B3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE