使用 SetWindowsHookEx 设置的低级键盘钩子停止调用 C# 中的函数
我正在创建一个程序,用于监视按键以全局控制 iTunes。它还具有一些 WinForm(用于显示曲目信息和编辑选项)。
低级键盘钩子在一段时间内工作得很好。如果我只是启动该程序,则会设置键盘挂钩并打开 iTunes。然后我打开记事本,可以非常快地输入大量内容,并且捕获每个笔画,最多 30 毫秒花在钩子函数上(并且大多数情况下< 10 毫秒)。钩子函数只是将事件添加到由另一个线程处理的队列中。它使用自己的 Application.Run() 在自己的高优先级线程上运行。
但是,如果我开始在 iTunes 中(例如在程序中生成事件的几次播放/暂停单击)或在程序中(例如打开选项窗口)执行操作,则挂钩函数将停止被调用!即使从未使用过键盘(例如,启动、在 iTunes 中单击播放并暂停几次,然后按某个键),也可能会发生这种情况。
钩子没有被调用的原因不是因为钩子函数花费了太多时间。
当我调用UnhookWindowsHookEx时,无论钩子函数是否仍在被调用,它总是返回true。
那么,可能是什么原因呢?
一种想法(尽管我没有证据或解决方案)是托管线程不再是正确的本机线程。我在程序中使用了许多(托管)线程,并且我读到单个本机线程可以运行许多托管线程,并且托管线程可以更改正在运行它的本机线程。钩子是否有可能仍在生成消息,但将它们发送到错误的线程?如果是这种情况,我该如何解决?
编辑:挂钩和回调
我的 KeyMonitor 的一个稍微精简的完成版本。为了清晰起见,它被精简了。我删除了一些实用程序(例如 Key 枚举的大多数值和 Keys 类的许多函数,例如 ToString() 和 FromString())以及一些错误处理。
大多数重要的东西都在 KeyMonitor 类中。 KeyMonitor.Start() 启动消息线程,KeyMonitor.HookThread() 是该线程,并为消息循环创建钩子和 Application.Run(),KeyMonitor.KeyboardHookProc() 是回调函数,KeyMonitor.HookProc() 是回调函数。 HookEventDispatchThread() 负责调度回调记录的事件。
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace KeyTest
{
enum Key : int
{
Shift = 0x10, Ctrl, Alt,
Left_Win = 0x5B, Right_Win,
Left_Shift = 0xA0, Right_Shift, Left_Ctrl, Right_Ctrl, Left_Alt, Right_Alt,
}
class Keys
{
[DllImport("user32.dll")]
private static extern int GetKeyboardState(byte[] pbKeyState);
public const int Count = 256; // vkCode are from 1 to 254, but GetKeyboardState uses 0-255
private readonly bool[] keys = new bool[Count];
public Keys() { }
private void DoModifier(Key x, Key l, Key r) { keys[(int)x] = keys[(int)l] || keys[(int)r]; }
private void DoModifiers()
{
DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift);
DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl);
DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt);
}
private void DoModifier(Key x, Key l, Key r, Key k) { if (k == l || k == r) keys[(int)x] = keys[(int)l] || keys[(int)r]; }
private void DoModifiers(Key k)
{
DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift, k);
DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl, k);
DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt, k);
}
public bool this[int i] { get { return this.keys[i]; } set { this.keys[i] = value; DoModifiers((Key)i); } }
public bool this[Key k] { get { return this.keys[(int)k]; } set { this.keys[(int)k] = value; DoModifiers(k); } }
public void LoadCurrentState()
{
byte[] keyState = new byte[Count];
if (GetKeyboardState(keyState) != 0)
for (int i = 0; i < Count; ++i)
keys[i] = (keyState[i] & 0x80) != 0;
DoModifiers();
}
}
static class KeyMonitor
{
#region Windows API
private delegate int HookProc(int nCode, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern int UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll")]
private static extern int CallNextHookEx(int idHook, int nCode, UIntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
private readonly static UIntPtr WM_KEYDOWN = new UIntPtr(0x100), WM_SYSKEYDOWN = new UIntPtr(0x104);
#endregion
public static event KeyEventHandler OverridingKeyChange;
public static event KeyEventHandler KeyChange;
private struct KeyEventData { public int vk; public bool down; }
private static int hook = 0;
private static Thread dispatchThread = null, hookThread = null;
private static Keys keys = new Keys();
private static Queue<KeyEventData> queue = new Queue<KeyEventData>();
private static void Enqueue(int vk, bool down)
{
lock (queue)
{
queue.Enqueue(new KeyEventData() { vk = vk, down = down });
Monitor.Pulse(queue);
}
}
public static Keys Keys { get { return keys; } }
public static void Start()
{
if (hook == 0)
{
dispatchThread = new Thread(HookEventDispatchThread);
hookThread = new Thread(HookThread);
hookThread.Priority = ThreadPriority.Highest;
dispatchThread.Start();
hookThread.Start();
}
}
public static void Stop()
{
if (hook != 0)
{
// Minimal cleanup...
UnhookWindowsHookEx(hook);
Application.Exit();
dispatchThread.Interrupt();
}
}
private static void HookThread()
{
hook = SetWindowsHookEx(WH_KEYBOARD_LL, new HookProc(KeyboardHookProc), IntPtr.Zero, 0);
if (hook == 0) { /* Handle error */ }
keys.LoadCurrentState();
Application.Run();
}
private static int KeyboardHookProc(int nCode, UIntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
Enqueue(Marshal.ReadInt32(lParam), wParam == WM_SYSKEYDOWN || wParam == WM_KEYDOWN);
return CallNextHookEx(hook, nCode, wParam, lParam);
}
private static void HookEventDispatchThread()
{
for (; ; )
{
KeyEventData data;
lock (queue)
{
if (queue.Count == 0)
try
{
Monitor.Wait(queue);
}
catch (ThreadInterruptedException) { return; }
data = queue.Dequeue();
}
if (data.vk == -1)
{
// Done!
keys = new Keys();
queue.Clear();
return;
}
else if (keys[data.vk] == data.down)
continue;
keys[data.vk] = data.down;
KeyEventArgs e = new KeyEventArgs((System.Windows.Forms.Keys)data.vk);
if (OverridingKeyChange != null) OverridingKeyChange(null, e);
if (!e.Handled && KeyChange != null) KeyChange(null, e);
}
}
}
}
I am creating a program that monitors key presses for controlling iTunes globally. It also has a few WinForms (for displaying track information and editing options).
The low-level keyboard hook works great for awhile. If I just start up the program, keyboard hook is set and iTunes opens. Then I open Notepad and can type tons of stuff really fast and every stroke is captured, with at most 30ms being spent in the hook function (and for the most part <10ms). The hook function simply adds the events onto a queue which is processed by another thread. It is running on its own high-priority thread using it's own Application.Run().
However if I start doing things within iTunes (such as a couple of play/pause clicks which generate events in my program) or within the program (like opening the options window) then the hook function stops being called! This can happen even if the keyboard has never been used (e.g. startup, click play and pause a few times in iTunes, then press a key).
The cause of the hook not being called is not due to too much time being spent in the hook function.
When I call UnhookWindowsHookEx it always returns true, regardless if the hook function was still being called or not.
So, what could be the cause?
One idea (although I have no proof or solutions) is that the managed thread is no longer the correct native thread. I use numerous (managed) threads in my program and I have read that a single native thread can run many managed threads and that a managed thread can change which native thread is running it. Is it possible that the hook is still producing messages but sending them to the wrong thread? If this is the case, how can I work around it?
Edit: The hook and callbacks
A slightly stripped done version of my KeyMonitor. It is stripped down for clarity. I have removed some utilities (like most of the values of the Key enum and many functions of the Keys class like ToString() and FromString()) along with some error handling.
Most of the important stuff is in the KeyMonitor class. KeyMonitor.Start() starts a thread for the messages, KeyMonitor.HookThread() is that thread and creates the hook along with an Application.Run() for the message loop, KeyMonitor.KeyboardHookProc() is the callback function, and KeyMonitor.HookEventDispatchThread() is what dispatches events recorded by the callback.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace KeyTest
{
enum Key : int
{
Shift = 0x10, Ctrl, Alt,
Left_Win = 0x5B, Right_Win,
Left_Shift = 0xA0, Right_Shift, Left_Ctrl, Right_Ctrl, Left_Alt, Right_Alt,
}
class Keys
{
[DllImport("user32.dll")]
private static extern int GetKeyboardState(byte[] pbKeyState);
public const int Count = 256; // vkCode are from 1 to 254, but GetKeyboardState uses 0-255
private readonly bool[] keys = new bool[Count];
public Keys() { }
private void DoModifier(Key x, Key l, Key r) { keys[(int)x] = keys[(int)l] || keys[(int)r]; }
private void DoModifiers()
{
DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift);
DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl);
DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt);
}
private void DoModifier(Key x, Key l, Key r, Key k) { if (k == l || k == r) keys[(int)x] = keys[(int)l] || keys[(int)r]; }
private void DoModifiers(Key k)
{
DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift, k);
DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl, k);
DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt, k);
}
public bool this[int i] { get { return this.keys[i]; } set { this.keys[i] = value; DoModifiers((Key)i); } }
public bool this[Key k] { get { return this.keys[(int)k]; } set { this.keys[(int)k] = value; DoModifiers(k); } }
public void LoadCurrentState()
{
byte[] keyState = new byte[Count];
if (GetKeyboardState(keyState) != 0)
for (int i = 0; i < Count; ++i)
keys[i] = (keyState[i] & 0x80) != 0;
DoModifiers();
}
}
static class KeyMonitor
{
#region Windows API
private delegate int HookProc(int nCode, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern int UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll")]
private static extern int CallNextHookEx(int idHook, int nCode, UIntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
private readonly static UIntPtr WM_KEYDOWN = new UIntPtr(0x100), WM_SYSKEYDOWN = new UIntPtr(0x104);
#endregion
public static event KeyEventHandler OverridingKeyChange;
public static event KeyEventHandler KeyChange;
private struct KeyEventData { public int vk; public bool down; }
private static int hook = 0;
private static Thread dispatchThread = null, hookThread = null;
private static Keys keys = new Keys();
private static Queue<KeyEventData> queue = new Queue<KeyEventData>();
private static void Enqueue(int vk, bool down)
{
lock (queue)
{
queue.Enqueue(new KeyEventData() { vk = vk, down = down });
Monitor.Pulse(queue);
}
}
public static Keys Keys { get { return keys; } }
public static void Start()
{
if (hook == 0)
{
dispatchThread = new Thread(HookEventDispatchThread);
hookThread = new Thread(HookThread);
hookThread.Priority = ThreadPriority.Highest;
dispatchThread.Start();
hookThread.Start();
}
}
public static void Stop()
{
if (hook != 0)
{
// Minimal cleanup...
UnhookWindowsHookEx(hook);
Application.Exit();
dispatchThread.Interrupt();
}
}
private static void HookThread()
{
hook = SetWindowsHookEx(WH_KEYBOARD_LL, new HookProc(KeyboardHookProc), IntPtr.Zero, 0);
if (hook == 0) { /* Handle error */ }
keys.LoadCurrentState();
Application.Run();
}
private static int KeyboardHookProc(int nCode, UIntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
Enqueue(Marshal.ReadInt32(lParam), wParam == WM_SYSKEYDOWN || wParam == WM_KEYDOWN);
return CallNextHookEx(hook, nCode, wParam, lParam);
}
private static void HookEventDispatchThread()
{
for (; ; )
{
KeyEventData data;
lock (queue)
{
if (queue.Count == 0)
try
{
Monitor.Wait(queue);
}
catch (ThreadInterruptedException) { return; }
data = queue.Dequeue();
}
if (data.vk == -1)
{
// Done!
keys = new Keys();
queue.Clear();
return;
}
else if (keys[data.vk] == data.down)
continue;
keys[data.vk] = data.down;
KeyEventArgs e = new KeyEventArgs((System.Windows.Forms.Keys)data.vk);
if (OverridingKeyChange != null) OverridingKeyChange(null, e);
if (!e.Handled && KeyChange != null) KeyChange(null, e);
}
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您需要将委托保存到一个变量,该变量将在您的应用程序持续时间内保留。否则,委托将被垃圾收集(奇怪的是应用程序没有崩溃!)。
You need to save the delegate to a variable that will survive for the duration of your application. Otherwise, delegate is garbage-collected (strange the app did not crash!).