Files
personal-toolbox/PersonalToolBox/ViewModels/MainViewModel.cs
home-PC 92beb46f22 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)
2026-05-09 23:07:44 +08:00

251 lines
8.3 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.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using PersonalToolBox.Helpers;
using PersonalToolBox.Models;
using PersonalToolBox.Services;
using PersonalToolBox.Views;
namespace PersonalToolBox.ViewModels;
/// <summary>
/// 主窗口 ViewModel管理 UI 状态、数据绑定和用户交互逻辑
/// </summary>
public partial class MainViewModel : ObservableObject
{
private readonly ILogService _logService;
private readonly IDataService _dataService;
private readonly IProcessExecutionService _processService;
private readonly IServiceProvider _serviceProvider;
private readonly HotKeyManager _hotKeyManager;
/// <summary>
/// 全部分类的虚拟对象
/// </summary>
private static readonly Category AllCategory = new() { Id = "", Name = "全部" };
public MainViewModel(
ILogService logService,
IDataService dataService,
IProcessExecutionService processService,
IServiceProvider serviceProvider,
HotKeyManager hotKeyManager)
{
_logService = logService;
_dataService = dataService;
_processService = processService;
_serviceProvider = serviceProvider;
_hotKeyManager = hotKeyManager;
LoadData();
}
// ───────────────────────────── 可观察属性 ─────────────────────────────
[ObservableProperty]
private string _searchText = string.Empty;
[ObservableProperty]
private Category? _selectedCategory;
public ObservableCollection<LogEntry> Logs => _logService.Logs;
public ObservableCollection<Category> Categories { get; } = new();
public ObservableCollection<ToolItem> Tools { get; } = new();
[ObservableProperty]
private ObservableCollection<ToolItem> _filteredTools = new();
[ObservableProperty]
private string _currentTheme = "Dark";
// ───────────────────────────── 命令 ─────────────────────────────
[RelayCommand]
private void ClearLogs() => _logService.Clear();
[RelayCommand]
private void ToggleTheme()
{
CurrentTheme = CurrentTheme == "Dark" ? "Light" : "Dark";
ThemeHelper.ApplyTheme(CurrentTheme);
_dataService.Config.Theme = CurrentTheme;
_dataService.Save();
_logService.Info($"已切换到{(CurrentTheme == "Dark" ? "" : "")}主题");
}
/// <summary>
/// 打开添加工具弹窗
/// </summary>
[RelayCommand]
private void AddTool()
{
var vm = _serviceProvider.GetRequiredService<ToolEditViewModel>();
var window = new ToolEditWindow(vm);
window.ShowDialog();
if (vm.Saved)
{
RefreshData();
}
}
/// <summary>
/// 打开编辑工具弹窗
/// </summary>
[RelayCommand]
private void EditTool(ToolItem tool)
{
if (tool == null) return;
// 创建编辑 ViewModel需要新建实例DI 容器无法区分参数)
var dataService = _serviceProvider.GetRequiredService<IDataService>();
var logService = _serviceProvider.GetRequiredService<ILogService>();
var editVm = new ToolEditViewModel(dataService, logService, tool);
var window = new ToolEditWindow(editVm);
window.ShowDialog();
if (editVm.Saved)
{
RefreshData();
}
}
/// <summary>
/// 执行工具(双击卡片或右键菜单)
/// </summary>
[RelayCommand]
private void ExecuteTool(ToolItem? tool)
{
if (tool == null) return;
_processService.Execute(tool);
}
// ───────────────────────────── 分类管理命令 ─────────────────────────────
/// <summary>
/// 添加分类
/// </summary>
[RelayCommand]
private void AddCategory()
{
var vm = _serviceProvider.GetRequiredService<CategoryEditViewModel>();
var window = new CategoryEditWindow(vm);
window.ShowDialog();
if (vm.Saved) RefreshData();
}
/// <summary>
/// 编辑分类(右键菜单触发)
/// </summary>
[RelayCommand]
private void EditCategory(Category? category)
{
if (category == null || string.IsNullOrEmpty(category.Id)) return;
var dataService = _serviceProvider.GetRequiredService<IDataService>();
var logService = _serviceProvider.GetRequiredService<ILogService>();
var editVm = new CategoryEditViewModel(dataService, logService, category);
var window = new CategoryEditWindow(editVm);
window.ShowDialog();
if (editVm.Saved) RefreshData();
}
/// <summary>
/// 删除分类,其下所有工具移入"全部"
/// </summary>
[RelayCommand]
private void DeleteCategory(Category? category)
{
if (category == null || string.IsNullOrEmpty(category.Id)) return;
if (MessageBox.Show(
$"确定删除分类 \"{category.Name}\" 吗?\n该分类下的所有工具将移入「全部」。",
"确认删除", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
return;
// 将属于该分类的工具设为"全部"
foreach (var tool in _dataService.Config.Tools.Where(t => t.CategoryId == category.Id))
tool.CategoryId = string.Empty;
// 从配置中移除分类
_dataService.Config.Categories.Remove(category);
_dataService.Save();
_logService.Info($"已删除分类: {category.Name}");
RefreshData();
}
// ───────────────────────────── 数据刷新 ─────────────────────────────
/// <summary>
/// 从配置重新加载数据并刷新 UI
/// </summary>
public void RefreshData()
{
var previousCategoryId = SelectedCategory?.Id;
Categories.Clear();
Categories.Add(AllCategory);
foreach (var cat in _dataService.Config.Categories)
Categories.Add(cat);
Tools.Clear();
foreach (var tool in _dataService.Config.Tools)
Tools.Add(tool);
// 恢复之前选中的分类(若仍存在),否则选中"全部"
SelectedCategory = Categories.FirstOrDefault(c => c.Id == previousCategoryId) ?? AllCategory;
ApplyFilter();
// 工具列表变更后重新注册快捷键
RegisterAllHotKeys();
}
// ───────────────────────────── 快捷键 ─────────────────────────────
/// <summary>
/// 注册所有工具的全局快捷键(由 MainWindow.OnSourceInitialized 及数据变更后调用)
/// </summary>
public void RegisterAllHotKeys()
{
_hotKeyManager.RegisterAll(Tools);
}
private void LoadData()
{
RefreshData();
SelectedCategory = AllCategory;
CurrentTheme = _dataService.Config.Theme;
ThemeHelper.ApplyTheme(CurrentTheme);
}
// ───────────────────────────── 过滤逻辑 ─────────────────────────────
partial void OnSearchTextChanged(string value) => ApplyFilter();
partial void OnSelectedCategoryChanged(Category? value) => ApplyFilter();
private void ApplyFilter()
{
var filtered = Tools.AsEnumerable();
if (SelectedCategory != null && !string.IsNullOrEmpty(SelectedCategory.Id))
filtered = filtered.Where(t => t.CategoryId == SelectedCategory.Id);
if (!string.IsNullOrWhiteSpace(SearchText))
filtered = filtered.Where(t =>
t.Name.Contains(SearchText, System.StringComparison.OrdinalIgnoreCase));
FilteredTools = new ObservableCollection<ToolItem>(filtered);
}
}