Phase 4: 全局快捷键拦截 (Win32 API)

- 新增 HotKeyManager (Helpers/), 使用 user32.dll RegisterHotKey/UnregisterHotKey 注册系统级快捷键

- 支持 Ctrl/Alt/Shift/Win 修饰键组合, 单字符键和命名键(F1-F12等)解析

- MainWindow.OnSourceInitialized 挂载 HwndSource.AddHook 拦截 WM_HOTKEY 消息

- 启动时自动注册所有含快捷键的工具, 工具增删改后自动重新注册

- 快捷键冲突时记录 Win32 错误码, 无效格式打印警告

- 新增 12 个 HotKeyManager 单元测试 (73 tests total)
This commit is contained in:
2026-05-09 23:07:44 +08:00
parent dc94d17d99
commit 92beb46f22
6 changed files with 362 additions and 4 deletions

View File

@@ -68,6 +68,7 @@ public partial class App : Application
services.AddSingleton<ILogService, LogService>();
services.AddSingleton<IDataService, JsonDataService>();
services.AddSingleton<IProcessExecutionService, ProcessExecutionService>();
services.AddSingleton<Helpers.HotKeyManager>();
services.AddSingleton<ViewModels.MainViewModel>();
services.AddTransient<ViewModels.ToolEditViewModel>();
services.AddTransient<ViewModels.CategoryEditViewModel>();

View File

@@ -0,0 +1,174 @@
using System.Runtime.InteropServices;
using System.Windows.Input;
using PersonalToolBox.Models;
using PersonalToolBox.Services;
namespace PersonalToolBox.Helpers;
/// <summary>
/// 全局快捷键管理器,使用 Win32 API RegisterHotKey / UnregisterHotKey
/// </summary>
public class HotKeyManager
{
private IntPtr _hwnd;
private int _nextId = 1;
private readonly Dictionary<int, ToolItem> _hotkeyMap = new();
private readonly ILogService _logService;
private readonly IProcessExecutionService _processService;
// ──────────────── Win32 API ────────────────
private const int WM_HOTKEY = 0x0312;
private const uint MOD_ALT = 0x0001;
private const uint MOD_CONTROL = 0x0002;
private const uint MOD_SHIFT = 0x0004;
private const uint MOD_WIN = 0x0008;
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// ──────────────── 构造函数 ────────────────
public HotKeyManager(ILogService logService, IProcessExecutionService processService)
{
_logService = logService;
_processService = processService;
}
/// <summary>
/// 设置窗口句柄(在 MainWindow.OnSourceInitialized 中调用)
/// </summary>
public void Initialize(IntPtr hwnd)
{
_hwnd = hwnd;
}
// ──────────────── 注册/注销 ────────────────
/// <summary>
/// 批量注册所有工具的快捷键
/// </summary>
public void RegisterAll(IEnumerable<ToolItem> tools)
{
UnregisterAll();
foreach (var tool in tools.Where(t => !string.IsNullOrWhiteSpace(t.HotKey)))
Register(tool);
}
/// <summary>
/// 为单个工具注册快捷键
/// </summary>
public void Register(ToolItem tool)
{
if (_hwnd == IntPtr.Zero) return;
if (string.IsNullOrWhiteSpace(tool.HotKey)) return;
if (!TryParseHotKey(tool.HotKey, out uint modifiers, out uint vk))
{
_logService.Warning($"快捷键格式无效: {tool.HotKey}");
return;
}
int id = _nextId++;
if (RegisterHotKey(_hwnd, id, modifiers, vk))
{
_hotkeyMap[id] = tool;
_logService.Info($"已注册快捷键: {tool.HotKey} → {tool.Name}");
}
else
{
int err = Marshal.GetLastWin32Error();
_logService.Error($"快捷键注册失败: {tool.HotKey} (错误码: {err},可能与其他程序冲突)");
}
}
/// <summary>
/// 注销所有已注册的快捷键
/// </summary>
private void UnregisterAll()
{
foreach (var kvp in _hotkeyMap)
UnregisterHotKey(_hwnd, kvp.Key);
_hotkeyMap.Clear();
}
// ──────────────── 消息处理 ────────────────
/// <summary>
/// 由 WndProc 钩子调用,处理 WM_HOTKEY 消息
/// </summary>
public bool TryHandleMessage(int msg, IntPtr wParam, IntPtr lParam)
{
if (msg != WM_HOTKEY) return false;
int id = wParam.ToInt32();
if (_hotkeyMap.TryGetValue(id, out var tool))
{
_logService.Info($"通过快捷键启动: {tool.Name}");
_processService.Execute(tool);
}
return true;
}
// ──────────────── 快捷键解析 ────────────────
/// <summary>
/// 解析快捷键字符串(如 "Ctrl+Alt+T")为修饰键和虚拟键码
/// </summary>
public static bool TryParseHotKey(string hotkey, out uint modifiers, out uint vk)
{
modifiers = 0;
vk = 0;
if (string.IsNullOrWhiteSpace(hotkey)) return false;
var parts = hotkey.Split('+', StringSplitOptions.TrimEntries);
if (parts.Length < 2) return false;
// 解析修饰键(最后一段之前的部分)
foreach (var part in parts.Take(parts.Length - 1))
{
uint mod = part.ToUpperInvariant() switch
{
"CTRL" or "CONTROL" => MOD_CONTROL,
"ALT" => MOD_ALT,
"SHIFT" => MOD_SHIFT,
"WIN" or "WINDOWS" => MOD_WIN,
_ => 0
};
if (mod == 0) return false;
modifiers |= mod;
}
if (modifiers == 0) return false;
// 解析按键(最后一段)
var keyStr = parts.Last();
if (keyStr.Length == 1)
{
vk = char.ToUpperInvariant(keyStr[0]);
}
else if (Enum.TryParse<Key>(keyStr, true, out var key))
{
vk = (uint)KeyInterop.VirtualKeyFromKey(key);
}
else
{
return false;
}
return true;
}
/// <summary>
/// 程序退出时清理所有快捷键
/// </summary>
public void Dispose()
{
UnregisterAll();
}
}

View File

@@ -20,6 +20,7 @@ public partial class MainViewModel : ObservableObject
private readonly IDataService _dataService;
private readonly IProcessExecutionService _processService;
private readonly IServiceProvider _serviceProvider;
private readonly HotKeyManager _hotKeyManager;
/// <summary>
/// 全部分类的虚拟对象
@@ -30,12 +31,14 @@ public partial class MainViewModel : ObservableObject
ILogService logService,
IDataService dataService,
IProcessExecutionService processService,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
HotKeyManager hotKeyManager)
{
_logService = logService;
_dataService = dataService;
_processService = processService;
_serviceProvider = serviceProvider;
_hotKeyManager = hotKeyManager;
LoadData();
}
@@ -201,9 +204,20 @@ public partial class MainViewModel : ObservableObject
// 恢复之前选中的分类(若仍存在),否则选中"全部"
SelectedCategory = Categories.FirstOrDefault(c => c.Id == previousCategoryId) ?? AllCategory;
ApplyFilter();
// 工具列表变更后重新注册快捷键
RegisterAllHotKeys();
}
// ───────────────────────────── 初始化 ─────────────────────────────
// ───────────────────────────── 快捷键 ─────────────────────────────
/// <summary>
/// 注册所有工具的全局快捷键(由 MainWindow.OnSourceInitialized 及数据变更后调用)
/// </summary>
public void RegisterAllHotKeys()
{
_hotKeyManager.RegisterAll(Tools);
}
private void LoadData()
{

View File

@@ -3,6 +3,8 @@ using System.Collections.Specialized;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Interop;
using PersonalToolBox.Helpers;
using PersonalToolBox.ViewModels;
namespace PersonalToolBox.Views;
@@ -10,17 +12,45 @@ namespace PersonalToolBox.Views;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private readonly HotKeyManager _hotKeyManager;
public MainWindow(MainViewModel viewModel)
public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager)
{
InitializeComponent();
_viewModel = viewModel;
_hotKeyManager = hotKeyManager;
DataContext = viewModel;
viewModel.Logs.CollectionChanged += OnLogsCollectionChanged;
}
/// <summary>
/// 窗口句柄就绪后注册全局快捷键
/// </summary>
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
var hwndSource = (HwndSource)PresentationSource.FromVisual(this)!;
_hotKeyManager.Initialize(hwndSource.Handle);
// 挂载 WndProc 钩子,拦截 WM_HOTKEY 消息
hwndSource.AddHook(WndProcHook);
// 注册所有已配置快捷键的工具
_viewModel.RegisterAllHotKeys();
}
/// <summary>
/// 窗口消息钩子
/// </summary>
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam);
return IntPtr.Zero;
}
private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && LogListBox.Items.Count > 0)