feat: 搭建工具箱核心服务层

新增工具、分类、组合、自动运行和日志等基础模型。

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

View File

@@ -0,0 +1,285 @@
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
};
}
}