Глобальное сочетание клавиш с клавишей Windows в Windows с .NET 8

Я пишу переключатель приложений, вдохновленный Контекстами, вроде как ради развлечения, и застрял в реакции на глобальное сочетание клавиш. Я использую .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, но это ничего не дало.

Каков наилучший подход к этому? Есть ли что-то встроенное в Авалонию? InputManager помечен как внутренний, так что это исключено.

🤔 А знаете ли вы, что...
С C# можно разрабатывать мобильные приложения для платформы Android с использованием Xamarin.


51
1

Ответ:

Решено

С помощью Клода мне удалось придумать следующее:

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 просто открывает меню «Пуск».