feat: 实现工具箱主界面与编辑流程

新增主窗口 MVVM、分类列表、工具卡片网格、底部信息区、工具编辑窗口、组合编辑窗口和设置窗口。

界面中的主要输入、按钮、卡片和管理项均提供悬浮帮助说明,并接入搜索、启动、增删改、自动运行、快捷键和配置管理入口。
This commit is contained in:
2026-05-27 13:55:47 +08:00
parent fde1c6bc0f
commit 9e43ddbcf0
15 changed files with 2495 additions and 18 deletions

View File

@@ -1,9 +1,32 @@
<Application x:Class="ToolboxApp.App" <Application x:Class="ToolboxApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ToolboxApp"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F7F8FA" />
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="BorderBrushSoft" Color="#D9DEE7" />
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#172033" />
<SolidColorBrush x:Key="SecondaryTextBrush" Color="#5C667A" />
<Style TargetType="Button">
<Setter Property="MinHeight" Value="32" />
<Setter Property="Padding" Value="12,4" />
</Style>
<Style TargetType="TextBox">
<Setter Property="Padding" Value="8,4" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style TargetType="ComboBox">
<Setter Property="Padding" Value="8,4" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,12 +1,295 @@
<Window x:Class="ToolboxApp.MainWindow" <Window x:Class="ToolboxApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ToolboxApp" xmlns:vm="clr-namespace:ToolboxApp.ViewModels"
mc:Ignorable="d" mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"> Title="个人工具箱"
<Grid> Height="720"
Width="1120"
MinHeight="620"
MinWidth="960"
Background="{StaticResource AppBackgroundBrush}"
Loaded="Window_OnLoaded"
Closing="Window_OnClosing">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Background="{StaticResource PanelBackgroundBrush}"
BorderBrush="{StaticResource BorderBrushSoft}"
BorderThickness="1"
CornerRadius="8"
Padding="12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="SearchTextBox"
Width="360"
HorizontalAlignment="Left"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
ToolTip="搜索工具名称、说明、分类、路径或网址。按 Ctrl + F 可快速聚焦。"
KeyDown="SearchTextBox_OnKeyDown" />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Content="添加本地"
Margin="8,0,0,0"
Command="{Binding AddLocalToolCommand}"
ToolTip="添加 exe、lnk、bat、cmd、文件或文件夹等本地工具。" />
<Button Content="添加网址"
Margin="8,0,0,0"
Command="{Binding AddUrlToolCommand}"
ToolTip="添加使用默认浏览器打开的网址工具。" />
<Button Content="添加组合"
Margin="8,0,0,0"
Command="{Binding AddCombinationCommand}"
ToolTip="创建由多个工具或组合组成的一键启动组合。" />
<Button Content="启动"
Margin="8,0,0,0"
Command="{Binding LaunchSelectedCommand}"
ToolTip="启动当前选中的工具。也可双击卡片或按 Enter。" />
<Button Content="编辑"
Margin="8,0,0,0"
Command="{Binding EditSelectedCommand}"
ToolTip="编辑当前选中的工具或组合。" />
<Button Content="删除"
Margin="8,0,0,0"
Command="{Binding DeleteSelectedCommand}"
ToolTip="删除当前选中的工具。删除前会确认。" />
<Button Content="设置"
Margin="8,0,0,0"
Command="{Binding OpenSettingsCommand}"
ToolTip="打开常规、自动运行、快捷键和数据设置。" />
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1" Margin="0,12,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="230" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Background="{StaticResource PanelBackgroundBrush}"
BorderBrush="{StaticResource BorderBrushSoft}"
BorderThickness="1"
CornerRadius="8"
Padding="12">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="分类"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource PrimaryTextBrush}"
ToolTip="一级分类列表,工具只能属于一个分类。" />
<ListBox Grid.Row="1"
Margin="0,10,0,10"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
BorderThickness="0"
ToolTip="单击切换分类;搜索时会跨分类显示结果。">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"
Padding="8,7"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2">
<Button Content="新建分类"
Command="{Binding AddCategoryCommand}"
ToolTip="创建一个新的一级分类。" />
<Button Content="重命名分类"
Margin="0,8,0,0"
Command="{Binding RenameCategoryCommand}"
ToolTip="重命名当前选中的分类。" />
<Button Content="删除分类"
Margin="0,8,0,0"
Command="{Binding DeleteCategoryCommand}"
ToolTip="删除当前分类,并把其中工具移动到未分类。" />
</StackPanel>
</Grid>
</Border>
<Border Grid.Column="2"
Background="{StaticResource PanelBackgroundBrush}"
BorderBrush="{StaticResource BorderBrushSoft}"
BorderThickness="1"
CornerRadius="8"
Padding="12">
<ListBox x:Name="ToolsListBox"
ItemsSource="{Binding Tools}"
SelectedItem="{Binding SelectedTool}"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
MouseDoubleClick="ToolsListBox_OnMouseDoubleClick"
PreviewMouseRightButtonDown="ToolsListBox_OnPreviewMouseRightButtonDown"
KeyDown="ToolsListBox_OnKeyDown"
ToolTip="单击选中卡片,双击启动,右键打开管理菜单。">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ToolCardViewModel}">
<Border Width="210"
Height="146"
Margin="6"
Padding="12"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrushSoft}"
Background="#FBFCFE"
ToolTip="{Binding DetailText}">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="启动"
ToolTip="启动该工具或组合。"
Click="ContextLaunch_OnClick" />
<MenuItem Header="编辑"
ToolTip="编辑该工具或组合。"
Click="ContextEdit_OnClick" />
<MenuItem Header="删除"
ToolTip="删除该工具。"
Click="ContextDelete_OnClick" />
</ContextMenu>
</Border.ContextMenu>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel>
<Border Width="42"
Height="32"
CornerRadius="6"
Background="#E8F0FF"
DockPanel.Dock="Left"
ToolTip="工具图标占位;后续会扩展自动图标和图标库。">
<TextBlock Text="{Binding IconText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{StaticResource PrimaryBrush}" />
</Border>
<TextBlock Text="{Binding TypeLabel}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{StaticResource SecondaryTextBrush}"
ToolTip="工具类型标记。" />
</DockPanel>
<TextBlock Grid.Row="1"
Text="{Binding Name}"
Margin="0,10,0,0"
FontSize="15"
FontWeight="SemiBold"
Foreground="{StaticResource PrimaryTextBrush}"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Name}" />
<TextBlock Grid.Row="2"
Text="{Binding Description}"
Margin="0,6,0,0"
Foreground="{StaticResource SecondaryTextBrush}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Description}" />
<ItemsControl Grid.Row="3"
ItemsSource="{Binding StatusBadges}"
Margin="0,8,0,0"
ToolTip="状态标记:路径失效、快捷键、自动运行或管理员。">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#EDF2F7"
CornerRadius="4"
Padding="5,2"
Margin="0,0,5,4">
<TextBlock Text="{Binding}"
FontSize="11"
Foreground="{StaticResource SecondaryTextBrush}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
<Expander Grid.Row="2"
Header="底部信息"
IsExpanded="{Binding IsLogPanelExpanded}"
ToolTip="显示启动、组合、自动运行、导入导出和错误信息。">
<Border Background="{StaticResource PanelBackgroundBrush}"
BorderBrush="{StaticResource BorderBrushSoft}"
BorderThickness="1"
CornerRadius="8"
Padding="10"
Margin="0,8,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="120" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="复制"
Width="70"
Command="{Binding CopyLogsCommand}"
ToolTip="复制当前底部信息区内容。" />
<Button Content="清空"
Width="70"
Margin="8,0,0,0"
Command="{Binding ClearLogsCommand}"
ToolTip="清空本次运行的底部信息。" />
</StackPanel>
<ListBox Grid.Row="1"
Margin="0,8,0,0"
ItemsSource="{Binding Logs}"
BorderThickness="0"
ToolTip="普通错误不会打断操作,会显示在这里。">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayText}"
FontFamily="Consolas"
FontSize="12"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</Expander>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,23 +1,202 @@
using System.Text; using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using ToolboxApp.Models;
using System.Windows.Navigation; using ToolboxApp.Services;
using System.Windows.Shapes; using ToolboxApp.ViewModels;
using MessageBox = System.Windows.MessageBox;
namespace ToolboxApp; namespace ToolboxApp;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly MainWindowViewModel _viewModel;
private readonly HotkeyService _hotkeyService = new();
private TrayService? _trayService;
private bool _initialized;
private bool _exitRequested;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
_viewModel = new MainWindowViewModel();
_viewModel.HotkeysRefreshRequested += (_, _) => RegisterHotkeys();
DataContext = _viewModel;
}
private async void Window_OnLoaded(object sender, RoutedEventArgs e)
{
if (_initialized)
{
return;
}
_initialized = true;
await _viewModel.InitializeAsync(this);
Width = _viewModel.Data.Settings.MainWindowWidth;
Height = _viewModel.Data.Settings.MainWindowHeight;
_trayService = new TrayService(
this,
() => _viewModel.OpenSettingsCommand.Execute(null),
enabled => _viewModel.SetGlobalHotkeysEnabled(enabled),
() => _viewModel.Data.Settings.GlobalHotkeysEnabled,
RequestExit);
RegisterHotkeys();
if (_viewModel.Data.Settings.StartHiddenToTray)
{
Hide();
_trayService.RefreshMenuText();
}
await _viewModel.RunAutoRunAsync();
}
private void Window_OnClosing(object? sender, CancelEventArgs e)
{
if (!_exitRequested && _viewModel.Data.Settings.HideOnClose)
{
e.Cancel = true;
Hide();
_trayService?.RefreshMenuText();
return;
}
_viewModel.Data.Settings.MainWindowWidth = Width;
_viewModel.Data.Settings.MainWindowHeight = Height;
_viewModel.SaveAsync().GetAwaiter().GetResult();
_hotkeyService.Dispose();
_trayService?.Dispose();
}
private void SearchTextBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SearchTextBox.Clear();
}
}
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.F)
{
SearchTextBox.Focus();
SearchTextBox.SelectAll();
e.Handled = true;
}
}
private void ToolsListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (_viewModel.LaunchSelectedCommand.CanExecute(null))
{
_viewModel.LaunchSelectedCommand.Execute(null);
}
}
private void ToolsListBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Enter && _viewModel.LaunchSelectedCommand.CanExecute(null))
{
_viewModel.LaunchSelectedCommand.Execute(null);
e.Handled = true;
}
}
private void ToolsListBox_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
var item = FindParent<ListBoxItem>((DependencyObject)e.OriginalSource);
if (item is not null)
{
item.IsSelected = true;
item.Focus();
}
}
private void ContextLaunch_OnClick(object sender, RoutedEventArgs e)
{
if (_viewModel.LaunchSelectedCommand.CanExecute(null))
{
_viewModel.LaunchSelectedCommand.Execute(null);
}
}
private void ContextEdit_OnClick(object sender, RoutedEventArgs e)
{
if (_viewModel.EditSelectedCommand.CanExecute(null))
{
_viewModel.EditSelectedCommand.Execute(null);
}
}
private void ContextDelete_OnClick(object sender, RoutedEventArgs e)
{
if (_viewModel.DeleteSelectedCommand.CanExecute(null))
{
_viewModel.DeleteSelectedCommand.Execute(null);
}
}
private void RegisterHotkeys()
{
if (!_initialized)
{
return;
}
_hotkeyService.RegisterAll(
this,
_viewModel.Data.Tools,
_viewModel.Data.Settings.GlobalHotkeysEnabled,
_viewModel.LaunchToolByIdAsync,
message => _viewModel.AddLog(message.Level, message.Message));
_trayService?.RefreshMenuText();
}
private void RequestExit()
{
if (_viewModel.Data.Settings.ConfirmExit)
{
var confirmed = MessageBox.Show(
this,
"确定退出个人工具箱吗?退出后托盘和全局快捷键都会停止。",
"退出",
MessageBoxButton.YesNo,
MessageBoxImage.Question) == MessageBoxResult.Yes;
if (!confirmed)
{
return;
}
}
_exitRequested = true;
Close();
System.Windows.Application.Current.Shutdown();
}
private static T? FindParent<T>(DependencyObject child)
where T : DependencyObject
{
var current = child;
while (current is not null)
{
if (current is T result)
{
return result;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
} }
} }

View File

@@ -34,6 +34,7 @@ public sealed class ConfigurationService
public async Task<ToolboxData> LoadAsync(CancellationToken cancellationToken = default) public async Task<ToolboxData> LoadAsync(CancellationToken cancellationToken = default)
{ {
EnsureDirectories(); EnsureDirectories();
var toolsFileExists = File.Exists(ToolsPath);
var data = new ToolboxData var data = new ToolboxData
{ {
@@ -45,7 +46,11 @@ public sealed class ConfigurationService
data.Settings.DataVersion = CurrentDataVersion; data.Settings.DataVersion = CurrentDataVersion;
SystemToolService.EnsureDefaultCategories(data.Categories); SystemToolService.EnsureDefaultCategories(data.Categories);
if (!toolsFileExists || data.Tools.Count == 0)
{
SystemToolService.RestoreDefaultSystemTools(data.Categories, data.Tools); SystemToolService.RestoreDefaultSystemTools(data.Categories, data.Tools);
}
MergeAutoRunEntries(data); MergeAutoRunEntries(data);
await SaveAsync(data, cancellationToken); await SaveAsync(data, cancellationToken);

View File

@@ -0,0 +1,55 @@
using ToolboxApp.Models;
namespace ToolboxApp.ViewModels;
public sealed class CombinationMemberViewModel : ObservableObject
{
private readonly Func<string, ToolItem?> _toolResolver;
private bool _enabled;
private int _intervalAfterMs;
public CombinationMemberViewModel(CombinationMember member, Func<string, ToolItem?> toolResolver)
{
Member = member;
_toolResolver = toolResolver;
_enabled = member.Enabled;
_intervalAfterMs = member.IntervalAfterMs;
}
public CombinationMember Member { get; }
public string ToolId => Member.ToolId;
public string ToolName => _toolResolver(Member.ToolId)?.Name ?? "引用不存在";
public string ToolTypeLabel => _toolResolver(Member.ToolId)?.Type switch
{
ToolType.System => "系统",
ToolType.Local => "本地",
ToolType.Url => "网址",
ToolType.Combination => "组合",
_ => "未知"
};
public bool Enabled
{
get => _enabled;
set
{
if (SetProperty(ref _enabled, value))
{
Member.Enabled = value;
}
}
}
public int IntervalAfterMs
{
get => _intervalAfterMs;
set
{
var normalized = Math.Max(0, value);
if (SetProperty(ref _intervalAfterMs, normalized))
{
Member.IntervalAfterMs = normalized;
}
}
}
}

View File

@@ -0,0 +1,552 @@
using System.Collections.ObjectModel;
using System.Windows;
using ToolboxApp.Commands;
using ToolboxApp.Models;
using ToolboxApp.Services;
using ToolboxApp.Views;
using Clipboard = System.Windows.Clipboard;
using MessageBox = System.Windows.MessageBox;
namespace ToolboxApp.ViewModels;
public sealed class MainWindowViewModel : ObservableObject
{
private readonly ConfigurationService _configurationService;
private readonly ToolLaunchService _toolLaunchService;
private readonly AutoRunService _autoRunService;
private readonly PathValidationService _pathValidationService;
private readonly StartupService _startupService;
private ToolboxData _data = new();
private CategoryItem? _selectedCategory;
private ToolCardViewModel? _selectedTool;
private string _searchText = "";
private bool _isLogPanelExpanded = true;
public MainWindowViewModel()
{
_configurationService = new ConfigurationService();
_toolLaunchService = new ToolLaunchService();
_autoRunService = new AutoRunService(_toolLaunchService);
_pathValidationService = new PathValidationService();
_startupService = new StartupService();
AddLocalToolCommand = new RelayCommand(AddLocalTool);
AddUrlToolCommand = new RelayCommand(AddUrlTool);
AddCombinationCommand = new RelayCommand(AddCombination);
LaunchSelectedCommand = new AsyncRelayCommand(LaunchSelectedAsync, () => SelectedTool is not null);
EditSelectedCommand = new RelayCommand(EditSelectedTool, () => SelectedTool is not null);
DeleteSelectedCommand = new RelayCommand(DeleteSelectedTool, () => SelectedTool is not null);
OpenSettingsCommand = new RelayCommand(OpenSettings);
AddCategoryCommand = new RelayCommand(AddCategory);
RenameCategoryCommand = new RelayCommand(RenameCategory, () => SelectedCategory is not null);
DeleteCategoryCommand = new RelayCommand(DeleteCategory, () => SelectedCategory is not null && SelectedCategory.Id != SystemToolService.UncategorizedCategoryId);
ClearLogsCommand = new RelayCommand(() => Logs.Clear());
CopyLogsCommand = new RelayCommand(CopyLogs, () => Logs.Count > 0);
ToggleLogPanelCommand = new RelayCommand(() => IsLogPanelExpanded = !IsLogPanelExpanded);
}
public event EventHandler? HotkeysRefreshRequested;
public Window? Owner { get; set; }
public ObservableCollection<CategoryItem> Categories { get; } = [];
public ObservableCollection<ToolCardViewModel> Tools { get; } = [];
public ObservableCollection<LogMessage> Logs { get; } = [];
public RelayCommand AddLocalToolCommand { get; }
public RelayCommand AddUrlToolCommand { get; }
public RelayCommand AddCombinationCommand { get; }
public AsyncRelayCommand LaunchSelectedCommand { get; }
public RelayCommand EditSelectedCommand { get; }
public RelayCommand DeleteSelectedCommand { get; }
public RelayCommand OpenSettingsCommand { get; }
public RelayCommand AddCategoryCommand { get; }
public RelayCommand RenameCategoryCommand { get; }
public RelayCommand DeleteCategoryCommand { get; }
public RelayCommand ClearLogsCommand { get; }
public RelayCommand CopyLogsCommand { get; }
public RelayCommand ToggleLogPanelCommand { get; }
public ToolboxData Data => _data;
public ConfigurationService ConfigurationService => _configurationService;
public StartupService StartupService => _startupService;
public CategoryItem? SelectedCategory
{
get => _selectedCategory;
set
{
if (SetProperty(ref _selectedCategory, value))
{
RefreshTools();
RaiseSelectionCommandState();
}
}
}
public ToolCardViewModel? SelectedTool
{
get => _selectedTool;
set
{
if (SetProperty(ref _selectedTool, value))
{
RaiseSelectionCommandState();
}
}
}
public string SearchText
{
get => _searchText;
set
{
if (SetProperty(ref _searchText, value))
{
RefreshTools();
}
}
}
public bool IsLogPanelExpanded
{
get => _isLogPanelExpanded;
set
{
if (SetProperty(ref _isLogPanelExpanded, value))
{
_data.Settings.LogPanelExpanded = value;
_ = SaveAsync();
}
}
}
public async Task InitializeAsync(Window owner)
{
Owner = owner;
_data = await _configurationService.LoadAsync();
_isLogPanelExpanded = _data.Settings.LogPanelExpanded;
OnPropertyChanged(nameof(IsLogPanelExpanded));
var invalidCount = _pathValidationService.ValidateTools(_data.Tools);
RefreshCategories();
RefreshTools();
AddLog(LogLevel.Info, $"配置目录:{_configurationService.ConfigDirectory}");
AddLog(LogLevel.Success, "个人工具箱已启动。");
if (invalidCount > 0)
{
AddLog(LogLevel.Warning, $"路径检查完成:发现 {invalidCount} 个失效工具。");
}
await SaveAsync();
}
public async Task RunAutoRunAsync(CancellationToken cancellationToken = default)
{
_configurationService.MergeAutoRunEntries(_data);
await SaveAsync(cancellationToken);
await _autoRunService.ExecuteAsync(_data, AddLog, cancellationToken);
}
public async Task LaunchToolByIdAsync(string toolId)
{
var tool = _data.Tools.FirstOrDefault(item => item.Id == toolId && !item.IsDeleted);
if (tool is null)
{
AddLog(LogLevel.Warning, $"快捷键目标不存在:{toolId}");
return;
}
await _toolLaunchService.LaunchAsync(tool, _data.Tools, AddLog);
}
public async Task SaveAsync(CancellationToken cancellationToken = default)
{
_configurationService.MergeAutoRunEntries(_data);
await _configurationService.SaveAsync(_data, cancellationToken);
}
public void RefreshHotkeys()
{
HotkeysRefreshRequested?.Invoke(this, EventArgs.Empty);
RefreshTools();
}
public void SetGlobalHotkeysEnabled(bool enabled)
{
_data.Settings.GlobalHotkeysEnabled = enabled;
AddLog(LogLevel.Info, enabled ? "全局快捷键已开启。" : "全局快捷键已关闭。");
_ = SaveAsync();
RefreshHotkeys();
}
public void AddLog(LogLevel level, string message)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
Logs.Add(new LogMessage
{
Level = level,
Message = message
});
while (Logs.Count > 500)
{
Logs.RemoveAt(0);
}
CopyLogsCommand.RaiseCanExecuteChanged();
});
}
private void AddLog(LogMessage message)
{
AddLog(message.Level, message.Message);
}
private async Task LaunchSelectedAsync()
{
if (SelectedTool is null)
{
return;
}
await _toolLaunchService.LaunchAsync(SelectedTool.Tool, _data.Tools, AddLog);
RefreshTools();
}
private void AddLocalTool()
{
var tool = CreateBaseTool(ToolType.Local);
tool.Name = "新的本地工具";
tool.IconKey = "folder";
var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
if (edited is null)
{
return;
}
edited.SortOrder = NextToolSortOrder();
_data.Tools.Add(edited);
_pathValidationService.ValidateTools(_data.Tools);
RefreshTools();
AddLog(LogLevel.Success, $"已添加本地工具:{edited.Name}");
_ = SaveAsync();
RefreshHotkeys();
}
private void AddUrlTool()
{
var tool = CreateBaseTool(ToolType.Url);
tool.Name = "新的网址";
tool.IconKey = "link";
var edited = ToolEditorWindow.Edit(tool, Categories, Owner);
if (edited is null)
{
return;
}
edited.SortOrder = NextToolSortOrder();
_data.Tools.Add(edited);
RefreshTools();
AddLog(LogLevel.Success, $"已添加网址工具:{edited.Name}");
_ = SaveAsync();
RefreshHotkeys();
}
private void AddCombination()
{
var tool = CreateBaseTool(ToolType.Combination);
tool.Name = "新的组合";
tool.IconKey = "combination";
tool.Combination = new CombinationConfig();
var edited = CombinationEditorWindow.Edit(tool, Categories, _data.Tools, _toolLaunchService, Owner);
if (edited is null)
{
return;
}
edited.SortOrder = NextToolSortOrder();
_data.Tools.Add(edited);
RefreshTools();
AddLog(LogLevel.Success, $"已添加组合:{edited.Name}");
_ = SaveAsync();
RefreshHotkeys();
}
private void EditSelectedTool()
{
if (SelectedTool is null)
{
return;
}
var original = SelectedTool.Tool;
var candidate = original.Clone();
ToolItem? edited = original.Type == ToolType.Combination
? CombinationEditorWindow.Edit(candidate, Categories, _data.Tools, _toolLaunchService, Owner)
: ToolEditorWindow.Edit(candidate, Categories, Owner);
if (edited is null)
{
return;
}
CopyToolValues(edited, original);
_pathValidationService.ValidateTools(_data.Tools);
RefreshTools();
AddLog(LogLevel.Success, $"已保存工具:{original.Name}");
_ = SaveAsync();
RefreshHotkeys();
}
private void DeleteSelectedTool()
{
if (SelectedTool is null)
{
return;
}
var tool = SelectedTool.Tool;
var confirmed = MessageBox.Show(
Owner,
$"确定删除“{tool.Name}”吗?组合中对它的引用也会失效。",
"删除工具",
MessageBoxButton.YesNo,
MessageBoxImage.Warning) == MessageBoxResult.Yes;
if (!confirmed)
{
return;
}
tool.IsDeleted = true;
foreach (var combination in _data.Tools.Where(item => item.Type == ToolType.Combination && item.Combination is not null))
{
combination.Combination!.Members.RemoveAll(member => member.ToolId == tool.Id);
}
RefreshTools();
AddLog(LogLevel.Warning, $"已删除工具:{tool.Name}");
_ = SaveAsync();
RefreshHotkeys();
}
private void OpenSettings()
{
var window = new SettingsWindow(_data, _configurationService, _startupService, _pathValidationService, _toolLaunchService)
{
Owner = Owner
};
if (window.ShowDialog() == true)
{
_data = window.Data;
_pathValidationService.ValidateTools(_data.Tools);
RefreshCategories();
RefreshTools();
AddLog(LogLevel.Success, "设置已保存。");
_ = SaveAsync();
RefreshHotkeys();
}
}
private void AddCategory()
{
var name = PromptWindow.Prompt("新建分类", "分类名称", "", Owner);
if (string.IsNullOrWhiteSpace(name))
{
return;
}
_data.Categories.Add(new CategoryItem
{
Name = name.Trim(),
IconKey = "category",
SortOrder = _data.Categories.Count == 0 ? 0 : _data.Categories.Max(category => category.SortOrder) + 1
});
RefreshCategories();
AddLog(LogLevel.Success, $"已新建分类:{name.Trim()}");
_ = SaveAsync();
}
private void RenameCategory()
{
if (SelectedCategory is null)
{
return;
}
var name = PromptWindow.Prompt("重命名分类", "分类名称", SelectedCategory.Name, Owner);
if (string.IsNullOrWhiteSpace(name))
{
return;
}
SelectedCategory.Name = name.Trim();
RefreshCategories();
RefreshTools();
AddLog(LogLevel.Success, $"已重命名分类:{name.Trim()}");
_ = SaveAsync();
}
private void DeleteCategory()
{
if (SelectedCategory is null)
{
return;
}
if (SelectedCategory.Id == SystemToolService.UncategorizedCategoryId)
{
AddLog(LogLevel.Warning, "未分类为固定分类,不能删除。");
return;
}
var confirmed = MessageBox.Show(
Owner,
$"删除分类“{SelectedCategory.Name}”后,其中工具会移动到“未分类”。是否继续?",
"删除分类",
MessageBoxButton.YesNo,
MessageBoxImage.Warning) == MessageBoxResult.Yes;
if (!confirmed)
{
return;
}
foreach (var tool in _data.Tools.Where(tool => tool.CategoryId == SelectedCategory.Id))
{
tool.CategoryId = SystemToolService.UncategorizedCategoryId;
}
var deletedName = SelectedCategory.Name;
_data.Categories.Remove(SelectedCategory);
RefreshCategories();
SelectedCategory = _data.Categories.FirstOrDefault(category => category.Id == SystemToolService.UncategorizedCategoryId);
AddLog(LogLevel.Warning, $"已删除分类:{deletedName}");
_ = SaveAsync();
}
private void CopyLogs()
{
if (Logs.Count == 0)
{
return;
}
Clipboard.SetText(string.Join(Environment.NewLine, Logs.Select(log => log.DisplayText)));
AddLog(LogLevel.Success, "已复制底部信息。");
}
private ToolItem CreateBaseTool(ToolType type)
{
return new ToolItem
{
Id = Guid.NewGuid().ToString("N"),
Type = type,
CategoryId = SelectedCategory?.Id ?? Categories.FirstOrDefault()?.Id ?? SystemToolService.UncategorizedCategoryId,
SortOrder = NextToolSortOrder()
};
}
private int NextToolSortOrder()
{
return _data.Tools.Count == 0 ? 0 : _data.Tools.Max(tool => tool.SortOrder) + 1;
}
private void RefreshCategories()
{
Categories.Clear();
foreach (var category in _data.Categories.OrderBy(category => category.SortOrder).ThenBy(category => category.Name))
{
Categories.Add(category);
}
SelectedCategory ??= Categories.FirstOrDefault();
if (SelectedCategory is not null && Categories.All(category => category.Id != SelectedCategory.Id))
{
SelectedCategory = Categories.FirstOrDefault();
}
DeleteCategoryCommand.RaiseCanExecuteChanged();
RenameCategoryCommand.RaiseCanExecuteChanged();
}
private void RefreshTools()
{
var selectedId = SelectedTool?.Id;
Tools.Clear();
var query = _data.Tools.Where(tool => !tool.IsDeleted);
if (!string.IsNullOrWhiteSpace(SearchText))
{
var keyword = SearchText.Trim();
query = query.Where(tool => MatchesSearch(tool, keyword));
}
else if (SelectedCategory is not null)
{
query = query.Where(tool => tool.CategoryId == SelectedCategory.Id);
}
foreach (var tool in query.OrderBy(tool => tool.SortOrder).ThenBy(tool => tool.Name))
{
Tools.Add(new ToolCardViewModel(tool, ResolveCategoryName));
}
SelectedTool = Tools.FirstOrDefault(tool => tool.Id == selectedId);
RaiseSelectionCommandState();
}
private bool MatchesSearch(ToolItem tool, string keyword)
{
return Contains(tool.Name, keyword)
|| Contains(tool.Description, keyword)
|| Contains(tool.LaunchTarget, keyword)
|| Contains(tool.Url, keyword)
|| Contains(ResolveCategoryName(tool.CategoryId), keyword);
}
private static bool Contains(string? source, string keyword)
{
return !string.IsNullOrWhiteSpace(source)
&& source.Contains(keyword, StringComparison.CurrentCultureIgnoreCase);
}
private string ResolveCategoryName(string categoryId)
{
return _data.Categories.FirstOrDefault(category => category.Id == categoryId)?.Name ?? "未分类";
}
private void RaiseSelectionCommandState()
{
LaunchSelectedCommand.RaiseCanExecuteChanged();
EditSelectedCommand.RaiseCanExecuteChanged();
DeleteSelectedCommand.RaiseCanExecuteChanged();
RenameCategoryCommand.RaiseCanExecuteChanged();
DeleteCategoryCommand.RaiseCanExecuteChanged();
}
private static void CopyToolValues(ToolItem source, ToolItem target)
{
target.Name = source.Name;
target.Description = source.Description;
target.Type = source.Type;
target.CategoryId = source.CategoryId;
target.IconKey = source.IconKey;
target.LaunchTarget = source.LaunchTarget;
target.Url = source.Url;
target.Arguments = source.Arguments;
target.WorkingDirectory = source.WorkingDirectory;
target.SystemToolKey = source.SystemToolKey;
target.Hotkey = source.Hotkey;
target.AutoRunEnabled = source.AutoRunEnabled;
target.RunAsAdmin = source.RunAsAdmin;
target.Combination = source.Combination?.Clone();
}
}

View File

@@ -0,0 +1,124 @@
using ToolboxApp.Models;
namespace ToolboxApp.ViewModels;
public sealed class ToolCardViewModel : ObservableObject
{
private readonly Func<string, string> _categoryNameResolver;
public ToolCardViewModel(ToolItem tool, Func<string, string> categoryNameResolver)
{
Tool = tool;
_categoryNameResolver = categoryNameResolver;
}
public ToolItem Tool { get; }
public string Id => Tool.Id;
public string Name => Tool.Name;
public string Description => string.IsNullOrWhiteSpace(Tool.Description) ? "暂无说明" : Tool.Description;
public string CategoryName => _categoryNameResolver(Tool.CategoryId);
public string IconText => IconKeyToText(Tool.IconKey, Tool.Type);
public string TypeLabel => Tool.Type switch
{
ToolType.System => "系统",
ToolType.Local => "本地",
ToolType.Url => "网址",
ToolType.Combination => "组合",
_ => "工具"
};
public IReadOnlyList<string> StatusBadges
{
get
{
var badges = new List<string>();
if (Tool.PathInvalid)
{
badges.Add("路径失效");
}
if (!string.IsNullOrWhiteSpace(Tool.Hotkey))
{
badges.Add("快捷键");
}
if (Tool.AutoRunEnabled)
{
badges.Add("自动运行");
}
if (Tool.RunAsAdmin)
{
badges.Add("管理员");
}
return badges;
}
}
public string DetailText
{
get
{
return Tool.Type switch
{
ToolType.Local => Tool.LaunchTarget ?? "",
ToolType.Url => Tool.Url ?? "",
ToolType.System => Tool.LaunchTarget ?? "",
ToolType.Combination => $"{Tool.Combination?.Members.Count ?? 0} 个成员",
_ => ""
};
}
}
public void Refresh()
{
OnPropertyChanged(nameof(Name));
OnPropertyChanged(nameof(Description));
OnPropertyChanged(nameof(CategoryName));
OnPropertyChanged(nameof(IconText));
OnPropertyChanged(nameof(TypeLabel));
OnPropertyChanged(nameof(StatusBadges));
OnPropertyChanged(nameof(DetailText));
}
private static string IconKeyToText(string iconKey, ToolType type)
{
if (!string.IsNullOrWhiteSpace(iconKey))
{
var mapped = iconKey.ToLowerInvariant() switch
{
"notepad" => "TXT",
"calculator" => "123",
"taskmgr" => "CPU",
"control" => "CTL",
"settings" => "SET",
"device" => "DEV",
"disk" => "DSK",
"service" => "SVC",
"registry" => "REG",
"network" => "NET",
"apps" => "APP",
"link" => "URL",
"folder" => "DIR",
"combination" => "COM",
_ => ""
};
if (!string.IsNullOrWhiteSpace(mapped))
{
return mapped;
}
}
return type switch
{
ToolType.System => "SYS",
ToolType.Local => "LOC",
ToolType.Url => "URL",
ToolType.Combination => "COM",
_ => "BOX"
};
}
}

View File

@@ -0,0 +1,166 @@
<Window x:Class="ToolboxApp.Views.CombinationEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="编辑组合"
Width="760"
Height="640"
MinWidth="680"
MinHeight="560"
WindowStartupLocation="CenterOwner">
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="96" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="96" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="名称" VerticalAlignment="Center" />
<TextBox x:Name="NameTextBox"
Grid.Column="1"
MinHeight="32"
Margin="0,0,16,8"
ToolTip="组合卡片显示名称。" />
<TextBlock Grid.Column="2" Text="分类" VerticalAlignment="Center" />
<ComboBox x:Name="CategoryComboBox"
Grid.Column="3"
MinHeight="32"
Margin="0,0,0,8"
DisplayMemberPath="Name"
SelectedValuePath="Id"
ToolTip="组合也作为一种工具,只能属于一个一级分类。" />
<TextBlock Grid.Row="1" Text="说明" VerticalAlignment="Top" Margin="0,8,0,0" />
<TextBox x:Name="DescriptionTextBox"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="3"
MinHeight="58"
TextWrapping="Wrap"
AcceptsReturn="True"
ToolTip="描述这个组合会打开哪些环境或工具。" />
<TextBlock Grid.Row="2" Text="快捷键" VerticalAlignment="Center" />
<TextBox x:Name="HotkeyTextBox"
Grid.Row="2"
Grid.Column="1"
MinHeight="32"
Margin="0,8,16,0"
ToolTip="格式示例Ctrl + Alt + D。组合可被全局快捷键直接启动。" />
<StackPanel Grid.Row="2"
Grid.Column="2"
Grid.ColumnSpan="2"
Orientation="Horizontal"
Margin="0,8,0,0">
<CheckBox x:Name="AutoRunCheckBox"
Content="工具箱启动时自动运行"
VerticalAlignment="Center"
ToolTip="开启后组合会出现在自动运行列表中。" />
<ComboBox x:Name="FailurePolicyComboBox"
Width="180"
Margin="18,0,0,0"
ToolTip="设置成员启动失败后继续执行,还是停止整个组合。">
<ComboBoxItem Content="失败后继续" Tag="Continue" />
<ComboBoxItem Content="失败后停止" Tag="Stop" />
</ComboBox>
</StackPanel>
</Grid>
<Grid Grid.Row="1" Margin="0,16,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="ToolPickerComboBox"
Width="300"
DisplayMemberPath="Name"
ToolTip="从已有工具或组合中选择一个成员。" />
<Button Content="添加成员"
Width="96"
Margin="8,0,0,0"
ToolTip="把选择的工具加入组合成员列表。"
Click="AddMemberButton_OnClick" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Content="上移"
Width="70"
Margin="0,0,8,0"
ToolTip="将选中成员提前执行。"
Click="MoveUpButton_OnClick" />
<Button Content="下移"
Width="70"
Margin="0,0,8,0"
ToolTip="将选中成员延后执行。"
Click="MoveDownButton_OnClick" />
<Button Content="移除"
Width="70"
ToolTip="从组合中移除选中成员。"
Click="RemoveMemberButton_OnClick" />
</StackPanel>
<DataGrid x:Name="MembersDataGrid"
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,10,0,0"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
ToolTip="组合成员会按列表顺序启动,可临时禁用成员并设置执行后的等待间隔。">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="启用"
Width="70"
Binding="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="名称"
Width="*"
IsReadOnly="True"
Binding="{Binding ToolName}" />
<DataGridTextColumn Header="类型"
Width="90"
IsReadOnly="True"
Binding="{Binding ToolTypeLabel}" />
<DataGridTextColumn Header="间隔(ms)"
Width="110"
Binding="{Binding IntervalAfterMs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,14,0,0">
<Button Content="保存"
Width="88"
Margin="0,0,8,0"
IsDefault="True"
ToolTip="校验循环引用和重复工具后保存组合。"
Click="SaveButton_OnClick" />
<Button Content="取消"
Width="88"
IsCancel="True"
ToolTip="放弃本次组合修改。"
Click="CancelButton_OnClick" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,189 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using ToolboxApp.Models;
using ToolboxApp.Services;
using ToolboxApp.ViewModels;
using MessageBox = System.Windows.MessageBox;
namespace ToolboxApp.Views;
public partial class CombinationEditorWindow : Window
{
private readonly ToolItem _tool;
private readonly IReadOnlyList<ToolItem> _allTools;
private readonly ToolLaunchService _launchService;
private readonly ObservableCollection<CombinationMemberViewModel> _members = [];
private CombinationEditorWindow(
ToolItem tool,
IEnumerable<CategoryItem> categories,
IEnumerable<ToolItem> allTools,
ToolLaunchService launchService)
{
InitializeComponent();
_tool = tool;
_allTools = allTools.Where(item => !item.IsDeleted).ToList();
_launchService = launchService;
_tool.Combination ??= new CombinationConfig();
var combination = _tool.Combination;
CategoryComboBox.ItemsSource = categories.ToList();
CategoryComboBox.SelectedValue = tool.CategoryId;
ToolPickerComboBox.ItemsSource = _allTools.Where(item => item.Id != tool.Id).OrderBy(item => item.Name).ToList();
MembersDataGrid.ItemsSource = _members;
NameTextBox.Text = tool.Name;
DescriptionTextBox.Text = tool.Description;
HotkeyTextBox.Text = tool.Hotkey;
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
FailurePolicyComboBox.SelectedIndex = combination.FailurePolicy == FailurePolicy.Stop ? 1 : 0;
foreach (var member in combination.Members.OrderBy(member => member.SortOrder))
{
_members.Add(new CombinationMemberViewModel(member.Clone(), ResolveTool));
}
}
public ToolItem EditedTool => _tool;
public static ToolItem? Edit(
ToolItem tool,
IEnumerable<CategoryItem> categories,
IEnumerable<ToolItem> allTools,
ToolLaunchService launchService,
Window? owner)
{
var window = new CombinationEditorWindow(tool, categories, allTools, launchService)
{
Owner = owner
};
return window.ShowDialog() == true ? window.EditedTool : null;
}
private void AddMemberButton_OnClick(object sender, RoutedEventArgs e)
{
if (ToolPickerComboBox.SelectedItem is not ToolItem selectedTool)
{
return;
}
if (_members.Any(member => member.ToolId == selectedTool.Id))
{
MessageBox.Show(this, "该成员已经在当前组合中。", "添加成员", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
_members.Add(new CombinationMemberViewModel(new CombinationMember
{
ToolId = selectedTool.Id,
Enabled = true,
SortOrder = _members.Count,
IntervalAfterMs = 0
}, ResolveTool));
}
private void RemoveMemberButton_OnClick(object sender, RoutedEventArgs e)
{
if (MembersDataGrid.SelectedItem is CombinationMemberViewModel selected)
{
_members.Remove(selected);
ReorderMembers();
}
}
private void MoveUpButton_OnClick(object sender, RoutedEventArgs e)
{
MoveSelected(-1);
}
private void MoveDownButton_OnClick(object sender, RoutedEventArgs e)
{
MoveSelected(1);
}
private void SaveButton_OnClick(object sender, RoutedEventArgs e)
{
var name = NameTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show(this, "请输入组合名称。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (CategoryComboBox.SelectedValue is not string categoryId || string.IsNullOrWhiteSpace(categoryId))
{
MessageBox.Show(this, "请选择分类。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var hotkey = HotkeyTextBox.Text.Trim();
if (!string.IsNullOrWhiteSpace(hotkey) && !HotkeyParser.TryParse(hotkey, out _, out _))
{
MessageBox.Show(this, "快捷键格式无效,请使用类似 Ctrl + Alt + D 的格式。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
ReorderMembers();
_tool.Name = name;
_tool.Description = DescriptionTextBox.Text.Trim();
_tool.CategoryId = categoryId;
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
_tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
_tool.IconKey = "combination";
_tool.Combination = new CombinationConfig
{
FailurePolicy = FailurePolicyComboBox.SelectedIndex == 1 ? FailurePolicy.Stop : FailurePolicy.Continue,
Members = _members.Select(member => member.Member.Clone()).ToList()
};
var validationTools = _allTools.Where(item => item.Id != _tool.Id).Concat([_tool]).ToList();
var validation = _launchService.ValidateCombination(_tool, validationTools);
if (!validation.Success)
{
MessageBox.Show(this, validation.ErrorMessage ?? "组合校验失败。", "保存组合", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
}
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private ToolItem? ResolveTool(string toolId)
{
return _allTools.FirstOrDefault(tool => tool.Id == toolId);
}
private void MoveSelected(int offset)
{
if (MembersDataGrid.SelectedItem is not CombinationMemberViewModel selected)
{
return;
}
var index = _members.IndexOf(selected);
var targetIndex = index + offset;
if (targetIndex < 0 || targetIndex >= _members.Count)
{
return;
}
_members.Move(index, targetIndex);
MembersDataGrid.SelectedItem = selected;
ReorderMembers();
}
private void ReorderMembers()
{
for (var index = 0; index < _members.Count; index++)
{
_members[index].Member.SortOrder = index;
}
}
}

View File

@@ -0,0 +1,45 @@
<Window x:Class="ToolboxApp.Views.PromptWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="输入"
Width="380"
Height="180"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner">
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="PromptText"
FontSize="14"
TextWrapping="Wrap"
ToolTip="当前需要输入的信息名称。" />
<TextBox x:Name="ValueTextBox"
Grid.Row="1"
Margin="0,12,0,0"
MinHeight="32"
ToolTip="请输入名称,按 Enter 确认。"
KeyDown="ValueTextBox_OnKeyDown" />
<StackPanel Grid.Row="3"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="确定"
Width="82"
Margin="0,0,8,0"
IsDefault="True"
ToolTip="保存输入内容并关闭窗口。"
Click="OkButton_OnClick" />
<Button Content="取消"
Width="82"
IsCancel="True"
ToolTip="放弃本次输入。"
Click="CancelButton_OnClick" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,47 @@
using System.Windows;
using System.Windows.Input;
namespace ToolboxApp.Views;
public partial class PromptWindow : Window
{
private PromptWindow(string title, string prompt, string defaultValue)
{
InitializeComponent();
Title = title;
PromptText.Text = prompt;
ValueTextBox.Text = defaultValue;
ValueTextBox.SelectAll();
Loaded += (_, _) => ValueTextBox.Focus();
}
public string Value => ValueTextBox.Text.Trim();
public static string? Prompt(string title, string prompt, string defaultValue, Window? owner)
{
var window = new PromptWindow(title, prompt, defaultValue)
{
Owner = owner
};
return window.ShowDialog() == true ? window.Value : null;
}
private void OkButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private void ValueTextBox_OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DialogResult = true;
}
}
}

View File

@@ -0,0 +1,196 @@
<Window x:Class="ToolboxApp.Views.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="设置"
Width="820"
Height="620"
MinWidth="740"
MinHeight="540"
WindowStartupLocation="CenterOwner">
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl>
<TabItem Header="常规" ToolTip="管理窗口、托盘和快捷键总开关。">
<StackPanel Margin="18">
<CheckBox x:Name="StartHiddenCheckBox"
Content="启动后默认隐藏到托盘"
Margin="0,0,0,10"
ToolTip="开启后应用启动时不显示主窗口,只保留托盘图标。" />
<CheckBox x:Name="HideOnCloseCheckBox"
Content="关闭窗口时隐藏到托盘"
Margin="0,0,0,10"
ToolTip="开启后点击窗口关闭按钮不会退出应用。" />
<CheckBox x:Name="ConfirmExitCheckBox"
Content="退出前确认"
Margin="0,0,0,10"
ToolTip="通过托盘菜单退出时是否显示确认提示。" />
<CheckBox x:Name="LogExpandedCheckBox"
Content="默认展开底部信息区"
Margin="0,0,0,10"
ToolTip="控制主界面底部日志区域的默认展开状态。" />
<CheckBox x:Name="GlobalHotkeysCheckBox"
Content="启用全局快捷键"
Margin="0,0,0,10"
ToolTip="关闭后所有已配置快捷键临时失效,但配置不会删除。" />
<CheckBox x:Name="StartupCheckBox"
Content="开机自启"
Margin="0,0,0,10"
ToolTip="使用当前用户注册表启动项,不需要管理员权限。" />
</StackPanel>
</TabItem>
<TabItem Header="自动运行" ToolTip="动态汇总所有开启启动时自动运行的工具。">
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="AutoRunDataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
ToolTip="自动运行项来自工具自身设置,可调整顺序、启用状态和间隔。">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="启用"
Width="70"
Binding="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="顺序"
Width="70"
Binding="{Binding SortOrder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="工具"
Width="*"
IsReadOnly="True"
Binding="{Binding ToolName}" />
<DataGridTextColumn Header="类型"
Width="90"
IsReadOnly="True"
Binding="{Binding ToolTypeLabel}" />
<DataGridTextColumn Header="间隔(ms)"
Width="110"
Binding="{Binding IntervalAfterMs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,10,0,0">
<Button Content="上移"
Width="76"
Margin="0,0,8,0"
ToolTip="提高选中自动运行项的执行顺序。"
Click="AutoRunMoveUpButton_OnClick" />
<Button Content="下移"
Width="76"
Margin="0,0,8,0"
ToolTip="降低选中自动运行项的执行顺序。"
Click="AutoRunMoveDownButton_OnClick" />
<Button Content="移除"
Width="76"
ToolTip="从自动运行中移除,等价于关闭该工具的自动运行设置。"
Click="AutoRunRemoveButton_OnClick" />
</StackPanel>
</Grid>
</TabItem>
<TabItem Header="快捷键" ToolTip="查看和编辑工具快捷键状态。">
<Grid Margin="18">
<DataGrid x:Name="HotkeysDataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
ToolTip="快捷键状态会在主窗口注册后更新;可直接编辑快捷键文本。">
<DataGrid.Columns>
<DataGridTextColumn Header="快捷键"
Width="160"
Binding="{Binding Hotkey, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="工具"
Width="*"
IsReadOnly="True"
Binding="{Binding Name}" />
<DataGridTextColumn Header="类型"
Width="90"
IsReadOnly="True"
Binding="{Binding Type}" />
<DataGridTextColumn Header="状态"
Width="130"
IsReadOnly="True"
Binding="{Binding HotkeyStatus}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</TabItem>
<TabItem Header="数据" ToolTip="打开、导入、导出、重置和检查本地配置。">
<StackPanel Margin="18">
<Button Content="打开配置目录"
Width="160"
HorizontalAlignment="Left"
Margin="0,0,0,10"
ToolTip="在资源管理器中打开 %AppData%\ToolboxApp。"
Click="OpenConfigButton_OnClick" />
<Button Content="导出全部配置"
Width="160"
HorizontalAlignment="Left"
Margin="0,0,0,10"
ToolTip="导出 appsettings、categories、tools、autorun 和 icons 目录为 zip。"
Click="ExportButton_OnClick" />
<Button Content="导入配置"
Width="160"
HorizontalAlignment="Left"
Margin="0,0,0,10"
ToolTip="从 zip 导入配置,导入后会建议检查自动运行和快捷键。"
Click="ImportButton_OnClick" />
<Button Content="检查路径失效"
Width="160"
HorizontalAlignment="Left"
Margin="0,0,0,10"
ToolTip="检查本地工具路径和工作目录是否仍然存在。"
Click="ValidatePathsButton_OnClick" />
<Button Content="重置配置"
Width="160"
HorizontalAlignment="Left"
ToolTip="备份当前配置后恢复默认配置。"
Click="ResetButton_OnClick" />
<TextBlock x:Name="DataStatusTextBlock"
Margin="0,18,0,0"
TextWrapping="Wrap"
ToolTip="显示最近一次配置操作结果。" />
</StackPanel>
</TabItem>
<TabItem Header="系统工具" ToolTip="恢复默认 Windows 系统工具。">
<StackPanel Margin="18">
<Button Content="恢复默认系统工具"
Width="180"
HorizontalAlignment="Left"
ToolTip="只补回缺失的默认系统工具,不重复创建已有项。"
Click="RestoreSystemToolsButton_OnClick" />
<TextBlock x:Name="SystemToolsStatusTextBlock"
Margin="0,18,0,0"
TextWrapping="Wrap"
ToolTip="显示默认系统工具恢复结果。" />
</StackPanel>
</TabItem>
</TabControl>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,14,0,0">
<Button Content="保存"
Width="88"
Margin="0,0,8,0"
IsDefault="True"
ToolTip="保存设置并关闭窗口。"
Click="SaveButton_OnClick" />
<Button Content="取消"
Width="88"
IsCancel="True"
ToolTip="关闭设置窗口。已执行的导入、导出和重置操作不会回滚。"
Click="CancelButton_OnClick" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,302 @@
using System.Collections.ObjectModel;
using System.Windows;
using ToolboxApp.Models;
using ToolboxApp.Services;
using MessageBox = System.Windows.MessageBox;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
namespace ToolboxApp.Views;
public partial class SettingsWindow : Window
{
private readonly ConfigurationService _configurationService;
private readonly StartupService _startupService;
private readonly PathValidationService _pathValidationService;
private readonly ToolLaunchService _toolLaunchService;
private readonly ObservableCollection<AutoRunRow> _autoRunRows = [];
private ToolboxData _data;
public SettingsWindow(
ToolboxData data,
ConfigurationService configurationService,
StartupService startupService,
PathValidationService pathValidationService,
ToolLaunchService toolLaunchService)
{
InitializeComponent();
_configurationService = configurationService;
_startupService = startupService;
_pathValidationService = pathValidationService;
_toolLaunchService = toolLaunchService;
_data = CloneData(data);
LoadControls();
}
public ToolboxData Data => _data;
private void LoadControls()
{
StartHiddenCheckBox.IsChecked = _data.Settings.StartHiddenToTray;
HideOnCloseCheckBox.IsChecked = _data.Settings.HideOnClose;
ConfirmExitCheckBox.IsChecked = _data.Settings.ConfirmExit;
LogExpandedCheckBox.IsChecked = _data.Settings.LogPanelExpanded;
GlobalHotkeysCheckBox.IsChecked = _data.Settings.GlobalHotkeysEnabled;
StartupCheckBox.IsChecked = _startupService.IsEnabled();
RebuildAutoRunRows();
AutoRunDataGrid.ItemsSource = _autoRunRows;
HotkeysDataGrid.ItemsSource = _data.Tools.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey)).OrderBy(tool => tool.Name).ToList();
}
private void RebuildAutoRunRows()
{
_configurationService.MergeAutoRunEntries(_data);
_autoRunRows.Clear();
foreach (var entry in _data.AutoRunEntries.OrderBy(entry => entry.SortOrder))
{
var tool = _data.Tools.FirstOrDefault(item => item.Id == entry.ToolId);
if (tool is null)
{
continue;
}
_autoRunRows.Add(new AutoRunRow(entry, tool));
}
}
private void SaveButton_OnClick(object sender, RoutedEventArgs e)
{
_data.Settings.StartHiddenToTray = StartHiddenCheckBox.IsChecked == true;
_data.Settings.HideOnClose = HideOnCloseCheckBox.IsChecked == true;
_data.Settings.ConfirmExit = ConfirmExitCheckBox.IsChecked == true;
_data.Settings.LogPanelExpanded = LogExpandedCheckBox.IsChecked == true;
_data.Settings.GlobalHotkeysEnabled = GlobalHotkeysCheckBox.IsChecked == true;
foreach (var row in _autoRunRows)
{
row.Apply();
}
_data.AutoRunEntries = _autoRunRows.Select(row => row.Entry).OrderBy(row => row.SortOrder).ToList();
try
{
_startupService.SetEnabled(StartupCheckBox.IsChecked == true);
}
catch (Exception ex)
{
MessageBox.Show(this, $"设置开机自启失败:{ex.Message}", "保存设置", MessageBoxButton.OK, MessageBoxImage.Warning);
}
DialogResult = true;
}
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private void AutoRunMoveUpButton_OnClick(object sender, RoutedEventArgs e)
{
MoveAutoRunRow(-1);
}
private void AutoRunMoveDownButton_OnClick(object sender, RoutedEventArgs e)
{
MoveAutoRunRow(1);
}
private void AutoRunRemoveButton_OnClick(object sender, RoutedEventArgs e)
{
if (AutoRunDataGrid.SelectedItem is not AutoRunRow row)
{
return;
}
row.Tool.AutoRunEnabled = false;
_autoRunRows.Remove(row);
ReorderAutoRunRows();
}
private void OpenConfigButton_OnClick(object sender, RoutedEventArgs e)
{
_configurationService.OpenConfigDirectory();
DataStatusTextBlock.Text = "已打开配置目录。";
}
private async void ExportButton_OnClick(object sender, RoutedEventArgs e)
{
var dialog = new SaveFileDialog
{
Title = "导出配置",
Filter = "Zip 文件 (*.zip)|*.zip",
FileName = $"ToolboxApp_Config_{DateTime.Now:yyyyMMdd_HHmmss}.zip"
};
if (dialog.ShowDialog(this) != true)
{
return;
}
await _configurationService.ExportAsync(dialog.FileName, _data);
DataStatusTextBlock.Text = $"配置已导出:{dialog.FileName}";
}
private async void ImportButton_OnClick(object sender, RoutedEventArgs e)
{
var confirmed = MessageBox.Show(
this,
"导入配置会先备份当前配置,然后覆盖当前配置。是否继续?",
"导入配置",
MessageBoxButton.YesNo,
MessageBoxImage.Warning) == MessageBoxResult.Yes;
if (!confirmed)
{
return;
}
var dialog = new OpenFileDialog
{
Title = "导入配置",
Filter = "Zip 文件 (*.zip)|*.zip|所有文件 (*.*)|*.*"
};
if (dialog.ShowDialog(this) != true)
{
return;
}
_data = await _configurationService.ImportAsync(dialog.FileName);
_pathValidationService.ValidateTools(_data.Tools);
LoadControls();
DataStatusTextBlock.Text = "配置导入完成。建议检查自动运行列表和快捷键,确认其中没有不需要启动的工具。";
}
private void ValidatePathsButton_OnClick(object sender, RoutedEventArgs e)
{
var invalidCount = _pathValidationService.ValidateTools(_data.Tools);
DataStatusTextBlock.Text = $"路径检查完成:发现 {invalidCount} 个失效工具。";
}
private async void ResetButton_OnClick(object sender, RoutedEventArgs e)
{
var confirmed = MessageBox.Show(
this,
"重置配置会先备份当前配置,然后恢复默认系统工具。是否继续?",
"重置配置",
MessageBoxButton.YesNo,
MessageBoxImage.Warning) == MessageBoxResult.Yes;
if (!confirmed)
{
return;
}
_data = await _configurationService.ResetAsync();
LoadControls();
DataStatusTextBlock.Text = "配置已重置。";
}
private void RestoreSystemToolsButton_OnClick(object sender, RoutedEventArgs e)
{
var added = SystemToolService.RestoreDefaultSystemTools(_data.Categories, _data.Tools);
SystemToolsStatusTextBlock.Text = added == 0
? "默认系统工具已经完整,无需补回。"
: $"已补回 {added} 个默认系统工具。";
RebuildAutoRunRows();
}
private void MoveAutoRunRow(int offset)
{
if (AutoRunDataGrid.SelectedItem is not AutoRunRow selected)
{
return;
}
var index = _autoRunRows.IndexOf(selected);
var targetIndex = index + offset;
if (targetIndex < 0 || targetIndex >= _autoRunRows.Count)
{
return;
}
_autoRunRows.Move(index, targetIndex);
AutoRunDataGrid.SelectedItem = selected;
ReorderAutoRunRows();
}
private void ReorderAutoRunRows()
{
for (var index = 0; index < _autoRunRows.Count; index++)
{
_autoRunRows[index].SortOrder = index;
}
}
private static ToolboxData CloneData(ToolboxData source)
{
return new ToolboxData
{
Settings = new AppSettings
{
DataVersion = source.Settings.DataVersion,
StartHiddenToTray = source.Settings.StartHiddenToTray,
HideOnClose = source.Settings.HideOnClose,
ConfirmExit = source.Settings.ConfirmExit,
GlobalHotkeysEnabled = source.Settings.GlobalHotkeysEnabled,
LogPanelExpanded = source.Settings.LogPanelExpanded,
Theme = source.Settings.Theme,
CardSize = source.Settings.CardSize,
MainWindowWidth = source.Settings.MainWindowWidth,
MainWindowHeight = source.Settings.MainWindowHeight
},
Categories = source.Categories.Select(category => category.Clone()).ToList(),
Tools = source.Tools.Select(tool => tool.Clone()).ToList(),
AutoRunEntries = source.AutoRunEntries.Select(entry => new AutoRunEntry
{
ToolId = entry.ToolId,
Enabled = entry.Enabled,
SortOrder = entry.SortOrder,
IntervalAfterMs = entry.IntervalAfterMs
}).ToList()
};
}
public sealed class AutoRunRow
{
public AutoRunRow(AutoRunEntry entry, ToolItem tool)
{
Entry = entry;
Tool = tool;
Enabled = entry.Enabled;
SortOrder = entry.SortOrder;
IntervalAfterMs = entry.IntervalAfterMs;
}
public AutoRunEntry Entry { get; }
public ToolItem Tool { get; }
public string ToolName => Tool.Name;
public string ToolTypeLabel => Tool.Type switch
{
ToolType.System => "系统",
ToolType.Local => "本地",
ToolType.Url => "网址",
ToolType.Combination => "组合",
_ => "工具"
};
public bool Enabled { get; set; }
public int SortOrder { get; set; }
public int IntervalAfterMs { get; set; }
public void Apply()
{
Entry.Enabled = Enabled;
Entry.SortOrder = SortOrder;
Entry.IntervalAfterMs = Math.Max(0, IntervalAfterMs);
}
}
}

View File

@@ -0,0 +1,154 @@
<Window x:Class="ToolboxApp.Views.ToolEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="编辑工具"
Width="560"
Height="560"
MinWidth="520"
WindowStartupLocation="CenterOwner">
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="112" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="类型" VerticalAlignment="Center" />
<TextBlock x:Name="TypeTextBlock"
Grid.Column="1"
MinHeight="32"
VerticalAlignment="Center"
ToolTip="工具类型由创建入口决定,用户侧只暴露系统、本地、网址和组合。" />
<TextBlock Grid.Row="1" Text="名称" VerticalAlignment="Center" />
<TextBox x:Name="NameTextBox"
Grid.Row="1"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="32"
ToolTip="卡片显示名称,建议简短清晰。" />
<TextBlock Grid.Row="2" Text="分类" VerticalAlignment="Center" />
<ComboBox x:Name="CategoryComboBox"
Grid.Row="2"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="32"
DisplayMemberPath="Name"
SelectedValuePath="Id"
ToolTip="工具只能属于一个一级分类。" />
<TextBlock Grid.Row="3" Text="说明" VerticalAlignment="Top" Margin="0,14,0,0" />
<TextBox x:Name="DescriptionTextBox"
Grid.Row="3"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="72"
TextWrapping="Wrap"
AcceptsReturn="True"
ToolTip="用于描述这个工具的用途,会显示在卡片上。" />
<TextBlock x:Name="TargetLabel"
Grid.Row="4"
Text="目标路径"
VerticalAlignment="Center" />
<Grid x:Name="TargetPanel"
Grid.Row="4"
Grid.Column="1"
Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="TargetTextBox"
MinHeight="32"
ToolTip="本地工具路径、系统工具启动目标或网址。" />
<Button Grid.Column="1"
Content="文件"
Width="64"
Margin="8,0,0,0"
ToolTip="选择本地程序、快捷方式、脚本或普通文件。"
Click="BrowseFileButton_OnClick" />
<Button Grid.Column="2"
Content="文件夹"
Width="72"
Margin="8,0,0,0"
ToolTip="选择一个文件夹作为本地工具。"
Click="BrowseFolderButton_OnClick" />
</Grid>
<TextBlock Grid.Row="5" Text="启动参数" VerticalAlignment="Center" />
<TextBox x:Name="ArgumentsTextBox"
Grid.Row="5"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="32"
ToolTip="传给可执行文件或脚本的参数,普通文件和文件夹通常不需要。" />
<TextBlock Grid.Row="6" Text="工作目录" VerticalAlignment="Center" />
<TextBox x:Name="WorkingDirectoryTextBox"
Grid.Row="6"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="32"
ToolTip="可执行文件或脚本启动时使用的工作目录,可留空。" />
<TextBlock Grid.Row="7" Text="快捷键" VerticalAlignment="Center" />
<TextBox x:Name="HotkeyTextBox"
Grid.Row="7"
Grid.Column="1"
Margin="0,8,0,0"
MinHeight="32"
ToolTip="格式示例Ctrl + Alt + T。第一版要求至少包含一个修饰键。" />
<StackPanel Grid.Row="8"
Grid.Column="1"
Margin="0,12,0,0">
<CheckBox x:Name="AutoRunCheckBox"
Content="工具箱启动时自动运行"
ToolTip="开启后会出现在设置页的自动运行列表中。" />
<CheckBox x:Name="RunAsAdminCheckBox"
Content="以管理员身份运行"
Margin="0,8,0,0"
ToolTip="仅启动该工具时触发 UAC工具箱自身不提权。" />
</StackPanel>
</Grid>
</ScrollViewer>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,14,0,0">
<Button Content="保存"
Width="88"
Margin="0,0,8,0"
IsDefault="True"
ToolTip="校验并保存工具设置。"
Click="SaveButton_OnClick" />
<Button Content="取消"
Width="88"
IsCancel="True"
ToolTip="放弃本次修改。"
Click="CancelButton_OnClick" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,157 @@
using System.Windows;
using Forms = System.Windows.Forms;
using ToolboxApp.Models;
using ToolboxApp.Services;
using MessageBox = System.Windows.MessageBox;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
namespace ToolboxApp.Views;
public partial class ToolEditorWindow : Window
{
private readonly ToolItem _tool;
private ToolEditorWindow(ToolItem tool, IEnumerable<CategoryItem> categories)
{
InitializeComponent();
_tool = tool;
CategoryComboBox.ItemsSource = categories.ToList();
CategoryComboBox.SelectedValue = tool.CategoryId;
Title = tool.Type switch
{
ToolType.System => "编辑系统工具",
ToolType.Local => "编辑本地工具",
ToolType.Url => "编辑网址工具",
_ => "编辑工具"
};
TypeTextBlock.Text = tool.Type switch
{
ToolType.System => "系统工具",
ToolType.Local => "本地工具",
ToolType.Url => "网址",
_ => "工具"
};
NameTextBox.Text = tool.Name;
DescriptionTextBox.Text = tool.Description;
TargetTextBox.Text = tool.Type == ToolType.Url ? tool.Url : tool.LaunchTarget;
ArgumentsTextBox.Text = tool.Arguments;
WorkingDirectoryTextBox.Text = tool.WorkingDirectory;
HotkeyTextBox.Text = tool.Hotkey;
AutoRunCheckBox.IsChecked = tool.AutoRunEnabled;
RunAsAdminCheckBox.IsChecked = tool.RunAsAdmin;
if (tool.Type == ToolType.Url)
{
TargetLabel.Text = "网址";
ArgumentsTextBox.IsEnabled = false;
WorkingDirectoryTextBox.IsEnabled = false;
RunAsAdminCheckBox.IsEnabled = false;
}
if (tool.Type == ToolType.System)
{
TargetLabel.Text = "启动目标";
}
}
public ToolItem EditedTool => _tool;
public static ToolItem? Edit(ToolItem tool, IEnumerable<CategoryItem> categories, Window? owner)
{
var window = new ToolEditorWindow(tool, categories)
{
Owner = owner
};
return window.ShowDialog() == true ? window.EditedTool : null;
}
private void BrowseFileButton_OnClick(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "选择本地工具",
CheckFileExists = false,
Filter = "所有文件 (*.*)|*.*"
};
if (dialog.ShowDialog(this) == true)
{
TargetTextBox.Text = dialog.FileName;
}
}
private void BrowseFolderButton_OnClick(object sender, RoutedEventArgs e)
{
using var dialog = new Forms.FolderBrowserDialog
{
Description = "选择文件夹作为本地工具",
UseDescriptionForTitle = true
};
if (dialog.ShowDialog() == Forms.DialogResult.OK)
{
TargetTextBox.Text = dialog.SelectedPath;
}
}
private void SaveButton_OnClick(object sender, RoutedEventArgs e)
{
var name = NameTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show(this, "请输入工具名称。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (CategoryComboBox.SelectedValue is not string categoryId || string.IsNullOrWhiteSpace(categoryId))
{
MessageBox.Show(this, "请选择分类。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var hotkey = HotkeyTextBox.Text.Trim();
if (!string.IsNullOrWhiteSpace(hotkey) && !HotkeyParser.TryParse(hotkey, out _, out _))
{
MessageBox.Show(this, "快捷键格式无效,请使用类似 Ctrl + Alt + T 的格式。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (_tool.Type == ToolType.Url)
{
if (!PathValidationService.IsValidUrl(TargetTextBox.Text, out var normalizedUrl))
{
MessageBox.Show(this, "请输入有效的网址。", "保存工具", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
_tool.Url = normalizedUrl;
_tool.LaunchTarget = null;
}
else
{
_tool.LaunchTarget = TargetTextBox.Text.Trim();
_tool.Url = null;
}
_tool.Name = name;
_tool.Description = DescriptionTextBox.Text.Trim();
_tool.CategoryId = categoryId;
_tool.Arguments = ArgumentsTextBox.Text.Trim();
_tool.WorkingDirectory = WorkingDirectoryTextBox.Text.Trim();
_tool.Hotkey = string.IsNullOrWhiteSpace(hotkey) ? null : HotkeyParser.Normalize(hotkey);
_tool.AutoRunEnabled = AutoRunCheckBox.IsChecked == true;
_tool.RunAsAdmin = RunAsAdminCheckBox.IsChecked == true;
DialogResult = true;
}
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}