Phase 5: 系统托盘与开机自启
- 系统托盘: NotifyIcon 常驻, 程序化生成 32x32 图标, 左键切换显示/隐藏, 右键菜单(显示主界面/设置/彻底退出) - 窗口关闭拦截: OnClosing 取消关闭改为 Hide(), 退出菜单执行真实 Shutdown() - 开机自启: AutoStartHelper 写入/删除 HKCU\...\Run 注册表项, UI 开关按钮显示 ✔/⊗ 状态 - 静默启动: -autostart 参数下 mainWindow.Hide() 直接最小化到托盘 - 类型冲突解决: ImplicitUsings=disable + GlobalUsings.cs 统一全局 using, 消除 WPF/WinForms 共享类型冲突 - 测试: 75 tests total (含 AutoStartHelper 2 tests)
This commit is contained in:
22
PersonalToolBox.Tests/Helpers/AutoStartHelperTests.cs
Normal file
22
PersonalToolBox.Tests/Helpers/AutoStartHelperTests.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using PersonalToolBox.Helpers;
|
||||||
|
|
||||||
|
namespace PersonalToolBox.Tests.Helpers;
|
||||||
|
|
||||||
|
public class AutoStartHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SetAutoStart_Disable_NoExceptions()
|
||||||
|
{
|
||||||
|
// 确保不抛异常
|
||||||
|
AutoStartHelper.SetAutoStart(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetAutoStart_Enable_NoExceptions()
|
||||||
|
{
|
||||||
|
// 确保不抛异常
|
||||||
|
AutoStartHelper.SetAutoStart(true);
|
||||||
|
// 清理
|
||||||
|
AutoStartHelper.SetAutoStart(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace PersonalToolBox;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// WPF 应用程序入口,负责依赖注入容器初始化和启动主窗口
|
/// WPF 应用程序入口,负责依赖注入容器初始化和启动主窗口
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : System.Windows.Application
|
||||||
{
|
{
|
||||||
public static IServiceProvider Services { get; private set; } = null!;
|
public static IServiceProvider Services { get; private set; } = null!;
|
||||||
|
|
||||||
@@ -49,7 +49,16 @@ public partial class App : Application
|
|||||||
dataService.Load();
|
dataService.Load();
|
||||||
|
|
||||||
var mainWindow = Services.GetRequiredService<MainWindow>();
|
var mainWindow = Services.GetRequiredService<MainWindow>();
|
||||||
mainWindow.Show();
|
|
||||||
|
// -autostart 参数:开机自启时隐藏窗口
|
||||||
|
if (e.Args.Contains("-autostart", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mainWindow.Hide();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
20
PersonalToolBox/GlobalUsings.cs
Normal file
20
PersonalToolBox/GlobalUsings.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Collections.ObjectModel;
|
||||||
|
global using System.Collections.Specialized;
|
||||||
|
global using System.ComponentModel;
|
||||||
|
global using System.Diagnostics;
|
||||||
|
global using System.IO;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Runtime.InteropServices;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
|
global using System.Windows;
|
||||||
|
global using System.Windows.Controls;
|
||||||
|
global using System.Windows.Data;
|
||||||
|
global using System.Windows.Input;
|
||||||
|
global using System.Windows.Interop;
|
||||||
|
global using System.Windows.Media;
|
||||||
|
global using System.Windows.Threading;
|
||||||
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
|
global using Microsoft.Win32;
|
||||||
55
PersonalToolBox/Helpers/AutoStartHelper.cs
Normal file
55
PersonalToolBox/Helpers/AutoStartHelper.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace PersonalToolBox.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开机自启动管理,通过写入 Windows 注册表 Run 键实现
|
||||||
|
/// </summary>
|
||||||
|
public static class AutoStartHelper
|
||||||
|
{
|
||||||
|
private const string RunKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
private const string AppName = "PersonalToolBox";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查当前是否已设置开机自启动
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsAutoStartEnabled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath);
|
||||||
|
return key?.GetValue(AppName) != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置或取消开机自启动
|
||||||
|
/// </summary>
|
||||||
|
public static void SetAutoStart(bool enable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, writable: true);
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
var exePath = Environment.ProcessPath;
|
||||||
|
if (exePath != null)
|
||||||
|
key.SetValue(AppName, $"\"{exePath}\" -autostart");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
key.DeleteValue(AppName, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 注册表操作失败时静默忽略(可能是权限不足)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,9 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ public partial class MainViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _currentTheme = "Dark";
|
private string _currentTheme = "Dark";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isAutoStart;
|
||||||
|
|
||||||
// ───────────────────────────── 命令 ─────────────────────────────
|
// ───────────────────────────── 命令 ─────────────────────────────
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -78,6 +81,27 @@ public partial class MainViewModel : ObservableObject
|
|||||||
_logService.Info($"已切换到{(CurrentTheme == "Dark" ? "暗黑" : "明亮")}主题");
|
_logService.Info($"已切换到{(CurrentTheme == "Dark" ? "暗黑" : "明亮")}主题");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换开机自启动
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleAutoStart()
|
||||||
|
{
|
||||||
|
IsAutoStart = !IsAutoStart;
|
||||||
|
AutoStartHelper.SetAutoStart(IsAutoStart);
|
||||||
|
_dataService.Config.AutoStart = IsAutoStart;
|
||||||
|
_dataService.Save();
|
||||||
|
_logService.Info(IsAutoStart ? "已启用开机自启动" : "已关闭开机自启动");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 供 MainWindow 外部调用,用于输出提示信息
|
||||||
|
/// </summary>
|
||||||
|
public void LogServiceMessage(string message)
|
||||||
|
{
|
||||||
|
_logService.Info(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 打开添加工具弹窗
|
/// 打开添加工具弹窗
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -225,6 +249,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
|
|
||||||
SelectedCategory = AllCategory;
|
SelectedCategory = AllCategory;
|
||||||
CurrentTheme = _dataService.Config.Theme;
|
CurrentTheme = _dataService.Config.Theme;
|
||||||
|
IsAutoStart = _dataService.Config.AutoStart;
|
||||||
ThemeHelper.ApplyTheme(CurrentTheme);
|
ThemeHelper.ApplyTheme(CurrentTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
<RowDefinition Height="50"/>
|
<RowDefinition Height="50"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
<RowDefinition Height="42"/>
|
<RowDefinition Height="42"/>
|
||||||
|
<RowDefinition Height="42"/>
|
||||||
<RowDefinition Height="45"/>
|
<RowDefinition Height="45"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
@@ -160,6 +161,28 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button Grid.Row="3"
|
<Button Grid.Row="3"
|
||||||
|
Command="{Binding ToggleAutoStartCommand}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="1"
|
||||||
|
BorderBrush="{DynamicResource Theme.CardBorder}"
|
||||||
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
FontSize="12"
|
||||||
|
Height="35" Margin="10,0,10,3"
|
||||||
|
Cursor="Hand">
|
||||||
|
<Button.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="⊗ 开机自启: 关"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsAutoStart}" Value="True">
|
||||||
|
<Setter Property="Text" Value="✔ 开机自启: 开"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Resources>
|
||||||
|
<TextBlock VerticalAlignment="Center" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Grid.Row="4"
|
||||||
Command="{Binding ToggleThemeCommand}"
|
Command="{Binding ToggleThemeCommand}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Forms;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using PersonalToolBox.Helpers;
|
using PersonalToolBox.Helpers;
|
||||||
using PersonalToolBox.ViewModels;
|
using PersonalToolBox.ViewModels;
|
||||||
@@ -13,6 +16,8 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
private readonly MainViewModel _viewModel;
|
private readonly MainViewModel _viewModel;
|
||||||
private readonly HotKeyManager _hotKeyManager;
|
private readonly HotKeyManager _hotKeyManager;
|
||||||
|
private NotifyIcon? _notifyIcon;
|
||||||
|
private bool _canActuallyClose;
|
||||||
|
|
||||||
public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager)
|
public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager)
|
||||||
{
|
{
|
||||||
@@ -23,6 +28,8 @@ public partial class MainWindow : Window
|
|||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
|
|
||||||
viewModel.Logs.CollectionChanged += OnLogsCollectionChanged;
|
viewModel.Logs.CollectionChanged += OnLogsCollectionChanged;
|
||||||
|
|
||||||
|
CreateTrayIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -34,17 +41,108 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
var hwndSource = (HwndSource)PresentationSource.FromVisual(this)!;
|
var hwndSource = (HwndSource)PresentationSource.FromVisual(this)!;
|
||||||
_hotKeyManager.Initialize(hwndSource.Handle);
|
_hotKeyManager.Initialize(hwndSource.Handle);
|
||||||
|
|
||||||
// 挂载 WndProc 钩子,拦截 WM_HOTKEY 消息
|
|
||||||
hwndSource.AddHook(WndProcHook);
|
hwndSource.AddHook(WndProcHook);
|
||||||
|
|
||||||
// 注册所有已配置快捷键的工具
|
|
||||||
_viewModel.RegisterAllHotKeys();
|
_viewModel.RegisterAllHotKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────── 系统托盘 ────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 窗口消息钩子
|
/// 创建系统托盘图标及右键菜单
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
private void CreateTrayIcon()
|
||||||
|
{
|
||||||
|
_notifyIcon = new NotifyIcon
|
||||||
|
{
|
||||||
|
Icon = CreateAppIcon(),
|
||||||
|
Visible = true,
|
||||||
|
Text = "个人工具箱"
|
||||||
|
};
|
||||||
|
|
||||||
|
_notifyIcon.MouseClick += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButtons.Left)
|
||||||
|
ToggleVisibility();
|
||||||
|
};
|
||||||
|
|
||||||
|
var menu = new ContextMenuStrip();
|
||||||
|
menu.Items.Add("显示主界面", null, (_, _) => ShowMainWindow());
|
||||||
|
menu.Items.Add("设置", null, (_, _) => OpenSettings());
|
||||||
|
menu.Items.Add(new ToolStripSeparator());
|
||||||
|
menu.Items.Add("彻底退出", null, (_, _) => ExitApplication());
|
||||||
|
_notifyIcon.ContextMenuStrip = menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 程序化生成 32x32 托盘图标
|
||||||
|
/// </summary>
|
||||||
|
private static System.Drawing.Icon CreateAppIcon()
|
||||||
|
{
|
||||||
|
var bmp = new System.Drawing.Bitmap(32, 32);
|
||||||
|
using var g = System.Drawing.Graphics.FromImage(bmp);
|
||||||
|
g.Clear(System.Drawing.Color.FromArgb(30, 30, 46));
|
||||||
|
using var font = new System.Drawing.Font(System.Drawing.FontFamily.GenericSansSerif, 16, System.Drawing.FontStyle.Bold);
|
||||||
|
using var brush = new System.Drawing.SolidBrush(System.Drawing.Color.FromArgb(137, 180, 250));
|
||||||
|
g.DrawString("T", font, brush, new System.Drawing.PointF(6, 3));
|
||||||
|
return System.Drawing.Icon.FromHandle(bmp.GetHicon());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 左键托盘图标:切换窗口显示/隐藏
|
||||||
|
/// </summary>
|
||||||
|
private void ToggleVisibility()
|
||||||
|
{
|
||||||
|
if (IsVisible)
|
||||||
|
Hide();
|
||||||
|
else
|
||||||
|
ShowMainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示主窗口并置前
|
||||||
|
/// </summary>
|
||||||
|
private void ShowMainWindow()
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
WindowState = WindowState.Normal;
|
||||||
|
Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开设置(当前无额外设置界面,提示使用内置功能)
|
||||||
|
/// </summary>
|
||||||
|
private void OpenSettings()
|
||||||
|
{
|
||||||
|
ShowMainWindow();
|
||||||
|
_viewModel.LogServiceMessage("设置功能可通过左侧栏管理分类和主题切换");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 彻底退出程序
|
||||||
|
/// </summary>
|
||||||
|
public void ExitApplication()
|
||||||
|
{
|
||||||
|
_canActuallyClose = true;
|
||||||
|
_notifyIcon?.Dispose();
|
||||||
|
_hotKeyManager.Dispose();
|
||||||
|
System.Windows.Application.Current.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 点击关闭按钮时隐藏窗口而非退出
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnClosing(CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_canActuallyClose)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
base.OnClosing(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────── 其他 ────────────────
|
||||||
|
|
||||||
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
{
|
{
|
||||||
handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam);
|
handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam);
|
||||||
|
|||||||
Reference in New Issue
Block a user