Я пишу переключатель приложений, вдохновленный Контекстами, вроде как ради развлечения, и застрял в реакции на глобальное сочетание клавиш. Я использую .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
помечен как внутренний, так что это исключено.
Ключ Windows принадлежит Microsoft. Кроме того, глобальные горячие клавиши регистрируются с помощью RegisterHotKey.
Я посмотрю на способ RegisterHotKey, спасибо!
С помощью Клода мне удалось придумать следующее:
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, зарезервированы для использования операционной системой».
Можно только догадываться, почему «Клод» рекомендовал создать окно только для сообщений, не создавая на самом деле окна только для сообщений. Gen-AI не умеет писать качественный код. Не распространяйте эту мерзость.
Я не знаю Авалонию, но, возможно, это поможет stackoverflow.com/a/78624356/403671