1

I am trying to get this keyboard hook to echo registered keys to a Label when pressed. My goal is to do this while the Window is out of focus.

This program is only looking for the keys*: w, a, s, d


The issue I'm running into is that I can't type the letters w, a, s, d in any other programs (example: notepad) while this program is running.


Here is my Form class:

Form1.cs:

public partial class Form1 : Form
{
    private KeyHandler _keyHandler;
    private List<Keys> keys = new List<Keys>() { Keys.W, Keys.A, Keys.S, Keys.D };

    public Form1()
    {
        InitializeComponent();

        foreach (var key in keys) (_keyHandler = new KeyHandler(key, this)).Register();
    }

    private void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        label1.Text = e.KeyCode.ToString();
    }
    
    // Key Logger =======================
    private void HandleHotkey(int keyPress)
    {
        if (keyPress == 5701632)
            label1.Text = "W pressed";
        else if (keyPress == 4259840)
            label1.Text = "A pressed";
        else if (keyPress == 5439488)
            label1.Text = "S pressed";
        else if (keyPress == 4456448)
            label1.Text = "D pressed";
    }
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == Constants.WM_HOTKEY_MSG_ID)
            HandleHotkey(m.LParam.ToInt32());
        base.WndProc(ref m);
    }
}

and the class it's using to register keys:

KeyHandler.cs:

public static class Constants
{
    //windows message id for hotkey
    public const int WM_HOTKEY_MSG_ID = 0x0312;
}

public class KeyHandler
{
    [DllImport("user32.dll")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);

    [DllImport("user32.dll")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);

    private int key;
    private IntPtr hWnd;
    private int id;

    public KeyHandler(Keys key, Form form)
    {
        this.key = (int)key;
        this.hWnd = form.Handle;
        id = this.GetHashCode();
    }

    public override int GetHashCode()
    {
        return key ^ hWnd.ToInt32();
    }

    public bool Register()
    {
        return RegisterHotKey(hWnd, id, 0, key);
    }

    public bool Unregiser()
    {
        return UnregisterHotKey(hWnd, id);
    }
}

I'll take all suggestions/answers. Thank you!


Update 1

I was able to use what Jimi suggested, GetAsyncKeyState(). While this does not interfere with typing in other windows, it does not update my application window until it is back in focus.

Form1.cs

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void HandleHotkey(Keys key)
        {
            label1.Text = key.ToString() + " done";
        }

        protected override void WndProc(ref Message m)
        {
            KeyState keyState = new KeyState();
            if (keyState.GetKeyState(Keys.W) == 1) HandleHotkey(Keys.W);

            base.WndProc(ref m);
        }
    }

KeyState.cs

    public class KeyState
    {
        [DllImport("user32.dll")]
        public static extern short GetAsyncKeyState(Keys key);

        public int GetKeyState(Keys key)
        {
            return GetAsyncKeyState(key);
        }
    }
bocodes
  • 387
  • 1
  • 4
  • 12
  • Issue #2 has been solved by using `m.LParam` values in a `switch` method – bocodes Jan 22 '21 at 22:53
  • 1
    In your code, you set the HotKey ID as `id = this.GetHashCode();` in your handler class. You should probably use a base ID, increment for each HotKey you add and store that ID in your Handler class. Instead, you overwrite a single handler object each time you create a new HotKey. -- In LParam, the HotKey ID is `int hotKeyID = m.WParam.ToInt32();`, the Key pressed is `var keyPressed = (Keys)(m.LParam.ToInt32() >> 16);` and the Modifiers `var modifier = (KeyModifier)(m.LParam.ToInt32() & 0xFF);`. – Jimi Jan 22 '21 at 23:20
  • HotKeys are global, processed before the Keyborad input, so you actively remove the Key from the standard Input. Maybe you need [GetAsyncKeyState()](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate) instead? -- BTW, you should really post your code here – Jimi Jan 22 '21 at 23:20
  • I think you're consuming all the messages, so that no other app gets them –  Jan 23 '21 at 01:37
  • @EricSchneider you’re completely right. I just don’t know how to keep it from doing that – bocodes Jan 23 '21 at 04:34
  • 2
    To get the keystrokes when your window is inactive you probably need to register a WH_KEYBOARD_LL this should not consume message – mrogal.ski Jan 23 '21 at 17:16
  • @Mateusz good idea. Im testing a bit with GetAsyncKeyState() but I will keep this in mind! – bocodes Jan 23 '21 at 17:42
  • @Jimi This has been updated with the GetAsyncKeyState() method. – bocodes Jan 23 '21 at 19:21
  • I tried to dig for it, but couldn't find info. Thought it might be a ",Handled = false;" thing but there seems to be no property or event for it. –  Jan 26 '21 at 00:31

1 Answers1

2

Solution

With the help of Jimi and this stack overflow form, I was able to solve the problem I had.

By using the method GetAsyncKeyState() and a timer, I was able to constantly check if the keys w, a, s, d were being pressed down.


Form1.cs

    public partial class Form1 : Form
    {
        [DllImport("user32.dll")]
        public static extern short GetAsyncKeyState(Keys key);

        public Form1()
        {
            InitializeComponent();
            StartTimer(timer1_Tick);
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (GetAsyncKeyState(Keys.W) <= -32767) label1.Text = Keys.W + " special";
            else if (GetAsyncKeyState(Keys.A) <= -32767) label1.Text = Keys.A + " special";
            else if (GetAsyncKeyState(Keys.S) <= -32767) label1.Text = Keys.S + " special";
            else if (GetAsyncKeyState(Keys.D) <= -32767) label1.Text = Keys.D + " special";

            else label1.Text = "Nothing Pressed";
        }

        private void StartTimer(EventHandler eventHandler)
        {
            timer1.Tick += new EventHandler(eventHandler);
            timer1.Interval = 35;
            timer1.Start();
        }
    }

Add this to Form1.Designer.cs

public System.Windows.Forms.Timer timer1;
bocodes
  • 387
  • 1
  • 4
  • 12
  • `GetAsyncKeyState()` doesn't return a `bool`, it returns a `SHORT` (`short`). -- A `16ms` interval is too low for the WinForms Timer, don't set it below `35ms` (the *official* maximum resolution is `55ms`). – Jimi Jan 23 '21 at 22:51
  • @Jimi I agree with the 35ms timer. The `GetAsyncKeyState()` however does return a bool if needed. I tested this with `if (GetAsyncKeyState(Keys.W) == true) label1.Text = GetAsyncKeyState(Keys.W).ToString();` and it printed `True` on the screen. I believe there's a good reason to why you say to return a short so please let me know. – bocodes Jan 23 '21 at 23:29
  • No, it doesn't, it returns a `SHORT` where the `MSB` and `LSB` has specific meaning. You shouldn't marshal a value for what it's not, it may get funky, usually with bad timing :) – Jimi Jan 24 '21 at 00:22
  • @Jimi updated my if statements to check if the key is `<= -32767` since that's the value it returns when pressed. I used the `<=` since for half a second the value goes to `-32768` – bocodes Jan 24 '21 at 00:43
  • 1
    More or less. You can check whether the value returned by `GetAsyncKeyState()` has the `MSB` set (as in `unchecked((short)0b1000_0000_0000_0001)` which is of course `-32767`). In the answer you linked, the `MSB` is shifted 15 positions to the right (`((keyState >> 15) & 0x0001) == 0x0001`) and compared to `1`, to see whether the `MSB` is set. See the Docs about it. – Jimi Jan 24 '21 at 01:06