Using global keyboard hook (WH_KEYBOARD_LL) in WPF / C#
Asked Answered
E

5

61

I stitched together from code I found in internet myself WH_KEYBOARD_LL helper class:

Put the following code to some of your utils libs, let it be 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);
}

Which I use like this:

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

The problem is that it stops working after hitting keys a while. No error is raised what so ever, I just don't get anything to output after a while. I can't find a solid pattern when it stops working.

Reproducing this problem is quiet simple, hit some keys like a mad man, usually outside the window.

I suspect there is some evil threading problem behind, anyone got idea how to keep this working?


What I tried already:

  1. Replacing return HookCallbackInner(nCode, wParam, lParam); with something simple.
  2. Replacing it with asynchronous call, trying to put Sleep 5000ms (etc).

Asynchronous call didn't make it work any better, it seems stop always when user keeps single letter down for a while.

Euphemie answered 28/10, 2009 at 18:52 Comment(7)
can we intercept the key and send a different key instead of the one pressed? For Example pressing a sends the key e .Ionopause
Complete, usable and documented. This is what i like in StackOverflowEdwinaedwine
Yes, indeed it is a new version. I'll add link to body of text.Euphemie
I have a problem with this code and a USB card reader. Sometimes keys are not shifted right. In the same session I can read one time %WHATEVERò1234_ and the next 5WHAteVERò!"34_ (where ò should be ; and _ should be ?, but this depends on my keyboard). Any help before I write my own virtual key parser?Edwinaedwine
Unsure what has happened here. Matt's revisions have reverted it back to a version that is still affected by the garbage collection. Check the earlier revisions; 15 for Ciantic's last, or 16 for Matt's first, which I tend to prefer the naming on. 17 and 18 are the broken versions.Amalburga
My understanding is that Dispose is meant to be idempotent, that it could get called multiple times on a given object. I recommend altering the Dispose implementation to a) only make the call if hookId is not IntPtr.Zero, and b) set hookId to IntPtr.Zero after making the call.Tm
gist.github.com/Ciantic/471698Ringhals
H
22

You're creating your callback delegate inline in the SetHook method call. That delegate will eventually get garbage collected, since you're not keeping a reference to it anywhere. And once the delegate is garbage collected, you will not get any more callbacks.

To prevent that, you need to keep a reference to the delegate alive as long as the hook is in place (until you call UnhookWindowsHookEx).

Homosexual answered 30/10, 2009 at 10:2 Comment(3)
Yes! You are totally right, I will put the fix to the question, since someone else might come here ponder, hopefully you don't mind...Euphemie
can we intercept the key and send a different key instead of the one pressed? For Example pressing a sends the key e .Ionopause
I dont know why, but this does not seem to work in debug mode on Microsoft Visual Studio 2010. any ideas?Dwayne
N
4

I have used the Dylan's method to hook global keyword in WPF application and refresh hook after each key press to prevent events stop firing after few clicks . IDK, if it is good or bad practice but gets the job done.

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

Implementation details here

Nozzle answered 28/10, 2009 at 18:52 Comment(0)
I
4

The winner is: Capture Keyboard Input in WPF, which suggests doing :

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

...and then simply use the event handler argument’s Text property:

private void OnTextComposition(object sender, TextCompositionEventArgs e)
{
    string key = e.Text;
    ...
}
Ib answered 28/10, 2009 at 18:52 Comment(1)
Works only when window is focused.Bleak
A
3

IIRC, when using global hooks, if your DLL isn't returning from the callback quick enough, you're removed from the chain of call-backs.

So if you're saying that its working for a bit but if you type too quickly it stops working, I might suggest just storing the keys to some spot in memory and the dumping the keys later. For an example, you might check the source for some keyloggers since they use this same technique.

While this may not solve your problem directly, it should at least rule out one possibility.

Have you thought about using GetAsyncKeyState instead of a global hook to log keystrokes? For your application, it might be sufficient, there's lots of fully implemented examples, and was personally easier to implement.

Alboin answered 28/10, 2009 at 19:7 Comment(6)
No, this is supposed to be universal snippetter application. GetAsyncKeyState won't do... This should work like keyboard sniffer. Btw, your tone is demeaning, I have done this same thing in C, and it works like meant to be. But I'll take your advice and look into this "quick enough" thing.Euphemie
I apologize if my tone was insulting at all, it wasn't intended to be. I just know that using GetAsyncKeyState as my keylogger was a lot simpler to get right than using a global hook due to threading/chain/storage issues.Alboin
GetAsyncKeyState does not work will in multitasking OSes. It is designed for win3.x apps when no other programs can call GetAsyncKeyState and receive the "recently pressed" bit instead of your application.Sympathetic
@Sheng Jiang, that's a good point. Empirically though, it's often been sufficient for my purposes and many projects that use keyloggers seem to use it, including Metasploit's Meterpreter. The lower memory footprint is also nice.Alboin
I would like to inform you, since it is now working, that I also implemented the asynchronous behavior to it so that the quickness is not a problem... as long as BeginInvoke in my code works (see above code). So far it feels like it works since I tried it with some insane threading sleeps and it keeps working.Euphemie
I've found that GetAsyncKeyState is the ONLY way to properly retrieve complete keystroke information from within a LL callback, particularly the state of shift, control, and alt, when another application is the foreground window. GetKeyboardState will incorrectly report those modifier key values, because it has thread affinities. On the other hand, GetAsyncKeyState does not return toggle values for caps, scroll, and num. I actually do this. Avoid GetKeyboardState altogether. Loop over all keys calling GetAsyncKeyState. Call GetKeyState for just caps, num, and scroll, checking toggle bit.Washday
H
0

I really was looking for this. Thank you for posting this here.
Now, when I tested your code I found a few bugs. The code did not work at first. And it could not handle two buttons click i.e.: CTRL + P.
What I have changed are those values look below:
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();
        }
    }
}

this code work 100% in windows 10 for me :) I hope this help u

Higgs answered 28/10, 2009 at 18:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.