SetWindowsHookEx对媒体键没有反应
本文关键字:有反应 媒体 SetWindowsHookEx | 更新日期: 2023-09-27 18:13:08
我想捕获用户键盘输入,以便对键盘媒体键做出很好的反应:播放/暂停,下一个和上一个键。我试图使用SetWindowsHookEx与低级WH_KEYBOARD_LL参数,以确保我可以从这个WINAPI函数获得最大的责任,我陷入了什么都没有。如果我在钩子回调中设置断点,当我按常规键(如字母或F修饰符)时,调试器会在任何键盘事件上停止,但当我按媒体键(如Play/Payse)时,它永远不会停止。我试图下载演示SetWindowsHookEx使用的演示应用程序,但它都不能与MM键一起工作。我也读过这个单独的相关问题,但是里面没有答案。
我使用这段代码来设置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);
我发现RegisterHotKey
应该有助于我的任务,但是因为我也想对常规键做出反应,所以使用一个API成员来处理所有常规键和MM键会更舒服。这是死法还是我漏了什么?
这是因为这些键不生成键盘消息。它们会生成WM_APPCOMMAND消息。例如,按下Play按钮会生成APPCOMMAND_MEDIA_PLAY。
该消息被发送到拥有焦点的任何窗口。这样的窗口几乎不会对消息起作用,而是将其传递给默认的窗口过程。它把它捡起来并传递给壳层。钩子在技术上是可能的,你需要一个WH_SHELL钩子。回调得到HSHELL_APPCOMMAND通知。
但这通常是好消息结束的地方,您不能在托管语言中编写全局钩子。因为它们需要一个可以注入到创建窗口的每个进程中的DLL。这样的DLL不能是托管DLL,进程不会加载CLR。这里有一个实现这样一个DLL的项目,并展示了一个如何挂钩shell通知的示例,不知道它的工作效果如何。这样的项目在需要32位和64位钩子并且需要为UAC提升进程做一些合理的事情的现代Windows版本上往往做不到。
我在问这个问题之前努力搜索了一下,所有我得到的都是其他令人沮丧的家伙在一些论坛上没有回答的帖子。所以,当我终于得到它的工作,我想张贴完整的答案为未来的游客。
序言
基于Lib的最终解决方案,其中Hans Passant在他的回答中建议,但在此之前我试图创建自己的东西,但我失败了。不知道我是否缺乏c++经验(我写了最后一行cpp代码大约3年前)或我选择了死的方法,但是我的库调用一些SHELL消息的回调,但从来没有对MM键。所以我从codeproject中阅读了文章并下载了代码。如果你试着演示一下,你会发现它对MM键也没有反应。幸运的是,它只需要很小的修改就可以实现。
解决方案lib是c++项目的核心是GlobalCbtHook。您需要在stdafx.h:
中进行更改#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
Version是0x0400,因此您不能使用winuser.h中的一些常量,特别是HSHELL_APPCOMMAND
。但是正如Hans Passant所提到的,这个消息正是我们想要的!因此,在修改标题后,您可以在GlobalCbtHook.cpp(方法ShellHookCallback,对我来说是第136行)中注册新的自定义消息:
else if (code == HSHELL_APPCOMMAND)
msg = RegisterWindowMessage("WILSON_HOOK_HSHELL_APPCOMMAND");
这就是我们在非托管lib中需要更改的所有内容。重新编译dll。请注意,与。net代码不同,c++代码的调试和发布配置会在编译后的代码大小和性能上产生巨大差异。
你还需要在你的c#应用程序中处理新的自定义WILSON_HOOK_HSHELL_APPCOMMAND消息。你可以从演示应用中使用帮助类GlobalHooks.cs,你需要添加消息id成员,适当的APPCOMMAND事件,并在自定义消息到来时触发它。您可以在项目页面上找到有关使用帮助器的所有信息。然而,因为我只需要MM键钩子,我的应用程序是WPF,所以我不能使用来自helper的ProcessMessage,我决定不使用helper并编写小包装器,下面是它:
#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 of message中解码key代码并触发空事件之外,它实际上什么都不做,但这就是你所需要的;你可以为事件定义你自己的委托,你可以创建你喜欢的keys enum等等。对我来说,我得到适当的键码为播放暂停,上一步,下一步键-这正是我所寻找的。
问题是多媒体(mm)键不像普通键那样标准化。不管windows是否有特殊的代码来捕捉mm- key按压,这都取决于键盘供应商是否使用它。有时你还需要安装驱动程序才能使其工作。
首先检查你的键盘是否触发了这些事件:使用某种实用程序,比如KeyMapper。还要检查它是否在一些流行的mm-player中工作。
试着抓住所有的WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
在你的钩子。
使用spy++,也许你会发现一些不寻常的事件,帮助你解决你的任务。
编码快乐!div =)