【问题标题】:How to stop further processing global hotkeys in C#如何在 C# 中停止进一步处理全局热键
【发布时间】:2020-09-23 20:32:47
【问题描述】:

我正在尝试创建一个将对全局热键做出反应的 C# 应用程序; ALT+H 然后当我释放 ALT 键时。这实际上工作得很好,但我的问题是,当我的应用程序完成了它应该使用热键做的任何事情时,它应该阻止这个热键被其他应用程序处理。我从 StackOverflow 帖子 Global keyboard capture in C# application 获得了下面的大部分代码,但我在其他地方看到 return (IntPtr)1; 应该能够停止对密钥的进一步处理。

但是...当我在例如Word 或写字板,然后我按ALT+H 然后 Word 会显示各种菜单 - 我不希望它显示,因为我只希望我的应用程序做某事:-)

我很抱歉这段相当长的代码,但我认为它很重要,因此您可以获得完整的概述。我用return (IntPtr)1;的地方只有1个。

我的代码:

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

我不是一个可靠的 C# 开发人员,因为我在这里开始了我的前几个婴儿步骤 - 那么为什么我会把自己作为第一件事之一投入到全局热键的深水中 ;-) 有没有人可以提供一些提示在这里,因为我可能在某个地方犯了一个愚蠢的错误?

【问题讨论】:

  • "但是...当我在例如 Word 中并按 ALT+H 时,Word 会显示所有类型的菜单 - 我不希望它显示,因为我只想让我的应用程序执行某些操作"当您单独按下 Alt 时,Word 会做出反应。在 H 也被击中之前,您不会知道最终的组合是否是 Alt-H。不幸的是,到那时,Word 已经显示了它的菜单。此外,您可以阻止 Alt-H 在您之后到达链中的其他键盘挂钩,但您不知道在您之前的链中有多少人已经处理了它。
  • 我可以理解ALT 最初会显示菜单,但是当我按下H 时,我的应用程序不能释放ALT 键,因此 Word 会认为它从未被按下?我有另一个应用程序,它的工作原理就是这样。如果我在那里重新创建完全相同的设置,那么当我在 Word 中时,它会首先进入 Word 菜单,但是当我按下 H 时,它会再次释放 ALT 并且光标回到Word 中的编辑 - 至少看起来这是正在发生的事情?
  • 修改它,它应该给你想要的:docs.microsoft.com/en-gb/windows/win32/dxtecharts/…。郑重声明,这是一个可怕的功能,它基本上会在您的应用程序运行时破坏其他应用程序。

标签: c# .net winforms keyhook


【解决方案1】:

背后的想法是,每当按下 alt 键时,它就会开始将键吞入一个本地列表。如果我们需要看到的模式已经被看到,它就不会发送,但是对于每隔一个模式,它会按照接收到的顺序再次开始发送密钥。

考试阶段代码:

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

当按下alt+h并释放时,输出:

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

如您所见,Windows 发送了 alt 和 h 键。您提供的代码仅捕获 H 键,因此接收键盘消息的窗口认为按下了 alt。

没有人可以获取并过滤掉之前按下的键,所以我们需要在看到它被按下的时候抓住它。如果下一个键不是 H,我们应该按照我们收到的顺序发送我们已经获取的键。

我已经编写了下面的代码来处理这种情况,但我不能确定它在真正的 Windows 机器上是如何工作的,因为我有一个 osx 操作系统,只是因为我在虚拟机中运行 Windows当我按下时也会改变击键。

如果还不够,您可以等待,我也许可以在我的办公室用真正的 Windows 机器尝试解决。但我认为你明白了想法并自己解决。

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

【讨论】:

  • 键盘对此反应不正确。试试这个确切的场景 => 不要使用这个热键实现 => 转到写字板 => 输入一些文本 => 按(并按住)ALT,然后按 BACKSPACE 撤消您刚刚输入的内容。它会立即撤消。然后尝试使用此热键执行完全相同的操作,您会注意到它在您释放ALT 之前不会做出反应。还有其他类似的例子,在启用此热键时键盘看起来不稳定/错误。
  • 按住ALT 并按下TAB 以浏览Windows 中打开的应用程序,在ALT 发布之前不会显示任何内容 - 很抱歉,但这并不好,并且会不工作;-)
  • 我已根据您遇到的问题编辑了答案。如果您遇到任何意外行为,请尝试并随时更新
  • 这也好不到哪里去。在例如写字板然后输入一些文本,然后按住ALT 并按H。发布ALT 时,我希望它返回到类型模式,但它卡在菜单的某个位置。我不确定这是否是一种可行的方法,因为它看起来很不稳定,而且在我看来,您需要捕获某些东西并以相同的顺序再次发送它是错误的 - 这听起来像是有时会失败的东西?
  • 我只是忘记了一行代码,你可以再试一次看看有什么不同。
猜你喜欢
  • 1970-01-01
  • 2021-11-15
  • 1970-01-01
  • 1970-01-01
  • 2017-12-20
  • 2010-09-10
  • 1970-01-01
  • 2020-02-21
  • 2016-06-04
相关资源
最近更新 更多