#0005 增加日志

This commit is contained in:
zhengxuan.zhang
2026-03-13 16:59:31 +08:00
parent 75cb502d0d
commit 029752e231
18 changed files with 997 additions and 105 deletions
+31
View File
@@ -0,0 +1,31 @@
.vs/ProjectEvaluation/xploreplane.metadata.v10.bin
.vs/ProjectEvaluation/xploreplane.projects.v10.bin
.vs/ProjectEvaluation/xploreplane.strings.v10.bin
.vs/XplorePlane/DesignTimeBuild/.dtbcache.v2
.vs/XplorePlane/FileContentIndex/241be4f9-f3c1-44c3-a625-51f3a7efa276.vsidx
.vs/XplorePlane/FileContentIndex/a28e3b89-b000-44c7-aab5-785c933af59b.vsidx
.vs/XplorePlane/FileContentIndex/a475b41e-8352-4745-8040-08886d83ddf4.vsidx
.vs/XplorePlane/FileContentIndex/bdb864e9-e54b-49df-bf87-9b121265e567.vsidx
.vs/XplorePlane/v18/.futdcache.v2
.vs/XplorePlane/v18/.suo
.vs/XplorePlane/v18/DocumentLayout.backup.json
.vs/XplorePlane/v18/DocumentLayout.json
XplorePlane/obj/project.assets.json
XplorePlane/obj/*
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
XplorePlane/Libs/Hardware/*.dll
XplorePlane/Libs/Hardware/*.pdb
XplorePlane/Libs/Native/*.dll
XplorePlane/Libs/Native/*.pdb
# 保留 .gitkeep 文件以维持目录结构
!XplorePlane/Libs/**/.gitkeep
# 排除构建输出
XplorePlane/bin/
XplorePlane/obj/
# 排除日志文件
logs/
*.log
+105 -16
View File
@@ -1,20 +1,109 @@
#简介
TODO: 简要介绍你的项目。通过此节说明此项目的目标或动机。
## XplorePlane 平面CT软件
#入门
TODO: 指导用户在自己的系统上设置和运行代码。在本节中,可讨论:
1. 安装过程
2. 软件依赖项
3. 最新发布
4. API 参考
### 系统目标
#生成与测试
TODO: 说明并展示如何生成代码和运行测试。
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
#参与
TODO: 说明其他用户和开发人员可如何帮助改善代码。
**总体架构风格**
如需深入了解如何创建优秀的自述文件,请参阅以下[指南](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops)。还可从以下自述文件中寻求灵感:
- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
- 客户端框架: WPF + Prism MVVM(项目 XplorePlane,目标框架 net8.0-windows
- 图像处理内核: 独立类库 ImageProcessing.Core(算子基类与参数模型)和 ImageProcessing.Processors(具体算子实现),基于 EmguCV
- 基础设施: 日志使用 Serilog,序列化使用 Newtonsoft.Json,资源统一通过 WPF 资源系统管理
**开发目标**
在现有图像处理与 UI 基础上,引入并集成:
- 射线源子系统(X-Ray Source
- 探测器子系统(Detector
- 运动控制子系统(Motion Control
- 通过统一的 CT 扫描工作流,在 UI 中实现一键式扫描、实时状态监控与图像后处理
### 项目框架
```css
XplorePlane/
├── XplorePlane.csproj # .NET 8 WPF project file
├── App.xaml # Application + global ResourceDictionary
├── App.xaml.cs
├── Views/
│ └── MainWindow.xaml # Main window (Grid + StackPanel layout)
│ └── MainWindow.xaml.cs # Code-behind (minimal only TreeView event)
├── ViewModels/
│ └── MainViewModel.cs # Root VM: navigation, callouts, props, commands
│ └── NavGroupNode.cs # Tree group node VM
│ └── NavLeafNode.cs # Tree leaf node VM
│ └── InspectionCalloutVM.cs # Overlay callout card VM
│ └── CalloutRowVM.cs # Single callout data row VM
│ └── RelayCommand.cs # ICommand implementation
├── Models/
│ └── FeatureProperties.cs # Bindable domain model for right panel
└── Assets/
└── Icons/ # 28×28 toolbar icon PNGs
```
### XplorePlane.Hardware(硬件库)
### XplorePlane.ImageProcessing (图像库)
### 日志系统
项目已集成 Serilog 日志框架,提供统一的日志服务:
- **日志框架**: Serilog 4.3.1
- **日志输出**: 控制台、文件(按天滚动)、调试输出
- **日志路径**: `logs/xploreplane-YYYYMMDD.log`
- **配置文件**: `App.config`
- **服务接口**: `ILoggerService`
**使用示例**:
```csharp
public class MyService
{
private readonly ILoggerService _logger;
public MyService(ILoggerService logger)
{
// 使用泛型自动推断模块名
_logger = logger?.ForModule<MyService>() ?? throw new ArgumentNullException(nameof(logger));
}
public void DoSomething()
{
_logger.Info("执行操作");
_logger.Debug("调试信息:参数={Value}", someValue);
}
}
```
详细使用指南请参考:`Doc/Logging.README.md`
### TO-DO List
- [x] 软件基于 WPF + Prism 基础的框架
- [x] 日志库的引用
- [ ] 界面的布局
- [ ] 打通与硬件层的调用流程
- [ ] 打通与图像层的调用流程
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Serilog 日志配置 -->
<add key="Serilog:LogPath" value="logs" />
<add key="Serilog:MinimumLevel" value="Information" />
<add key="Serilog:EnableConsole" value="true" />
<add key="Serilog:RollingInterval" value="Day" />
<add key="Serilog:FileSizeLimitMB" value="100" />
<add key="Serilog:RetainedFileCountLimit" value="30" />
<!-- 射线源配置 -->
<add key="RaySource.Type" value="Comet225"/>
<add key="RaySource.StationName" value="XRAY_STATION"/>
<add key="RaySource.ConnectionTimeout" value="5000"/>
<add key="RaySource.VoltageMin" value="20"/>
<add key="RaySource.VoltageMax" value="225"/>
<add key="RaySource.CurrentMin" value="10"/>
<add key="RaySource.CurrentMax" value="1000"/>
<!-- 探测器配置 -->
<add key="Detector.Type" value="Varex4343"/>
<add key="Detector.IPAddress" value="192.168.1.101"/>
<add key="Detector.Port" value="8080"/>
<!-- PLC 配置 -->
<add key="PLC.Type" value="B&amp;R"/>
<add key="PLC.StationName" value="PLC_STATION"/>
<add key="PLC.ConnectionTimeout" value="5000"/>
</appSettings>
</configuration>
+56 -1
View File
@@ -1,8 +1,11 @@
using System.Windows;
using System;
using System.Windows;
using XplorePlane.Views;
using XplorePlane.ViewModels;
using XplorePlane.Services;
using Prism.Ioc;
using Prism.DryIoc;
using Serilog;
namespace XplorePlane
{
@@ -13,12 +16,58 @@ namespace XplorePlane
{
protected override void OnStartup(StartupEventArgs e)
{
// 配置 Serilog 日志系统
SerilogConfig.Configure();
// 捕获未处理的异常
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
DispatcherUnhandledException += OnDispatcherUnhandledException;
base.OnStartup(e);
// Initialize Prism with DryIoc
var bootstrapper = new AppBootstrapper();
bootstrapper.Run();
}
protected override void OnExit(ExitEventArgs e)
{
// 关闭并刷新日志
SerilogConfig.CloseAndFlush();
base.OnExit(e);
}
/// <summary>
/// 处理未捕获的异常
/// </summary>
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
Log.Fatal(exception, "应用程序发生未处理的异常");
MessageBox.Show(
$"应用程序发生严重错误:\n\n{exception?.Message}\n\n请查看日志文件获取详细信息。",
"严重错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
/// <summary>
/// 处理 UI 线程未捕获的异常
/// </summary>
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
Log.Error(e.Exception, "UI 线程发生未处理的异常");
MessageBox.Show(
$"应用程序发生错误:\n\n{e.Exception.Message}\n\n请查看日志文件获取详细信息。",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
// 标记为已处理,防止应用程序崩溃
e.Handled = true;
}
}
public class AppBootstrapper : PrismBootstrapper
@@ -30,8 +79,14 @@ namespace XplorePlane
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册日志服务
containerRegistry.RegisterSingleton<ILoggerService, LoggerService>();
// 注册视图和视图模型
containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.Register<MainViewModel>();
Log.Information("依赖注入容器配置完成");
}
}
}
View File
+1 -2
View File
@@ -1,10 +1,9 @@
<telerik:RadRibbonWindow x:Class="TelerikWpfApp2.MainWindow"
<telerik:RadRibbonWindow x:Class="XplorePlane.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="1280"
Icon="Images\TelerikExcel.ico"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:spreadsheetControls="clr-namespace:Telerik.Windows.Controls.Spreadsheet.Controls;assembly=Telerik.Windows.Controls.Spreadsheet"
xmlns:spreadsheet="clr-namespace:Telerik.Windows.Controls.Spreadsheet;assembly=Telerik.Windows.Controls.Spreadsheet"
+2 -2
View File
@@ -6,9 +6,9 @@ using Telerik.Windows.Controls;
using Telerik.Windows.Documents.Spreadsheet.FormatProviders;
using Telerik.Windows.Documents.Spreadsheet.FormatProviders.OpenXml.Xlsx;
using Telerik.Windows.Documents.Spreadsheet.FormatProviders.Pdf;
using TelerikWpfApp2.ViewModel;
using XplorePlane.ViewModels;
namespace TelerikWpfApp2
namespace XplorePlane
{
/// <summary>
/// Interaction logic for MainWindow.xaml
-65
View File
@@ -1,65 +0,0 @@
## XplorePlane 平面CT软件
### 系统目标
XplorePlane 系统用于控制平面 CT 设备的各个子系统(射线源、探测器、运动控制)并完成采集图像的处理与分析,为研发与调试提供统一的软件平台。
总体架构风格
- 客户端框架: WPF + Prism MVVM(项目 XplorePlane,目标框架 net8.0-windows)。
- 图像处理内核: 独立类库 ImageProcessing.Core(算子基类与参数模型)和 ImageProcessing.Processors(具体算子实现),基于 EmguCV。
- 基础设施: 日志使用 Serilog,序列化使用 Newtonsoft.Json,资源统一通过 WPF 资源系统管理。
开发目标
- 在现有图像处理与 UI 基础上,引入并集成:
- 射线源子系统(X-Ray Source
- 探测器子系统(Detector
- 运动控制子系统(Motion Control
- 通过统一的 CT 扫描工作流,在 UI 中实现一键式扫描、实时状态监控与图像后处理。
### 项目框架
```css
XplorePlane/
├── XplorePlane.csproj # .NET 8 WPF project file
├── App.xaml # Application + global ResourceDictionary
├── App.xaml.cs
├── Views/
│ └── MainWindow.xaml # Main window (Grid + StackPanel layout)
│ └── MainWindow.xaml.cs # Code-behind (minimal only TreeView event)
├── ViewModels/
│ └── MainViewModel.cs # Root VM: navigation, callouts, props, commands
│ └── NavGroupNode.cs # Tree group node VM
│ └── NavLeafNode.cs # Tree leaf node VM
│ └── InspectionCalloutVM.cs # Overlay callout card VM
│ └── CalloutRowVM.cs # Single callout data row VM
│ └── RelayCommand.cs # ICommand implementation
├── Models/
│ └── FeatureProperties.cs # Bindable domain model for right panel
└── Assets/
└── Icons/ # 28×28 toolbar icon PNGs
```
### XplorePlane.Hardware(硬件库)
### XplorePlane.ImageProcessing (图像库)
### TO-DO List
[] 软件基于WPF + Prism 基础的框架, 主页面设计
[] 打通与硬件 和图像的调用流程
+90
View File
@@ -0,0 +1,90 @@
using System;
namespace XplorePlane.Services
{
/// <summary>
/// 日志服务接口
/// </summary>
public interface ILoggerService
{
/// <summary>
/// 为指定模块创建日志器(使用泛型自动推断类型名)
/// </summary>
ILoggerService ForModule<T>();
/// <summary>
/// 为指定模块创建日志器(手动指定模块名)
/// </summary>
ILoggerService ForModule(string moduleName);
/// <summary>
/// 记录调试信息
/// </summary>
void Debug(string message);
/// <summary>
/// 记录调试信息(带参数)
/// </summary>
void Debug(string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录一般信息
/// </summary>
void Info(string message);
/// <summary>
/// 记录一般信息(带参数)
/// </summary>
void Info(string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录警告信息
/// </summary>
void Warn(string message);
/// <summary>
/// 记录警告信息(带参数)
/// </summary>
void Warn(string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录错误信息
/// </summary>
void Error(string message);
/// <summary>
/// 记录错误信息(带参数)
/// </summary>
void Error(string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录错误信息(带异常)
/// </summary>
void Error(Exception exception, string message);
/// <summary>
/// 记录错误信息(带异常和参数)
/// </summary>
void Error(Exception exception, string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录致命错误
/// </summary>
void Fatal(string message);
/// <summary>
/// 记录致命错误(带参数)
/// </summary>
void Fatal(string messageTemplate, params object[] propertyValues);
/// <summary>
/// 记录致命错误(带异常)
/// </summary>
void Fatal(Exception exception, string message);
/// <summary>
/// 记录致命错误(带异常和参数)
/// </summary>
void Fatal(Exception exception, string messageTemplate, params object[] propertyValues);
}
}
+150
View File
@@ -0,0 +1,150 @@
using System;
using Serilog;
using Serilog.Core;
namespace XplorePlane.Services
{
/// <summary>
/// 日志服务实现
/// </summary>
public class LoggerService : ILoggerService
{
private readonly ILogger _logger;
private readonly string _moduleName;
/// <summary>
/// 构造函数
/// </summary>
public LoggerService() : this(Log.Logger, null)
{
}
/// <summary>
/// 构造函数(指定 Serilog Logger
/// </summary>
public LoggerService(ILogger logger) : this(logger, null)
{
}
/// <summary>
/// 私有构造函数(用于创建带模块名的实例)
/// </summary>
private LoggerService(ILogger logger, string moduleName)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_moduleName = moduleName;
}
/// <summary>
/// 为指定模块创建日志器(使用泛型自动推断类型名)
/// </summary>
public ILoggerService ForModule<T>()
{
var typeName = typeof(T).FullName ?? typeof(T).Name;
return new LoggerService(_logger, typeName);
}
/// <summary>
/// 为指定模块创建日志器(手动指定模块名)
/// </summary>
public ILoggerService ForModule(string moduleName)
{
if (string.IsNullOrWhiteSpace(moduleName))
throw new ArgumentException("模块名不能为空", nameof(moduleName));
return new LoggerService(_logger, moduleName);
}
/// <summary>
/// 获取带模块名的日志器
/// </summary>
private ILogger GetLogger()
{
if (string.IsNullOrEmpty(_moduleName))
return _logger;
return _logger.ForContext("Module", _moduleName);
}
public void Debug(string message)
{
GetLogger().Debug(FormatMessage(message));
}
public void Debug(string messageTemplate, params object[] propertyValues)
{
GetLogger().Debug(FormatMessage(messageTemplate), propertyValues);
}
public void Info(string message)
{
GetLogger().Information(FormatMessage(message));
}
public void Info(string messageTemplate, params object[] propertyValues)
{
GetLogger().Information(FormatMessage(messageTemplate), propertyValues);
}
public void Warn(string message)
{
GetLogger().Warning(FormatMessage(message));
}
public void Warn(string messageTemplate, params object[] propertyValues)
{
GetLogger().Warning(FormatMessage(messageTemplate), propertyValues);
}
public void Error(string message)
{
GetLogger().Error(FormatMessage(message));
}
public void Error(string messageTemplate, params object[] propertyValues)
{
GetLogger().Error(FormatMessage(messageTemplate), propertyValues);
}
public void Error(Exception exception, string message)
{
GetLogger().Error(exception, FormatMessage(message));
}
public void Error(Exception exception, string messageTemplate, params object[] propertyValues)
{
GetLogger().Error(exception, FormatMessage(messageTemplate), propertyValues);
}
public void Fatal(string message)
{
GetLogger().Fatal(FormatMessage(message));
}
public void Fatal(string messageTemplate, params object[] propertyValues)
{
GetLogger().Fatal(FormatMessage(messageTemplate), propertyValues);
}
public void Fatal(Exception exception, string message)
{
GetLogger().Fatal(exception, FormatMessage(message));
}
public void Fatal(Exception exception, string messageTemplate, params object[] propertyValues)
{
GetLogger().Fatal(exception, FormatMessage(messageTemplate), propertyValues);
}
/// <summary>
/// 格式化消息(添加模块名前缀)
/// </summary>
private string FormatMessage(string message)
{
if (string.IsNullOrEmpty(_moduleName))
return message;
return $"[{_moduleName}] {message}";
}
}
}
+139
View File
@@ -0,0 +1,139 @@
using System;
using System.Threading.Tasks;
namespace XplorePlane.Services
{
/// <summary>
/// 日志服务使用示例
/// </summary>
public class LoggingExample
{
private readonly ILoggerService _logger;
public LoggingExample(ILoggerService logger)
{
// 推荐方式:使用泛型自动推断类型名
_logger = logger?.ForModule<LoggingExample>() ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 基本日志示例
/// </summary>
public void BasicLoggingExample()
{
// 调试信息
_logger.Debug("这是调试信息");
// 一般信息
_logger.Info("应用程序启动成功");
// 警告信息
_logger.Warn("连接不稳定,正在重试...");
// 错误信息
_logger.Error("操作失败");
// 致命错误
_logger.Fatal("系统崩溃");
}
/// <summary>
/// 结构化日志示例
/// </summary>
public void StructuredLoggingExample()
{
var userId = 12345;
var action = "登录";
var voltage = 150.5f;
var current = 500;
// 使用占位符(推荐)
_logger.Info("用户 {UserId} 执行了操作 {Action}", userId, action);
_logger.Info("设置电压为 {Voltage} kV,电流为 {Current} μA", voltage, current);
// 不推荐:字符串拼接
// _logger.Info($"用户 {userId} 执行了操作 {action}");
}
/// <summary>
/// 异常日志示例
/// </summary>
public async Task ExceptionLoggingExample()
{
try
{
// 模拟操作
await Task.Delay(100);
throw new InvalidOperationException("模拟异常");
}
catch (Exception ex)
{
// 记录异常(带上下文信息)
_logger.Error(ex, "操作失败:参数={Parameter}", "test");
// 或者简单记录
_logger.Error(ex, "操作失败");
// 致命错误
_logger.Fatal(ex, "系统发生致命错误");
}
}
/// <summary>
/// 不同模块名示例
/// </summary>
public void DifferentModuleNameExample(ILoggerService logger)
{
// 方式 1:使用泛型(推荐)
var logger1 = logger.ForModule<LoggingExample>();
logger1.Info("使用泛型推断的模块名");
// 输出: [XplorePlane.Services.LoggingExample] 使用泛型推断的模块名
// 方式 2:手动指定模块名
var logger2 = logger.ForModule("CustomModule");
logger2.Info("使用自定义模块名");
// 输出: [CustomModule] 使用自定义模块名
// 方式 3:不指定模块名
logger.Info("没有模块名");
// 输出: 没有模块名
}
/// <summary>
/// 实际业务场景示例
/// </summary>
public async Task<bool> BusinessScenarioExample()
{
_logger.Info("开始初始化硬件...");
try
{
// 步骤 1
_logger.Debug("步骤 1: 检查硬件连接");
await Task.Delay(100);
// 步骤 2
_logger.Debug("步骤 2: 加载配置");
var config = LoadConfig();
_logger.Info("配置加载成功:{ConfigName}", config);
// 步骤 3
_logger.Debug("步骤 3: 建立连接");
await Task.Delay(100);
_logger.Info("硬件初始化成功");
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "硬件初始化失败");
return false;
}
}
private string LoadConfig()
{
return "DefaultConfig";
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Threading.Tasks;
namespace XplorePlane.Services
{
/// <summary>
/// 日志系统验证类 - 用于测试日志功能是否正常工作
/// </summary>
public class LoggingVerification
{
private readonly ILoggerService _logger;
public LoggingVerification(ILoggerService logger)
{
_logger = logger?.ForModule<LoggingVerification>() ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 运行完整的日志验证测试
/// </summary>
public async Task RunVerificationAsync()
{
_logger.Info("========================================");
_logger.Info("开始日志系统验证测试");
_logger.Info("========================================");
// 测试 1: 基本日志级别
TestBasicLogLevels();
// 测试 2: 结构化日志
TestStructuredLogging();
// 测试 3: 异常日志
await TestExceptionLoggingAsync();
// 测试 4: 模块名
TestModuleNames();
_logger.Info("========================================");
_logger.Info("日志系统验证测试完成");
_logger.Info("========================================");
}
private void TestBasicLogLevels()
{
_logger.Info("测试 1: 基本日志级别");
_logger.Debug("这是 Debug 级别日志");
_logger.Info("这是 Info 级别日志");
_logger.Warn("这是 Warn 级别日志");
_logger.Error("这是 Error 级别日志");
_logger.Info("✓ 基本日志级别测试完成");
}
private void TestStructuredLogging()
{
_logger.Info("测试 2: 结构化日志");
var userId = 12345;
var userName = "张三";
var voltage = 150.5f;
var current = 500;
var timestamp = DateTime.Now;
_logger.Info("用户登录: UserId={UserId}, UserName={UserName}", userId, userName);
_logger.Info("设置参数: Voltage={Voltage}kV, Current={Current}μA", voltage, current);
_logger.Info("操作时间: {Timestamp:yyyy-MM-dd HH:mm:ss}", timestamp);
_logger.Info("✓ 结构化日志测试完成");
}
private async Task TestExceptionLoggingAsync()
{
_logger.Info("测试 3: 异常日志");
try
{
await Task.Delay(10);
throw new InvalidOperationException("这是一个测试异常");
}
catch (Exception ex)
{
_logger.Error(ex, "捕获到异常: {Message}", ex.Message);
_logger.Info("✓ 异常日志测试完成");
}
}
private void TestModuleNames()
{
_logger.Info("测试 4: 模块名功能");
_logger.Info("当前模块名应该是: XplorePlane.Services.LoggingVerification");
_logger.Info("✓ 模块名测试完成");
}
}
}
+75
View File
@@ -0,0 +1,75 @@
# Services 目录说明
此目录用于存放应用程序的服务层代码,负责封装硬件库的调用和业务逻辑。
## 目录结构
```
Services/
├── IHardwareService.cs # 硬件服务接口
├── HardwareService.cs # 硬件服务实现
├── IConfigurationService.cs # 配置服务接口
└── ConfigurationService.cs # 配置服务实现
```
## 服务说明
### 1. HardwareService
统一管理所有硬件模块的服务适配器。
**功能**
- 初始化和关闭所有硬件
- 射线源控制(开关、参数设置)
- 探测器控制(图像采集、配置)
- PLC 通讯(变量读写)
**使用示例**
```csharp
// 在 ViewModel 中注入
public class MainViewModel
{
private readonly IHardwareService _hardwareService;
public MainViewModel(IHardwareService hardwareService)
{
_hardwareService = hardwareService;
}
public async Task InitializeAsync()
{
await _hardwareService.InitializeAllAsync();
}
}
```
### 2. ConfigurationService
管理应用程序配置的服务。
**功能**
- 读取 App.config 配置
- 类型安全的配置访问
- 默认值支持
## 依赖注入配置
`App.xaml.cs` 中注册服务:
```csharp
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册硬件服务
containerRegistry.RegisterSingleton<IHardwareService, HardwareService>();
// 注册配置服务
containerRegistry.RegisterSingleton<IConfigurationService, ConfigurationService>();
}
```
## 注意事项
1. **服务生命周期**:硬件服务应注册为 Singleton,确保全局唯一
2. **异步操作**:所有硬件操作都应使用异步方法
3. **错误处理**:服务层应捕获并记录所有异常
4. **资源释放**:实现 IDisposable 接口,确保资源正确释放
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.Configuration;
using System.IO;
using Serilog;
using Serilog.Events;
namespace XplorePlane.Services
{
/// <summary>
/// Serilog 配置类
/// </summary>
public static class SerilogConfig
{
/// <summary>
/// 配置 Serilog
/// </summary>
public static void Configure()
{
// 读取配置
var logPath = GetConfigValue("Serilog:LogPath", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"));
var minimumLevel = GetLogLevel(GetConfigValue("Serilog:MinimumLevel", "Information"));
var enableConsole = GetConfigValue("Serilog:EnableConsole", "true").ToLower() == "true";
var rollingInterval = GetRollingInterval(GetConfigValue("Serilog:RollingInterval", "Day"));
var fileSizeLimitMB = int.Parse(GetConfigValue("Serilog:FileSizeLimitMB", "100"));
var retainedFileCountLimit = int.Parse(GetConfigValue("Serilog:RetainedFileCountLimit", "30"));
// 确保日志目录存在
if (!Directory.Exists(logPath))
{
Directory.CreateDirectory(logPath);
}
// 配置 Serilog
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Is(minimumLevel)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "XplorePlane")
.Enrich.WithProperty("MachineName", Environment.MachineName)
.WriteTo.File(
path: Path.Combine(logPath, "xploreplane-.log"),
rollingInterval: rollingInterval,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
fileSizeLimitBytes: fileSizeLimitMB * 1024 * 1024,
retainedFileCountLimit: retainedFileCountLimit,
shared: true
);
// 添加控制台输出
if (enableConsole)
{
loggerConfig.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);
}
// 添加调试输出(仅在 Debug 模式)
#if DEBUG
loggerConfig.WriteTo.Debug(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
);
#endif
// 创建全局 Logger
Log.Logger = loggerConfig.CreateLogger();
Log.Information("========================================");
Log.Information("XplorePlane 应用程序启动");
Log.Information("日志路径: {LogPath}", logPath);
Log.Information("日志级别: {MinimumLevel}", minimumLevel);
Log.Information("========================================");
}
/// <summary>
/// 关闭并刷新日志
/// </summary>
public static void CloseAndFlush()
{
Log.Information("========================================");
Log.Information("XplorePlane 应用程序退出");
Log.Information("========================================");
Log.CloseAndFlush();
}
/// <summary>
/// 获取配置值
/// </summary>
private static string GetConfigValue(string key, string defaultValue)
{
try
{
return ConfigurationManager.AppSettings[key] ?? defaultValue;
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 获取日志级别
/// </summary>
private static LogEventLevel GetLogLevel(string level)
{
return level.ToLower() switch
{
"verbose" => LogEventLevel.Verbose,
"debug" => LogEventLevel.Debug,
"information" => LogEventLevel.Information,
"warning" => LogEventLevel.Warning,
"error" => LogEventLevel.Error,
"fatal" => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
}
/// <summary>
/// 获取滚动间隔
/// </summary>
private static RollingInterval GetRollingInterval(string interval)
{
return interval.ToLower() switch
{
"infinite" => RollingInterval.Infinite,
"year" => RollingInterval.Year,
"month" => RollingInterval.Month,
"day" => RollingInterval.Day,
"hour" => RollingInterval.Hour,
"minute" => RollingInterval.Minute,
_ => RollingInterval.Day
};
}
}
}
+56 -9
View File
@@ -1,13 +1,20 @@
using Prism.Commands;
using Prism.Mvvm;
using System.Collections.ObjectModel;
using XplorePlane.Services;
namespace XplorePlane.ViewModels
{
public class MainViewModel : BindableBase
{
private string _licenseInfo = "µ±Ç°Ê±¼ä";
private readonly ILoggerService _logger;
private string _licenseInfo = "当前时间";
public string LicenseInfo
{
get => _licenseInfo;
set => SetProperty(ref _licenseInfo, value);
}
public ObservableCollection<object> NavigationTree { get; set; }
@@ -18,16 +25,56 @@ namespace XplorePlane.ViewModels
public DelegateCommand ClearCommand { get; set; }
public DelegateCommand EditPropertiesCommand { get; set; }
public MainViewModel()
public MainViewModel(ILoggerService logger)
{
_logger = logger?.ForModule<MainViewModel>() ?? throw new System.ArgumentNullException(nameof(logger));
NavigationTree = new ObservableCollection<object>();
NavigateHomeCommand = new DelegateCommand(() => { });
NavigateInspectCommand = new DelegateCommand(() => { });
OpenFileCommand = new DelegateCommand(() => { });
ExportCommand = new DelegateCommand(() => { });
ClearCommand = new DelegateCommand(() => { });
EditPropertiesCommand = new DelegateCommand(() => { });
NavigateHomeCommand = new DelegateCommand(OnNavigateHome);
NavigateInspectCommand = new DelegateCommand(OnNavigateInspect);
OpenFileCommand = new DelegateCommand(OnOpenFile);
ExportCommand = new DelegateCommand(OnExport);
ClearCommand = new DelegateCommand(OnClear);
EditPropertiesCommand = new DelegateCommand(OnEditProperties);
_logger.Info("MainViewModel 已初始化");
}
private void OnNavigateHome()
{
_logger.Info("导航到主页");
LicenseInfo = "主页";
}
private void OnNavigateInspect()
{
_logger.Info("导航到检测页面");
LicenseInfo = "检测页面";
}
private void OnOpenFile()
{
_logger.Info("打开文件");
LicenseInfo = "打开文件";
}
private void OnExport()
{
_logger.Info("导出数据");
LicenseInfo = "导出数据";
}
private void OnClear()
{
_logger.Info("清除数据");
LicenseInfo = "清除数据";
}
private void OnEditProperties()
{
_logger.Info("编辑属性");
LicenseInfo = "编辑属性";
}
}
}
+6 -9
View File
@@ -11,13 +11,10 @@
Background="#F5F5F5"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="169*"/>
<ColumnDefinition Width="47*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@@ -26,7 +23,7 @@
</Grid.RowDefinitions>
<!-- Row 0: Fluent Ribbon -->
<fluent:Ribbon Grid.Row="0" x:Name="Ribbon">
<fluent:Ribbon Grid.Row="0" x:Name="Ribbon" Grid.ColumnSpan="2" Grid.RowSpan="2">
<fluent:RibbonTabItem Header="主页">
<fluent:RibbonGroupBox Header="导航">
<fluent:Button Header="主页" Command="{Binding NavigateHomeCommand}" LargeIcon="pack://application:,,,/Assets/Icons/home.png" Size="Large"/>
@@ -46,7 +43,7 @@
</fluent:Ribbon>
<!-- Row 1: Main Content -->
<Grid Grid.Row="1">
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="0,0,0,24" Grid.RowSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300
" MinWidth="200"/>
@@ -76,7 +73,7 @@
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#FFFFFF" BorderBrush="#DDDDDD" BorderThickness="1" Margin="0,0,0,3" Grid.RowSpan="2">
<TextBlock Text="3D Viewport" Foreground="#666666" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="16"/>
<TextBlock Text="2D Viewport" Foreground="#666666" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="16"/>
</Border>
</Grid>
@@ -120,7 +117,7 @@
</Grid>
<!-- Row 2: Status Bar -->
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0" Grid.ColumnSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
+3 -1
View File
@@ -1,4 +1,5 @@
using System.Windows;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
{
@@ -7,9 +8,10 @@ namespace XplorePlane.Views
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}
+25
View File
@@ -6,8 +6,33 @@
<RootNamespace>XplorePlane</RootNamespace>
<AssemblyName>XplorePlane</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Remove="MainWindow.xaml.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="XplorePlane.csproj.Backup.tmp" />
</ItemGroup>
<ItemGroup>
<Page Remove="MainWindow.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
<PackageReference Include="Fluent.Ribbon" Version="9.0.0" />
<!-- Serilog 日志框架 -->
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<!-- 配置管理 -->
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup>
<!-- 确保 App.config 复制到输出目录 -->
<ItemGroup>
<None Update="App.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>