Question

I want to capture user keyboard input to react well on keyboard media keys: play/pause, next and previous in particular. I tried to use SetWindowsHookEx with low-level WH_KEYBOARD_LL param to ensure I can get maximum responsibility from this WINAPI function, and I stucked with nothing. If I set breakpoint in hook callback, debugger stops on any keyboard event when I hit regular keys, such as letters or F modifiers, but it never stops when I hit one of media keys, such as Play/Payse. I tried to download demo applications which demonstrate usage of SetWindowsHookEx and none of it works with MM keys. Also I have read this single related question, but there is no answers in it.

I used this piece of code to set up hook:

    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYUP = 0x0101;
    private static LowLevelKeyboardProc _proc = HookCallback;
    private static IntPtr _hookID = IntPtr.Zero;

    public static IntPtr SetHook()
    {
        using (var curProcess = Process.GetCurrentProcess())
        {
            using (var curModule = curProcess.MainModule)
            {
                return SetWindowsHookEx(WH_KEYBOARD_LL, _proc, GetModuleHandle(curModule.ModuleName), 0);
            }
        }
    }

    public static void UnsetHook()
    {
        UnhookWindowsHookEx(_hookID);
    }

    private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            Debug.WriteLine((Keys)vkCode);
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

I found info that RegisterHotKey should help in my task, but since I want react on regular keys too, it will be much comfortable to work with one API member for all regular and MM keys. Is this dead approach or I missing something?

Was it helpful?

Solution

That is because those keys don't generate a keyboard message. They generate the WM_APPCOMMAND message instead. So pressing the Play button generates APPCOMMAND_MEDIA_PLAY for example.

That message is sent to whatever window owns the focus. Such a window almost never acts on the message and passes it to the default window procedure. Which picks it up and passes it to the shell. Hooking that is technically possible, you need a WH_SHELL hook. The callback gets the HSHELL_APPCOMMAND notification.

But this is usually where the good news ends, you cannot write global hooks in a managed language. Since they require a DLL that can be injected in every process that creates a window. Such a DLL cannot be a managed DLL, the process won't have the CLR loaded. Here's a project that implements such a DLL, and shows an example of how to hook shell notifications, no idea how well it works. Such projects tend to fall short on modern Windows versions that require both 32-bit and 64-bit hooks and need to do something reasonable for UAC elevated processes.

OTHER TIPS

I searched this hard before ask and all I've got is unanswered posts on some forums from other frustrating guys. So, when I finally got it work, I want to post complete answer for future visitors.

Preamble

Final solution based on Lib which Hans Passant suggested in his answer, but before it I tried to create something my own, and I failed. Don't know if I lacked of c++ experience (I wrote last line of cpp code about 3 years ago) or I picked dead approach, but my lib calls callback on some SHELL messages, but never on MM keys. So I have read article from codeproject and downloaded code. If you'll try demo you'll see that it does not react on MM keys as well. Fortunately, it requires tiny modification to achieve it.

Solution

The heart of lib is c++ project is GlobalCbtHook. You need to made changes in stdafx.h:

#define WINVER          0x0500
#define _WIN32_WINNT    0x0500

Version was 0x0400 and therefore you can't use some constants from winuser.h, HSHELL_APPCOMMAND in particular. But as Hans Passant mentioned, this message is exactly what we want! So, after modifying header, you can register new custom message in GlobalCbtHook.cpp (method ShellHookCallback, it's line 136 for me):

else if (code == HSHELL_APPCOMMAND)
    msg = RegisterWindowMessage("WILSON_HOOK_HSHELL_APPCOMMAND");

And that's all we need to change in unmanaged lib. Recompile dll. Please note, unlike .net code Debug and Release configurations for c++ code makes huge difference in compiled code size and perfomance.

Also you'll need to process new custom WILSON_HOOK_HSHELL_APPCOMMAND message in your C# application. You can use helper class GlobalHooks.cs from demo app, you'll need to add message id member, appropriate event for APPCOMMAND, and fire it when custom message comes. All info about usage helper you can find on project page. However, since I was need only MM keys hook and my application is WPF so I can't use ProcessMessage from helper, I decided to not use helper and write small wrapper, and here is it:

        #region MM keyboard
        [DllImport("GlobalCbtHook.dll")]
        public static extern bool InitializeShellHook(int threadID, IntPtr DestWindow);
        [DllImport("GlobalCbtHook.dll")]
        public static extern void UninitializeShellHook();
        [DllImport("user32.dll")]
        public static extern int RegisterWindowMessage(string lpString);

        private const int FAPPCOMMAND_MASK = 0xF000;

        private const int APPCOMMAND_MEDIA_NEXTTRACK     = 11;
        private const int APPCOMMAND_MEDIA_PREVIOUSTRACK = 12;
        private const int VK_MEDIA_STOP                  = 13;
        private const int VK_MEDIA_PLAY_PAUSE            = 14;

        private int AppCommandMsgId;
        private HwndSource SenderHwndSource;

        private void _SetMMHook()
        {
            AppCommandMsgId = RegisterWindowMessage("WILSON_HOOK_HSHELL_APPCOMMAND");
            var hwnd = new WindowInteropHelper(Sender).Handle;
            InitializeShellHook(0, hwnd);
            SenderHwndSource = HwndSource.FromHwnd(hwnd);
            SenderHwndSource.AddHook(WndProc);
        }

        private static int HIWORD(int p) { return (p >> 16) & 0xffff; }

        public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == AppCommandMsgId)
            {
                int keycode = HIWORD(lParam.ToInt32()) & ~FAPPCOMMAND_MASK; //#define GET_APPCOMMAND_LPARAM(lParam) ((short)(HIWORD(lParam) & ~FAPPCOMMAND_MASK))
                // Do work
                Debug.WriteLine(keycode);
                if (OnKeyMessage != null)
                {
                    OnKeyMessage(null, EventArgs.Empty);
                }
            }
            return IntPtr.Zero;
        }
        #endregion

        #region Managed
        private readonly Window Sender;

        private KeyboardHook() { }
        public KeyboardHook(Window sender)
        {
            Sender = sender;
        }

        public void SetMMHook()
        {
            _SetMMHook();
        }

        public event EventHandler OnKeyMessage;

        private bool Released = false;
        public void ReleaseAll()
        {
            if (Released) return;
            UninitializeShellHook();
            SenderHwndSource.RemoveHook(WndProc);
            Released = true;
        }

        ~KeyboardHook()
        {
            ReleaseAll();
        }
        #endregion

Well, it actually don't do anything except decode key code from lParam of message and fire empty event, but this is all you'll need to; you can define your own delegate for event, you can create keys enum as you like etc. For me, I get proper keycodes for Play_Pause, Prev, Next keys - this is exactly what I looked for.

The problem is that multimedia (mm) keys not as well standartized as plain keys. And regardless of that windows has special codes to catch mm-keypresses, it is up to keyboard vendor to use it or not. Sometimes you also need to install driver to get it working.

First of all check that your keyboard triggers that events at all: use some kind of utility, such as KeyMapper. Also check if its working in some popular mm-player.

Try to catch all of WM_KEYDOWN WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP in your hook.

Use Spy++, maybe you will find some unusual events that help you to solve your task.

Happy coding! =)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top