如何将未处理的KeyDown事件传递给父控件?

4

背景:

我有几个相当复杂的C# GUI应用程序,其中包含了控件内嵌至其它控件内部,需要全局处理热键(即,需要一个顶级处理程序,可以捕获无论焦点在哪里都能捕获按键)。我的要求可能有些不同寻常,因为一些热键是常按的键,例如字母或空格键。当按下类似空格键这样的键时,显然已经有某些控件像文本框一样处理了它。在焦点控制处理键的情况下,我希望避免调用全局热键处理程序。

我的当前解决方案是使用PreFilterMessage来全局处理热键,然后在PreFilterMessage调用中,我有绕过全局热键的代码,如果知道聚焦控件处理该键。 (但是,由于IsInputKey是受保护的,我无法询问控件是否处理该键,所以我只能自己在其中混乱地确定哪些控件应该被绕过热键处理)。

我对PreFilterMessage的解决方案不太满意,似乎应该有一种更优雅的方法来解决它。从概念上讲,我想要的行为非常简单。如果聚焦控件处理KeyDown,则我不希望其他任何东西处理它。否则,父控件应该尝试处理它,如果该控件没有处理该键,则应尝试其父控件,直到达到Form的KeyDown处理程序。

问题:

是否有一种方式可以在Control上设置KeyDown处理程序,以便仅在以下情况下接收事件:

  • 该控件或其后代之一具有焦点,并且
  • 没有任何后代控件具有焦点,或聚焦控件未处理KeyDown事件

我已经进行了尽可能多的研究。我知道PreFilterMessageForm.KeyPreview,但据我所知,它们没有干净的方式在某些更具体的控件处理按键时忽略该键,因为它们会在焦点控件之前获取事件。我真正想要的几乎是相反的 - 对于表单在聚焦控件决定是否处理它之后再获取KeyDown。


嗯,这是一类相似的问题,但那里没有任何有用的答案。 这也有点不同--在那种情况下,他只是试图让工具提示忽略鼠标点击并将其传递给父对象,而我有一种情况,即如果子控件希望处理按键,则允许其处理,否则将其传递给父对象。 - uglycoyote
@uglycyote - 我能想到的唯一方法就是为每个控件编写包装器。因为你需要访问 OnEvent 变体方法。 - Parimal Raj
你的意思是在所有控件中实现OnKeyDown,然后手动调用Parent.OnKeyDown吗?我想过这个方法,但我的应用程序使用了各种各样的内部和第三方控件,必须要包装它们,这将非常繁琐。 - uglycoyote
是的!就是那样! - Parimal Raj
它已经按照这种方式工作了,ProcessCmdKey()虚拟方法从内到外运行。我不太清楚这是否是适当的解决方案。但应该是的。 - Hans Passant
显示剩余4条评论
1个回答

0
你正在寻找一个键盘钩子。
创建一个名为KeyboardHook的类,它应该长这样:
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
}

在你的表格中应该长这样:

    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
        }
    }

某些键的组合不起作用,并且会在RegisterHotKey(ModifierKeys modifier, Keys key)方法中抛出异常。

似乎使用Ctrl作为修饰符和任何字母作为键可以正常工作。

享受吧。


感谢您的建议。但是,这个解决方案似乎会遇到与我当前解决方案相同的问题(使用IMessageFilter :: PreFilterMessage实现全局热键处理程序)。如果您使用RegisterHotKey注册了类似空格键的内容,是否有一些机制可以避免在焦点控件已经在其KeyDown处理程序中处理空格键时调用hook_keyPressed? - uglycoyote
你需要在全局 hook_KeyPressed 事件中手动检查。 - string.Empty
是的,基本上这就是我现在正在做的。我的PreFilterMessage函数包含了一些逻辑,它说“如果当前焦点控件是X、Y或Z”,那么就绕过全局热键。但是这种机制很脆弱。考虑到KeyDown事件允许控件通过KeyPressedEventArgs.Handled来通信,以确定它们是否处理该键,如果全局热键系统有一些使用该信息的方法,而不是让我猜测哪些键被处理,那将是很好的。 - uglycoyote
我稍微修改了代码,有方法可以在没有事件的情况下查看键盘上按下的键。 - string.Empty
我遇到的问题是,在你的示例中,“if (textBox1 == ActiveControl)”在我的代码中变得复杂且有错误。我的应用程序中有大量控件,还有大量不同的热键集合。我正在根据ActiveControl.GetType()进行分支。在“if”内部最简单的事情就是返回,以便如果任何文本框被聚焦,则不会使用任何热键。但这太过激进了,我真正想要所有可以在没有被双重映射到文本框中已处理的热键的情况下工作的热键都能够工作。 - uglycoyote
这只是一个例子,您可以用自己的方法来完成。 - string.Empty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接