如何在 wpf C# 中正确创建全局鼠标热键

How properly create a global mouse hotkey in wpf C#

我正在尝试使用 SetWindowsHookEx() 在我的程序中创建全局鼠标热键。到目前为止,我已经尝试像我在这里看到的 post 一样创建它们,但是,我真的不知道如何完成它。 目前的问题是我不完全知道 _globalMouseHookCallback 是什么。

这是我到目前为止写的:

class GlobalHotkey
{

    [DllImport("user32.dll")]
    static extern IntPtr SetWindowsHookEx(int idHook, HookProc callback, IntPtr hInstance, uint threadId);

    [DllImport("user32.dll")]
    static extern bool UnhookWindowsHookEx(IntPtr hInstance);

    [DllImport("user32.dll")]
    static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);

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

    private IntPtr _hGlobalMouseHook;


    MainWindow _m;

    private const int WH_KEYBOARD_LL = 13;
    private const int WH_MOUSE_LL = 14;

    private const int WM_LBUTTONDOWN = 0x0201;
    private const int WM_LBUTTONUP = 0x0202;
    private const int WM_RBUTTONDOWN = 0x0204;
    private const int WM_RBUTTONUP = 0x0205;

    private static IntPtr hook = IntPtr.Zero;

    public GlobalHotkey(MainWindow m)
    {
        _m = m;
    }

    public void SetUpHook()
    {
        _m.rtbLog.AppendText("Setting up global Hotkey \n");

        _globalMouseHookCallback = LowLevelMouseProc;

        _hGlobalMouseHook = SetWindowsHookEx(WH_MOUSE_LL, _globalMouseHookCallback, Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]), 0);

        if (_hGlobalMouseHook == IntPtr.Zero)
        {
            _m.rtbLog.AppendText("Unable to set up global mouse hook\n");
        }
    }

    public void ClearHook()
    {
        _m.rtbLog.AppendText("Deleting global mouse hook\n");

        if (_hGlobalMouseHook != IntPtr.Zero)
        {
            if (!UnhookWindowsHookEx(_hGlobalMouseHook))
            {
                _m.rtbLog.AppendText("Unable to delete global mouse hook\n");
            }

            _hGlobalMouseHook = IntPtr.Zero;
        }

    }

    public int LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            var wmMouse = wParam;

            if (wmMouse == (IntPtr)WM_LBUTTONDOWN)
            {
                _m.rtbLog.AppendText("Right Mouse down");
            }
            if (wmMouse == (IntPtr)WM_LBUTTONUP)
            {
                _m.rtbLog.AppendText("Left Mouse up");
            }

            if (wmMouse == (IntPtr)WM_RBUTTONDOWN)
            {
                _m.rtbLog.AppendText("Right Mouse down");
            }
            if (wmMouse == (IntPtr)WM_RBUTTONUP)
            {
                _m.rtbLog.AppendText("Right Mouse up");
            }
        }

        return CallNextHookEx(_hGlobalMouseHook, nCode, wParam, lParam);
    }
}

这个全局热键的东西非常难,有没有像我这样的新手可以轻松解释它的教程:P?

编辑 所以我尝试将 Koby Ducks 示例改编为我的代码。

这是我的热键class:

class GlobalHotkey
{

    MainWindow _m;

    private static readonly object sSyncObj = new object();
    private static readonly HashSet<Key> sDownKeys = new HashSet<Key>();
    private static readonly Dictionary<Key, Action> sPressActions = new Dictionary<Key, Action>();
    private static readonly Dictionary<Key, Action> sReleaseActions = new Dictionary<Key, Action>();

    public GlobalHotkey(MainWindow m)
    {
        _m = m;

    }

    public static void ProcessKeyDown(KeyEventArgs args)
    {
        var key = args.Key;
        var action = default(Action);
        lock (sSyncObj)
        {
            if (!sDownKeys.Contains(key))
            {
                sDownKeys.Add(key);

                if (sPressActions.TryGetValue(key, out action))
                {
                    args.Handled = true;
                }
            }
        }

        action.Invoke();
    }

    public static void ProcessKeyUp(KeyEventArgs args)
    {
        var key = args.Key;
        var action = default(Action);
        lock (sSyncObj)
        {
            if (sDownKeys.Remove(key))
            {
                if (sReleaseActions.TryGetValue(key, out action))
                {
                    args.Handled = true;
                }
            }
        }

        action.Invoke();
    }

    public static void AttachPressAction(Key key, Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException(nameof(action));
        }

        lock (sSyncObj)
        {
            sPressActions.Add(key, action);
        }
    }

    public static bool DetachPressAction(Key key)
    {
        lock (sSyncObj)
        {
            return sPressActions.Remove(key);
        }
    }

    public static void AttachReleaseAction(Key key, Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException(nameof(action));
        }

        lock (sSyncObj)
        {
            sReleaseActions.Add(key, action);
        }
    }

    public static bool DetachReleaseAction(Key key)
    {
        lock (sSyncObj)
        {
            return sReleaseActions.Remove(key);
        }
    }
}

然后我创建了我的动作

public void MyTestAction()
    {
        rtbLog.AppendText("The B key was pressed");
    }
myAction = new Action(MyTestAction);

但是当我将我的事件处理程序添加到 PreviewKeyUp- 和 Down 事件时,它给我一个错误,指出 ProcessKeyUp- 和 Down 的参数与 PreviewKeyUp- 和 Down 不同。

PreviewKeyDown += GlobalHotkey.ProcessKeyDown;
PreviewKeyUp += GlobalHotkey.ProcessKeyUp;

编辑: 无论当前焦点如何 window(又名 "global"),请参阅 this answer on the Win32 keyboard API.

对于在您的应用处于焦点状态时的输入(也称为 "local"),您可以使用预览事件。

public static class HotKeySystem
{
    public static void ProcessKeyDown(object sender, KeyEventArgs args)
    {
        var key = args.Key;
        var action = default(Action);
        lock (sSyncObj) {
            if (!sDownKeys.Contains(key)) {
                sDownKeys.Add(key);
                if (sPressActions.TryGetValue(key, out action)) {
                    args.Handled = true;
                }
            }
        }
        // Invoke outside of the lock.
        action?.Invoke();
    }
    public static void ProcessKeyUp(object sender, KeyEventArgs args)
    {
        var key = args.Key;
        var action = default(Action);
        lock (sSyncObj) {
            if (sDownKeys.Remove(key)) {
                if (sReleaseActions.TryGetValue(key, out action)) {
                    args.Handled = true;
                }
            }
        }
        // Invoke outside of the lock.
        action?.Invoke();
    }
    public static void AttachPressAction(KeyCode key, Action action)
    {
        if (action == null) {
            throw new ArgumentNullException(nameof(action));
        }
        lock (sSyncObj) {
            sPressActions.Add(key, action);
        }
    }
    public static bool DetachPressAction(KeyCode key)
    {
        lock (sSyncObj) {
            return sPressActions.Remove(key);
        }
    }
    public static void AttachReleaseAction(KeyCode key, Action action)
    {
        if (action == null) {
            throw new ArgumentNullException(nameof(action));
        }
        lock (sSyncObj) {
            sReleaseActions.Add(key, action);
        }
    }
    public static bool DetachReleaseAction(KeyCode key)
    {
        lock (sSyncObj) {
            return sReleaseActions.Remove(key);
        }
    }

    private static readonly object sSyncObj = new object();
    // The keys that are currently down.
    private static readonly HashSet<KeyCode> sDownKeys = new HashSet<KeyCode>();
    // Actions triggered when a key was up, but is now down.
    private static readonly Dictionary<KeyCode, Action> sPressActions = new Dictionary<KeyCode, Action>();
    // Actions triggered when a key was down, but is now up.
    private static readonly Dictionary<KeyCode, Action> sReleaseActions = new Dictionary<KeyCode, Action>();
}

// When possible, subclass your windows from this to automatically add hotkey support.
public class HotKeyWindow : Window
{
    protected override void OnPreviewKeyDown(KeyEventArgs args)
    {
        HotKeySystem.ProcessKeyDown(this, args);
        base.OnPreviewKeyDown(args);
    }
    protected override void OnPreviewKeyUp(KeyEventArgs args)
    {
        HotKeySystem.ProcessKeyUp(this, args);
        base.OnPreviewKeyUp(args);
    }
}

// When not possible, attach event handlers like this:
window.PreviewKeyDown += HotKeySystem.ProcessKeyDown;
window.PreviewKeyUp += HotKeySystem.ProcessKeyUp;

// Use it like this:
HotKeySystem.AttachPressAction(KeyCode.F1, () => {
    // F1 hotkey functionality.
});

无论您使用的是此方法还是 Win32 API,请考虑其中的含义。如果您绑定了 'A',那么您将无法在文本输入控件中输入 'a' 或 'A'。解决此问题的一种方法是:

public static void ProcessKeyDown(object sender, KeyEventArgs args)
{
    // Detect keyboard input controls you may have issues with.
    // If one has keyboard focus, skip hotkey processing.
    if (Keyboard.FocusedElement is TextBox) {
        return;
    }

    // ...
}