C# global keyboard hook, that opens a form from a console application [duplicate]
Asked Answered
P

1

9

So I have a C# Console Application with a Form, which I want to open using hotkeys. Let's say for example Ctrl + < opens the form. So I got the code to handle a globalkeylistener now, but it looks like I failed by implementing it. It made a while loop to prevent it from closing the program and I tryed to get an input from the user with the kbh_OnKeyPressed method.

I tryed to implement it this way:

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace Globalkey
{
    static class Program
    {
        [DllImport("user32.dll")]
        private static extern bool RegisterHotkey(int id, uint fsModifiers, uint vk);

        private static bool lctrlKeyPressed;
        private static bool f1KeyPressed;


        [STAThread]
        static void Main()
        {

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            LowLevelKeyboardHook kbh = new LowLevelKeyboardHook();
            kbh.OnKeyPressed += kbh_OnKeyPressed;
            kbh.OnKeyUnpressed += kbh_OnKeyUnpressed;
            kbh.HookKeyboard();

            while(true) { }

        }

        private static void kbh_OnKeyUnpressed(object sender, Keys e)
        {
            if (e == Keys.LControlKey)
            {
                lctrlKeyPressed = false;
                Console.WriteLine("CTRL unpressed");
            }
            else if (e == Keys.F1)
            {
                f1KeyPressed = false;
                Console.WriteLine("F1 unpressed");
            }
        }

        private static void kbh_OnKeyPressed(object sender, Keys e)
        {
            if (e == Keys.LControlKey)
            {
                lctrlKeyPressed = true;
                Console.WriteLine("CTRL pressed");
            }
            else if (e == Keys.F1)
            {
                f1KeyPressed = true;
                Console.WriteLine("F1 pressed");
            }
            CheckKeyCombo();
        }

        static void CheckKeyCombo()
        {
            if (lctrlKeyPressed && f1KeyPressed)
            {
                Application.Run(new Form1());
            }
        }
    }
}
Pompidou answered 2/9, 2017 at 11:25 Comment(4)
Just forgot to say that I want to open it while the program is running, so "ctrl + <" would have to trigger: Application.Run(new Form1());Pompidou
Just catch this key combo in keydown event, and then call the formSpencer
Well it has to be global which means that I can access the hotkey while the program isn't focusedPompidou
Then look no further, SetWindowsHookEx comes to the rescueSpencer
A
16

What you need is a Lowlevel Keyboard Hook.

This could look somewhat like this:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public class LowLevelKeyboardHook
{
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    private const int WM_SYSKEYDOWN = 0x0104;
    private const int WM_KEYUP = 0x101;
    private const int WM_SYSKEYUP = 0x105;

    [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);

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

    public event EventHandler<Keys> OnKeyPressed;
    public event EventHandler<Keys> OnKeyUnpressed;

    private LowLevelKeyboardProc _proc;
    private IntPtr _hookID = IntPtr.Zero;

    public LowLevelKeyboardHook()
    {
        _proc = HookCallback;
    }

    public void HookKeyboard()
    {
        _hookID = SetHook(_proc);
    }

    public void UnHookKeyboard()
    {
        UnhookWindowsHookEx(_hookID);
    }

    private IntPtr SetHook(LowLevelKeyboardProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);

            OnKeyPressed.Invoke(this, ((Keys)vkCode));
        }
        else if(nCode >= 0 && wParam == (IntPtr)WM_KEYUP ||wParam == (IntPtr)WM_SYSKEYUP)
        {
            int vkCode = Marshal.ReadInt32(lParam);

            OnKeyUnpressed.Invoke(this, ((Keys)vkCode));
        }

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

To implement it, you could use something like this:

kbh = new LowLevelKeyboardHook();
kbh.OnKeyPressed += kbh_OnKeyPressed;
kbh.OnKeyUnpressed += kbh_OnKeyUnpressed;
kbh.HookKeyboard();

The event could be handled like that:

bool lctrlKeyPressed;
bool f1KeyPressed;

void kbh_OnKeyPressed(object sender, Keys e)
{
    if (e == Keys.LControlKey)
    {
        lctrlKeyPressed = true;
    }
    else if (e == Keys.F1)
    {
        f1KeyPressed= true;
    }
    CheckKeyCombo();
}

void kbh_OnKeyUnPressed(object sender, Keys e)
{
    if (e == Keys.LControlKey)
    {
        lctrlKeyPressed = false;
    }
    else if (e == Keys.F1)
    {
        f1KeyPressed= false;
    }
}

void CheckKeyCombo()
{
    if (lctrlKeyPressed && f1KeyPressed)
    {
        //Open Form
    }
}

For actual understanding, i would recommend you to have a read on P/Invoke. That is making use of the unmanaged APIs that windows provides.

For a full list of P/Invoke possibilites, pinvoke.net is a great source.

For better understanding in general, The official MSDN Website is a good source, too.

EDIT:

It seems like you're actually using a Console Application, not a WinForm one. In that case, you have to run the program a bit differently:

[STAThread]
static void Main()
{

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    LowLevelKeyboardHook kbh = new LowLevelKeyboardHook();
    kbh.OnKeyPressed += kbh_OnKeyPressed;
    kbh.OnKeyUnpressed += kbh_OnKeyUnpressed;
    kbh.HookKeyboard();

    Application.Run();

    kbh.UnHookKeyboard();

}

The Run() method of the Application Class starts a standard loop for your application. This is necessary for the Hook to work, because a mere Console Application without this loop is, as far as I know, not capable of triggering those global key events.

Using this implementation, pressing and releasing the defined keys gives the following output:

Console Output

Note: I obviously replaced

Application.Run(new Form1());

in the CheckKeyCombo() method with

Console.WriteLine("KeyCombo pressed");

Atterbury answered 2/9, 2017 at 12:54 Comment(8)
Hmm thanks, but how can I handle multiple key inputs with that code?Pompidou
I've extended the event handling code sample to fit your needs. Note, I've used CTRL + F1, because I'm really not sure about the "<" sign in the Keys enum. For a full list, visit msdn.microsoft.com/de-de/library/…Atterbury
Haha it isn't a console applicationPompidou
hmm okay, i was pretty sure because the solution fits console applications :D apologies!Atterbury
How to set hook to another process ?Surveillance
@Surveillance Can you give me a bit more context? Some sample code or anything? Something that can help me understand what you're trying to achieve. :)Atterbury
thank you so much for answering this question. The linked apparent answer is far far below the standard of your answer.Ludwig
Dotnet core doesn't know what the type Keys is / it doesn't know how to importOmega

© 2022 - 2024 — McMap. All rights reserved.