marko devcic

  • github:
    deva666
  • email:
    madevcic {at} gmail.com

.NET Keyboard Hook

Posted on 25 October 2014

There's a ton of code samples on the web with possible low level keyboard hook implementations, but I haven't found one that takes into account users keyboard layout.

All of these samples showed how you can get a keycode that user pressed, but for one project I did, I needed to scan keyboard input exactly as it appeared on the users screen. So if the users switches the keyboard layout to let's say German, then I want to get the actual German keys he pressed. If he switches to English back, again I'd like the actual English characters that are appearing on the screen.
Here's a full code for a class that does that.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;


namespace KeyboardHook
{
    public delegate IntPtr KeyboardHookCallback(int code, IntPtr wParam, IntPtr lParam);

    public sealed class KeyBoardHook : IDisposable
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_KEYUP = 0x0101; 
        private const int WM_SYSKEYDOWN = 0x0104;
        private const int WM_SYSKEYUP = 0x0105;
        private const int VK_SHIFT = 0x10;
        private const int VK_CONTROL = 0x11;
        private const int VK_MENU = 0x12;
        private const int VK_CAPITAL = 0x14;

        public event EventHandler<KeyboardHookArgs> KeyDown;

        KeyboardHookCallback _hookCallBack;
        IntPtr _hookID = IntPtr.Zero;

        private bool _isDisposed;

        public KeyBoardHook()
        {
            _hookCallBack = new KeyboardHookCallback(HookCallback);
            using (Process process = Process.GetCurrentProcess())
            {
                using (ProcessModule module = process.MainModule)
                {
                    _hookID = WinAPI.SetWindowsHookEx(WH_KEYBOARD_LL, _hookCallBack, WinAPI.GetModuleHandle(module.ModuleName), 0);                   
                }
            }
        }

        private IntPtr HookCallback(int code, IntPtr wParam, IntPtr lParam)
        {
            if ( code < 0)
                return WinAPI.CallNextHookEx(_hookID, code, wParam, lParam);

            WinAPI.KBDLLHOOKSTRUCT keybStruct = (WinAPI.KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(WinAPI.KBDLLHOOKSTRUCT));

            if ((int)wParam == WM_KEYDOWN || (int)wParam == WM_SYSKEYDOWN)
            {
                bool isDownShift = ((WinAPI.GetKeyState(VK_SHIFT) & 0x80) == 0x80 ? true : false);
                bool isDownCapslock = (WinAPI.GetKeyState(VK_CAPITAL) != 0 ? true : false);
                StringBuilder builder = new StringBuilder(10);
                byte[] lpKeyState = new byte[256];
                if (WinAPI.GetKeyboardState(lpKeyState))
                {
                    if (WinAPI.ToUnicodeEx((uint)keybStruct.vkCode
                                            , (uint)keybStruct.scanCode
                                            , lpKeyState
                                            , builder
                                            , builder.Capacity
                                            , 0
                                            , WinAPI.GetKeyboardLayout(0)) != -1)
                    {
                        KeyDown.InvokeSafely<KeyboardHookArgs>(this, new KeyboardHookArgs(keybStruct.vkCode, builder.ToString()));
                    }
                }
            }
            return WinAPI.CallNextHookEx(_hookID, code, wParam, lParam);
        }

        ~KeyBoardHook()
        {
            Dispose(false);            
        }

        private void Dispose(bool disposing)
        {            
            if (_isDisposed) return;
            WinAPI.UnhookWindowsHookEx(_hookID);
            _isDisposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

}

In the hook call back method, after we obtain the keycode that was pressed, we need to call a few more Win32 functions to get the keyboard layout and keyboard state.
Here are all of the Win32 functions that we need to import.

public class WinAPI
{
        [StructLayout(LayoutKind.Sequential)]
        public struct KBDLLHOOKSTRUCT
        {
            public int vkCode;
            public int scanCode;
            public int flags;
            int time;
            IntPtr dwExtraInfo;
        }
        
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SetWindowsHookEx(int idHook, KeyboardHookCallback lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern IntPtr GetModuleHandle(string lpModuleName);
        
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
        
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetKeyboardState(byte[] lpKeyState);
        
        [DllImport("user32.dll")]
        internal static extern IntPtr GetKeyboardLayout(uint idThread);
        
        [DllImport("user32.dll")]
        internal static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[]
           lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff,
           int cchBuff, uint wFlags, IntPtr dwhkl);
 } 

And this is what the EventArgs class looks like

public class KeyboardHookArgs : EventArgs
{       
        int _keyCode;
        string _keyName;        
        string _string;

        public int KeyCode { get { return _keyCode; } }
        public string KeyName { get { return _keyName; } }        
        public string String { get { return _string; } }

        public KeyboardHookArgs(int keyCode)
        {
            _keyCode = keyCode;
            _keyName = System.Windows.Input.KeyInterop.KeyFromVirtualKey(keyCode).ToString();
        }

        public KeyboardHookArgs(int keyCode, string str)
            : this(keyCode)
        {
            _string = str;
        }
 }

Happy coding!