4

Background:

I have a couple of rather complex C# gui applications with controls nested inside of controls inside of controls, which require hotkeys to be handled globally (i.e., there needs to be a top-level handler that can catch the key presses regardless of where the focus is).

My requirements are probably a bit out of the ordinary here, because some of the hotkeys are commonly pressed keys like letters, or even the space bar. When a key like the space bar is pressed, obviously there will be certain controls like text boxes which handle it already. In the case where the focused control handles the key, I want to avoid calling the global hotkey handler.

My current solution is to use PreFilterMessage to handle hotkeys globally, and then Inside of the PreFilterMessage call I have code that bypasses the global hotkey if the focused control is known to handle that key. (But, since IsInputKey is protected, I can't ask the control whether it handles the key, so I kind of just have my own messy logic about which controls the hotkeys should be bypassed inside of).

I'm not very happy with the PreFilterMessage solution, and it seems like their ought to be a more elegant way to do it. Conceptually, the behaviour I want is pretty simple. If the focused control handles the KeyDown, then I don't want anything else to handle it. Otherwise, the Parent control should try to handle it, and if that control doesn't handle the key it should try the parent of that, until it gets all the way up to the Form's KeyDown handler.

Question:

Is there a way to set up a KeyDown handler on a Control such that it only receives events if and only if:

  • The Control or one of its descendents is focused, and
  • Either none of the descendent controls has focus, or the focused control does not handle the KeyDown event

I have done as much research as I can on this. I'm aware of PreFilterMessage and Form.KeyPreview, but as far as I can tell, those don't have a clean way of ignoring the key when it's supposed to be handled by some more-specific control, because they get the event before the focused control gets it. What I really want is almost the opposite -- for the form not to get the KeyDown until after the focused control has decided whether or not to handle it.

uglycoyote
  • 1,555
  • 1
  • 19
  • 25
  • http://stackoverflow.com/questions/547172/pass-through-mouse-events-to-parent-control may help! – Parimal Raj Apr 04 '13 at 20:19
  • Hmmm, it's a similar kind of question, but no answers that help there. It's a bit different too -- in that scenario he was just trying to get his tooltip to ignore mouse clicks and pass them to the parent, whereas I've kind of got this situation where I want the child control to handle the key if it wants to, and then pass it to the parent if it doesn't want to handle it. – uglycoyote Apr 04 '13 at 20:22
  • @uglycyote - the only can i can think of is writing wrapper for each control. as you will need to access `OnEvent` variant method. – Parimal Raj Apr 04 '13 at 20:30
  • You mean implementing OnKeyDown in all of the controls, and then calling Parent.OnKeyDown manually? I thought of that but I'm using a pretty wide variety of both in-house and 3rd Party controls in my application, would be pretty painful to have to wrap them all. – uglycoyote Apr 04 '13 at 20:36
  • yes! something like that! – Parimal Raj Apr 04 '13 at 20:39
  • It already works that way, the ProcessCmdKey() virtual method works from the inside out. It isn't exactly clear to me whether that's the appropriate solution. It should be. – Hans Passant Apr 04 '13 at 23:19
  • @HansPassant - looking at the docs it seems like ProcessCmdKey is really only relevant to context menu shortcuts, which is not my situation. You are correct that it does "bubble up" the control hierarchy in the way that I want. Unfortunately, one of my requirements is that some of the hotkeys are single-letter keys, and it throws an execption if you try to do something like "menuItem1.ShortcutKeys = Keys.A", and even if it allowed that it still would not work properly because "A" would not be registered as a "CmdKey" on controls contained within the control owning the context menu. – uglycoyote Apr 05 '13 at 19:58
  • @uglycoyote did you ever find a solution to this? – CoderBrien Apr 06 '22 at 17:47
  • No.. 10 years later and I'm still using the same brittle solution as I described with PreFilterMessage! – uglycoyote Apr 07 '22 at 22:52

1 Answers1

0

You are looking for a keyboard hook.

Create a class called KeyboardHook that looks like this:

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

public sealed class KeyboardHook : IDisposable
{
    // Registers a hot key with Windows.
    [DllImport("user32.dll")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
    // Unregisters the hot key with Windows.
    [DllImport("user32.dll")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
    /// <summary>
    /// Represents the window that is used internally to get the messages.
    /// </summary>
    private class Window : NativeWindow, IDisposable
    {
        private static int WM_HOTKEY = 0X0312;

        public Window()
        {
            // create the handle for the window.
            this.CreateHandle(new CreateParams());
        }

        /// <summary>
        /// Overridden to get the notifications.
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m)
        {
            base.WndProc(ref m);

            // check if we got a hot key pressed.
            if (m.Msg == WM_HOTKEY)
            {
                // get the keys.
                Keys key = (Keys)(((int)m.LParam >> 16) & 0xFFFF);
                ModifierKeys modifier = (ModifierKeys)((int)m.LParam & 0xFFFF);

                // invoke the event to notify the parent.
                if (KeyPressed != null)
                    KeyPressed(this, new KeyPressedEventArgs(modifier, key));
            }
        }

        public event EventHandler<KeyPressedEventArgs> KeyPressed;

        #region IDisposable Members

        public void Dispose()
        {
            this.DestroyHandle();
        }

        #endregion
    }

    private Window _window = new Window();
    private int _currentId;

    public KeyboardHook()
    {
        // register the event of the inner native window.
        _window.KeyPressed += delegate(object sender, KeyPressedEventArgs args)
        {
            if (KeyPressed != null)
                KeyPressed(this, args);
        };
    }

    /// <summary>
    /// Registers a hot key in the system.
    /// </summary>
    /// <param name="modifier">The modifiers that are associated with the hot key.</param>
    /// <param name="key">The key itself that is associated with the hot key.</param>
    public void RegisterHotKey(ModifierKeys modifier, Keys key)
    {
        // increment the counter.
        _currentId = _currentId + 1;

        // register the hot key.
        if (!RegisterHotKey(_window.Handle, _currentId, (uint)modifier, (uint)key))
            throw new InvalidOperationException("Couldn’t register the hot key.");
    }

    /// <summary>
    /// A hot key has been pressed.
    /// </summary>
    public event EventHandler<KeyPressedEventArgs> KeyPressed;

    #region IDisposable Members

    public void Dispose()
    {
        // unregister all the registered hot keys.
        for (int i = _currentId; i > 0; i--)
        {
            UnregisterHotKey(_window.Handle, i);
        }

        // dispose the inner native window.
        _window.Dispose();
    }

    #endregion
}

/// <summary>
/// Event Args for the event that is fired after the hot key has been pressed.
/// </summary>
public class KeyPressedEventArgs : EventArgs
{
    private ModifierKeys _modifier;
    private Keys _key;

    internal KeyPressedEventArgs(ModifierKeys modifier, Keys key)
    {
        _modifier = modifier;
        _key = key;
    }

    public ModifierKeys Modifier
    {
        get { return _modifier; }
    }

    public Keys Key
    {
        get { return _key; }
    }
}

/// <summary>
/// The enumeration of possible modifiers.
/// </summary>
[Flags]
public enum ModifierKeys : uint
{
    Alt = 1,
    Control = 2,
    Shift = 4,
    Win = 8
}

And in your form should look something like this:

    KeyboardHook hook = new KeyboardHook();

    public Form1()
    {
        InitializeComponent();

        // register the event that is fired after the key press.
        hook.KeyPressed +=
        new EventHandler<KeyPressedEventArgs>(hook_KeyPressed);
        // register the control + alt + F12 combination as hot key.
        hook.RegisterHotKey(global::ModifierKeys.Control, Keys.A);
    }

    void hook_KeyPressed(object sender, KeyPressedEventArgs e)
    {
        if (e.Modifier == global::ModifierKeys.Control && e.Key == Keys.A)
        {
             //Some code here when hotkey is pressed.
        }

        if (textBox1 == ActiveControl)
        {
            // if textBox1 is in focus
        }
    }

Some combinations of keys do not work and will throw an exception on the RegisterHotKey(ModifierKeys modifier, Keys key) method.

It seems to work well with Ctrl as a modifier and any letter as the key.

Enjoy.

string.Empty
  • 10,393
  • 4
  • 39
  • 67
  • Thanks for your suggestion. But this solution seems like it would have the same problems as my current solution (which uses IMessageFilter::PreFilterMessage to implement a global hotkey handler). If you registered something like the space key using RegisterHotKey, is there some mechanism for avoiding hook_keyPressed getting called when the focused control already handles the space key in it's KeyDown handler? – uglycoyote Apr 04 '13 at 22:59
  • well you would have to manually check that in the global hook_KeyPressed event. – string.Empty Apr 04 '13 at 23:04
  • yes, that's basically what I'm doing now. My PreFilterMessage function contains a bunch of logic that says "if current focused control is X,Y, or Z", then bypass the global hotkey. But that mechanism is brittle. Seeing as how the KeyDown event allows controls to communicate about whether they handle the key via [KeyPressedEventArgs.Handled](http://msdn.microsoft.com/en-us/library/system.windows.forms.keypresseventargs.handled.aspx), it would be nice if the global hotkey system had some way of using that information rather than me having to guess about what keys are handled – uglycoyote Apr 04 '13 at 23:10
  • I have modified the code slightly, There are ways to see what keys on the kb are down without an event. – string.Empty Apr 04 '13 at 23:18
  • The problem I'm having is, the "if (textBox1 == ActiveControl)" in your sample becomes something complex and buggy in my code. I have a large number of controls in my application, and also have a large collection of different hotkeys. I'm branching on ActiveControl.GetType(). The easiest thing to do inside the 'if', is just return so that if any textbox is focused none of the hotkeys work. But that's overzealous, I really want all the hotkeys to work which can work without being double-mapped to hotkeys that are already handled by in the textbox. – uglycoyote Apr 05 '13 at 00:21
  • this is just an example, you may do it your method. – string.Empty Apr 05 '13 at 05:11