使用 .NET 8 的 Windows 上带有 windows 键的全局键盘快捷键

问题描述 投票:0回答:1

我正在编写一个受Contexts启发的应用程序切换器,有点有趣,但陷入了对全局键盘快捷键的反应。我正在使用 .NET 8 + Avalonia。

到目前为止我得到了什么:

using System;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;

namespace WindowSwitcher.Services.Keyboard;

public class KeyboardInterceptor2 : IKeyboardInterceptor
{
    private const int WhKeyboardLl = 13;
    private const int WmKeydown = 0x0100;
    private const int WmSyskeydown = 0x0104;
    private const int WmKeyup = 0x0101;
    private const int WmSyskeyup = 0x0105;

    private readonly Subject<Unit> _signalSubject = new();
    private readonly IntPtr _hookId;
    private bool _consumeNextWinKeyUp;

    public IObservable<Unit> Signal => _signalSubject.AsObservable();

    public KeyboardInterceptor2()
    {
        _hookId = SetHook(HookCallback);
    }

    private IntPtr SetHook(LowLevelKeyboardProc proc)
    {
        using var curProcess = Process.GetCurrentProcess();
        using var curModule = curProcess.MainModule!;

        return SetWindowsHookEx(WhKeyboardLl, proc, GetModuleHandle(curModule.ModuleName), 0);
    }

    private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            int vkCode = Marshal.ReadInt32(lParam);

            if (wParam == WmKeydown || wParam == WmSyskeydown)
            {
                if (vkCode == (int)VirtualKeyStates.VkS)
                {
                    if ((GetAsyncKeyState(VirtualKeyStates.VkLwin) & 0x8000) != 0 ||
                        (GetAsyncKeyState(VirtualKeyStates.VkRwin) & 0x8000) != 0)
                    {
                        _signalSubject.OnNext(Unit.Default);
                        _consumeNextWinKeyUp = true;
                        return 1; // Consume the S key press when Windows key is pressed
                    }
                }
            }
            else if (wParam == WmKeyup || wParam == WmSyskeyup)
            {
                if ((vkCode == (int)VirtualKeyStates.VkLwin || vkCode == (int)VirtualKeyStates.VkRwin) && _consumeNextWinKeyUp)
                {
                    _consumeNextWinKeyUp = false;
                    return 1; // Consume the Windows key up event
                }
            }
        }

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

    public void Dispose()
    {
        UnhookWindowsHookEx(_hookId);
        _signalSubject.Dispose();
    }

    private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

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

    [DllImport("user32.dll")]
    private static extern short GetAsyncKeyState(VirtualKeyStates nVirtKey);

    private enum VirtualKeyStates
    {
        VkLwin = 0x5B,
        VkRwin = 0x5C,
        VkS = 0x53
    }
}

基本上,我想要的只是按 Windows + S 来激活我的程序,并且它可以工作,只是不是每次都有效,大多数时候都会弹出菜单启动。对我来说这看起来不错,但也许存在一些奇怪的同步问题?

我有一个想法,尝试仅使用两个将分别更新的可观察量(每个键一个),如果它们都已设置,我将触发信号。

我也尝试过 GlobalHotKey,但是对于 .NET 8,

Key
类不可用 - 我什至将
TargetFramework
设置为
net8.0-windows
,但它没有做任何事情。

最好的方法是什么? Avalonia 有内置的东西吗?

InputManager
被标记为内部,因此不包含在内。

c# winapi .net-8.0 avaloniaui avalonia
1个回答
0
投票

在克劳德的帮助下,我得出了以下结论:

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;

namespace WindowSwitcher.Services.Keyboard
{
    public class HotKeyInterceptor : IKeyboardInterceptor
    {
        private const int WmHotkey = 0x0312;
        const int ModControl = 0x0002;
        const int ModWin = 0x0008;
        const int VkTab = 0x09;

        private readonly Subject<Unit> _signalSubject = new Subject<Unit>();
        private readonly int _hotkeyId = 213769420;
        private readonly MessageWindow _messageWindow;

        public IObservable<Unit> Signal => _signalSubject.AsObservable();

        [RequiresAssemblyFiles()]
        public HotKeyInterceptor()
        {
            _messageWindow = new MessageWindow();
            _messageWindow.HotKeyPressed += OnHotKeyPressed;

            UnregisterHotKey(_messageWindow.Handle, _hotkeyId);
            
            if (!RegisterHotKey(_messageWindow.Handle, _hotkeyId, ModWin | ModControl, VkTab))
            {
                int error = Marshal.GetLastWin32Error();
                string errorMessage = GetErrorMessage(error);
                throw new Win32Exception(error, $"Could not register the hot key. {errorMessage}");
            }
        }

        private void OnHotKeyPressed(object? sender, EventArgs e)
        {
            _signalSubject.OnNext(Unit.Default);
        }

        public void Dispose()
        {
            UnregisterHotKey(_messageWindow.Handle, _hotkeyId);
            _messageWindow.Dispose();
            _signalSubject.Dispose();
        }
        
        
        private string GetErrorMessage(int errorCode)
        {
            switch (errorCode)
            {
                case 1409:
                    return "The hotkey is already registered by another application.";
                case 1400:
                    return "The window handle is not valid.";
                case 87:
                    return "An invalid parameter was passed to the function.";
                default:
                    return $"Unknown error occurred. Error code: {errorCode}";
            }
        }

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);

        [DllImport("user32.dll")]
        private static extern bool UnregisterHotKey(IntPtr hWnd, int id);

        private class MessageWindow : IDisposable
        {
            private const int WsExToolwindow = 0x80;
            private const int WsPopup = unchecked((int)0x80000000);

            public event EventHandler? HotKeyPressed;

            private readonly IntPtr _hwnd;

            public IntPtr Handle => _hwnd;

            [RequiresAssemblyFiles("Calls System.Runtime.InteropServices.Marshal.GetHINSTANCE(Module)")]
            public MessageWindow()
            {
                var wndClass = new WindowClass
                {
                    lpfnWndProc = Marshal.GetFunctionPointerForDelegate(WndProc),
                    hInstance = Marshal.GetHINSTANCE(typeof(MessageWindow).Module),
                    lpszClassName = "MessageWindowClass"
                };

                var classAtom = RegisterClass(ref wndClass);
                if (classAtom == 0)
                    throw new InvalidOperationException("Failed to register window class");

                _hwnd = CreateWindowEx(
                    WsExToolwindow,
                    classAtom,
                    "MessageWindow",
                    WsPopup,
                    0, 0, 0, 0,
                    IntPtr.Zero,
                    IntPtr.Zero,
                    wndClass.hInstance,
                    IntPtr.Zero);

                if (_hwnd == IntPtr.Zero)
                    throw new InvalidOperationException("Failed to create message window");
            }

            private IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
            {
                if (msg == WmHotkey)
                {
                    HotKeyPressed?.Invoke(this, EventArgs.Empty);
                    return IntPtr.Zero;
                }
                return DefWindowProc(hwnd, msg, wParam, lParam);
            }

            public void Dispose()
            {
                if (_hwnd != IntPtr.Zero)
                {
                    DestroyWindow(_hwnd);
                }
            }

            [DllImport("user32.dll")]
            private static extern ushort RegisterClass(ref WindowClass lpWndClass);

            [DllImport("user32.dll")]
            private static extern IntPtr CreateWindowEx(
                int dwExStyle,
                ushort classAtom,
                string lpWindowName,
                int dwStyle,
                int x, int y,
                int nWidth, int nHeight,
                IntPtr hWndParent,
                IntPtr hMenu,
                IntPtr hInstance,
                IntPtr lpParam);

            [DllImport("user32.dll")]
            private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);

            [DllImport("user32.dll")]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool DestroyWindow(IntPtr hwnd);

            [StructLayout(LayoutKind.Sequential)]
            private struct WindowClass
            {
                public int style;
                public IntPtr lpfnWndProc;
                public int cbClsExtra;
                public int cbWndExtra;
                public IntPtr hInstance;
                public IntPtr hIcon;
                public IntPtr hCursor;
                public IntPtr hbrBackground;
                [MarshalAs(UnmanagedType.LPStr)]
                public string lpszMenuName;
                [MarshalAs(UnmanagedType.LPStr)]
                public string lpszClassName;
            }
        }
    }
}

每次按快捷键都会起作用,所以我想问题已经解决了。我还必须选择 ctrl+win+tab 作为快捷方式,因为 Win+S 只是打开开始菜单。

© www.soinside.com 2019 - 2024. All rights reserved.