Nach einer langen suche ist es mir endlich gellungen einen richtig einfachen global keyboard hook fuer wpf zu finden 🙂
Was ist ein global key hook?
er ermöglicht dir keys abzufangen die gedrueckt werden waerend deine app nicht aktiv ist. so wie es bei keyloggern eingesetzt wird. Abgesehen von dieser eher kriminellen idee giebt es auch pracktische anwendungsmoeglichkeiten. z. B. kann man befehle in einer applikation ausfuehren ohne das diese den fokus von der aktuellen anwendung stielt. um ein pracktisches beispiel zu nenen: Man wechselt mit einem shortcut „Ctrl + N“ zum naechsten musik titel in der playlist waerend man gerade zockt und nicht ins windows tappen will bzw kann.
und nun zum Fleisch!
oeffnet VisualStudio 2010. (sollte auch in aelteren funktionieren)
Erstellt oder offnet euer projekt indem ihr den key hook testen moechted. (ich habe einfach ein neues wpf projekt namens „global_key_hook“ erstellt wobei es mit ner consolen anwengung genau so geht.)
Dann erstellt eine neue klassen datei (DEINPROJEKT[Rechtsklick] -> Neu -> Klasse ) Name vergeben (wie GlobalKeyHook.cs)
dan fuege diesen code in die klassen datei ein
using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Windows.Input; namespace GlobalKeyHook.Keyboard { /// <summary> /// Listens keyboard globally. /// /// <remarks>Uses WH_KEYBOARD_LL.</remarks> /// </summary> public class KeyboardListener : IDisposable { /// <summary> /// Creates global keyboard listener. /// </summary> public KeyboardListener() { // We have to store the HookCallback, so that it is not garbage collected runtime hookedLowLevelKeyboardProc = (InterceptKeys.LowLevelKeyboardProc)LowLevelKeyboardProc; // Set the hook hookId = InterceptKeys.SetHook(hookedLowLevelKeyboardProc); // Assign the asynchronous callback event hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync); } /// <summary> /// Destroys global keyboard listener. /// </summary> ~KeyboardListener() { Dispose(); } /// <summary> /// Fired when any of the keys is pressed down. /// </summary> public event RawKeyEventHandler KeyDown; /// <summary> /// Fired when any of the keys is released. /// </summary> public event RawKeyEventHandler KeyUp; #region Inner workings /// <summary> /// Hook ID /// </summary> private IntPtr hookId = IntPtr.Zero; /// <summary> /// Asynchronous callback hook. /// </summary> /// <param name="nCode"></param> /// <param name="wParam"></param> /// <param name="lParam"></param> private delegate void KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode); /// <summary> /// Actual callback hook. /// /// <remarks>Calls asynchronously the asyncCallback.</remarks> /// </summary> /// <param name="nCode"></param> /// <param name="wParam"></param> /// <param name="lParam"></param> /// <returns></returns> [MethodImpl(MethodImplOptions.NoInlining)] private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam) { if (nCode >= 0) if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN || wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP || wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYDOWN || wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_SYSKEYUP) hookedKeyboardCallbackAsync.BeginInvoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), null, null); return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); } /// <summary> /// Event to be invoked asynchronously (BeginInvoke) each time key is pressed. /// </summary> private KeyboardCallbackAsync hookedKeyboardCallbackAsync; /// <summary> /// Contains the hooked callback in runtime. /// </summary> private InterceptKeys.LowLevelKeyboardProc hookedLowLevelKeyboardProc; /// <summary> /// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events. /// </summary> /// <param name="keyEvent">Keyboard event</param> /// <param name="vkCode">VKCode</param> void KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode) { switch (keyEvent) { // KeyDown events case InterceptKeys.KeyEvent.WM_KEYDOWN: if (KeyDown != null) KeyDown(this, new RawKeyEventArgs(vkCode, false)); break; case InterceptKeys.KeyEvent.WM_SYSKEYDOWN: if (KeyDown != null) KeyDown(this, new RawKeyEventArgs(vkCode, true)); break; // KeyUp events case InterceptKeys.KeyEvent.WM_KEYUP: if (KeyUp != null) KeyUp(this, new RawKeyEventArgs(vkCode, false)); break; case InterceptKeys.KeyEvent.WM_SYSKEYUP: if (KeyUp != null) KeyUp(this, new RawKeyEventArgs(vkCode, true)); break; default: break; } } #endregion #region IDisposable Members /// <summary> /// Disposes the hook. /// <remarks>This call is required as it calls the UnhookWindowsHookEx.</remarks> /// </summary> public void Dispose() { InterceptKeys.UnhookWindowsHookEx(hookId); } #endregion } /// <summary> /// Raw KeyEvent arguments. /// </summary> public class RawKeyEventArgs : EventArgs { /// <summary> /// VKCode of the key. /// </summary> public int VKCode; /// <summary> /// WPF Key of the key. /// </summary> public Key Key; /// <summary> /// Is the hitted key system key. /// </summary> public bool IsSysKey; /// <summary> /// Create raw keyevent arguments. /// </summary> /// <param name="VKCode"></param> /// <param name="isSysKey"></param> public RawKeyEventArgs(int VKCode, bool isSysKey) { this.VKCode = VKCode; this.IsSysKey = isSysKey; this.Key = System.Windows.Input.KeyInterop.KeyFromVirtualKey(VKCode); } } /// <summary> /// Raw keyevent handler. /// </summary> /// <param name="sender">sender</param> /// <param name="args">raw keyevent arguments</param> public delegate void RawKeyEventHandler(object sender, RawKeyEventArgs args); #region WINAPI Helper class /// <summary> /// Winapi Key interception helper class. /// </summary> internal static class InterceptKeys { public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); public static int WH_KEYBOARD_LL = 13; public enum KeyEvent : int { WM_KEYDOWN = 256, WM_KEYUP = 257, WM_SYSKEYUP = 261, WM_SYSKEYDOWN = 260 } public static IntPtr SetHook(LowLevelKeyboardProc proc) { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); } } [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr GetModuleHandle(string lpModuleName); } #endregion }
um den hook nun zu verwenden muss in der App.xaml noch zwei events hinzugefuegt werden:
Startup=“Application_Startup“
Exit=“Application_Exit“
<Application x:Class="global_key_hook.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml" Startup="Application_Startup" Exit="Application_Exit" > <Application.Resources> </Application.Resources> </Application>
und in die App.xaml.cs sollte ungefaer so aussehen:
using System.Data; using System.Linq; using System.Windows; using GlobalKeyHook.Keyboard; namespace global_key_hook { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { KeyboardListener KListener = new KeyboardListener(); private void Application_Startup(object sender, StartupEventArgs e) { KListener.KeyDown += new RawKeyEventHandler(KListener_KeyDown); } void KListener_KeyDown(object sender, RawKeyEventArgs args) { MessageBox.Show(args.Key.ToString()); } private void Application_Exit(object sender, ExitEventArgs e) { KListener.Dispose(); } } }
nun kann man mit F5 das debugging starten und mann wird festellen das jeder gedrueckte key in einer MessageBox angezeigt wird und Fertig. so einfach kann es sein ^^
ein wenig tricky wird es noch wenn man versucht die keys an z.B. ein fenster zu senden. Da der KeyEvent in einem thread laeuft. muss man als erst einen Dispatcher aufrufen. in der Funktion KListener_KeyDown schlagen die key events auf. Ich habe in meinem MainWindow eine textbox hinzugefuegt in die ich die keys schreibe mit:
this.Dispatcher.BeginInvoke((System.Threading.ThreadStart)delegate { ((MainWindow)this.MainWindow).textBox1.Text += args.Key.ToString() + @" "; });
so das sollte erklaeren wie das ganze funktioniert. falls ich mich irgendwo vertahn habe oder ihr noch irgendwelche anregungen habt bitte Hinterlast mir ein kommentar. auser es geht um rechtschreibung in diesem fall ist jeder versuch zwecklos 😀
Ach ja ist nich so als waer das ganze auf meinem mist gewachse. Hier der link zum orginal(english)
Hi,
nette Sache. Ich wollt es verwenden, um Shortcuts zu definieren. Dazu mach ich folgendes, dass ich die Keys beim KeyDown in eine Liste eintrage (nicht doppelt) und beim KeyUp sozusagen die Tastenkombination (aus mehreren Tasten) auslese. Dann lösch ich im gleichen Zug die Liste wieder, damit es von vorn losgehen kann.
Nun hab ich das Problem, dass ich beim ersten KeyUp richtiger Weise noch alle Keys in der Liste hab. Aber das KeyUp wird ja für jede Taste gefeuert. Komischer Weise ist die Liste nach dem ersten KeyUp nicht leer, sondern hat noch genauso viele Elemente.
Ist schwer zu erklären. Ziel ist es, dass der Nutzer zB eine Tastenkombination per Drücken festlegen kann und wenn er die Tasten los lässt, soll diese gespeichert werden.
Hier mal Code. 🙂
private static List keySet = new List();
KeyboardListener KListener = new KeyboardListener();
private void Application_Startup(object sender, StartupEventArgs e)
{
KListener.KeyDown += new RawKeyEventHandler(KListener_KeyDown);
KListener.KeyUp += new RawKeyEventHandler(KListener_KeyUp);
}
void KListener_KeyDown(object sender, RawKeyEventArgs e)
{
if (!keySet.Contains(e.Key))
keySet.Add(e.Key);
}
void KListener_KeyUp(object sender, RawKeyEventArgs e)
{
if (keySet.Count != 0)
{
MessageBox.Show(keySet.Count().ToString());
keySet.Clear();
}
}
der „fehler“ ist ganz einfach behoben.
und zwar ist der ausloeser fuer dieses problem der MessageBox.Show befehl. in seinem natuerlichen umfeld unterbricht MessageBox.Show den ausgefuerten code um auf den user input zu warten. Damit wenn zum beispiel eine ja/nein abfrage gesendet wird dann im code entschieden wird welche aktion ausgefuert werden soll.
in diesem fall verhindert es das der keySet.Clear(); ausgefuert wird (zumindest so lange biss man auf „ok“ drueckt) und somit sind die eintraege noch vorhanden wenn beide tasten losgelassen werden.
Das heist sobald du den code für die Aktion die ausgefuert werden soll erstellst wird dieser fehler nicht mehr auftreten.
ich hab in meinem test den output an eine textbox im window1 gesendet. dann sendet er nur einmal eine 2 und dan nix mehr so wies sein soll 🙂
hier der code:
void KListener_KeyUp(object sender, RawKeyEventArgs e)
{
if (keySet.Count != 0)
{
string keycount = keySet.Count().ToString();
this.Dispatcher.BeginInvoke((System.Threading.ThreadStart)delegate
{
((Window1)this.MainWindow).bla.Text += keycount + @“
„;
});
//MessageBox.Show(keySet.Count().ToString());
keySet.Clear();
}
}
Howdy! I basically would like to give a huge thumbs up for the good data you’ve got here on this post. I will probably be coming once again to your weblog for far more soon. eageddgekeck
Absolut top. ich danke dir. Die Stelle mit dem Dispatcher ist die Lösung, die ich gesucht habe.
Schade dass das im Original nicht drin steht.. oder nicht so offensichtlich
Danke nochmals.
das läuft prima unter WPF, Windows 10