Files
personal-toolbox/src/ToolboxApp/Services/HotkeyService.cs
home-PC fde1c6bc0f feat: 搭建工具箱核心服务层
新增工具、分类、组合、自动运行和日志等基础模型。

实现本地 JSON 配置读写、默认系统工具恢复、路径校验、统一启动服务、自动运行服务、托盘服务、开机自启服务和全局快捷键注册服务,为后续界面集成提供稳定边界。
2026-05-27 13:41:09 +08:00

286 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using ToolboxApp.Models;
namespace ToolboxApp.Services;
public sealed class HotkeyService : IDisposable
{
private const int WmHotkey = 0x0312;
private readonly Dictionary<int, string> _registeredHotkeys = new();
private HwndSource? _source;
private Func<string, Task>? _onTriggered;
private Action<LogMessage>? _log;
private int _nextId = 1000;
private nint _handle;
public void RegisterAll(
Window window,
IEnumerable<ToolItem> tools,
bool enabled,
Func<string, Task> onTriggered,
Action<LogMessage> log)
{
UnregisterAll();
_onTriggered = onTriggered;
_log = log;
var helper = new WindowInteropHelper(window);
_handle = helper.Handle;
if (_handle == 0)
{
return;
}
_source ??= HwndSource.FromHwnd(_handle);
_source?.RemoveHook(WndProc);
_source?.AddHook(WndProc);
foreach (var tool in tools)
{
tool.HotkeyStatus = string.IsNullOrWhiteSpace(tool.Hotkey) ? "未设置" : "等待注册";
}
if (!enabled)
{
foreach (var tool in tools.Where(tool => !string.IsNullOrWhiteSpace(tool.Hotkey)))
{
tool.HotkeyStatus = "已禁用";
}
return;
}
var activeTools = tools
.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey))
.ToList();
var conflicts = activeTools
.GroupBy(tool => HotkeyParser.Normalize(tool.Hotkey!))
.Where(group => group.Count() > 1)
.SelectMany(group => group)
.ToHashSet();
foreach (var conflict in conflicts)
{
conflict.HotkeyStatus = "内部冲突";
log(CreateLog(LogLevel.Warning, $"快捷键内部冲突:{conflict.Hotkey}{conflict.Name}"));
}
foreach (var tool in activeTools.Where(tool => !conflicts.Contains(tool)))
{
if (!HotkeyParser.TryParse(tool.Hotkey, out var modifiers, out var key))
{
tool.HotkeyStatus = "格式无效";
log(CreateLog(LogLevel.Warning, $"快捷键格式无效:{tool.Name}{tool.Hotkey}"));
continue;
}
var id = _nextId++;
if (RegisterHotKey(_handle, id, modifiers, key))
{
_registeredHotkeys[id] = tool.Id;
tool.HotkeyStatus = "正常";
}
else
{
tool.HotkeyStatus = "系统注册失败";
log(CreateLog(LogLevel.Warning, $"快捷键注册失败:{tool.Name}{tool.Hotkey}"));
}
}
}
public void UnregisterAll()
{
if (_handle != 0)
{
foreach (var id in _registeredHotkeys.Keys.ToList())
{
UnregisterHotKey(_handle, id);
}
}
_registeredHotkeys.Clear();
}
public void Dispose()
{
UnregisterAll();
_source?.RemoveHook(WndProc);
}
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
{
if (msg == WmHotkey && _registeredHotkeys.TryGetValue(wParam.ToInt32(), out var toolId))
{
handled = true;
_ = TriggerAsync(toolId);
}
return 0;
}
private async Task TriggerAsync(string toolId)
{
if (_onTriggered is null)
{
return;
}
try
{
await _onTriggered(toolId);
}
catch (Exception ex)
{
_log?.Invoke(CreateLog(LogLevel.Error, $"快捷键触发失败:{ex.Message}"));
}
}
private static LogMessage CreateLog(LogLevel level, string message)
{
return new LogMessage
{
Level = level,
Message = message
};
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(nint hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnregisterHotKey(nint hWnd, int id);
}
public static class HotkeyParser
{
private const uint ModAlt = 0x0001;
private const uint ModControl = 0x0002;
private const uint ModShift = 0x0004;
private const uint ModWin = 0x0008;
public static string Normalize(string hotkey)
{
return string.Join("+", hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(NormalizePart)
.Where(part => part.Length > 0)
.OrderBy(GetPartOrder)
.ThenBy(part => part)
.ToArray());
}
public static bool TryParse(string? hotkey, out uint modifiers, out uint key)
{
modifiers = 0;
key = 0;
if (string.IsNullOrWhiteSpace(hotkey))
{
return false;
}
var keyPart = "";
foreach (var rawPart in hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var part = NormalizePart(rawPart);
switch (part)
{
case "Ctrl":
modifiers |= ModControl;
break;
case "Alt":
modifiers |= ModAlt;
break;
case "Shift":
modifiers |= ModShift;
break;
case "Win":
modifiers |= ModWin;
break;
default:
keyPart = part;
break;
}
}
if (modifiers == 0 || string.IsNullOrWhiteSpace(keyPart))
{
return false;
}
key = KeyToVirtualKey(keyPart);
return key != 0;
}
private static string NormalizePart(string value)
{
var part = value.Trim();
return part.ToLowerInvariant() switch
{
"control" or "ctrl" => "Ctrl",
"menu" or "alt" => "Alt",
"shift" => "Shift",
"windows" or "win" => "Win",
_ => part.Length == 1 ? part.ToUpperInvariant() : part.ToUpperInvariant().StartsWith("F", StringComparison.Ordinal) ? part.ToUpperInvariant() : part
};
}
private static int GetPartOrder(string part)
{
return part switch
{
"Ctrl" => 0,
"Alt" => 1,
"Shift" => 2,
"Win" => 3,
_ => 4
};
}
private static uint KeyToVirtualKey(string keyPart)
{
if (keyPart.Length == 1)
{
var character = keyPart[0];
if (character is >= 'A' and <= 'Z')
{
return character;
}
if (character is >= '0' and <= '9')
{
return character;
}
}
if (keyPart.StartsWith("F", StringComparison.OrdinalIgnoreCase)
&& int.TryParse(keyPart[1..], out var functionKey)
&& functionKey is >= 1 and <= 24)
{
return (uint)(0x70 + functionKey - 1);
}
return keyPart.ToLowerInvariant() switch
{
"escape" or "esc" => 0x1B,
"tab" => 0x09,
"space" => 0x20,
"enter" => 0x0D,
"backspace" => 0x08,
"delete" => 0x2E,
"insert" => 0x2D,
"home" => 0x24,
"end" => 0x23,
"pageup" => 0x21,
"pagedown" => 0x22,
"left" => 0x25,
"up" => 0x26,
"right" => 0x27,
"down" => 0x28,
_ => 0
};
}
}