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.