SetWindowsHookEx对媒体键没有反应

3
我想捕获用户键盘输入,特别是针对媒体播放器的按键:播放/暂停、下一曲和上一曲。我尝试使用低级 WH_KEYBOARD_LL 参数的 SetWindowsHookEx 函数来确保能够最大程度地利用这个 WINAPI 函数,但我却没有成功。如果我在挂钩回调中设置断点,在按下通常的键(如字母或 F 修改键)时,调试器会在任何键盘事件上停止,但当我按下媒体键之一(例如 Play/Pause)时,它却从未停止过。我尝试下载了演示如何使用 SetWindowsHookEx 的演示应用程序,但其中没有一个能够使用 MM 键。此外,我已经阅读了 这篇相关问题,但里面没有答案。
我使用以下代码片段来设置钩子:
    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);

我找到了有关RegisterHotKey的信息,它应该能帮助我完成任务,但由于我也想对常规按键做出反应,所以使用一个API成员处理所有常规和MM按键会更加方便。这种方法是否可行,或者我漏掉了什么?

3个回答

6
那是因为这些键不会生成键盘消息,而是生成WM_APPCOMMAND消息。例如,按下播放按钮会生成APPCOMMAND_MEDIA_PLAY。
该消息发送到拥有焦点的窗口。这样的窗口几乎从不处理该消息并将其传递给默认的窗口过程。然后由shell接收。在技术上钩取它是可能的,你需要一个WH_SHELL钩子。 回调函数得到HSHELL_APPCOMMAND通知。
但这通常是好消息的终点,你不能使用托管语言编写全局钩子。因为它们需要一个可以注入每个创建窗口的进程的DLL。这样的DLL不能是托管DLL,因为该进程未加载CLR。 这里有一个项目,它实现了这样的DLL,并展示了如何钩取shell通知,不知道它的工作效果如何。这样的项目往往在需要32位和64位钩子以及需要对UAC提升的进程进行合理处理的现代Windows版本上存在问题。

明白了。在你回答之前,我尝试使用了RegisterHotKey,但它只在窗口聚焦时接收WM_APPCOMMAND消息,所以我猜你指出的项目是达成我想要的唯一方法。感谢你详细的解释。 - Tommi

1

在提问前我已经进行了很多搜索,但是所有的帖子都没有得到解答。所以,当我最终让它工作时,我想为未来的访客发布完整的答案。

前言

最终的解决方案基于Lib,这是Hans Passant在他的回答中建议的,但在此之前,我尝试创建了自己的东西,但失败了。不知道是我缺乏c++经验(我大约3年前写过cpp代码的最后一行)还是我选择了错误的方法,但我的lib在一些SHELL消息上调用回调,但从未在MM键上调用。因此,我阅读了来自codeproject的文章并下载了代码。如果您尝试演示,您会发现它也不会对MM键做出反应。幸运的是,只需要进行微小的修改就可以实现它。

解决方案

该lib的核心是c++项目GlobalCbtHook。您需要在stdafx.h中进行更改:

#define WINVER          0x0500
#define _WIN32_WINNT    0x0500

版本是0x0400,因此您无法使用winuser.h中的某些常量,特别是HSHELL_APPCOMMAND。但正如Hans Passant所提到的,这条消息正是我们想要的!因此,在修改头文件后,您可以在GlobalCbtHook.cpp(方法ShellHookCallback,对我来说是第136行)中注册新的自定义消息:

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

这就是我们需要在非托管库中更改的全部内容。重新编译dll。请注意,与.net代码不同,对于c++代码,调试和发布配置会对编译后的代码大小和性能产生巨大影响。

此外,您需要在C#应用程序中处理新的自定义WILSON_HOOK_HSHELL_APPCOMMAND消息。您可以使用演示应用程序中的帮助器类GlobalHooks.cs,您需要添加消息ID成员,适当的APPCOMMAND事件,并在收到自定义消息时激活它。您可以在项目页面上找到有关使用帮助程序的所有信息。但是,由于我只需要MM键钩子并且我的应用程序是WPF,因此我无法使用来自帮助程序的ProcessMessage,我决定不使用帮助程序并编写小包装器,以下是:

        #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

实际上,它除了从消息的lParam解码键代码并触发空事件外,什么也不做,但这就是你所需要的;你可以为事件定义自己的委托,你可以创建任何你喜欢的键枚举等。对我来说,我得到了适当的Play_Pause,Prev,Next键的键码-这正是我所寻找的。


请发布一个可工作的WPF示例,我尝试让它工作了很多次,但都失败了。 - MrBi
$exception {"值不能为 null。\r\n参数名: window"} System.ArgumentNullException - MrBi
现在出现以下问题:{"无法在DLL“GlobalCbtHook.dll”中找到名为“InitializeShellHook”的入口点。":""} - MrBi
好的,伙计。这是一个非常古老的问题(6年以上)。我甚至不再写C#了。我仍然有这个小项目的源代码,所以给你链接:https://mega.nz/#!CqRx1KqA!yGrL64YEoiJlpKUxpm9VAdeeL3JXga2USiIcsyABByw 但这并不容易。首先看一下Kick解决方案。它包括钩子项目,但钩子实际上位于它们自己的解决方案中。您可能需要修复Kick解决方案中GlobalMouseKeyHook项目的绝对路径才能使其编译。此外,请注意,这段代码也是6年以上的,所以我不能保证现在可以编译。 - Tommi
如果你遇到更多的障碍,我建议你发布一个新问题,并描述你的确切问题。 - Tommi

0

问题在于多媒体(mm)键不像普通键那样标准化。尽管Windows有特殊代码来捕获mm键的按下,但是否使用取决于键盘供应商。有时您还需要安装驱动程序才能使其正常工作。

首先检查您的键盘是否触发了所有事件:使用某种实用程序,例如KeyMapper。还要检查它是否在一些流行的mm播放器中工作。

尝试在钩子中捕获所有的WM_KEYDOWN WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP

使用Spy++,也许您会发现一些不寻常的事件,帮助您解决任务。

愉快的编码!=)


键盘会触发事件,因为它在媒体播放器(如Foobar 2K)中使用;这篇文档http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx说键码已经很好地标准化了。当然,我不能确定我的键盘供应商没有添加自己的东西,但是需要的上一个/下一个/播放按钮可以正常工作。在调用提供的回调之前,检查事件类型(WM_KEYDOWN/WM_KEYUP等)是有意义的。但很遗憾,即使程序调用,也不会有回调。无论如何,感谢您的答复。 - Tommi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接