Использование глобальной клавиатуры (WH_KEYBOARD_LL) в WPF / C#

Я соединил воедино из кода, который я нашел в интернете сам WH_KEYBOARD_LL вспомогательный класс:

Поместите следующий код в некоторые из ваших библиотек утилит, пусть это будет 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);
}

Который я использую так:

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

Проблема в том, что он перестает работать после нажатия клавиш. Никаких ошибок не возникает, но я просто ничего не получаю через некоторое время. Я не могу найти твердый образец, когда он перестает работать.

Воспроизвести эту проблему очень просто, нажимайте на клавиши, как на сумасшедшего, обычно за окном.

Я подозреваю, что есть какая-то злая проблема с многопоточностью, у кого-нибудь есть идеи, как сохранить эту работу?


Что я уже пробовал:

  1. Замена return HookCallbackInner(nCode, wParam, lParam); с чем-то простым.
  2. Замена его асинхронным вызовом, попытка поставить Sleep 5000ms (и т. Д.).

Асинхронный вызов не улучшил его работу, кажется, он останавливается всегда, когда пользователь некоторое время удерживает одну букву.

5 ответов

Решение

Вы создаете свой делегат обратного вызова, встроенный в вызов метода SetHook. Этот делегат в конечном итоге будет собирать мусор, поскольку вы нигде не будете на него ссылаться. И после того, как делегат собран мусором, вы не получите больше обратных вызовов.

Чтобы предотвратить это, вам нужно поддерживать ссылку на делегат до тех пор, пока хук находится на месте (пока вы не вызовете UnhookWindowsHookEx).

Победитель: Capture Keyboard Input в WPF, который предлагает сделать:

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

... а затем просто используйте свойство Text аргумента обработчика события:

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

Я использовал метод Дилана для перехвата глобального ключевого слова в приложении WPF и обновления хука после каждого нажатия клавиши, чтобы предотвратить прекращение запуска событий после нескольких щелчков. ИДК, если это хорошая или плохая практика, но она выполняет свою работу.

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

Подробности реализации здесь

IIRC, при использовании глобальных хуков, если ваша DLL не возвращается из обратного вызова достаточно быстро, вы удаляетесь из цепочки обратных вызовов.

Так что, если вы говорите, что он работает некоторое время, но если вы набираете слишком быстро, он перестает работать, я мог бы предложить просто сохранить ключи в некотором месте в памяти и затем сбросить их. Например, вы можете проверить источник для некоторых клавиатурных шпионов, так как они используют эту же технику.

Хотя это может не решить вашу проблему напрямую, это должно как минимум исключить одну возможность.

Вы думали об использовании GetAsyncKeyState вместо глобального хука для регистрации нажатий клавиш? Для вашего приложения этого может быть достаточно, есть много полностью реализованных примеров, и его было проще реализовать.

Я действительно искал это. Спасибо за размещение этого здесь.
Теперь, когда я проверял ваш код, я обнаружил несколько ошибок. Код не работал сначала. И он не может обрабатывать две кнопки, т. Е. CTRL + P.
То, что я изменил, эти значения выглядят ниже:
private void HookCallbackInner в

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

этот код работает на 100% в Windows 10 для меня:) Я надеюсь, что это поможет вам

Другие вопросы по тегам