3

我想捕获用户键盘输入以对键盘媒体键做出良好反应:播放/暂停,尤其是下一个和上一个。我尝试将 SetWindowsHookEx 与低级 WH_KEYBOARD_LL 参数一起使用,以确保我可以从这个 WINAPI 函数中获得最大的责任,但我一无所获。如果我在钩子回调中设置断点,当我按下常规键(例如字母或 F 修饰符)时,调试器会在任何键盘事件上停止,但当我按下某个媒体键(例如 Play/Payse)时它永远不会停止。我尝试下载演示 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 密钥会很舒服。这是死的方法还是我错过了什么?

4

3 回答 3

6

那是因为这些键不会生成键盘消息。它们改为生成WM_APPCOMMAND 消息。例如,按下播放按钮会生成 APPCOMMAND_MEDIA_PLAY。

该消息被发送到拥有焦点的任何窗口。这样的窗口几乎从不作用于消息并将其传递给默认窗口过程。它捡起它并将其传递给外壳。技术上可行的挂钩,您需要一个 WH_SHELL 挂钩。 回调获取 HSHELL_APPCOMMAND 通知。

但这通常是好消息结束的地方,您不能用托管语言编写全局挂钩。因为它们需要一个可以注入到每个创建窗口的进程中的 DLL。这样的 DLL 不能是托管 DLL,进程不会加载 CLR。 这是一个实现这样一个 DLL 的项目,并展示了一个如何挂钩 shell 通知的示例,不知道它的工作情况如何。此类项目往往无法满足需要 32 位和 64 位挂钩的现代 Windows 版本,并且需要为 UAC 提升的进程做一些合理的事情。

于 2013-05-12T16:54:18.697 回答
1

在询问之前我努力搜索了这个,我所得到的只是其他令人沮丧的人在一些论坛上未答复的帖子。所以,当我终于让它工作时,我想为未来的访问者发布完整的答案。

前言

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

解决方案

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

#define WINVER          0x0500
#define _WIN32_WINNT    0x0500

版本是 0x0400,因此您不能使用 winuser.hHSHELL_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,您需要为 APPCOMMAND 添加消息 ID 成员、适当的事件,并在自定义消息出现时触发它。您可以在项目页面上找到有关使用助手的所有信息。但是,由于我只需要 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 键的正确键码——这正是我所寻找的。

于 2013-05-14T06:38:29.773 回答
0

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

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

试着抓住WM_KEYDOWN WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP你的钩子。

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

快乐编码!=)

于 2013-05-12T16:07:44.283 回答