首页 > 解决方案 > 如何在 C# 中停止进一步处理全局热键

问题描述

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

但是...当我在例如 Word 或 Wordpad 中并按下时ALT+H,Word 会显示所有类型的菜单 - 我不想要它,因为我只想让我的应用程序做某事:-)

对于这个相当长的代码,我很抱歉,但我认为它很重要,因此您可以获得完整的概述。我用的地方只有 1 个return (IntPtr)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# 开发人员,因为我在这里开始了我的前几个婴儿步骤 - 那么为什么我会把自己作为第一件事之一投入到全球热键的深水中 ;-) 有没有人可以在这里给出一些提示可能在某个地方有一个愚蠢的错误?

标签: c#.netwinformskeyhook

解决方案


背后的想法是,每当按下 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);
    }

推荐阅读