web-dev-qa-db-fra.com

Utilisation du raccordement au clavier global (WH_KEYBOARD_LL) dans WPF/C #

J'ai assemblé le code que j'ai trouvé sur Internet moi-même WH_KEYBOARD_LL helper class:

Mettez le code suivant dans certaines de vos bibliothèques utils, laissez-le être 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);
}

Que j'utilise comme ceci:

App.xaml :

<Application ...
    Startup="Application_Startup"
    Exit="Application_Exit">
    ...

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();
    }
}

Le problème est qu'il cesse de fonctionner après avoir appuyé sur les touches un moment . Aucune erreur n'est générée, de sorte que je ne reçois rien après un certain temps. Je ne peux pas trouver un motif solide quand il cesse de fonctionner.

Reproduire ce problème est simple, appuyez sur certaines touches comme un homme fou, généralement en dehors de la fenêtre.

Je soupçonne qu'il y a du mal problème de filetage derrière, quelqu'un a-t-il une idée de comment continuer à fonctionner?


Ce que j'ai déjà essayé:

  1. Remplacer return HookCallbackInner(nCode, wParam, lParam); par quelque chose de simple.
  2. Le remplacer par un appel asynchrone, en essayant de mettre Sleep 5000ms (etc.).

L'appel asynchrone ne l'a pas rendu meilleur, il semble toujours s'arrêter quand l'utilisateur garde une lettre simple pendant un moment.

54
Ciantic

Vous créez votre délégué callback en ligne dans l'appel de la méthode SetHook. Ce délégué finira par faire ramasser les ordures, car vous ne gardez aucune référence à cela. Et une fois que le délégué aura été nettoyé, vous ne recevrez plus de rappels.

Pour éviter cela, vous devez garder une référence au délégué actif tant que le hook est en place (jusqu'à ce que vous appeliez UnhookWindowsHookEx).

17
Mattias S

Le gagnant est: Capture Keyboard Input dans WPF , ce qui suggère de faire:

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

... puis utilisez simplement la propriété Text de l’argument du gestionnaire d’événements:

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

Lors de l'utilisation de points d'accrochage globaux, IIRC, si votre DLL ne revient pas du rappel assez rapidement, vous êtes retiré de la chaîne des rappels. 

Donc, si vous dites que cela fonctionne un peu, mais que vous tapez trop rapidement, le logiciel cesse de fonctionner, je vous suggèrerais peut-être simplement de stocker les clés dans une mémoire et de les relâcher ultérieurement. Par exemple, vous pouvez vérifier le source de certains enregistreurs de frappe car ils utilisent cette même technique. 

Bien que cela ne résolve pas votre problème directement, cela devrait au moins exclure une possibilité.

Avez-vous pensé à utiliser GetAsyncKeyState au lieu d'un crochet global pour enregistrer les frappes au clavier? Pour votre application, cela pourrait être suffisant, il existe de nombreux exemples entièrement mis en œuvre, et était personnellement plus facile à mettre en œuvre.

1
mrduclaw

Je cherchais vraiment cela. Merci d'avoir posté ceci ici.
Maintenant, quand j'ai testé votre code, j'ai trouvé quelques bugs. Le code n'a pas fonctionné au début. Et il ne pouvait pas gérer deux boutons, cliquez i.e .: CTRL + P.
Ce que j'ai changé, ce sont les valeurs ci-dessous:
private void HookCallbackInner to

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
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    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<Key> _keysPressedIntowSecound = new List<Key>();
        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<Key>();
            };
            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();
        }
    }
}

ce code fonctionne à 100% dans Windows 10 pour moi :) J'espère que cette aide

0
Alen.Toma

J'ai utilisé la méthode de Dylan pour accrocher le mot clé global dans l'application WPF et actualiser le crochet après chaque pression sur une touche pour empêcher les événements de cesser de se déclencher après quelques clics. IDK, si c'est une bonne ou une mauvaise pratique mais que le travail soit fait.

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

Détails de la mise en oeuvre ici

0
EvilInside