Ich erstelle ein Programm, das Tastendrücke zur globalen Steuerung von iTunes überwacht. Es hat auch ein paar WinForms (für die Anzeige von Titelinformationen und Bearbeitungsoptionen).
Der Low-Level-Tastaturhaken funktioniert eine Zeit lang gut. Wenn ich einfach das Programm starte, ist der Tastaturhaken gesetzt und iTunes öffnet sich. Dann öffne ich Notepad und kann tonnenweise Zeug wirklich schnell tippen und jeder Anschlag wird erfasst, wobei höchstens 30ms in der Hook-Funktion verbracht werden (und meistens <10ms). Die Hook-Funktion fügt die Ereignisse einfach in eine Warteschlange ein, die von einem anderen Thread verarbeitet wird. Sie läuft auf einem eigenen Thread mit hoher Priorität unter Verwendung einer eigenen Application.Run().
Wenn ich jedoch anfange, Dinge innerhalb von iTunes zu tun (z. B. ein paar Play/Pause-Klicks, die Ereignisse in meinem Programm erzeugen) oder innerhalb des Programms (wie das Öffnen des Optionsfensters), dann wird die Hook-Funktion nicht mehr aufgerufen! Dies kann auch dann passieren, wenn die Tastatur nie benutzt wurde (z.B. beim Starten, ein paar Mal auf Play und Pause in iTunes klicken, dann eine Taste drücken).
Die Ursache dafür, dass der Hook nicht aufgerufen wird, liegt nicht daran, dass zu viel Zeit in der Hook-Funktion verbracht wird.
Wenn ich UnhookWindowsHookEx aufrufe, gibt es immer true zurück, unabhängig davon, ob die Hook-Funktion noch aufgerufen wurde oder nicht.
Was könnte also die Ursache sein?
Eine Idee (obwohl ich keine Beweise oder Lösungen habe) ist, dass der verwaltete Thread nicht mehr der richtige native Thread ist. Ich verwende zahlreiche (verwaltete) Threads in meinem Programm und habe gelesen, dass ein einziger nativer Thread viele verwaltete Threads ausführen kann und dass ein verwalteter Thread ändern kann, welcher native Thread ihn ausführt. Ist es möglich, dass der Hook immer noch Nachrichten erzeugt, diese aber an den falschen Thread sendet? Wenn dies der Fall ist, wie kann ich das Problem umgehen?
Bearbeiten: Der Haken und die Rückrufe
Eine etwas abgespeckte Version meines KeyMonitors. Sie ist der Übersichtlichkeit halber etwas abgespeckt. Ich habe einige Hilfsprogramme (wie die meisten Werte des Key-Enums und viele Funktionen der Keys-Klasse wie ToString() und FromString()) sowie einige Fehlerbehandlungen entfernt.
Das meiste Wichtige befindet sich in der Klasse KeyMonitor. KeyMonitor.Start() startet einen Thread für die Nachrichten, KeyMonitor.HookThread() ist dieser Thread und erstellt den Hook zusammen mit einem Application.Run() für die Nachrichtenschleife, KeyMonitor.KeyboardHookProc() ist die Callback-Funktion, und KeyMonitor.HookEventDispatchThread() verteilt die vom Callback aufgezeichneten Ereignisse.
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);
}
}
}
}