3

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?

Community
  • 1
  • 1
Tommi
  • 3,199
  • 1
  • 24
  • 38

3 Answers3

6

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.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Got it. Just tried to use RegisterHotKey before your answer, but it receives WM_APPCOMMAND message only when window is focused, so I guess project you pointed is the only way to reach what I want. Thanks for detailed explanation. – Tommi May 12 '13 at 16:59
1

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.

Tommi
  • 3,199
  • 1
  • 24
  • 38
  • Please can publish a working wpf working example, all my try of make work this fail over and over again – MrBi Nov 18 '19 at 01:34
  • + $exception {"Value cannot be null.\r\nParameter name: window"} System.ArgumentNullException – MrBi Nov 18 '19 at 02:00
  • And now {"Unable to find an entry point named 'InitializeShellHook' in DLL 'GlobalCbtHook.dll'.":""} – MrBi Nov 18 '19 at 02:04
  • Ok, pal. This is a very old (6+ years) question. I don't even write in C# anymore. I still have sources of this pet projects, so here you go: https://mega.nz/#!CqRx1KqA!yGrL64YEoiJlpKUxpm9VAdeeL3JXga2USiIcsyABByw but it won't be easy. First take a look at the Kick solution. It includes hook project, but hooks are physically lays in their own solution. You'll probably need to fix an absolute path to the GlobalMouseKeyHook project in Kick solution to make it compile. Also please note that this code is also 6+ years old so I don't have a gurantee it compiles now. – Tommi Nov 18 '19 at 08:19
  • If you face more obstacles, I would suggest you to post a new question with your exact problem. – Tommi Nov 18 '19 at 08:20
0

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! =)

Ivan Aksamentov - Drop
  • 12,860
  • 3
  • 34
  • 61
  • Keyboard fires events, because it works in media players, such as Foobar 2K; this doc http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx says that keycodes standartized pretty well. Of course I'm unsure that my keyboard vendor didn't add something from himself, but again, required prev-next-play buttons works. It makes sense to check event type (WM_KEYDOWN/WM_KEYUP etc.) in case program even calls provided callback. But it does not, unfortunately. Anyway, thanks for you answer. – Tommi May 12 '13 at 16:16