Usando o gancho de teclado global (WH_KEYBOARD_LL) no WPF / C #

Eu costurei juntos a partir do código que eu encontrei na internet: WH_KEYBOARD_LL helper class:

Coloque o seguinte código em algumas das suas bibliotecas de utils, deixe-o ser YourUtils.cs :

 using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Windows.Input; namespace MYCOMPANYHERE.WPF.KeyboardHelper { public class KeyboardListener : IDisposable { private static IntPtr hookId = IntPtr.Zero; [MethodImpl(MethodImplOptions.NoInlining)] private IntPtr HookCallback( int nCode, IntPtr wParam, IntPtr lParam) { try { return HookCallbackInner(nCode, wParam, lParam); } catch { Console.WriteLine("There was some error somewhere..."); } return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); } private IntPtr HookCallbackInner(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { if (wParam == (IntPtr)InterceptKeys.WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); if (KeyDown != null) KeyDown(this, new RawKeyEventArgs(vkCode, false)); } else if (wParam == (IntPtr)InterceptKeys.WM_KEYUP) { int vkCode = Marshal.ReadInt32(lParam); if (KeyUp != null) KeyUp(this, new RawKeyEventArgs(vkCode, false)); } } return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); } public event RawKeyEventHandler KeyDown; public event RawKeyEventHandler KeyUp; public KeyboardListener() { hookId = InterceptKeys.SetHook((InterceptKeys.LowLevelKeyboardProc)HookCallback); } ~KeyboardListener() { Dispose(); } #region IDisposable Members public void Dispose() { InterceptKeys.UnhookWindowsHookEx(hookId); } #endregion } internal static class InterceptKeys { public delegate IntPtr LowLevelKeyboardProc( int nCode, IntPtr wParam, IntPtr lParam); public static int WH_KEYBOARD_LL = 13; public static int WM_KEYDOWN = 0x0100; public static int WM_KEYUP = 0x0101; 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, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr GetModuleHandle(string lpModuleName); } public class RawKeyEventArgs : EventArgs { public int VKCode; public Key Key; public bool IsSysKey; public RawKeyEventArgs(int VKCode, bool isSysKey) { this.VKCode = VKCode; this.IsSysKey = isSysKey; this.Key = System.Windows.Input.KeyInterop.KeyFromVirtualKey(VKCode); } } public delegate void RawKeyEventHandler(object sender, RawKeyEventArgs args); } 

Qual eu uso assim:

App.xaml :

  ... 

App.xaml.cs :

 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) { Console.WriteLine(args.Key.ToString()); // I tried writing the data in file here also, to make sure the problem is not in Console.WriteLine } private void Application_Exit(object sender, ExitEventArgs e) { KListener.Dispose(); } } 

O problema é que ele pára de funcionar depois de pressionar as teclas por algum tempo . Nenhum erro é levantado, de modo que nunca consigo obter nada após um tempo. Não consigo encontrar um padrão sólido quando ele para de funcionar.

Reproduzir este problema é simples e silencioso, acertar algumas teclas como um homem louco, geralmente fora da janela.

Eu suspeito que há algum problema de encadeamento mal atrás, alguém tem idéia de como manter isso funcionando?


O que eu já tentei:

  1. Substituindo return HookCallbackInner(nCode, wParam, lParam); com algo simples.
  2. Substituindo-o por chamada assíncrona, tentando colocar Sleep 5000ms (etc).

Chamada assíncrona não fez o trabalho melhor, parece parar sempre quando o usuário mantém a letra solta por um tempo.

Você está criando seu delegado de retorno de chamada em linha na chamada do método SetHook. Esse delegado acabará recebendo o lixo coletado, já que você não está mantendo uma referência a ele em qualquer lugar. E quando o delegado for coletado como lixo, você não receberá mais retornos de chamada.

Para evitar isso, você precisa manter uma referência ao delegado ativo enquanto o gancho estiver no lugar (até você chamar UnhookWindowsHookEx).

O vencedor é: Capture Input do Teclado no WPF , o que sugere fazer:

 TextCompositionManager.AddTextInputHandler(this, new TextCompositionEventHandler(OnTextComposition)); 

… e simplesmente use a propriedade Text do argumento do manipulador de events:

 private void OnTextComposition(object sender, TextCompositionEventArgs e) { string key = e.Text; ... } 

IIRC, ao usar ganchos globais, se sua DLL não estiver retornando do retorno de chamada rápido o suficiente, você será removido da cadeia de retornos de chamada.

Então, se você está dizendo que está funcionando um pouco, mas se você digitar muito rapidamente, ele pára de funcionar, eu poderia sugerir apenas armazenar as chaves em algum lugar na memory e despejar as chaves mais tarde. Por exemplo, você pode verificar a origem de alguns keyloggers, já que eles usam essa mesma técnica.

Embora isso possa não resolver seu problema diretamente, deve pelo menos descartar uma possibilidade.

Você já pensou em usar GetAsyncKeyState vez de um gancho global para registrar as teclas digitadas? Para sua aplicação, pode ser suficiente, há muitos exemplos totalmente implementados e foi pessoalmente mais fácil de implementar.

Eu realmente estava procurando por isso. Obrigado por postar isso aqui.
Agora, quando testei seu código, encontrei alguns bugs. O código não funcionou no começo. E não poderia lidar com dois botões, por exemplo: CTRL + P.
O que eu mudei são esses valores abaixo:
private void HookCallbackInner para

 private void HookCallbackInner(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { if (wParam == (IntPtr)InterceptKeys.WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); if (KeyDown != null) KeyDown(this, new RawKeyEventArgs(vkCode, false)); } } } using System; using System.Collections.Generic; using System.Windows; using System.Windows.Input; using System.Windows.Threading; using FileManagerLibrary.Objects; namespace FileCommandManager { ///  /// Interaction logic for App.xaml ///  public partial class App : Application { readonly KeyboardListener _kListener = new KeyboardListener(); private DispatcherTimer tm; private void Application_Startup(object sender, StartupEventArgs e) { _kListener.KeyDown += new RawKeyEventHandler(KListener_KeyDown); } private List _keysPressedIntowSecound = new List(); private void TmBind() { tm = new DispatcherTimer(); tm.Interval = new TimeSpan(0, 0, 2); tm.IsEnabled = true; tm.Tick += delegate(object sender, EventArgs args) { tm.Stop(); tm.IsEnabled = false; _keysPressedIntowSecound = new List(); }; tm.Start(); } void KListener_KeyDown(object sender, RawKeyEventArgs args) { var text = args.Key.ToString(); var m = args; _keysPressedIntowSecound.Add(args.Key); if (tm == null || !tm.IsEnabled) TmBind(); } private void Application_Exit(object sender, ExitEventArgs e) { _kListener.Dispose(); } } } 

este código funciona 100% no windows 10 para mim 🙂 Espero que isso ajude você

Eu tenho usado o método do Dylan para ligar palavra-chave global no aplicativo WPF e atualizar o gancho após cada pressionamento de tecla para evitar que os events parem de triggersr após alguns cliques. IDK, se for uma prática boa ou ruim, mas faz o trabalho.

  _listener.UnHookKeyboard(); _listener.HookKeyboard(); 

Detalhes de implementação aqui