.net C#应用程序中的全局键盘捕获

.net C#应用程序中的全局键盘捕获,.net,events,keyboard-hook,.net,Events,Keyboard Hook,我想在我的应用程序中捕获一个键盘快捷键,并在用户在应用程序之外按下键盘组合时触发一个对话框。类似于谷歌桌面搜索的Ctrl键,Ctrl键打开搜索对话框 我尝试过使用一些键盘钩子模块,这些模块基本上使用Win32 interop来获得这种效果,但我尝试过的每个实现都会在一定程度上限制键盘,当应用程序执行某些密集型操作时,会出现奇怪的行为。例如加载大量数据,这将导致键盘和鼠标锁定 我正在寻找一种轻量级的解决方案,它可以在不束缚键盘和鼠标的情况下实现这一点。Stephen Toub关于在C#中实现全局键

我想在我的应用程序中捕获一个键盘快捷键,并在用户在应用程序之外按下键盘组合时触发一个对话框。类似于谷歌桌面搜索的Ctrl键,Ctrl键打开搜索对话框

我尝试过使用一些键盘钩子模块,这些模块基本上使用Win32 interop来获得这种效果,但我尝试过的每个实现都会在一定程度上限制键盘,当应用程序执行某些密集型操作时,会出现奇怪的行为。例如加载大量数据,这将导致键盘和鼠标锁定

我正在寻找一种轻量级的解决方案,它可以在不束缚键盘和鼠标的情况下实现这一点。

Stephen Toub关于在C#中实现全局键盘挂钩的介绍:


如果一个全局热键就足够了,那么就可以实现这个技巧了

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace SnagFree.TrayApp.Core
{
    class GlobalKeyboardHookEventArgs : HandledEventArgs
    {
        public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
        public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }

        public GlobalKeyboardHookEventArgs(
            GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
            GlobalKeyboardHook.KeyboardState keyboardState)
        {
            KeyboardData = keyboardData;
            KeyboardState = keyboardState;
        }
    }

    //Based on https://gist.github.com/Stasonix
    class GlobalKeyboardHook : IDisposable
    {
        public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;

        public GlobalKeyboardHook()
        {
            _windowsHookHandle = IntPtr.Zero;
            _user32LibraryHandle = IntPtr.Zero;
            _hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.

            _user32LibraryHandle = LoadLibrary("User32");
            if (_user32LibraryHandle == IntPtr.Zero)
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }



            _windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
            if (_windowsHookHandle == IntPtr.Zero)
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // because we can unhook only in the same thread, not in garbage collector thread
                if (_windowsHookHandle != IntPtr.Zero)
                {
                    if (!UnhookWindowsHookEx(_windowsHookHandle))
                    {
                        int errorCode = Marshal.GetLastWin32Error();
                        throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
                    }
                    _windowsHookHandle = IntPtr.Zero;

                    // ReSharper disable once DelegateSubtraction
                    _hookProc -= LowLevelKeyboardProc;
                }
            }

            if (_user32LibraryHandle != IntPtr.Zero)
            {
                if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
                }
                _user32LibraryHandle = IntPtr.Zero;
            }
        }

        ~GlobalKeyboardHook()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private IntPtr _windowsHookHandle;
        private IntPtr _user32LibraryHandle;
        private HookProc _hookProc;

        delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibrary(string lpFileName);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool FreeLibrary(IntPtr hModule);

        /// <summary>
        /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
        /// You would install a hook procedure to monitor the system for certain types of events. These events are
        /// associated either with a specific thread or with all threads in the same desktop as the calling thread.
        /// </summary>
        /// <param name="idHook">hook type</param>
        /// <param name="lpfn">hook procedure</param>
        /// <param name="hMod">handle to application instance</param>
        /// <param name="dwThreadId">thread identifier</param>
        /// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
        [DllImport("USER32", SetLastError = true)]
        static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

        /// <summary>
        /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
        /// </summary>
        /// <param name="hhk">handle to hook procedure</param>
        /// <returns>If the function succeeds, the return value is true.</returns>
        [DllImport("USER32", SetLastError = true)]
        public static extern bool UnhookWindowsHookEx(IntPtr hHook);

        /// <summary>
        /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
        /// A hook procedure can call this function either before or after processing the hook information.
        /// </summary>
        /// <param name="hHook">handle to current hook</param>
        /// <param name="code">hook code passed to hook procedure</param>
        /// <param name="wParam">value passed to hook procedure</param>
        /// <param name="lParam">value passed to hook procedure</param>
        /// <returns>If the function succeeds, the return value is true.</returns>
        [DllImport("USER32", SetLastError = true)]
        static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);

        [StructLayout(LayoutKind.Sequential)]
        public struct LowLevelKeyboardInputEvent
        {
            /// <summary>
            /// A virtual-key code. The code must be a value in the range 1 to 254.
            /// </summary>
            public int VirtualCode;

            /// <summary>
            /// A hardware scan code for the key. 
            /// </summary>
            public int HardwareScanCode;

            /// <summary>
            /// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level.
            /// </summary>
            public int Flags;

            /// <summary>
            /// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
            /// </summary>
            public int TimeStamp;

            /// <summary>
            /// Additional information associated with the message. 
            /// </summary>
            public IntPtr AdditionalInformation;
        }

        public const int WH_KEYBOARD_LL = 13;
        //const int HC_ACTION = 0;

        public enum KeyboardState
        {
            KeyDown = 0x0100,
            KeyUp = 0x0101,
            SysKeyDown = 0x0104,
            SysKeyUp = 0x0105
        }

        public const int VkSnapshot = 0x2c;
        //const int VkLwin = 0x5b;
        //const int VkRwin = 0x5c;
        //const int VkTab = 0x09;
        //const int VkEscape = 0x18;
        //const int VkControl = 0x11;
        const int KfAltdown = 0x2000;
        public const int LlkhfAltdown = (KfAltdown >> 8);

        public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            bool fEatKeyStroke = false;

            var wparamTyped = wParam.ToInt32();
            if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
            {
                object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
                LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;

                var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);

                EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
                handler?.Invoke(this, eventArguments);

                fEatKeyStroke = eventArguments.Handled;
            }

            return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
        }
    }
}
根据的要求,我将发布我的修改版的答案。
如果要检查我的更改,请搜索
//EDT
。我已经评论了大部分

设置

class GlobalKeyboardHookEventArgs : HandledEventArgs
{
    public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
    public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }

    public GlobalKeyboardHookEventArgs(
        GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
        GlobalKeyboardHook.KeyboardState keyboardState)
    {
        KeyboardData = keyboardData;
        KeyboardState = keyboardState;
    }
}

//Based on https://gist.github.com/Stasonix
class GlobalKeyboardHook : IDisposable
{
    public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;

    // EDT: Added an optional parameter (registeredKeys) that accepts keys to restict
    // the logging mechanism.
    /// <summary>
    /// 
    /// </summary>
    /// <param name="registeredKeys">Keys that should trigger logging. Pass null for full logging.</param>
    public GlobalKeyboardHook(Keys[] registeredKeys = null)
    {
        RegisteredKeys = registeredKeys;
        _windowsHookHandle = IntPtr.Zero;
        _user32LibraryHandle = IntPtr.Zero;
        _hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.

        _user32LibraryHandle = LoadLibrary("User32");
        if (_user32LibraryHandle == IntPtr.Zero)
        {
            int errorCode = Marshal.GetLastWin32Error();
            throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
        }



        _windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
        if (_windowsHookHandle == IntPtr.Zero)
        {
            int errorCode = Marshal.GetLastWin32Error();
            throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // because we can unhook only in the same thread, not in garbage collector thread
            if (_windowsHookHandle != IntPtr.Zero)
            {
                if (!UnhookWindowsHookEx(_windowsHookHandle))
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
                }
                _windowsHookHandle = IntPtr.Zero;

                // ReSharper disable once DelegateSubtraction
                _hookProc -= LowLevelKeyboardProc;
            }
        }

        if (_user32LibraryHandle != IntPtr.Zero)
        {
            if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }
            _user32LibraryHandle = IntPtr.Zero;
        }
    }

    ~GlobalKeyboardHook()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private IntPtr _windowsHookHandle;
    private IntPtr _user32LibraryHandle;
    private HookProc _hookProc;

    delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    private static extern IntPtr LoadLibrary(string lpFileName);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private static extern bool FreeLibrary(IntPtr hModule);

    /// <summary>
    /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
    /// You would install a hook procedure to monitor the system for certain types of events. These events are
    /// associated either with a specific thread or with all threads in the same desktop as the calling thread.
    /// </summary>
    /// <param name="idHook">hook type</param>
    /// <param name="lpfn">hook procedure</param>
    /// <param name="hMod">handle to application instance</param>
    /// <param name="dwThreadId">thread identifier</param>
    /// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
    [DllImport("USER32", SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

    /// <summary>
    /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
    /// </summary>
    /// <param name="hhk">handle to hook procedure</param>
    /// <returns>If the function succeeds, the return value is true.</returns>
    [DllImport("USER32", SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);

    /// <summary>
    /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
    /// A hook procedure can call this function either before or after processing the hook information.
    /// </summary>
    /// <param name="hHook">handle to current hook</param>
    /// <param name="code">hook code passed to hook procedure</param>
    /// <param name="wParam">value passed to hook procedure</param>
    /// <param name="lParam">value passed to hook procedure</param>
    /// <returns>If the function succeeds, the return value is true.</returns>
    [DllImport("USER32", SetLastError = true)]
    static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);

    [StructLayout(LayoutKind.Sequential)]
    public struct LowLevelKeyboardInputEvent
    {
        /// <summary>
        /// A virtual-key code. The code must be a value in the range 1 to 254.
        /// </summary>
        public int VirtualCode;

        // EDT: added a conversion from VirtualCode to Keys.
        /// <summary>
        /// The VirtualCode converted to typeof(Keys) for higher usability.
        /// </summary>
        public Keys Key { get { return (Keys)VirtualCode; } }

        /// <summary>
        /// A hardware scan code for the key. 
        /// </summary>
        public int HardwareScanCode;

        /// <summary>
        /// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level.
        /// </summary>
        public int Flags;

        /// <summary>
        /// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
        /// </summary>
        public int TimeStamp;

        /// <summary>
        /// Additional information associated with the message. 
        /// </summary>
        public IntPtr AdditionalInformation;
    }

    public const int WH_KEYBOARD_LL = 13;
    //const int HC_ACTION = 0;

    public enum KeyboardState
    {
        KeyDown = 0x0100,
        KeyUp = 0x0101,
        SysKeyDown = 0x0104,
        SysKeyUp = 0x0105
    }

    // EDT: Replaced VkSnapshot(int) with RegisteredKeys(Keys[])
    public static Keys[] RegisteredKeys;
    const int KfAltdown = 0x2000;
    public const int LlkhfAltdown = (KfAltdown >> 8);

    public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        bool fEatKeyStroke = false;

        var wparamTyped = wParam.ToInt32();
        if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
        {
            object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
            LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;

            var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);

            // EDT: Removed the comparison-logic from the usage-area so the user does not need to mess around with it.
            // Either the incoming key has to be part of RegisteredKeys (see constructor on top) or RegisterdKeys
            // has to be null for the event to get fired.
            var key = (Keys)p.VirtualCode;
            if (RegisteredKeys == null || RegisteredKeys.Contains(key))
            {
                EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
                handler?.Invoke(this, eventArguments);

                fEatKeyStroke = eventArguments.Handled;
            }
        }

        return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }
}
谢谢你的来信。尽管我简化了使用,但这段初始代码对我来说非常有用

private void buttonHook_Click(object sender, EventArgs e)
{
    // Hooks only into specified Keys (here "A" and "B").
    // (***) Use this constructor

    _globalKeyboardHook = new GlobalKeyboardHook(new Keys[] { Keys.A, Keys.B });

    // Hooks into all keys.
    // (***) Or this - not both

    _globalKeyboardHook = new GlobalKeyboardHook();
    _globalKeyboardHook.KeyboardPressed += OnKeyPressed;
}

然后工作正常。

我的代表太低,无法发表评论,但关于
CallbackOnCollectedDelegate
异常,我修改了C4d的答案中的
public void SetupKeyboardHooks()
,如下所示:

public void SetupKeyboardHooks(out object hookProc)
{
  _globalKeyboardHook = new GlobalKeyboardHook();
  _globalKeyboardHook.KeyboardPressed += OnKeyPressed;


  hookProc = _globalKeyboardHook.GcSafeHookProc;
}
其中,
GcSafeHookProc
只是OPs中
\u hookProc
的公共getter

_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.

并将
hookProc
存储为调用
SetupKeyboardHooks(…)
的类中的私有字段,因此保持引用活动,从垃圾收集中保存,不再出现
CallbackOnCollectedDelegate
异常。似乎在
GlobalKeyboardHook
类中有此附加引用是不够的。可能要确保在关闭应用程序时也处理了此引用。

您可以指定已经尝试过的模块。请看,这太棒了!您甚至可以捕获alt-F4并防止应用程序关闭。您甚至在示例中使用了C#6.0:)谢谢!我使用了这段代码,它可以工作。但是如果我在一段时间后按键,就会出现一个异常,表示委托被垃圾收集,托管代码应该让它保持活动状态。然后有一个空引用异常。你能帮我吗?我建议将
公钥{get{return(Keys)VirtualCode;}}
添加到
低层键盘输入事件
。这就需要处理虚拟代码,每个人都需要谷歌。另外:您可以将
VkSnapshot
int
更改为
Keys
,然后轻松地放置一个键。我更进一步,放置了一个
公共静态密钥[]RegisteredKeys
。如果(!GlobalKeyboardHook.RegisteredKeys.Contains(e.KeyboardData.Key))返回,则按下按钮将出现
。您当然可以防止触发事件;)@10年后的杜比?我不确定。但我明白了。一年过去了,在看了我的评论后,似乎有点难以理解。我将为今天晚上设置一个提醒。也许我会有一些时间来重建它。@dube hehe,这个问题已经有10年了。我已经在下面发布了我的修改版本。玩得开心!如何使用该类?有什么原因会返回奇数字符吗?@VAAA向您的解决方案中添加一个类。如果Program.cs中已有main(),请将此类中的main()重命名为InitializeComponent(),然后在project App()方法中调用它,例如SysTrayApp()。请参阅链接文章评论,了解Stephen回答的更多问题和回答。这将为您提供按下的原始键,如“S”,但是否有方法确定将插入哪个键,例如,如果caps lock关闭,则为“S”,但如果它打开,则为“S”?代码存在一些问题。首先,因为这是最后一个有XNA包装器的版本,而且我还写了一些旧的3D软件,所以我仍然使用VS2012来处理一些事情,包括这个。C#6运营者取代了我坐在那里准备尝试它。它不起作用,但我在VS2012中需要它,所以这可能是一个问题。我想我发现的问题在C#6中也是如此。GlobalKeyboardHook构造函数(如上)以:RegisteredKeys=RegisteredKeys开始;不幸的是,registeredKeys不会持久化,因此指向它的指针在返回时变得无效,并且在此后的所有调用中都为null。我的修复方法是在传入时检查它是否为null,在构造函数中创建一个大小相等的数组(静态和持久),并将传递的数组数据复制到其中)。非常感谢。非常感谢!这似乎失败后,一些按键?托管调试助手'CallbackOnCollectedDelegate'消息=托管调试助手'CallbackOnCollectedDelegate':'对'Keyboard'类型的垃圾收集委托进行了回调!键盘.GlobalKeyboardHook+HookProc::Invoke'。这可能会导致应用程序崩溃、损坏和数据丢失。将委托传递给非托管代码时,托管应用程序必须将其保持活动状态,直到保证永远不会调用它们为止。@C4d热键已被捕获,但在显示
消息框后,我立即收到以下错误(以验证处理程序是否处于活动状态并正常工作):
托管调试助手“CallbackOnCollectedDelegate”:“对类型为“GlobalKeyboardHook+HookProc::Invoke”的垃圾收集委托进行了回调。”。这可能会导致应用程序崩溃、损坏和数据丢失。将委托传递给非托管代码时,托管应用程序必须将其保持活动状态,直到保证永远不会调用它们为止。
如何解决此异常?^上述异常发生在
LowLeveKeyboardProc()
底部的下一行:
返回按键?(IntPtr)1:CallNextHookEx(IntPtr.Z
private void buttonHook_Click(object sender, EventArgs e)
{
    // Hooks only into specified Keys (here "A" and "B").
    // (***) Use this constructor

    _globalKeyboardHook = new GlobalKeyboardHook(new Keys[] { Keys.A, Keys.B });

    // Hooks into all keys.
    // (***) Or this - not both

    _globalKeyboardHook = new GlobalKeyboardHook();
    _globalKeyboardHook.KeyboardPressed += OnKeyPressed;
}
public void SetupKeyboardHooks(out object hookProc)
{
  _globalKeyboardHook = new GlobalKeyboardHook();
  _globalKeyboardHook.KeyboardPressed += OnKeyPressed;


  hookProc = _globalKeyboardHook.GcSafeHookProc;
}
_hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.