Skip to content

Instantly share code, notes, and snippets.

@zhuochun
Forked from Ciantic/keyboardlistener.cs
Created February 1, 2014 14:45

Revisions

  1. @Ciantic Ciantic created this gist Jul 11, 2010.
    429 changes: 429 additions & 0 deletions keyboardlistener.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,429 @@
    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 Ownskit.Utils
    {
    /// <summary>
    /// Listens keyboard globally.
    ///
    /// <remarks>Uses WH_KEYBOARD_LL.</remarks>
    /// </summary>
    public class KeyboardListener : IDisposable
    {
    /// <summary>
    /// Creates global keyboard listener.
    /// </summary>
    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;

    /// <summary>
    /// Destroys global keyboard listener.
    /// </summary>
    ~KeyboardListener()
    {
    Dispose();
    }

    /// <summary>
    /// Fired when any of the keys is pressed down.
    /// </summary>
    public event RawKeyEventHandler KeyDown;

    /// <summary>
    /// Fired when any of the keys is released.
    /// </summary>
    public event RawKeyEventHandler KeyUp;

    #region Inner workings

    /// <summary>
    /// Hook ID
    /// </summary>
    private IntPtr hookId = IntPtr.Zero;

    /// <summary>
    /// Asynchronous callback hook.
    /// </summary>
    /// <param name="character">Character</param>
    /// <param name="keyEvent">Keyboard event</param>
    /// <param name="vkCode">VKCode</param>
    private delegate void KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character);

    /// <summary>
    /// Actual callback hook.
    ///
    /// <remarks>Calls asynchronously the asyncCallback.</remarks>
    /// </summary>
    /// <param name="nCode"></param>
    /// <param name="wParam"></param>
    /// <param name="lParam"></param>
    /// <returns></returns>
    [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);
    }

    /// <summary>
    /// Event to be invoked asynchronously (BeginInvoke) each time key is pressed.
    /// </summary>
    private KeyboardCallbackAsync hookedKeyboardCallbackAsync;

    /// <summary>
    /// Contains the hooked callback in runtime.
    /// </summary>
    private InterceptKeys.LowLevelKeyboardProc hookedLowLevelKeyboardProc;

    /// <summary>
    /// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events.
    /// </summary>
    /// <param name="keyEvent">Keyboard event</param>
    /// <param name="vkCode">VKCode</param>
    /// <param name="character">Character as string.</param>
    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

    /// <summary>
    /// Disposes the hook.
    /// <remarks>This call is required as it calls the UnhookWindowsHookEx.</remarks>
    /// </summary>
    public void Dispose()
    {
    InterceptKeys.UnhookWindowsHookEx(hookId);
    }

    #endregion
    }

    /// <summary>
    /// Raw KeyEvent arguments.
    /// </summary>
    public class RawKeyEventArgs : EventArgs
    {
    /// <summary>
    /// VKCode of the key.
    /// </summary>
    public int VKCode;

    /// <summary>
    /// WPF Key of the key.
    /// </summary>
    public Key Key;

    /// <summary>
    /// Is the hitted key system key.
    /// </summary>
    public bool IsSysKey;

    /// <summary>
    /// Convert to string.
    /// </summary>
    /// <returns>Returns string representation of this key, if not possible empty string is returned.</returns>
    public override string ToString()
    {
    return Character;
    }

    /// <summary>
    /// Unicode character of key pressed.
    /// </summary>
    public string Character;

    /// <summary>
    /// Create raw keyevent arguments.
    /// </summary>
    /// <param name="VKCode"></param>
    /// <param name="isSysKey"></param>
    /// <param name="Character">Character</param>
    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);
    }

    }

    /// <summary>
    /// Raw keyevent handler.
    /// </summary>
    /// <param name="sender">sender</param>
    /// <param name="args">raw keyevent arguments</param>
    public delegate void RawKeyEventHandler(object sender, RawKeyEventArgs args);

    #region WINAPI Helper class
    /// <summary>
    /// Winapi Key interception helper class.
    /// </summary>
    internal static class InterceptKeys
    {
    public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
    public static int WH_KEYBOARD_LL = 13;

    /// <summary>
    /// Key event
    /// </summary>
    public enum KeyEvent : int {
    /// <summary>
    /// Key down
    /// </summary>
    WM_KEYDOWN = 256,

    /// <summary>
    /// Key up
    /// </summary>
    WM_KEYUP = 257,

    /// <summary>
    /// System key up
    /// </summary>
    WM_SYSKEYUP = 261,

    /// <summary>
    /// System key down
    /// </summary>
    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;

    /// <summary>
    /// Convert VKCode to Unicode.
    /// <remarks>isKeyDown is required for because of keyboard state inconsistencies!</remarks>
    /// </summary>
    /// <param name="VKCode">VKCode</param>
    /// <param name="isKeyDown">Is the key down event?</param>
    /// <returns>String representing single unicode character.</returns>
    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
    }