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