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

单实例限制:通过命名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.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using PersonalToolBox.Services;
@@ -17,6 +19,17 @@ public partial class App : System.Windows.Application
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"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()
{
// 监听未处理的异常,写入文件日志
@@ -36,6 +49,15 @@ public partial class App : System.Windows.Application
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
{
base.OnStartup(e);

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

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

View File

@@ -178,6 +178,7 @@
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="45"/>
<RowDefinition Height="45"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
@@ -277,7 +278,7 @@
BorderBrush="{DynamicResource Theme.CardBorder}"
Foreground="{DynamicResource Theme.Foreground}"
FontSize="12"
Height="35" Margin="10,0,10,10"
Height="35" Margin="10,0,10,3"
Cursor="Hand">
<StackPanel Orientation="Horizontal">
<TextBlock Text="◐" FontSize="16" Margin="0,0,8,0"
@@ -285,6 +286,28 @@
<TextBlock Text="切换主题" VerticalAlignment="Center"/>
</StackPanel>
</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>
</Border>

View File

@@ -3,10 +3,13 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using FontAwesome.Sharp;
using PersonalToolBox.Helpers;
using PersonalToolBox.ViewModels;
@@ -18,8 +21,14 @@ public partial class MainWindow : Window
private readonly MainViewModel _viewModel;
private readonly HotKeyManager _hotKeyManager;
private NotifyIcon? _notifyIcon;
private ToolStripMenuItem? _hotKeyToggleMenuItem;
private bool _canActuallyClose;
[DllImport("user32.dll")]
private static extern uint RegisterWindowMessage(string lpString);
private readonly int _showMainMsg;
public MainWindow(MainViewModel viewModel, HotKeyManager hotKeyManager)
{
InitializeComponent();
@@ -28,7 +37,18 @@ public partial class MainWindow : Window
_hotKeyManager = hotKeyManager;
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.PropertyChanged += OnViewModelPropertyChanged;
CreateTrayIcon();
}
@@ -53,9 +73,14 @@ public partial class MainWindow : Window
/// </summary>
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
{
Icon = CreateAppIcon(),
Icon = trayIcon,
Visible = true,
Text = "个人工具箱"
};
@@ -68,26 +93,17 @@ public partial class MainWindow : Window
var menu = new ContextMenuStrip();
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("彻底退出", 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>
@@ -109,15 +125,6 @@ public partial class MainWindow : Window
Activate();
}
/// <summary>
/// 打开设置(当前无额外设置界面,提示使用内置功能)
/// </summary>
private void OpenSettings()
{
ShowMainWindow();
_viewModel.LogServiceMessage("设置功能可通过左侧栏管理分类和主题切换");
}
/// <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)
{
if (msg == _showMainMsg)
{
Dispatcher.Invoke(() => ShowMainWindow());
handled = true;
return IntPtr.Zero;
}
handled = _hotKeyManager.TryHandleMessage(msg, wParam, lParam);
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)
{
if (e.Action == NotifyCollectionChangedAction.Add && LogListBox.Items.Count > 0)