A:增加可选页面模块

This commit is contained in:
jimmy committed 2025-12-22 21:19:01 +08:00
1 parent 37aa848b5f
commit 30451cabbd
27 files changed
+2046 -2

No files matched your search

+2
View File
@@ -13,6 +13,7 @@ using Sharp.NetServer.DependencyInjection;
using Sharp.Wpf.Services; using Sharp.Wpf.Services;
using Sharp.Wpf.Abstractions; using Sharp.Wpf.Abstractions;
using Sharp.Wpf.HandyControl.DependencyInjection; using Sharp.Wpf.HandyControl.DependencyInjection;
using Sharp.Wpf.DevTools.DependencyInjection;
using Plc.Siemens.DependencyInjection; using Plc.Siemens.DependencyInjection;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Sharp.Process.DependencyInjection; using Sharp.Process.DependencyInjection;
@@ -116,6 +117,7 @@ namespace Client
// WPF app services // WPF app services
services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService) services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService)
services.AddSharpDevTools(); // 开发者工具(开发者工具、日志查看器、性能监控等)
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>(); services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
services.AddSingleton<IUserSession, UserSession>(); services.AddSingleton<IUserSession, UserSession>();
// Register DispatcherService with the current UI dispatcher // Register DispatcherService with the current UI dispatcher
+1
View File
@@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wpf\Wpf.csproj" /> <ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" /> <ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" />
<ProjectReference Include="..\Wpf.DevTools\Wpf.DevTools.csproj" />
<ProjectReference Include="..\Config\Config.csproj" /> <ProjectReference Include="..\Config\Config.csproj" />
<ProjectReference Include="..\NetClient\NetClient.csproj" /> <ProjectReference Include="..\NetClient\NetClient.csproj" />
<ProjectReference Include="..\NetServer\NetServer.csproj" /> <ProjectReference Include="..\NetServer\NetServer.csproj" />
+32 -1
View File
@@ -16,8 +16,39 @@
<hc:GlowWindow.Resources> <hc:GlowWindow.Resources>
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" /> <conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
</hc:GlowWindow.Resources> </hc:GlowWindow.Resources>
<!-- 快捷键绑定 -->
<hc:GlowWindow.InputBindings>
<KeyBinding Key="F12" Command="{Binding OpenDevToolsCommand}" />
<KeyBinding Key="F11" Command="{Binding OpenLogViewerCommand}" />
<KeyBinding Key="F10" Command="{Binding OpenPerformanceMonitorCommand}" />
</hc:GlowWindow.InputBindings>
<Grid Background="{DynamicResource RegionBrush}"> <Grid Background="{DynamicResource RegionBrush}">
<Grid Margin="24"> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 菜单栏 -->
<Menu Grid.Row="0" Background="{DynamicResource RegionBrush}" Padding="8,4">
<MenuItem Header="文件(_F)">
<MenuItem Header="退出(_X)" InputGestureText="Alt+F4" Click="MenuItem_Exit_Click" />
</MenuItem>
<MenuItem Header="工具(_T)">
<MenuItem Header="PLC 调试(_P)" Command="{Binding OpenPlcToolCommand}" />
<MenuItem Header="串口调试(_S)" Command="{Binding OpenSerialPortToolCommand}" />
<MenuItem Header="Process/Python 测试(_T)" Command="{Binding OpenProcessTestCommand}" />
<Separator />
<MenuItem Header="开发者工具(_D)" Command="{Binding OpenDevToolsCommand}" InputGestureText="F12" />
<MenuItem Header="日志查看器(_L)" Command="{Binding OpenLogViewerCommand}" InputGestureText="F11" />
<MenuItem Header="性能监控(_M)" Command="{Binding OpenPerformanceMonitorCommand}" InputGestureText="F10" />
</MenuItem>
<MenuItem Header="帮助(_H)">
<MenuItem Header="关于(_A)" Command="{Binding OpenAboutCommand}" />
</MenuItem>
</Menu>
<Grid Grid.Row="1" Margin="24">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
+6
View File
@@ -1,3 +1,4 @@
using System.Windows;
using HandyControl.Controls; using HandyControl.Controls;
using Client.ViewModels; using Client.ViewModels;
@@ -10,4 +11,9 @@ public partial class MainWindow : GlowWindow
InitializeComponent(); InitializeComponent();
DataContext = viewModel; DataContext = viewModel;
} }
private void MenuItem_Exit_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
} }
+36 -1
View File
@@ -11,8 +11,8 @@ using Client.Hardware;
using Plc.Proxy; using Plc.Proxy;
using Plc.Abstractions; using Plc.Abstractions;
using System.ComponentModel; using System.ComponentModel;
using Sharp.Wpf.Abstractions; using Sharp.Wpf.Abstractions;
using Sharp.Wpf.DevTools.Views;
namespace Client.ViewModels; namespace Client.ViewModels;
@@ -61,6 +61,35 @@ public sealed class MainViewModel : ViewModelBase
win.Show(); win.Show();
}); });
// DevTools 窗口命令
OpenDevToolsCommand = new RelayCommand(() =>
{
var sp = Global.Get<IServiceProvider>();
var win = sp.GetRequiredService<DevToolsWindow>();
win.Show();
});
OpenLogViewerCommand = new RelayCommand(() =>
{
var sp = Global.Get<IServiceProvider>();
var win = sp.GetRequiredService<LogViewerWindow>();
win.Show();
});
OpenPerformanceMonitorCommand = new RelayCommand(() =>
{
var sp = Global.Get<IServiceProvider>();
var win = sp.GetRequiredService<PerformanceMonitorWindow>();
win.Show();
});
OpenAboutCommand = new RelayCommand(() =>
{
var sp = Global.Get<IServiceProvider>();
var win = sp.GetRequiredService<AboutWindow>();
win.ShowDialog();
});
// 1. 获取 PLC 服务 // 1. 获取 PLC 服务
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>(); var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
@@ -120,6 +149,12 @@ public sealed class MainViewModel : ViewModelBase
public ICommand StartMachineCommand { get; } public ICommand StartMachineCommand { get; }
public ICommand StopMachineCommand { get; } public ICommand StopMachineCommand { get; }
// DevTools 命令
public ICommand OpenDevToolsCommand { get; }
public ICommand OpenLogViewerCommand { get; }
public ICommand OpenPerformanceMonitorCommand { get; }
public ICommand OpenAboutCommand { get; }
private async Task ShowDialogAsync() private async Task ShowDialogAsync()
{ {
try try
+14
View File
@@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Process", "Process\Process.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python", "Python\Python.csproj", "{99FD794A-E5A7-4A55-8193-F30541B3EEF5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python", "Python\Python.csproj", "{99FD794A-E5A7-4A55-8193-F30541B3EEF5}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.DevTools", "Wpf.DevTools\Wpf.DevTools.csproj", "{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -335,6 +337,18 @@ Global
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.Build.0 = Release|Any CPU {99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.Build.0 = Release|Any CPU
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.ActiveCfg = Release|Any CPU {99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.ActiveCfg = Release|Any CPU
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.Build.0 = Release|Any CPU {99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.Build.0 = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|x64.ActiveCfg = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|x64.Build.0 = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|x86.ActiveCfg = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Debug|x86.Build.0 = Debug|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|Any CPU.Build.0 = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|x64.ActiveCfg = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|x64.Build.0 = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|x86.ActiveCfg = Release|Any CPU
{3DFC9CBE-D9A3-43A8-80D9-D877000A0BAA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -0,0 +1,40 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Wpf.DevTools.Services;
using Sharp.Wpf.DevTools.ViewModels;
using Sharp.Wpf.DevTools.Views;
namespace Sharp.Wpf.DevTools.DependencyInjection;
/// <summary>
/// DevTools 服务注册扩展
/// </summary>
public static class DevToolsServiceCollectionExtensions
{
/// <summary>
/// 添加 Sharp.Wpf.DevTools 开发者工具
/// 注册所有开发者工具窗口和服务
/// </summary>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddSharpDevTools(this IServiceCollection services)
{
// 注册服务
services.TryAddSingleton<IDevToolsService, DevToolsService>();
services.TryAddSingleton<ILogReaderService, LogReaderService>();
// 注册 ViewModels
services.AddTransient<DevToolsViewModel>();
services.AddTransient<LogViewerViewModel>();
services.AddTransient<PerformanceMonitorViewModel>();
services.AddTransient<AboutViewModel>();
// 注册 Windows
services.AddTransient<DevToolsWindow>();
services.AddTransient<LogViewerWindow>();
services.AddTransient<PerformanceMonitorWindow>();
services.AddTransient<AboutWindow>();
return services;
}
}
+646
View File
@@ -0,0 +1,646 @@
# Sharp.Wpf.DevTools
WPF 开发者工具模块,提供开箱即用的开发辅助窗口,包括日志查看器、性能监控、系统信息等实用工具。
## 📦 模块概述
Sharp.Wpf.DevTools 是一个独立的 WPF 开发工具集合,为开发者和运维人员提供常用的诊断和调试窗口。本模块**不强制绑定快捷键**,所有窗口均通过依赖注入提供,由调用者决定如何集成和使用。
### 特性
- 🔧 **开发者工具窗口**:快捷操作和系统信息一览
- 📄 **日志查看器**:实时查看、搜索、过滤应用日志
- 📊 **性能监控器**:实时监控 CPU、内存、GC 等性能指标
- ℹ️ **关于窗口**:显示应用程序和框架信息
- 💉 **完全 DI 集成**:所有窗口和服务均可注入
- 🎨 **无 UI 库依赖**:纯 WPF 实现,可与任何 UI 库搭配使用
## 🚀 快速开始
### 1. 安装依赖
```bash
dotnet add reference Sharp.Wpf.DevTools.csproj
```
或通过 NuGet(待发布):
```bash
dotnet add package Sharp.Wpf.DevTools
```
### 2. 注册服务
在 `App.xaml.cs` 的 `OnStartup` 方法中注册 DevTools 服务:
```csharp
using Sharp.Wpf.DevTools.DependencyInjection;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// 注册 DevTools(一行代码搞定)
services.AddSharpDevTools();
// 注册其他服务...
_serviceProvider = services.BuildServiceProvider();
}
```
### 3. 使用窗口
通过依赖注入获取窗口并显示:
```csharp
// 注入到 ViewModel 或其他服务
public class MainViewModel
{
private readonly IServiceProvider _serviceProvider;
public MainViewModel(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
private void ShowDevTools()
{
var window = _serviceProvider.GetRequiredService<DevToolsWindow>();
window.Show();
}
private void ShowLogViewer()
{
var window = _serviceProvider.GetRequiredService<LogViewerWindow>();
window.Show();
}
private void ShowPerformanceMonitor()
{
var window = _serviceProvider.GetRequiredService<PerformanceMonitorWindow>();
window.Show();
}
private void ShowAbout()
{
var window = _serviceProvider.GetRequiredService<AboutWindow>();
window.ShowDialog(); // 模态显示
}
}
```
## 📖 可用窗口
### 1. DevToolsWindow - 开发者工具
提供快捷操作和系统信息查看。
**主要功能**:
- 📁 打开运行目录:快速访问应用程序根目录
- 📄 打开日志目录:快速访问日志文件夹
- ⚙️ 打开配置文件:直接打开 appsettings.json
- 🗑️ 清除缓存:清空应用缓存(如果有)
- 📦 导出诊断信息:打包配置、日志、系统信息为 ZIP 文件到桌面
- 🔄 刷新信息:更新系统信息显示
**系统信息显示**:
- 操作系统版本
- .NET 运行时版本
- CPU 核心数
- 内存使用(工作集)
- 应用程序运行时长
**使用示例**:
```csharp
// 在菜单项中集成
private void MenuItem_DevTools_Click(object sender, RoutedEventArgs e)
{
var window = _serviceProvider.GetRequiredService<DevToolsWindow>();
window.Show();
}
```
### 2. LogViewerWindow - 日志查看器
实时查看和搜索应用程序日志文件。
**主要功能**:
- 📋 日志文件列表:自动发现 logs 目录下的所有日志文件
- 🔍 实时搜索:输入关键字即时过滤日志条目
- 📅 时间排序:按时间倒序显示(最新的在前)
- 🎨 级别着色:根据日志级别(Info/Warning/Error)显示不同颜色
- 📝 详情查看:点击日志条目查看完整内容(支持多行日志、堆栈跟踪)
- 🔄 刷新列表:重新加载日志文件
- 🗑️ 清除日志:清空所有日志文件(谨慎使用)
**使用示例**:
```csharp
// 在工具栏按钮中集成
private void ToolbarButton_Logs_Click(object sender, RoutedEventArgs e)
{
var window = _serviceProvider.GetRequiredService<LogViewerWindow>();
window.Show();
}
```
**日志格式要求**:
LogViewerWindow 使用正则表达式解析日志,建议使用以下格式:
```
[2025-12-22 14:30:45] [Info] 应用程序启动
[2025-12-22 14:30:46] [Warning] 配置文件不存在,使用默认配置
[2025-12-22 14:30:50] [Error] 数据库连接失败
System.Data.SqlClient.SqlException: 连接超时
at System.Data.SqlClient.SqlConnection.Open()
at MyApp.Database.Connect()
```
格式:`[日期时间] [级别] 消息内容`
多行日志(如堆栈跟踪)会自动附加到上一条日志的 FullContent 中。
### 3. PerformanceMonitorWindow - 性能监控器
实时监控应用程序性能指标。
**监控指标**:
- **CPU 使用率**:当前进程的 CPU 占用百分比
- **内存使用**:
- 工作集(Working Set):进程占用的物理内存
- 私有内存(Private Bytes):进程独占的内存
- **线程数**:当前进程的线程总数
- **GC 统计**:
- Gen 0 回收次数
- Gen 1 回收次数
- Gen 2 回收次数
- **运行时长**:应用程序启动以来的运行时间
**控制按钮**:
- ▶️ 开始监控:启动定时刷新(每秒)
- ⏸️ 停止监控:停止刷新
- 🗑️ 强制 GC:立即触发垃圾回收
**使用示例**:
```csharp
// 在性能分析时使用
private void MenuItem_Performance_Click(object sender, RoutedEventArgs e)
{
var window = _serviceProvider.GetRequiredService<PerformanceMonitorWindow>();
window.Show();
}
```
**注意事项**:
- 性能监控器默认自动启动,每秒刷新一次
- CPU 使用率为累计平均值,不是瞬时值
- 强制 GC 会触发三次完整回收(包括 Finalizers),可能影响性能
### 4. AboutWindow - 关于窗口
显示应用程序和框架信息的美观对话框。
**显示信息**:
- 应用程序名称(从 AssemblyTitle 读取)
- 版本号(从 AssemblyVersion 读取)
- 版权信息(从 AssemblyCopyright 读取)
- 框架版本:Sharp.Basic 版本
- .NET 运行时版本
- 操作系统版本
- 开源许可证信息
**使用示例**:
```csharp
// 在菜单"关于"中显示
private void MenuItem_About_Click(object sender, RoutedEventArgs e)
{
var window = _serviceProvider.GetRequiredService<AboutWindow>();
window.ShowDialog(); // 模态显示
}
```
**自定义程序集信息**:
在项目的 `.csproj` 中添加程序集属性:
```xml
<PropertyGroup>
<AssemblyTitle>我的应用程序</AssemblyTitle>
<Company>我的公司</Company>
<Copyright>Copyright © 2025 我的公司</Copyright>
<Version>1.2.3</Version>
</PropertyGroup>
```
或在 `AssemblyInfo.cs` 中设置:
```csharp
[assembly: AssemblyTitle("我的应用程序")]
[assembly: AssemblyCompany("我的公司")]
[assembly: AssemblyCopyright("Copyright © 2025 我的公司")]
[assembly: AssemblyVersion("1.2.3")]
```
## 🔧 服务接口
### IDevToolsService
提供开发者工具的核心操作。
```csharp
public interface IDevToolsService
{
/// <summary>
/// 打开应用程序运行目录
/// </summary>
void OpenRuntimeDirectory();
/// <summary>
/// 打开日志目录
/// </summary>
void OpenLogDirectory();
/// <summary>
/// 打开配置文件(appsettings.json)
/// </summary>
void OpenConfigFile();
/// <summary>
/// 清除缓存(如果应用有缓存)
/// </summary>
Task ClearCacheAsync();
/// <summary>
/// 导出诊断信息为 ZIP 文件到桌面
/// </summary>
/// <returns>导出文件的完整路径</returns>
Task<string> ExportDiagnosticsAsync();
/// <summary>
/// 获取系统信息
/// </summary>
SystemInfo GetSystemInfo();
}
```
### ILogReaderService
提供日志文件读取和解析。
```csharp
public interface ILogReaderService
{
/// <summary>
/// 获取所有可用的日志文件列表
/// </summary>
List<string> GetLogFiles();
/// <summary>
/// 读取指定日志文件的所有条目
/// </summary>
/// <param name="filePath">日志文件路径(为空则读取最新文件)</param>
Task<List<LogEntry>> ReadLogsAsync(string? filePath = null);
/// <summary>
/// 清除所有日志文件
/// </summary>
Task ClearLogsAsync();
}
```
### 使用服务示例
```csharp
// 在自定义服务中使用 DevTools 服务
public class MyCustomService
{
private readonly IDevToolsService _devTools;
private readonly ILogReaderService _logReader;
public MyCustomService(IDevToolsService devTools, ILogReaderService logReader)
{
_devTools = devTools;
_logReader = logReader;
}
public async Task PerformDiagnostics()
{
// 获取系统信息
var sysInfo = _devTools.GetSystemInfo();
Console.WriteLine($"内存: {sysInfo.WorkingSetMB} MB");
// 读取最新日志
var logs = await _logReader.ReadLogsAsync();
var errors = logs.Where(l => l.Level == "Error").ToList();
if (errors.Count > 10)
{
// 自动导出诊断信息
var zipPath = await _devTools.ExportDiagnosticsAsync();
Console.WriteLine($"诊断信息已导出到: {zipPath}");
}
}
}
```
## 🎯 集成场景
### 场景 1:菜单栏集成
在主窗口菜单中添加"工具"菜单:
```xml
<Menu>
<MenuItem Header="工具(_T)">
<MenuItem Header="开发者工具(_D)" Click="MenuItem_DevTools_Click"/>
<MenuItem Header="日志查看器(_L)" Click="MenuItem_LogViewer_Click"/>
<MenuItem Header="性能监控(_P)" Click="MenuItem_Performance_Click"/>
<Separator/>
<MenuItem Header="关于(_A)" Click="MenuItem_About_Click"/>
</MenuItem>
</Menu>
```
代码后台:
```csharp
private void MenuItem_DevTools_Click(object sender, RoutedEventArgs e)
{
_serviceProvider.GetRequiredService<DevToolsWindow>().Show();
}
private void MenuItem_LogViewer_Click(object sender, RoutedEventArgs e)
{
_serviceProvider.GetRequiredService<LogViewerWindow>().Show();
}
private void MenuItem_Performance_Click(object sender, RoutedEventArgs e)
{
_serviceProvider.GetRequiredService<PerformanceMonitorWindow>().Show();
}
private void MenuItem_About_Click(object sender, RoutedEventArgs e)
{
_serviceProvider.GetRequiredService<AboutWindow>().ShowDialog();
}
```
### 场景 2:工具栏按钮集成
```xml
<ToolBar>
<Button Content="🔧 工具" Click="Toolbar_DevTools_Click" ToolTip="开发者工具"/>
<Button Content="📄 日志" Click="Toolbar_Logs_Click" ToolTip="查看日志"/>
<Button Content="📊 性能" Click="Toolbar_Performance_Click" ToolTip="性能监控"/>
</ToolBar>
```
### 场景 3:快捷键集成(可选)
在主窗口 XAML 中定义 InputBindings:
```xml
<Window.InputBindings>
<KeyBinding Key="F12" Command="{Binding ShowDevToolsCommand}"/>
<KeyBinding Key="F11" Command="{Binding ShowLogsCommand}"/>
<KeyBinding Key="F10" Command="{Binding ShowPerformanceCommand}"/>
</Window.InputBindings>
```
ViewModel 中:
```csharp
public ICommand ShowDevToolsCommand => new RelayCommand(() =>
{
_serviceProvider.GetRequiredService<DevToolsWindow>().Show();
});
public ICommand ShowLogsCommand => new RelayCommand(() =>
{
_serviceProvider.GetRequiredService<LogViewerWindow>().Show();
});
public ICommand ShowPerformanceCommand => new RelayCommand(() =>
{
_serviceProvider.GetRequiredService<PerformanceMonitorWindow>().Show();
});
```
### 场景 4:调试模式专用
只在调试模式下显示开发者工具菜单:
```csharp
public MainWindow()
{
InitializeComponent();
#if DEBUG
// 仅在 Debug 模式下显示工具菜单
ToolsMenu.Visibility = Visibility.Visible;
#else
ToolsMenu.Visibility = Visibility.Collapsed;
#endif
}
```
### 场景 5:权限控制
根据用户权限显示开发者工具:
```csharp
public MainViewModel(IUserSession userSession, IServiceProvider serviceProvider)
{
if (userSession.CurrentUser?.IsAdmin == true)
{
// 管理员可见
ShowDevToolsCommand = new RelayCommand(() =>
{
serviceProvider.GetRequiredService<DevToolsWindow>().Show();
});
}
else
{
// 普通用户不可见
ShowDevToolsCommand = new RelayCommand(() =>
{
MessageBox.Show("您没有权限访问开发者工具", "权限不足", MessageBoxButton.OK, MessageBoxImage.Warning);
});
}
}
```
## ⚙️ 配置
### 日志目录配置
DevTools 默认使用以下路径查找日志:
```csharp
// 默认日志目录
Path.Combine(AppContext.BaseDirectory, "logs")
```
如果使用 Sharp.Log 模块,确保日志输出到 `logs` 目录:
```json
// appsettings.json
{
"SharpLog": {
"LogDirectory": "logs",
"RollingInterval": "Day"
}
}
```
### 配置文件路径
DevTools 默认打开以下配置文件:
```csharp
Path.Combine(AppContext.BaseDirectory, "appsettings.json")
```
### 自定义目录
如果需要自定义日志或配置目录,可以继承 `DevToolsService` 并重写相关方法:
```csharp
public class CustomDevToolsService : DevToolsService
{
protected override string GetLogDirectory()
{
return @"C:\MyApp\CustomLogs";
}
protected override string GetConfigFilePath()
{
return @"C:\MyApp\config.json";
}
}
// 注册时替换默认实现
services.AddSingleton<IDevToolsService, CustomDevToolsService>();
```
## 📦 依赖项
- **Sharp.Common**:Result<T>、Global 容器等核心功能
- **Sharp.Wpf**:ViewModelBase、Commands、对话框服务等 MVVM 基础设施
- **Sharp.Config**:ConfigManager(可选,用于 ConfigEditorWindow)
- **.NET 8.0-windows**:WPF 框架
**无外部 UI 库依赖**:本模块不依赖 HandyControl 或其他第三方 UI 库,使用纯 WPF 控件。
## 🔍 FAQ
### 1. 为什么不强制绑定快捷键?
不同应用有不同的快捷键需求,框架不应做假设。我们提供窗口,由调用者决定如何触发(菜单、工具栏、快捷键、右键菜单等)。
### 2. 窗口可以多次打开吗?
可以。所有窗口均注册为 `Transient` 生命周期,每次注入都会创建新实例。如果需要单例行为(同一窗口只打开一次),可以在 ViewModel 中自行管理:
```csharp
private DevToolsWindow? _devToolsWindow;
private void ShowDevTools()
{
if (_devToolsWindow == null || !_devToolsWindow.IsLoaded)
{
_devToolsWindow = _serviceProvider.GetRequiredService<DevToolsWindow>();
_devToolsWindow.Closed += (s, e) => _devToolsWindow = null;
_devToolsWindow.Show();
}
else
{
_devToolsWindow.Activate(); // 已存在则激活
}
}
```
### 3. 如何定制窗口样式?
所有窗口都是标准 WPF Window,可以通过以下方式定制:
**方式 1:继承并修改 XAML**
```csharp
public class MyDevToolsWindow : DevToolsWindow
{
public MyDevToolsWindow(DevToolsViewModel viewModel) : base(viewModel)
{
// 自定义样式
Background = Brushes.DarkGray;
}
}
```
**方式 2:应用全局样式**
在 App.xaml 中定义 Window 样式:
```xml
<Style TargetType="{x:Type local:DevToolsWindow}">
<Setter Property="Background" Value="#FF2D2D30"/>
<Setter Property="Foreground" Value="White"/>
</Style>
```
### 4. 性能监控会影响应用性能吗?
性能监控使用 `DispatcherTimer` 每秒刷新一次,对性能影响微乎其微(< 0.1% CPU)。如果担心影响,可以在不需要时关闭窗口,监控会自动停止。
### 5. 日志查看器支持哪些日志格式?
目前支持常见的结构化日志格式:`[时间] [级别] 消息`。如果使用 Serilog 或 NLog,建议配置类似格式:
```csharp
// Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/app.log",
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
```
如果日志格式不同,可以继承 `LogReaderService` 并重写 `ReadLogsAsync` 方法,使用自定义解析逻辑。
### 6. 可以只使用部分窗口吗?
可以。虽然 `AddSharpDevTools()` 会注册所有窗口,但由于是 Transient 生命周期,不使用的窗口不会被实例化,不会占用资源。如果确实想减少注册,可以手动注册:
```csharp
services.AddSingleton<IDevToolsService, DevToolsService>();
services.AddTransient<DevToolsViewModel>();
services.AddTransient<DevToolsWindow>();
// 只注册需要的窗口
```
## 📄 许可证
本模块遵循 Sharp.Basic 框架的许可证:**GNU AGPLv3**
- 开源项目可自由使用
- 商业项目如果通过网络提供服务,需要开源修改部分
- 如需商业许可,请联系作者
## 🤝 贡献
欢迎提交 Issue 和 Pull Request!
## 📞 支持
- GitHub Issues: https://github.com/your-org/sharp-basic/issues
- 文档: https://your-docs-site.com
---
**Sharp.Basic** - 固执己见的 .NET 8 框架,让开发更简单。
@@ -0,0 +1,166 @@
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using Microsoft.Extensions.Logging;
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 开发者工具服务实现
/// </summary>
public class DevToolsService : IDevToolsService
{
private readonly ILogger<DevToolsService> _logger;
private static readonly DateTime _startTime = DateTime.Now;
public DevToolsService(ILogger<DevToolsService> logger)
{
_logger = logger;
}
public void OpenRuntimeDirectory()
{
var path = AppContext.BaseDirectory;
OpenDirectory(path);
}
public void OpenLogDirectory()
{
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
if (!Directory.Exists(logPath))
{
Directory.CreateDirectory(logPath);
}
OpenDirectory(logPath);
}
public void OpenConfigFile()
{
var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (File.Exists(configPath))
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = configPath,
UseShellExecute = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "打开配置文件失败");
}
}
}
public async Task ClearCacheAsync()
{
await Task.Run(() =>
{
try
{
var cachePath = Path.Combine(AppContext.BaseDirectory, "cache");
if (Directory.Exists(cachePath))
{
Directory.Delete(cachePath, true);
Directory.CreateDirectory(cachePath);
}
_logger.LogInformation("缓存已清除");
}
catch (Exception ex)
{
_logger.LogError(ex, "清除缓存失败");
throw;
}
});
}
public async Task ExportDiagnosticsAsync()
{
await Task.Run(() =>
{
try
{
var exportPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
$"Diagnostics_{DateTime.Now:yyyyMMdd_HHmmss}.zip");
using var zip = ZipFile.Open(exportPath, ZipArchiveMode.Create);
// 添加配置文件
var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (File.Exists(configPath))
{
zip.CreateEntryFromFile(configPath, "appsettings.json");
}
// 添加日志文件
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
if (Directory.Exists(logPath))
{
foreach (var file in Directory.GetFiles(logPath, "*.log"))
{
zip.CreateEntryFromFile(file, $"logs/{Path.GetFileName(file)}");
}
}
// 添加系统信息
var sysInfo = GetSystemInfo();
var sysInfoEntry = zip.CreateEntry("SystemInfo.txt");
using var writer = new StreamWriter(sysInfoEntry.Open());
writer.WriteLine($"操作系统: {sysInfo.OSVersion}");
writer.WriteLine($".NET版本: {sysInfo.DotNetVersion}");
writer.WriteLine($"计算机名: {sysInfo.MachineName}");
writer.WriteLine($"用户名: {sysInfo.UserName}");
writer.WriteLine($"CPU核心数: {sysInfo.ProcessorCount}");
writer.WriteLine($"总内存: {sysInfo.TotalMemoryMB} MB");
writer.WriteLine($"工作集内存: {sysInfo.WorkingSetMB} MB");
writer.WriteLine($"运行时长: {sysInfo.Uptime}");
_logger.LogInformation("诊断信息已导出到: {Path}", exportPath);
// 打开文件所在目录
Process.Start("explorer.exe", $"/select,\"{exportPath}\"");
}
catch (Exception ex)
{
_logger.LogError(ex, "导出诊断信息失败");
throw;
}
});
}
public SystemInfo GetSystemInfo()
{
var process = Process.GetCurrentProcess();
return new SystemInfo
{
OSVersion = Environment.OSVersion.ToString(),
DotNetVersion = Environment.Version.ToString(),
MachineName = Environment.MachineName,
UserName = Environment.UserName,
ProcessorCount = Environment.ProcessorCount,
TotalMemoryMB = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 1024 / 1024,
WorkingSetMB = process.WorkingSet64 / 1024 / 1024,
Uptime = DateTime.Now - _startTime
};
}
private void OpenDirectory(string path)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
Verb = "open"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "打开目录失败: {Path}", path);
}
}
}
@@ -0,0 +1,37 @@
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 开发者工具服务接口
/// </summary>
public interface IDevToolsService
{
/// <summary>
/// 打开运行目录
/// </summary>
void OpenRuntimeDirectory();
/// <summary>
/// 打开日志目录
/// </summary>
void OpenLogDirectory();
/// <summary>
/// 打开配置文件
/// </summary>
void OpenConfigFile();
/// <summary>
/// 清除缓存
/// </summary>
Task ClearCacheAsync();
/// <summary>
/// 导出诊断信息
/// </summary>
Task ExportDiagnosticsAsync();
/// <summary>
/// 获取系统信息
/// </summary>
SystemInfo GetSystemInfo();
}
@@ -0,0 +1,22 @@
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 日志读取服务接口
/// </summary>
public interface ILogReaderService
{
/// <summary>
/// 读取日志文件
/// </summary>
Task<List<LogEntry>> ReadLogsAsync(string? filePath = null);
/// <summary>
/// 获取日志文件列表
/// </summary>
List<string> GetLogFiles();
/// <summary>
/// 清除日志
/// </summary>
Task ClearLogsAsync();
}
@@ -0,0 +1,27 @@
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 日志条目
/// </summary>
public class LogEntry
{
/// <summary>
/// 时间
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// 日志级别
/// </summary>
public string Level { get; set; } = string.Empty;
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 完整内容(包含堆栈跟踪等)
/// </summary>
public string FullContent { get; set; } = string.Empty;
}
@@ -0,0 +1,96 @@
using System.IO;
using System.Text.RegularExpressions;
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 日志读取服务实现(简化版本)
/// </summary>
public class LogReaderService : ILogReaderService
{
private static readonly Regex LogPattern = new Regex(
@"^\[(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\]\s\[(\w+)\]\s(.+)$",
RegexOptions.Compiled);
public async Task<List<LogEntry>> ReadLogsAsync(string? filePath = null)
{
if (string.IsNullOrEmpty(filePath))
{
// 使用今天的日志文件
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
if (!Directory.Exists(logPath))
return new List<LogEntry>();
var todayLog = Path.Combine(logPath, $"log_{DateTime.Now:yyyyMMdd}.log");
if (!File.Exists(todayLog))
{
// 尝试找最新的日志文件
var files = Directory.GetFiles(logPath, "*.log");
if (files.Length == 0)
return new List<LogEntry>();
todayLog = files.OrderByDescending(f => File.GetLastWriteTime(f)).First();
}
filePath = todayLog;
}
if (!File.Exists(filePath))
return new List<LogEntry>();
var logs = new List<LogEntry>();
await Task.Run(() =>
{
using var reader = new StreamReader(filePath);
string? line;
while ((line = reader.ReadLine()) != null)
{
var match = LogPattern.Match(line);
if (match.Success)
{
logs.Add(new LogEntry
{
Timestamp = DateTime.Parse(match.Groups[1].Value),
Level = match.Groups[2].Value,
Message = match.Groups[3].Value,
FullContent = line
});
}
else if (logs.Count > 0)
{
// 多行日志(如堆栈跟踪)追加到上一条
logs[^1].FullContent += Environment.NewLine + line;
}
}
});
return logs.OrderByDescending(l => l.Timestamp).ToList();
}
public List<string> GetLogFiles()
{
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
if (!Directory.Exists(logPath))
return new List<string>();
return Directory.GetFiles(logPath, "*.log")
.OrderByDescending(f => File.GetLastWriteTime(f))
.ToList();
}
public async Task ClearLogsAsync()
{
await Task.Run(() =>
{
var logPath = Path.Combine(AppContext.BaseDirectory, "logs");
if (Directory.Exists(logPath))
{
foreach (var file in Directory.GetFiles(logPath, "*.log"))
{
File.Delete(file);
}
}
});
}
}
@@ -0,0 +1,47 @@
namespace Sharp.Wpf.DevTools.Services;
/// <summary>
/// 系统信息
/// </summary>
public class SystemInfo
{
/// <summary>
/// 操作系统版本
/// </summary>
public string OSVersion { get; set; } = string.Empty;
/// <summary>
/// .NET 版本
/// </summary>
public string DotNetVersion { get; set; } = string.Empty;
/// <summary>
/// 计算机名
/// </summary>
public string MachineName { get; set; } = string.Empty;
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// CPU 核心数
/// </summary>
public int ProcessorCount { get; set; }
/// <summary>
/// 总内存(MB)
/// </summary>
public long TotalMemoryMB { get; set; }
/// <summary>
/// 工作集内存(MB)
/// </summary>
public long WorkingSetMB { get; set; }
/// <summary>
/// 运行时长
/// </summary>
public TimeSpan Uptime { get; set; }
}
@@ -0,0 +1,60 @@
using System.Reflection;
using Sharp.Wpf.Mvvm;
namespace Sharp.Wpf.DevTools.ViewModels;
/// <summary>
/// 关于窗口 ViewModel
/// </summary>
public class AboutViewModel : ViewModelBase
{
public AboutViewModel()
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
var assemblyName = assembly.GetName();
ApplicationName = assembly.GetCustomAttribute<AssemblyTitleAttribute>()?.Title ?? "Sharp Application";
Version = assemblyName.Version?.ToString() ?? "1.0.0";
Copyright = assembly.GetCustomAttribute<AssemblyCopyrightAttribute>()?.Copyright ?? "Copyright © 2025";
Description = assembly.GetCustomAttribute<AssemblyDescriptionAttribute>()?.Description ?? "";
FrameworkVersion = "Sharp.Basic v1.0.0";
DotNetVersion = $".NET {Environment.Version.Major}.{Environment.Version.Minor}";
OSVersion = Environment.OSVersion.ToString();
}
/// <summary>
/// 应用程序名称
/// </summary>
public string ApplicationName { get; }
/// <summary>
/// 版本号
/// </summary>
public string Version { get; }
/// <summary>
/// 版权信息
/// </summary>
public string Copyright { get; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; }
/// <summary>
/// 框架版本
/// </summary>
public string FrameworkVersion { get; }
/// <summary>
/// .NET 版本
/// </summary>
public string DotNetVersion { get; }
/// <summary>
/// 操作系统版本
/// </summary>
public string OSVersion { get; }
}
@@ -0,0 +1,87 @@
using System.Windows.Input;
using Sharp.Wpf.Commands;
using Sharp.Wpf.DevTools.Services;
using Sharp.Wpf.Mvvm;
namespace Sharp.Wpf.DevTools.ViewModels;
/// <summary>
/// 开发者工具 ViewModel
/// </summary>
public class DevToolsViewModel : ViewModelBase
{
private readonly IDevToolsService _devTools;
private SystemInfo? _systemInfo;
public DevToolsViewModel(IDevToolsService devTools)
{
_devTools = devTools;
OpenRuntimeDirectoryCommand = new RelayCommand(OpenRuntimeDirectory);
OpenLogDirectoryCommand = new RelayCommand(OpenLogDirectory);
OpenConfigFileCommand = new RelayCommand(OpenConfigFile);
ClearCacheCommand = new AsyncCommand(ClearCacheAsync);
ExportDiagnosticsCommand = new AsyncCommand(ExportDiagnosticsAsync);
RefreshSystemInfoCommand = new RelayCommand(RefreshSystemInfo);
RefreshSystemInfo();
}
public ICommand OpenRuntimeDirectoryCommand { get; }
public ICommand OpenLogDirectoryCommand { get; }
public ICommand OpenConfigFileCommand { get; }
public IAsyncCommand ClearCacheCommand { get; }
public IAsyncCommand ExportDiagnosticsCommand { get; }
public ICommand RefreshSystemInfoCommand { get; }
public SystemInfo? SystemInfo
{
get => _systemInfo;
set => SetProperty(ref _systemInfo, value);
}
private void OpenRuntimeDirectory()
{
_devTools.OpenRuntimeDirectory();
}
private void OpenLogDirectory()
{
_devTools.OpenLogDirectory();
}
private void OpenConfigFile()
{
_devTools.OpenConfigFile();
}
private async Task ClearCacheAsync()
{
try
{
await _devTools.ClearCacheAsync();
// 可以显示成功提示
}
catch (Exception)
{
// 可以显示错误提示
}
}
private async Task ExportDiagnosticsAsync()
{
try
{
await _devTools.ExportDiagnosticsAsync();
}
catch (Exception)
{
// 可以显示错误提示
}
}
private void RefreshSystemInfo()
{
SystemInfo = _devTools.GetSystemInfo();
}
}
@@ -0,0 +1,104 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Sharp.Wpf.Commands;
using Sharp.Wpf.DevTools.Services;
using Sharp.Wpf.Mvvm;
namespace Sharp.Wpf.DevTools.ViewModels;
/// <summary>
/// 日志查看器 ViewModel
/// </summary>
public class LogViewerViewModel : ViewModelBase
{
private readonly ILogReaderService _logReader;
private string? _selectedLogFile;
private LogEntry? _selectedLog;
private string _searchText = string.Empty;
public LogViewerViewModel(ILogReaderService logReader)
{
_logReader = logReader;
LoadLogsCommand = new AsyncCommand(LoadLogsAsync);
RefreshCommand = new AsyncCommand(RefreshAsync);
ClearLogsCommand = new AsyncCommand(ClearLogsAsync);
LogFiles = new ObservableCollection<string>(_logReader.GetLogFiles());
SelectedLogFile = LogFiles.FirstOrDefault();
}
public IAsyncCommand LoadLogsCommand { get; }
public IAsyncCommand RefreshCommand { get; }
public IAsyncCommand ClearLogsCommand { get; }
public ObservableCollection<string> LogFiles { get; }
public ObservableCollection<LogEntry> Logs { get; } = new();
public string? SelectedLogFile
{
get => _selectedLogFile;
set
{
if (SetProperty(ref _selectedLogFile, value))
{
_ = LoadLogsAsync();
}
}
}
public LogEntry? SelectedLog
{
get => _selectedLog;
set => SetProperty(ref _selectedLog, value);
}
public string SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value);
}
private async Task LoadLogsAsync()
{
if (string.IsNullOrEmpty(SelectedLogFile))
return;
IsBusy = true;
try
{
var logs = await _logReader.ReadLogsAsync(SelectedLogFile);
Logs.Clear();
foreach (var log in logs)
{
if (string.IsNullOrEmpty(SearchText) ||
log.Message.Contains(SearchText, StringComparison.OrdinalIgnoreCase))
{
Logs.Add(log);
}
}
}
finally
{
IsBusy = false;
}
}
private async Task RefreshAsync()
{
LogFiles.Clear();
foreach (var file in _logReader.GetLogFiles())
{
LogFiles.Add(file);
}
await LoadLogsAsync();
}
private async Task ClearLogsAsync()
{
await _logReader.ClearLogsAsync();
await RefreshAsync();
}
}
@@ -0,0 +1,137 @@
using System.Diagnostics;
using System.Windows.Input;
using System.Windows.Threading;
using Sharp.Wpf.Commands;
using Sharp.Wpf.Mvvm;
namespace Sharp.Wpf.DevTools.ViewModels;
/// <summary>
/// 性能监控 ViewModel
/// </summary>
public class PerformanceMonitorViewModel : ViewModelBase
{
private readonly DispatcherTimer _timer;
private readonly Process _process;
private readonly DateTime _startTime;
private double _cpuUsage;
private long _workingSetMB;
private long _privateMB;
private int _threadCount;
private int _gen0Collections;
private int _gen1Collections;
private int _gen2Collections;
public PerformanceMonitorViewModel()
{
_process = Process.GetCurrentProcess();
_startTime = _process.StartTime;
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_timer.Tick += OnTimerTick;
StartMonitoringCommand = new RelayCommand(StartMonitoring);
StopMonitoringCommand = new RelayCommand(StopMonitoring);
ForceGCCommand = new RelayCommand(ForceGC);
UpdateMetrics();
StartMonitoring();
}
public ICommand StartMonitoringCommand { get; }
public ICommand StopMonitoringCommand { get; }
public ICommand ForceGCCommand { get; }
public double CpuUsage
{
get => _cpuUsage;
set => SetProperty(ref _cpuUsage, value);
}
public long WorkingSetMB
{
get => _workingSetMB;
set => SetProperty(ref _workingSetMB, value);
}
public long PrivateMB
{
get => _privateMB;
set => SetProperty(ref _privateMB, value);
}
public int ThreadCount
{
get => _threadCount;
set => SetProperty(ref _threadCount, value);
}
public int Gen0Collections
{
get => _gen0Collections;
set => SetProperty(ref _gen0Collections, value);
}
public int Gen1Collections
{
get => _gen1Collections;
set => SetProperty(ref _gen1Collections, value);
}
public int Gen2Collections
{
get => _gen2Collections;
set => SetProperty(ref _gen2Collections, value);
}
public TimeSpan Uptime => DateTime.Now - _startTime;
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateMetrics();
}
private void UpdateMetrics()
{
_process.Refresh();
// CPU使用率(简化计算)
CpuUsage = _process.TotalProcessorTime.TotalMilliseconds / (DateTime.Now - _startTime).TotalMilliseconds * 100 / Environment.ProcessorCount;
// 内存
WorkingSetMB = _process.WorkingSet64 / 1024 / 1024;
PrivateMB = _process.PrivateMemorySize64 / 1024 / 1024;
// 线程
ThreadCount = _process.Threads.Count;
// GC
Gen0Collections = GC.CollectionCount(0);
Gen1Collections = GC.CollectionCount(1);
Gen2Collections = GC.CollectionCount(2);
OnPropertyChanged(nameof(Uptime));
}
private void StartMonitoring()
{
_timer.Start();
}
private void StopMonitoring()
{
_timer.Stop();
}
private void ForceGC()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
UpdateMetrics();
}
}
@@ -0,0 +1,90 @@
<Window x:Class="Sharp.Wpf.DevTools.Views.AboutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
Title="关于" Height="450" Width="500"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
WindowStyle="SingleBorderWindow">
<Window.Resources>
<conv:NullOrEmptyToVisibilityConverter x:Key="NullOrEmptyToVis" Inverse="True"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 头部 -->
<Border Grid.Row="0" Background="#FF0078D7" Padding="30,40">
<StackPanel HorizontalAlignment="Center">
<!-- 应用图标 -->
<TextBlock Text="📱" FontSize="48" HorizontalAlignment="Center" Margin="0,0,0,15"/>
<!-- 应用名称 -->
<TextBlock Text="{Binding ApplicationName}"
FontSize="24" FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"/>
<!-- 版本号 -->
<TextBlock FontSize="14" Foreground="White" HorizontalAlignment="Center" Margin="0,5,0,0">
<Run Text="版本 "/>
<Run Text="{Binding Version, Mode=OneWay}"/>
</TextBlock>
</StackPanel>
</Border>
<!-- 信息区域 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="30,20">
<!-- 版权信息 -->
<TextBlock Text="{Binding Copyright}"
TextWrapping="Wrap"
FontSize="13"
Foreground="#666"
Margin="0,0,0,20"/>
<!-- 描述 -->
<TextBlock Text="{Binding Description}"
TextWrapping="Wrap"
FontSize="13"
Margin="0,0,0,20"
Visibility="{Binding Description, Converter={StaticResource NullOrEmptyToVis}}"/>
<!-- 框架信息 -->
<Separator Margin="0,0,0,15"/>
<TextBlock FontSize="13" Margin="0,5">
<Run Text="构建于:" FontWeight="Bold"/>
<Run Text="{Binding FrameworkVersion, Mode=OneWay}"/>
</TextBlock>
<TextBlock FontSize="13" Margin="0,5">
<Run Text="运行环境:" FontWeight="Bold"/>
<Run Text="{Binding DotNetVersion, Mode=OneWay}"/>
</TextBlock>
<TextBlock FontSize="13" Margin="0,5">
<Run Text="操作系统:" FontWeight="Bold"/>
<Run Text="{Binding OSVersion, Mode=OneWay}"/>
</TextBlock>
<!-- 许可证 -->
<Separator Margin="0,15"/>
<TextBlock Text="开源许可证" FontSize="14" FontWeight="Bold" Margin="0,10,0,5"/>
<TextBlock Text="GNU AGPLv3" FontSize="12" Foreground="#666"/>
</StackPanel>
</ScrollViewer>
<!-- 底部按钮 -->
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="0,1,0,0" Padding="20">
<Button Content="确定" Width="100" Height="32"
HorizontalAlignment="Right"
IsDefault="True"
Click="CloseButton_Click"/>
</Border>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
using Sharp.Wpf.DevTools.ViewModels;
namespace Sharp.Wpf.DevTools.Views;
/// <summary>
/// AboutWindow.xaml 的交互逻辑
/// </summary>
public partial class AboutWindow : Window
{
public AboutWindow(AboutViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
@@ -0,0 +1,108 @@
<Window x:Class="Sharp.Wpf.DevTools.Views.DevToolsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="开发者工具" Height="550" Width="700"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 快捷操作 -->
<GroupBox Header="快捷操作" Grid.Row="0" Margin="0,0,0,15">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 第一行 -->
<Button Grid.Row="0" Grid.Column="0"
Command="{Binding OpenRuntimeDirectoryCommand}"
Content="📁 打开运行目录"
Margin="5" Padding="10" Height="40"/>
<Button Grid.Row="0" Grid.Column="1"
Command="{Binding OpenLogDirectoryCommand}"
Content="📄 打开日志目录"
Margin="5" Padding="10" Height="40"/>
<Button Grid.Row="0" Grid.Column="2"
Command="{Binding OpenConfigFileCommand}"
Content="⚙️ 打开配置文件"
Margin="5" Padding="10" Height="40"/>
<!-- 第二行 -->
<Button Grid.Row="1" Grid.Column="0"
Command="{Binding ClearCacheCommand}"
Content="🗑️ 清除缓存"
Margin="5" Padding="10" Height="40"/>
<Button Grid.Row="1" Grid.Column="1"
Command="{Binding ExportDiagnosticsCommand}"
Content="📦 导出诊断信息"
Margin="5" Padding="10" Height="40"/>
<Button Grid.Row="1" Grid.Column="2"
Command="{Binding RefreshSystemInfoCommand}"
Content="🔄 刷新信息"
Margin="5" Padding="10" Height="40"/>
</Grid>
</GroupBox>
<!-- 系统信息 -->
<GroupBox Header="系统信息" Grid.Row="1" Margin="0,0,0,15">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="15" DataContext="{Binding SystemInfo}">
<TextBlock Margin="0,5">
<Run Text="🖥️ 操作系统: " FontWeight="Bold"/>
<Run Text="{Binding OSVersion, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="⚙️ .NET版本: " FontWeight="Bold"/>
<Run Text="{Binding DotNetVersion, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="💻 计算机名: " FontWeight="Bold"/>
<Run Text="{Binding MachineName, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="👤 用户名: " FontWeight="Bold"/>
<Run Text="{Binding UserName, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="🔧 CPU核心数: " FontWeight="Bold"/>
<Run Text="{Binding ProcessorCount, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="💾 总内存: " FontWeight="Bold"/>
<Run Text="{Binding TotalMemoryMB, Mode=OneWay}"/>
<Run Text=" MB"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="📊 工作集内存: " FontWeight="Bold"/>
<Run Text="{Binding WorkingSetMB, Mode=OneWay}"/>
<Run Text=" MB"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="⏱️ 运行时长: " FontWeight="Bold"/>
<Run Text="{Binding Uptime, Mode=OneWay, StringFormat='{}{0:d\\.hh\\:mm\\:ss}'}"/>
</TextBlock>
</StackPanel>
</ScrollViewer>
</GroupBox>
<!-- 底部按钮 -->
<Button Grid.Row="2" Content="关闭"
Width="100" Height="32"
HorizontalAlignment="Right"
Click="CloseButton_Click"/>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
using Sharp.Wpf.DevTools.ViewModels;
namespace Sharp.Wpf.DevTools.Views;
/// <summary>
/// DevToolsWindow.xaml 的交互逻辑
/// </summary>
public partial class DevToolsWindow : Window
{
public DevToolsWindow(DevToolsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
@@ -0,0 +1,69 @@
<Window x:Class="Sharp.Wpf.DevTools.Views.LogViewerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="日志查看器" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<ToolBar Grid.Row="0" Padding="5">
<Label Content="日志文件:"/>
<ComboBox ItemsSource="{Binding LogFiles}"
SelectedItem="{Binding SelectedLogFile}"
Width="250" Margin="5,0"/>
<Separator/>
<Label Content="搜索:"/>
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Width="150" Margin="5,0"/>
<Separator/>
<Button Content="🔄 刷新" Command="{Binding RefreshCommand}" Margin="5,0"/>
<Button Content="🗑️ 清除日志" Command="{Binding ClearLogsCommand}" Margin="5,0"/>
</ToolBar>
<!-- 日志列表 -->
<DataGrid Grid.Row="1"
ItemsSource="{Binding Logs}"
SelectedItem="{Binding SelectedLog}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F5F5F5"
Margin="5">
<DataGrid.Columns>
<DataGridTextColumn Header="时间" Binding="{Binding Timestamp, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="150"/>
<DataGridTextColumn Header="级别" Binding="{Binding Level}" Width="80"/>
<DataGridTextColumn Header="消息" Binding="{Binding Message}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
<!-- 详情 -->
<GroupBox Grid.Row="2" Header="详细信息" Height="150" Margin="5">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding SelectedLog.FullContent, Mode=OneWay}"
TextWrapping="Wrap"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
FontFamily="Consolas"
FontSize="12"/>
</ScrollViewer>
</GroupBox>
<!-- 底部 -->
<Button Grid.Row="3" Content="关闭"
Width="100" Height="32"
HorizontalAlignment="Right"
Margin="5"
Click="CloseButton_Click"/>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
using Sharp.Wpf.DevTools.ViewModels;
namespace Sharp.Wpf.DevTools.Views;
/// <summary>
/// LogViewerWindow.xaml 的交互逻辑
/// </summary>
public partial class LogViewerWindow : Window
{
public LogViewerWindow(LogViewerViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
@@ -0,0 +1,113 @@
<Window x:Class="Sharp.Wpf.DevTools.Views.PerformanceMonitorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="性能监控" Height="500" Width="600"
WindowStartupLocation="CenterScreen">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,15">
<Button Content="▶ 开始监控" Command="{Binding StartMonitoringCommand}" Margin="0,0,10,0" Padding="10,5"/>
<Button Content="⏸ 停止监控" Command="{Binding StopMonitoringCommand}" Margin="0,0,10,0" Padding="10,5"/>
<Button Content="🗑️ 强制GC" Command="{Binding ForceGCCommand}" Padding="10,5"/>
</StackPanel>
<!-- 指标显示 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- CPU -->
<GroupBox Header="CPU 使用率" Margin="0,0,0,15">
<StackPanel Margin="10">
<TextBlock FontSize="32" FontWeight="Bold">
<Run Text="{Binding CpuUsage, StringFormat='{}{0:F1}', Mode=OneWay}"/>
<Run Text="%" FontSize="20"/>
</TextBlock>
</StackPanel>
</GroupBox>
<!-- 内存 -->
<GroupBox Header="内存使用" Margin="0,0,0,15">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="工作集内存" FontSize="12" Foreground="#666"/>
<TextBlock FontSize="24" FontWeight="Bold">
<Run Text="{Binding WorkingSetMB, Mode=OneWay}"/>
<Run Text="MB" FontSize="14"/>
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="私有内存" FontSize="12" Foreground="#666"/>
<TextBlock FontSize="24" FontWeight="Bold">
<Run Text="{Binding PrivateMB, Mode=OneWay}"/>
<Run Text="MB" FontSize="14"/>
</TextBlock>
</StackPanel>
</Grid>
</GroupBox>
<!-- GC统计 -->
<GroupBox Header="GC 统计" Margin="0,0,0,15">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Gen 0" FontSize="12" Foreground="#666"/>
<TextBlock Text="{Binding Gen0Collections, Mode=OneWay}" FontSize="20" FontWeight="Bold"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Gen 1" FontSize="12" Foreground="#666"/>
<TextBlock Text="{Binding Gen1Collections, Mode=OneWay}" FontSize="20" FontWeight="Bold"/>
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Gen 2" FontSize="12" Foreground="#666"/>
<TextBlock Text="{Binding Gen2Collections, Mode=OneWay}" FontSize="20" FontWeight="Bold"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- 其他 -->
<GroupBox Header="其他信息">
<StackPanel Margin="10">
<TextBlock Margin="0,5">
<Run Text="线程数: " FontWeight="Bold"/>
<Run Text="{Binding ThreadCount, Mode=OneWay}"/>
</TextBlock>
<TextBlock Margin="0,5">
<Run Text="运行时长: " FontWeight="Bold"/>
<Run Text="{Binding Uptime, Mode=OneWay, StringFormat='{}{0:d\\.hh\\:mm\\:ss}'}"/>
</TextBlock>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
<!-- 说明 -->
<TextBlock Grid.Row="2" Text="提示:数据每秒自动更新"
FontSize="11" Foreground="#666"
Margin="0,10,0,10"/>
<!-- 底部按钮 -->
<Button Grid.Row="3" Content="关闭"
Width="100" Height="32"
HorizontalAlignment="Right"
Click="CloseButton_Click"/>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
using Sharp.Wpf.DevTools.ViewModels;
namespace Sharp.Wpf.DevTools.Views;
/// <summary>
/// PerformanceMonitorWindow.xaml 的交互逻辑
/// </summary>
public partial class PerformanceMonitorWindow : Window
{
public PerformanceMonitorWindow(PerformanceMonitorViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<RootNamespace>Sharp.Wpf.DevTools</RootNamespace>
<AssemblyName>Sharp.Wpf.DevTools</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Config\Config.csproj" />
</ItemGroup>
</Project>