0

I am trying to create a C# application that will react on a global hotkey; ALT+H and then when I release the ALT key. This actually does work very well but my problem is that when my application has done whatever it should do with the hotkey then it should stop this hotkey from being processed by other applications. I got most of my code below from this StackOverflow post, Global keyboard capture in C# application, but I have seen somewhere else that return (IntPtr)1; should be able to stop further processing of the key.

But... when I am in e.g. Word or Wordpad and I press ALT+H then Word shows all kind of menus - I don't want it to as I only want my application to do something :-)

I am sorry for this rather long code but I assume it is important so you can get the full overview. There is only 1 place where I use return (IntPtr)1;.

My code:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Input;
    
namespace MyNewProgram
{
    static class Program
    {
    
        // Enable hotkeys
        // https://stackoverflow.com/a/604417/2028935
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_KEYUP = 0x0101;
        private const int WM_SYSKEYUP = 0x0105;
        private const int VK_SHIFT = 0x10;
        private const int VK_MENU = 0x12;
        private static LowLevelKeyboardProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;

        // Other variables
        public static bool isAltPressedInThisApp = false;

        private static MainWindow MyMainWindow;

        // --------------------------------------------------------------------------------------

        [STAThread] // STAThreadAttribute indicates that the COM threading model for the application is single-threaded apartment, https://stackoverflow.com/a/1361048/2028935

        static void Main()
        {
            // Hook application to keyboard
            _hookID = SetHook(_proc);

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            MyMainWindow = new MainWindow();
            Application.Run(MyMainWindow);

            // Unhook application from keyboard
            UnhookWindowsHookEx(_hookID);
        }

        // --------------------------------------------------------------------------------------
        // Required functions for globally hooking the keyboard

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern short GetKeyState(int keyCode);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

        // --------------------------------------------------------------------------------------
        // Hook the keyboard - action

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            // React on KEYDOWN
            if (nCode >= 0 && ((wParam == (IntPtr)WM_KEYDOWN) || (wParam == (IntPtr)WM_SYSKEYUP)))
            {
                int vkCode = Marshal.ReadInt32(lParam);

                // H
                if ((Keys)vkCode == Keys.H)
                {
                    isAltPressedInThisApp = true;

                    // Is ALT pressed down
                    if ((GetKeyState(VK_MENU) & 0x8000) != 0)
                    {
                        Console.WriteLine("ALT + H");
                        return (IntPtr)1; // do not allow others to hook this key combo
                    }
                }
            }

            // React on KEYUP
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP)
            {
                int vkCode = Marshal.ReadInt32(lParam);

                // Is ALT not pressed down
                if ((Keys)vkCode == Keys.LMenu)
                {
                    Console.WriteLine("ALT UP");
                }
            }

            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        // --------------------------------------------------------------------------------------
        // Hook the keyboard

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

I am not a solid C# developer as I am taking my first few babysteps here - so why would I throw myself in the deep waters of global hotkeys as one of the first things ;-) Is there anyone that can give some hints here as I probably have a silly mistake somewhere?

Beauvais
  • 2,149
  • 4
  • 28
  • 63
  • "But... when I am in e.g. Word and I press ALT+H then Word shows all kind of menus - I don't want it to as I only want my application to do something" Word is reacting to when you press down just Alt by itself. You won't know if the final combo is Alt-H until have the H is hit as well. Unfortunately, by that point, Word has already displayed it's menus. Also, you can stop Alt-H from reaching other keyboard hooks down the chain AFTER you, but you have no idea how many people BEFORE you in the chain already processed it. – Idle_Mind Sep 23 '20 at 20:56
  • I can understand that `ALT` initially will show the menu but when I then press `H`, can't my application then release the `ALT` key so Word will think it has never been pressed? I have another application where it works just like that. If I recreate the exact same setup there, then when I am in Word then it will first go to the Word menu but when I then press `H` then it will act like it releases the `ALT` again and the cursor goes back to the editing in Word - at least it looks like this is what is happening? – Beauvais Sep 23 '20 at 21:04
  • Modify this and it should give you want you want: https://learn.microsoft.com/en-gb/windows/win32/dxtecharts/disabling-shortcut-keys-in-games?redirectedfrom=MSDN. For the record, this is a horrible feature, that basically breaks other applications while yours is running. – Nathan Cooper Sep 24 '20 at 21:09

1 Answers1

2

The idea behind, whenever the alt key pressed, it starts to swallow keys into one local list. if the pattern we need to see has been seen, it won't send, but for the every other pattern, it starts to send the keys again in the order it receives.

the code for examination phase:

    enum WM
    {
        WM_KEYDOWN = 0x0100,
        WM_KEYUP = 0x0101,
        WM_SYSKEYUP = 0x0105,
        WM_SYSKEYDOWN = 0x0104,
    }

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode < 0)
            return CallNextHookEx(_hookID, nCode, wParam, lParam);

        KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
        Enum.TryParse<Keys>($"{kbd.vkCode}", out Keys key);
        Enum.TryParse<WM>($"{wParam}", out WM message);

        Console.WriteLine($"{message}:{key}");

        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }

when pressing alt+h and releasing, output:

WM_SYSKEYDOWN:LMenu
WM_SYSKEYDOWN:H
WM_SYSKEYUP:H
WM_KEYUP:LMenu

as you can see, windows sent both alt and h key. the code you provided only catch H key, so the window which receives keyboard messages think that alt pressed.

no one can fetch and filter out previously pressed key, so we need to catch alt key whenever we see it is pressed. if the next key not H, we should send the keys we have fetched in the order we received.

i have written the below code to handle the situation, but i can not be pretty sure how it works on the real windows machine cause i have an osx operating system just because of that i am running windows in the virtual machine which is also change key strokes when i'm pressing.

if it's still not enough, you can wait and i may be able to try and figure it out at my office in the real windows machine. but i think you get the idea and work it out on your own.

    [StructLayout(LayoutKind.Sequential)]
    public class KBDLLHOOKSTRUCT
    {
        public uint vkCode;
        public uint scanCode;
        public KBDLLHOOKSTRUCTFlags flags;
        public uint time;
        public UIntPtr dwExtraInfo;
    }

    [Flags]
    public enum KBDLLHOOKSTRUCTFlags : uint
    {
        LLKHF_EXTENDED = 0x01,
        LLKHF_INJECTED = 0x10,
        LLKHF_ALTDOWN = 0x20,
        LLKHF_UP = 0x80,
    }
    [DllImport("user32.dll")]
    static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);

    enum WM
    {
        WM_KEYDOWN = 0x0100,
        WM_KEYUP = 0x0101,
        WM_SYSKEYUP = 0x0105,
        WM_SYSKEYDOWN = 0x0104,
    }

    private static void ReSendKeys(int index = -1)
    {
        _index = -1;

        var copiedKeys = _swallowedKeys.ToArray();
        for (int i = 0; i < copiedKeys.Length; ++i)
        {
            bool up = copiedKeys[i].Item1 == (IntPtr)WM.WM_SYSKEYUP || copiedKeys[i].Item1 == (IntPtr)WM.WM_KEYUP;
            keybd_event((byte)copiedKeys[i].Item2.vkCode, (byte)copiedKeys[i].Item2.scanCode, up ? 2u : 0u, UIntPtr.Zero);
        }

        _index = index;
        _swallowedKeys.Clear();
    }

    private static List<Tuple<IntPtr, KBDLLHOOKSTRUCT>> _swallowedKeys = new List<Tuple<IntPtr, KBDLLHOOKSTRUCT>>();

    private static int _index = 0;
    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode < 0)
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        
        KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
        Enum.TryParse<Keys>($"{kbd.vkCode}", out Keys key);
        Enum.TryParse<WM>($"{wParam}", out WM message);

        Console.Write($"{message}:{key}");

        // we know that when _index is -1, ReSendKeys function has been called
        // so do not filter out first alt key
        if (_index == -1)
        {
            _index++;
            Console.WriteLine();
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        // we are at the beginning of the sequence we will catch
        // if it's alt key filter it out, and increment the variable
        if (_index == 0)
        {
            _swallowedKeys.Add(new Tuple<IntPtr, KBDLLHOOKSTRUCT>(wParam, kbd));

            if (message == WM.WM_SYSKEYDOWN && key == Keys.LMenu)
            {
                _index++;

                Console.WriteLine(" filtered out");
                return (IntPtr)1;
            }
            else
            {
                _swallowedKeys.Clear();
                // do nothing
            }
        }

        if (_index == 1)
        {
            _swallowedKeys.Add(new Tuple<IntPtr, KBDLLHOOKSTRUCT>(wParam, kbd));

            // if the next key is H, then filter it out also
            if (message == WM.WM_SYSKEYDOWN && key == Keys.H)
            {
                _index++;
                _swallowedKeys.RemoveAt(_swallowedKeys.Count - 1);

                Console.WriteLine(" filtered out");
                return (IntPtr)1;
            }
            // if not, we filtered out wrong sequence, we need to resend them
            else
            {
                Console.WriteLine();
                ReSendKeys();
                return (IntPtr)1;
            }
        }

        if (_index == 2)
        {
            _swallowedKeys.Add(new Tuple<IntPtr, KBDLLHOOKSTRUCT>(wParam, kbd));

            if (message == WM.WM_SYSKEYUP && key == Keys.H)
            {
                _index++;
                _swallowedKeys.RemoveAt(_swallowedKeys.Count - 1);

                Console.WriteLine(" filtered out");
                return (IntPtr)1;
            }
            else
            {
                // if user pressed H but not released and pressed another key at the same time
                // i will pass that situation, if u need to handle something like that, you got the idea, please fill that block of code
            }
        }

        if (_index == 3)
        {
            _swallowedKeys.Add(new Tuple<IntPtr, KBDLLHOOKSTRUCT>(wParam, kbd));

            if (message == WM.WM_KEYUP && key == Keys.LMenu)
            {
                _index = 0;
                _swallowedKeys.Clear();

                Console.WriteLine(" filtered out");
                Console.WriteLine("shortcut disabled");
                return (IntPtr)1;
            }
            else
            {
                Console.WriteLine();
                // user has been pressed Alt + H, H released but not alt, so we can expect one H again, but we need to send this pressed key with Alt prefixed
                ReSendKeys(1);
                return (IntPtr)1;
            }
        }

        Console.WriteLine();
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
Gurhan Polat
  • 696
  • 5
  • 12
  • The keyboard does not react correctly with this. Try this exact scenario => Do not use this hotkey implementation => Go to Wordpad => Type in some text => Press (and keep holding down) `ALT` and then press `BACKSPACE` for undo what you just typed. Immediately it will undo. Then try and do the exact same thing with this hotkey implemented and you will notice that it does not react until you release `ALT`. There are other similar examples where the keyboard seems flaky/wrong with this hotkey enabled. – Beauvais Sep 24 '20 at 07:46
  • Holding down `ALT` and pressing `TAB`, to go through the open applications in Windows, will not show anything until `ALT` has been released - sorry to say but this is no good and will not work ;-) – Beauvais Sep 24 '20 at 08:00
  • i've edited the answer according to the problems you have been faced. please try that and feel free to update if you encounter any unexpected behaviours – Gurhan Polat Sep 24 '20 at 15:13
  • This is not any better. In e.g. Wordpad then type in some text and then hold down `ALT` and press `H`. When releasing `ALT` I would expect it to go back to type-mode but it is stuck somewhere in the menu. I am unsure if this is a viable way to go as it seems so flaky and it seems wrong to me that you need to catch something and send it back again in the same order - this sounds like something that will fail from time to time? – Beauvais Sep 24 '20 at 15:38
  • i have just forget one line of code, you may be try again to see the difference. – Gurhan Polat Sep 24 '20 at 16:16
  • i have tried lots of ways, but if anyone would not catch the first alt, alt seems pressed, so we must catch first alt. last change will solve your problem. – Gurhan Polat Sep 24 '20 at 16:18
  • Now this works but I guess I can keep finding difficulties with this approach the more I use it. E.g. go to Chrome (without the hotkey active) and press `ALT+F` and then release `ALT`. This will show the Chrome menu. Then you can press/release `ALT` again and the Chrome menu will disappear. If you do the same with the hotkey active (not pressing the global hotkey) then the Chrome menu will never disappear from Chrome - you can press/release `ALT` as many times you want but menu does not disappear. This looks for me like it is doing an unnecessary hijack of the keyboard that is too intrusive. – Beauvais Sep 24 '20 at 16:44
  • Ok, so if I come up with a different solution later, I will let you know. But some more if else statement will solve all these problems, but you don’t like this approach so I won’t be continuing – Gurhan Polat Sep 24 '20 at 18:32
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222035/discussion-between-beauvais-and-gurhan-polat). – Beauvais Sep 24 '20 at 19:02
  • I have noticed that the keyboard is not hooked when I am in a privileged application - e.g. an administrative command prompt or if I have started Task Manager and this is the active application. Then it does not react on e.g. ALT+H? Will this be possible without having my application running with highest privileges also, which I see as unnecessary? – Beauvais Oct 15 '20 at 11:48