Глобальное сочетание клавиш с клавишей 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 помечен как внутренний, так что это исключено.

Я не знаю Авалонию, но, возможно, это поможет stackoverflow.com/a/78624356/403671

Simon Mourier 03.09.2024 07:30

Ключ Windows принадлежит Microsoft. Кроме того, глобальные горячие клавиши регистрируются с помощью RegisterHotKey.

IInspectable 03.09.2024 07:46

Я посмотрю на способ RegisterHotKey, спасибо!

Krzysztof Skowronek 03.09.2024 14:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
51
1
Перейти к ответу Данный вопрос помечен как решенный

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

Из документации : «MOD_WIN: необходимо удерживать любую клавишу WINDOWS. Эти клавиши помечены логотипом Windows. Сочетания клавиш, включающие клавишу WINDOWS, зарезервированы для использования операционной системой».

IInspectable 05.09.2024 12:03

Можно только догадываться, почему «Клод» рекомендовал создать окно только для сообщений, не создавая на самом деле окна только для сообщений. Gen-AI не умеет писать качественный код. Не распространяйте эту мерзость.

IInspectable 05.09.2024 12:10

Другие вопросы по теме