Search notes:

Windows: Event hooking with C-Sharp

Win32Hook.cs is a C# source file that can be used for applications that need to hook keyboard events.
Win32Hook.cs can be turned into an assembly with create-assembly.ps1 (Compare Creating assemblies with add-type).
An application that uses Win32Hook.cs is swapKeys.cs: it exchanges the Caps Locks key with the ESC key.
The sources are hosted in this Github repository.

Win32Hook.cs

// vi: foldmarker={{{,}}} foldmethod=marker
//
// Version 0.3  2021-08-11
//
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace TQ84 { namespace Win32 {

   public enum VirtualKey : short { // {{{
   //
   //  Compare With enum Windows.System.VirtualKey
   //     https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkey?view=winrt-19041
       LBUTTON = 0x01, RBUTTON = 0x02, MBUTTON = 0x04, XBUTTON1 = 0x05, XBUTTON2 = 0x06,
       CANCEL = 0x03, // CANCEL between RBUTTON and MBUTTON
       BACK                = 0x08,
       TAB                 = 0x09,
       CLEAR               = 0x0C,
       RETURN              = 0x0D,
       SHIFT = 0x10, CONTROL = 0x11,
       MENU                = 0x12,
       PAUSE               = 0x13,
       CAPITAL             = 0x14, /* Caps Lock */
       KANA = 0x15, HANGUL = 0x15, JUNJA = 0x17, HANJA = 0x19, KANJI = 0x19,
       FINAL               = 0x18, // FINAL between HANGUL and HANJA
       ESCAPE              = 0x1B,
       CONVERT             = 0x1C,
       NONCONVERT          = 0x1D,
       ACCEPT              = 0x1E,
       MODECHANGE          = 0x1F,
       SPACE               = 0x20,
       PRIOR = 0x21, NEXT = 0x22,
       END = 0x23, HOME = 0x24, LEFT = 0x25, UP = 0x26, RIGHT = 0x27, DOWN = 0x28,
       SELECT              = 0x29,
       PRINT               = 0x2A,
       EXECUTE             = 0x2B,
       SNAPSHOT            = 0x2C,
       INSERT = 0x2D, DELETE = 0x2E,
       HELP                = 0x2F,
       KEY_0 = 0x30, KEY_1 = 0x31, KEY_2 = 0x32, KEY_3 = 0x33, KEY_4 = 0x34, KEY_5 = 0x35, KEY_6 = 0x36, KEY_7 = 0x37, KEY_8 = 0x38, KEY_9 = 0x39,
       KEY_A = 0x41, KEY_B = 0x42, KEY_C = 0x43, KEY_D = 0x44, KEY_E = 0x45, KEY_F = 0x46, KEY_G = 0x47, KEY_H = 0x48, KEY_I = 0x49, KEY_J = 0x4A, KEY_K = 0x4B, KEY_L = 0x4C, KEY_M = 0x4D, KEY_N = 0x4E, KEY_O = 0x4F, KEY_P = 0x50, KEY_Q = 0x51, KEY_R = 0x52, KEY_S = 0x53, KEY_T = 0x54, KEY_U = 0x55, KEY_V = 0x56, KEY_W = 0x57, KEY_X = 0x58, KEY_Y = 0x59, KEY_Z = 0x5A,
       LWIN  = 0x5B, RWIN = 0x5C,
       APPS                = 0x5D,
       SLEEP               = 0x5F,
       NUMPAD0 = 0x60, NUMPAD1 = 0x61, NUMPAD2 = 0x62, NUMPAD3 = 0x63, NUMPAD4 = 0x64, NUMPAD5 = 0x65, NUMPAD6 = 0x66, NUMPAD7 = 0x67, NUMPAD8 = 0x68, NUMPAD9 = 0x69,
       MULTIPLY = 0x6A, ADD = 0x6B, SEPARATOR = 0x6C, SUBTRACT = 0x6D, DECIMAL = 0x6E, DIVIDE = 0x6F,
       F1 = 0x70, F2 = 0x71, F3 = 0x72, F4 = 0x73, F5 = 0x74, F6 = 0x75, F7 = 0x76, F8 = 0x77, F9 = 0x78, F10 = 0x79, F11 = 0x7A, F12 = 0x7B, F13 = 0x7C, F14 = 0x7D, F15 = 0x7E, F16 = 0x7F, F17 = 0x80, F18 = 0x81, F19 = 0x82, F20 = 0x83, F21 = 0x84, F22 = 0x85, F23 = 0x86, F24 = 0x87, NUMLOCK             = 0x90,
       SCROLL              = 0x91,
       LSHIFT = 0xA0, LCONTROL  = 0xA2, LMENU = 0xA4,
       RSHIFT = 0xA1, RCONTROL  = 0xA3, RMENU = 0xA5,
       BROWSER_BACK = 0xA6, BROWSER_FORWARD = 0xA7, BROWSER_REFRESH = 0xA8, BROWSER_STOP = 0xA9, BROWSER_SEARCH = 0xAA, BROWSER_FAVORITES = 0xAB, BROWSER_HOME = 0xAC,
       VOLUME_MUTE = 0xAD, VOLUME_DOWN = 0xAE, VOLUME_UP = 0xAF,
       MEDIA_NEXT_TRACK = 0xB0, MEDIA_PREV_TRACK = 0xB1, MEDIA_STOP = 0xB2, MEDIA_PLAY_PAUSE = 0xB3,
       LAUNCH_MAIL = 0xB4, LAUNCH_MEDIA_SELECT = 0xB5, LAUNCH_APP1 = 0xB6, LAUNCH_APP2 = 0xB7,
       OEM_PLUS   = 0xBB, OEM_COMMA  = 0xBC, OEM_MINUS  = 0xBD, OEM_PERIOD = 0xBE, OEM_CLEAR  = 0xFE,
   //  Note the assigned numbers: OEM_PLUS .. OEM_PERIOD come between OEM_1 and OEM_2; OEM_CLEAR at the end.
       OEM_1 = 0xBA, OEM_2 = 0xBF, OEM_3 = 0xC0, OEM_4 = 0xDB, OEM_5 = 0xDC, OEM_6 = 0xDD, OEM_7 = 0xDE, OEM_8 = 0xDF,
       OEM_102             = 0xE2,
       PROCESSKEY          = 0xE5,
       PACKET              = 0xE7, // Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes.
                                   // The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods.
                                   // For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
       ATTN                = 0xF6,
       CRSEL               = 0xF7,
       EXSEL               = 0xF8,
       EREOF               = 0xF9,
       PLAY                = 0xFA,
       ZOOM                = 0xFB,
       NONAME              = 0xFC,
       PA1                 = 0xFD
    } // }}}

    public enum ScanCode   : short { // {{{
      NONE                =   0, // V0.3 - seems necessary to be able to assign 0 as scan code to inputs.U.ki.wScan in PowerShell
//    LBUTTON             =   0,
//    RBUTTON             =   0,
      CANCEL              =  70,
//    MBUTTON             =   0,
//    XBUTTON1            =   0,
//    XBUTTON2            =   0,
      BACK                =  14,
      TAB                 =  15,
      CLEAR               =  76,
      RETURN              =  28,
      SHIFT               =  42,
      CONTROL             =  29,
      MENU                =  56,
//    PAUSE               =   0,
      CAPITAL             =  58, /* Caps Lock */
//    KANA                =   0,
//    HANGUL              =   0,
//    JUNJA               =   0,
//    FINAL               =   0,
//    HANJA               =   0,
//    KANJI               =   0,
      ESCAPE              =   1,
//    CONVERT             =   0,
//    NONCONVERT          =   0,
//    ACCEPT              =   0,
//    MODECHANGE          =   0,
      SPACE               =  57,
      PRIOR               =  73,
      NEXT                =  81,
      END                 =  79,
      HOME                =  71,
      LEFT                =  75,
      UP                  =  72,
      RIGHT               =  77,
      DOWN                =  80,
//    SELECT              =   0,
//    PRINT               =   0,
//    EXECUTE             =   0,
      SNAPSHOT            =  84,
      INSERT              =  82,
      DELETE              =  83,
      HELP                =  99,
      KEY_0 = 11, KEY_1 = 2, KEY_2 = 3, KEY_3 = 4, KEY_4 = 5, KEY_5 = 6, KEY_6 = 7, KEY_7 = 8, KEY_8 = 9, KEY_9 = 10, KEY_A = 30, KEY_B = 48, KEY_C = 46, KEY_D = 32, KEY_E = 18, KEY_F = 33, KEY_G = 34, KEY_H = 35, KEY_I = 23, KEY_J = 36, KEY_K = 37, KEY_L = 38, KEY_M = 50, KEY_N = 49, KEY_O = 24, KEY_P = 25, KEY_Q = 16, KEY_R = 19, KEY_S = 31, KEY_T = 20, KEY_U = 22, KEY_V = 47, KEY_W = 17, KEY_X = 45, KEY_Y = 21, KEY_Z = 44,
      LWIN = 91, RWIN = 92, APPS = 93,
      SLEEP               = 95,
      NUMPAD0 = 82, NUMPAD1 = 79, NUMPAD2 = 80, NUMPAD3 = 81, NUMPAD4 = 75, NUMPAD5 = 76, NUMPAD6 = 77, NUMPAD7 = 71, NUMPAD8 = 72, NUMPAD9 = 73,
      MULTIPLY            =  55,
      ADD                 =  78,
//    SEPARATOR           =   0,
      SUBTRACT            =  74,
      DECIMAL             =  83,
      DIVIDE              =  53,
      F1 = 59, F2 = 60, F3 = 61, F4 = 62, F5 = 63, F6 = 64, F7 = 65, F8 = 66, F9 = 67, F10 = 68, F11 = 87, F12 = 88, F13 = 100, F14 = 101, F15 = 102, F16 = 103, F17 = 104, F18 = 105, F19 = 106, F20 = 107, F21 = 108, F22 = 109, F23 = 110, F24 = 118,
      NUMLOCK             =  69,
      SCROLL              =  70,
      LSHIFT              =  42,
      RSHIFT              =  54,
      LCONTROL            =  29,
      RCONTROL            =  29,
      LMENU               =  56,
      RMENU               =  56,
      BROWSER_BACK = 106, BROWSER_FORWARD = 105, BROWSER_REFRESH = 103, BROWSER_STOP = 104, BROWSER_SEARCH = 101, BROWSER_FAVORITES = 102, BROWSER_HOME =  50,
      VOLUME_MUTE =  32, VOLUME_DOWN =  46, VOLUME_UP =  48,
      MEDIA_NEXT_TRACK =  25, MEDIA_PREV_TRACK =  16, MEDIA_STOP =  36, MEDIA_PLAY_PAUSE =  34,
      LAUNCH_MAIL = 108, LAUNCH_MEDIA_SELECT = 109, LAUNCH_APP1 = 107, LAUNCH_APP2 =  33,
      OEM_1               =  39,
      OEM_PLUS            =  13,
      OEM_COMMA           =  51,
      OEM_MINUS           =  12,
      OEM_PERIOD          =  52,
      OEM_2 = 53, OEM_3 = 41, OEM_4 = 26, OEM_5 = 43, OEM_6 = 27, OEM_7 = 40,
//    OEM_8               =   0,
      OEM_102             =  86,
//    PROCESSKEY          =   0,
//    PACKET              =   0,
//    ATTN                =   0,
//    CRSEL               =   0,
//    EXSEL               =   0,
      EREOF               =  93,
//    PLAY                =   0,
      ZOOM                =  98,
//    NONAME              =   0,
//    PA1                 =   0,
//    OEM_CLEAR           =   0,
   } // }}}

   [StructLayout(LayoutKind.Sequential)] public struct POINT { // {{{ POINT
      public int       x; // long!
      public int       y; // long!
    } // }}}

    public   enum KEYEVENTF : uint { // {{{
        NONE        = 0x0000, // V0.3 - seems necessary to be able to assign 0 to input.U.ki.dwFlags in PowerShell
        EXTENDEDKEY = 0x0001,
        KEYUP       = 0x0002,
        SCANCODE    = 0x0008,
        UNICODE     = 0x0004
    } // }}}

   [StructLayout(LayoutKind.Sequential)] public   struct KEYBDINPUT { // http://pinvoke.net/default.aspx/Structures/KEYBDINPUT.html {{{
        public   VirtualKey      wVk;
        public   ScanCode        wScan;
        public   KEYEVENTF       dwFlags;
        public   int             time;           // TODO: should this be an uint?
        public   UIntPtr         dwExtraInfo;
    } // }}}

   [StructLayout(LayoutKind.Sequential)] public   struct MOUSEINPUT { // https://pinvoke.net/default.aspx/Structures/MOUSEINPUT.html {{{
        public   int         dx;
        public   int         dy;
        public   int         mouseData;
        public   MOUSEEVENTF dwFlags;
        public   uint        time;
        public   UIntPtr     dwExtraInfo;
    } // }}}

   [Flags] public enum MOUSEEVENTF : uint { // {{{
        ABSOLUTE        = 0x8000,
        HWHEEL          = 0x1000,
        MOVE            = 0x0001,
        MOVE_NOCOALESCE = 0x2000,
        LEFTDOWN        = 0x0002,
        LEFTUP          = 0x0004,
        RIGHTDOWN       = 0x0008,
        RIGHTUP         = 0x0010,
        MIDDLEDOWN      = 0x0020,
        MIDDLEUP        = 0x0040,
        VIRTUALDESK     = 0x4000,
        WHEEL           = 0x0800,
        XDOWN           = 0x0080,
        XUP             = 0x0100
    } // }}}

   [StructLayout(LayoutKind.Sequential)] public struct HARDWAREINPUT { // http://pinvoke.net/default.aspx/Structures/HARDWAREINPUT.html {{{
        public   int   uMsg;
        public   short wParamL;
        public   short wParamH;
    } // }}}

    [StructLayout(LayoutKind.Explicit)] public   struct InputUnion { // {{{
       [FieldOffset(0)] public MOUSEINPUT    mi;
       [FieldOffset(0)] public KEYBDINPUT    ki;
       [FieldOffset(0)] public HARDWAREINPUT hi;
     } // }}}

   [StructLayout(LayoutKind.Sequential)] public struct INPUT // https://pinvoke.net/default.aspx/Structures/INPUT.html // {{{
    {
      public uint       type;
      public InputUnion U;
   } // }}}

   public class Hook { // {{{

      private enum WH : int { // {{{
         KEYBOARD_LL =     13,
         MOUSE_LL    =     14,
      } // }}}

      private enum WM: int { // {{{
         KEYDOWN    = 0x0100,
         KEYUP      = 0x0101,
         SYSKEYDOWN = 0x0104,
         SYSKEYUP   = 0x0105,
      } // }}}

      private static IntPtr   hKeyboardLL   = IntPtr.Zero;
      private static IntPtr   hMouseLL      = IntPtr.Zero;

   [Flags] public enum KBDLLHOOKSTRUCTFlags : uint { // {{{
       LLKHF_EXTENDED = 0x01,
       LLKHF_INJECTED = 0x10,
       LLKHF_ALTDOWN  = 0x20,
       LLKHF_UP       = 0x80,
    } // }}}

   [StructLayout(LayoutKind.Sequential)] public class KBDLLHOOKSTRUCT { // {{{
       public uint                 vkCode;
       public uint                 scanCode;
       public KBDLLHOOKSTRUCTFlags flags;
       public uint                 time;
       public UIntPtr              dwExtraInfo;
    } // }}}

   [StructLayout(LayoutKind.Sequential)] public class MSLLHOOKSTRUCT { // {{{
       public POINT                pt;
       public uint                 mouseData;
       public MSLLHOOKSTRUCTFlags  flags;
       public uint                 time;
       public UIntPtr              dwExtraInfo;
    } // }}}

   [Flags] public enum MSLLHOOKSTRUCTFlags : uint { // {{{
       LLMHF_INJECTED          = 0x01,
       LLMHF_LOWER_IL_INJECTED = 0x02,
    } // }}}

    private   delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    public    delegate bool   fnKeyboardLL(IntPtr wParam, KBDLLHOOKSTRUCT kbd);
    public    delegate bool   fnMouseLL   (IntPtr wParam, MSLLHOOKSTRUCT  msl);

    private static fnKeyboardLL keyboardEvent_;
    private static fnMouseLL    mouseEvent_;

    public static void KeyboardLL(fnKeyboardLL keyboardEvent) { // {{{

        if (hKeyboardLL    == IntPtr.Zero) {
            hKeyboardLL     = SetHook(WH.KEYBOARD_LL, LowLevelKeyboardProc);
            keyboardEvent_  = keyboardEvent;
        }
    } // }}}

    public static void MouseLL(fnMouseLL mouseEvent) { // {{{

        if (hMouseLL == IntPtr.Zero) {
           hMouseLL     = SetHook(WH.MOUSE_LL, LowLevelMouseProc);
           mouseEvent_  = mouseEvent;
        }
    } // }}}

    public static void Stop() { // {{{
        if (hKeyboardLL != IntPtr.Zero) {
           UnhookWindowsHookEx(hKeyboardLL);
           hKeyboardLL = IntPtr.Zero;
        }
        if (hMouseLL != IntPtr.Zero) {
           UnhookWindowsHookEx(hMouseLL);
           hMouseLL = IntPtr.Zero;
        }
    } // }}}

    private static IntPtr SetHook(WH hookType, HookProc hookProc) { // {{{
        IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
        return SetWindowsHookEx((int) hookType, hookProc, moduleHandle, 0);
    } // }}}

    private static IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) { // {{{

        KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));

        if (nCode == 0 && keyboardEvent_ != null) {

           if (keyboardEvent_(wParam, kbd)) {
              return (IntPtr) 1;
           }
        }

        return CallNextHookEx(hKeyboardLL, nCode, wParam, lParam);
    } // }}}

    private static IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam) { // {{{

        MSLLHOOKSTRUCT kbd = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));

        if (nCode == 0 && mouseEvent_ != null) {
           if (mouseEvent_(wParam, kbd)) {
              return (IntPtr) 1;
           }
        }

        return CallNextHookEx(hKeyboardLL, nCode, wParam, lParam);
    } // }}}

    [DllImport("user32.dll"  )] private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll"  )] private static extern bool   UnhookWindowsHookEx(IntPtr hhk);
    [DllImport("user32.dll"  )] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string lpModuleName);

 //
 //    SendInput (imho) does not technically belong to Win32 Hooking, but it seems it cannot hurt to have it anyway.
 //
    [DllImport("user32.dll"  )] public  static extern uint SendInput (uint nInputs, [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, int cbSize);

  } // }}}

}}
Github repository cs-Win32-Hook, path: /Win32Hook.cs

create-assembly.ps1

create-assembly.ps1 creates the assembly from the source code:
$srcWin32Hook = get-content -raw "$psScriptRoot/Win32Hook.cs"

add-type                                          `
   -typeDefinition        $srcWin32Hook           `
   -outputAssembly       "$psSCriptRoot/Win32Hook.dll"     `
   -outputType            library
Github repository cs-Win32-Hook, path: /create-assembly.ps1

History

V0.2 Code re-formatting
V0.3 Add explicit NULL member to enums ScanCode and KEYEVENTF

Index