添加单实例限制、应用图标、移除托盘设置菜单、快捷键暂停/恢复功能

单实例限制:通过命名Mutex和窗口消息广播确保只能运行一个实例,再次启动时唤起已有窗口

应用图标:exe文件嵌入app.ico,托盘和窗口图标统一使用该图标文件

移除托盘右键菜单中与显示主界面重复的设置选项

快捷键暂停/恢复:托盘菜单和主界面侧边栏均添加切换按钮,通过MainViewModel.IsHotKeyEnabled双向同步
This commit is contained in:
2026-05-10 14:10:52 +08:00
parent b715904439
commit f33c89d2c4
8 changed files with 116 additions and 26 deletions

View File

@@ -1,4 +1,6 @@
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows; using System.Windows;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PersonalToolBox.Services; using PersonalToolBox.Services;
@@ -17,6 +19,17 @@ public partial class App : System.Windows.Application
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"PersonalToolBox", "crash.log"); "PersonalToolBox", "crash.log");
private const string AppMutexName = "PersonalToolBox_SingleInstance_8E2F4A1C";
private static Mutex? _mutex;
[DllImport("user32.dll")]
private static extern uint RegisterWindowMessage(string lpString);
[DllImport("user32.dll")]
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xFFFF);
public App() public App()
{ {
// 监听未处理的异常,写入文件日志 // 监听未处理的异常,写入文件日志
@@ -36,6 +49,15 @@ public partial class App : System.Windows.Application
protected override async void OnStartup(StartupEventArgs e) protected override async void OnStartup(StartupEventArgs e)
{ {
_mutex = new Mutex(true, AppMutexName, out bool createdNew);
if (!createdNew)
{
uint msg = RegisterWindowMessage("PersonalToolBox_ShowMain");
PostMessage(HWND_BROADCAST, msg, IntPtr.Zero, IntPtr.Zero);
Shutdown();
return;
}
try try
{ {
base.OnStartup(e); base.OnStartup(e);

View File

@@ -16,6 +16,8 @@ public class HotKeyManager
private readonly ILogService _logService; private readonly ILogService _logService;
private readonly IProcessExecutionService _processService; private readonly IProcessExecutionService _processService;
public bool IsEnabled { get; set; } = true;
// ──────────────── Win32 API ──────────────── // ──────────────── Win32 API ────────────────
private const int WM_HOTKEY = 0x0312; private const int WM_HOTKEY = 0x0312;
@@ -103,6 +105,7 @@ public class HotKeyManager
public bool TryHandleMessage(int msg, IntPtr wParam, IntPtr lParam) public bool TryHandleMessage(int msg, IntPtr wParam, IntPtr lParam)
{ {
if (msg != WM_HOTKEY) return false; if (msg != WM_HOTKEY) return false;
if (!IsEnabled) return true;
int id = wParam.ToInt32(); int id = wParam.ToInt32();
if (_hotkeyMap.TryGetValue(id, out var tool)) if (_hotkeyMap.TryGetValue(id, out var tool))

View File

@@ -7,6 +7,7 @@
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -16,4 +17,10 @@
<PackageReference Include="System.Text.Json" Version="10.0.7" /> <PackageReference Include="System.Text.Json" Version="10.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Resources\app.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -66,6 +66,9 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private bool _isAutoStart; private bool _isAutoStart;
[ObservableProperty]
private bool _isHotKeyEnabled = true;
// ───────────────────────────── 命令 ───────────────────────────── // ───────────────────────────── 命令 ─────────────────────────────
[RelayCommand] [RelayCommand]
@@ -94,6 +97,15 @@ public partial class MainViewModel : ObservableObject
_logService.Info(IsAutoStart ? "已启用开机自启动" : "已关闭开机自启动"); _logService.Info(IsAutoStart ? "已启用开机自启动" : "已关闭开机自启动");
} }
[RelayCommand]
private void ToggleHotKey()
{
IsHotKeyEnabled = !IsHotKeyEnabled;
_hotKeyManager.IsEnabled = IsHotKeyEnabled;
var status = IsHotKeyEnabled ? "已恢复" : "已暂停";
_logService.Info($"{status}快捷键功能");
}
/// <summary> /// <summary>
/// 供 MainWindow 外部调用,用于输出提示信息 /// 供 MainWindow 外部调用,用于输出提示信息
/// </summary> /// </summary>

View File

@@ -178,6 +178,7 @@
<RowDefinition Height="42"/> <RowDefinition Height="42"/>
<RowDefinition Height="42"/> <RowDefinition Height="42"/>
<RowDefinition Height="45"/> <RowDefinition Height="45"/>
<RowDefinition Height="45"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ListBox Grid.Row="0" <ListBox Grid.Row="0"
@@ -277,7 +278,7 @@
BorderBrush="{DynamicResource Theme.CardBorder}" BorderBrush="{DynamicResource Theme.CardBorder}"
Foreground="{DynamicResource Theme.Foreground}" Foreground="{DynamicResource Theme.Foreground}"
FontSize="12" FontSize="12"
Height="35" Margin="10,0,10,10" Height="35" Margin="10,0,10,3"
Cursor="Hand"> Cursor="Hand">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="◐" FontSize="16" Margin="0,0,8,0" <TextBlock Text="◐" FontSize="16" Margin="0,0,8,0"
@@ -285,6 +286,28 @@
<TextBlock Text="切换主题" VerticalAlignment="Center"/> <TextBlock Text="切换主题" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Grid.Row="4"
Command="{Binding ToggleHotKeyCommand}"
Background="Transparent"
BorderThickness="1"
BorderBrush="{DynamicResource Theme.CardBorder}"
Foreground="{DynamicResource Theme.Foreground}"
FontSize="12"
Height="35" Margin="10,0,10,10"
Cursor="Hand">
<Button.Resources>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="⏸ 暂停快捷键"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsHotKeyEnabled}" Value="False">
<Setter Property="Text" Value="▶ 恢复快捷键"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Resources>
<TextBlock VerticalAlignment="Center" TextAlignment="Center"/>
</Button>
</Grid> </Grid>
</Border> </Border>

View File

@@ -3,10 +3,13 @@ using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Drawing; using System.Drawing;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media.Imaging;
using FontAwesome.Sharp; using FontAwesome.Sharp;
using PersonalToolBox.Helpers; using PersonalToolBox.Helpers;
using PersonalToolBox.ViewModels; using PersonalToolBox.ViewModels;
@@ -18,8 +21,14 @@ 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 NotifyIcon? _notifyIcon;
private ToolStripMenuItem? _hotKeyToggleMenuItem;
private bool _canActuallyClose; private bool _canActuallyClose;
[DllImport("user32.dll")]
private static extern uint RegisterWindowMessage(string lpString);
private readonly int _showMainMsg;
public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager) public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager)
{ {
InitializeComponent(); InitializeComponent();
@@ -28,7 +37,18 @@ public partial class MainWindow : Window
_hotKeyManager = hotKeyManager; _hotKeyManager = hotKeyManager;
DataContext = viewModel; DataContext = viewModel;
_showMainMsg = (int)RegisterWindowMessage("PersonalToolBox_ShowMain");
var iconPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "app.ico");
if (File.Exists(iconPath))
{
using var stream = File.OpenRead(iconPath);
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
this.Icon = decoder.Frames[0];
}
viewModel.Logs.CollectionChanged += OnLogsCollectionChanged; viewModel.Logs.CollectionChanged += OnLogsCollectionChanged;
viewModel.PropertyChanged += OnViewModelPropertyChanged;
CreateTrayIcon(); CreateTrayIcon();
} }
@@ -53,9 +73,14 @@ public partial class MainWindow : Window
/// </summary> /// </summary>
private void CreateTrayIcon() private void CreateTrayIcon()
{ {
var iconPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "app.ico");
var trayIcon = File.Exists(iconPath)
? new System.Drawing.Icon(iconPath)
: System.Drawing.SystemIcons.Application;
_notifyIcon = new NotifyIcon _notifyIcon = new NotifyIcon
{ {
Icon = CreateAppIcon(), Icon = trayIcon,
Visible = true, Visible = true,
Text = "个人工具箱" Text = "个人工具箱"
}; };
@@ -68,26 +93,17 @@ public partial class MainWindow : Window
var menu = new ContextMenuStrip(); var menu = new ContextMenuStrip();
menu.Items.Add("显示主界面", null, (_, _) => ShowMainWindow()); menu.Items.Add("显示主界面", null, (_, _) => ShowMainWindow());
menu.Items.Add("设置", null, (_, _) => OpenSettings());
_hotKeyToggleMenuItem = new ToolStripMenuItem(
_viewModel.IsHotKeyEnabled ? "暂停快捷键功能" : "恢复快捷键功能");
_hotKeyToggleMenuItem.Click += (_, _) => _viewModel.ToggleHotKeyCommand.Execute(null);
menu.Items.Add(_hotKeyToggleMenuItem);
menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("彻底退出", null, (_, _) => ExitApplication()); menu.Items.Add("彻底退出", null, (_, _) => ExitApplication());
_notifyIcon.ContextMenuStrip = menu; _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>
/// 左键托盘图标:切换窗口显示/隐藏 /// 左键托盘图标:切换窗口显示/隐藏
/// </summary> /// </summary>
@@ -109,15 +125,6 @@ public partial class MainWindow : Window
Activate(); Activate();
} }
/// <summary>
/// 打开设置(当前无额外设置界面,提示使用内置功能)
/// </summary>
private void OpenSettings()
{
ShowMainWindow();
_viewModel.LogServiceMessage("设置功能可通过左侧栏管理分类和主题切换");
}
/// <summary> /// <summary>
/// 彻底退出程序 /// 彻底退出程序
/// </summary> /// </summary>
@@ -146,10 +153,26 @@ public partial class MainWindow : Window
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)
{ {
if (msg == _showMainMsg)
{
Dispatcher.Invoke(() => ShowMainWindow());
handled = true;
return IntPtr.Zero;
}
handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam); handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam);
return IntPtr.Zero; return IntPtr.Zero;
} }
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsHotKeyEnabled) && _hotKeyToggleMenuItem != null)
{
bool enabled = _viewModel.IsHotKeyEnabled;
_hotKeyToggleMenuItem.Text = enabled ? "暂停快捷键功能" : "恢复快捷键功能";
}
}
private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (e.Action == NotifyCollectionChangedAction.Add && LogListBox.Items.Count > 0) if (e.Action == NotifyCollectionChangedAction.Add && LogListBox.Items.Count > 0)

BIN
app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB