diff --git a/XP.Scan/Attributes/IniKeyAttribute.cs b/XP.Scan/Attributes/IniKeyAttribute.cs new file mode 100644 index 0000000..304f0cd --- /dev/null +++ b/XP.Scan/Attributes/IniKeyAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace XP.Scan.Attributes +{ + /// + /// 标记 INI Key 名称(可选,默认使用属性名)| Marks the INI key name (optional, defaults to property name) + /// + [AttributeUsage(AttributeTargets.Property)] + public class IniKeyAttribute : Attribute + { + public string KeyName { get; } + + public IniKeyAttribute(string keyName) + { + KeyName = keyName; + } + } +} diff --git a/XP.Scan/Attributes/IniSectionAttribute.cs b/XP.Scan/Attributes/IniSectionAttribute.cs new file mode 100644 index 0000000..ee67abb --- /dev/null +++ b/XP.Scan/Attributes/IniSectionAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace XP.Scan.Attributes +{ + /// + /// 标记 INI Section 名称 | Marks the INI section name + /// + [AttributeUsage(AttributeTargets.Class)] + public class IniSectionAttribute : Attribute + { + public string SectionName { get; } + + public IniSectionAttribute(string sectionName) + { + SectionName = sectionName; + } + } +} diff --git a/XP.Scan/Documents/ScanConfig.Design.md b/XP.Scan/Documents/ScanConfig.Design.md new file mode 100644 index 0000000..156e735 --- /dev/null +++ b/XP.Scan/Documents/ScanConfig.Design.md @@ -0,0 +1,366 @@ +# XP.Scan 扫描配置文件设计文档 + +## 1. 需求概述 + +在平面CT扫描采集过程中,需要将当前扫描的所有参数信息(项目信息、射线源参数、探测器参数、运动控制参数、扫描配置、校正配置)打包为一个配置对象,最终序列化为 INI 格式文件传递给重构电脑。 + +INI 文件结构与立式CT采集配置兼容,包含 6 个 Section: +- `[Project_Information]` — 项目基本信息 +- `[XRay]` — 射线源参数 +- `[Detector]` — 探测器参数 +- `[Move_Control]` — 运动控制轴位置 +- `[Scan_Config]` — 扫描配置 +- `[Correction_Config]` — 校正参数 + +## 2. 设计方案对比 + +| 方案 | 优点 | 缺点 | 推荐 | +|------|------|------|------| +| A. 单个大类 `ScanConfig` | 简单直接 | 字段太多,职责不清 | ✗ | +| B. 分组模型 + 序列化服务 | 职责清晰,可测试,可扩展 | 类稍多 | ✓ | +| C. Dictionary + 手写序列化 | 灵活 | 无类型安全,易出错 | ✗ | + +**选择方案 B**:用分组模型类映射 INI 的每个 Section,加一个序列化服务负责读写 INI 文件。 + +## 3. 数据模型设计 + +### 3.1 整体结构 + +``` +ScanConfigData (顶层聚合) +├── ProjectInfo → [Project_Information] +├── XRayConfig → [XRay] +├── DetectorConfig → [Detector] +├── MoveControlConfig → [Move_Control] +├── ScanSettings → [Scan_Config] +└── CorrectionConfig → [Correction_Config] +``` + +### 3.2 类定义 + +```csharp +namespace XP.Scan.Models +{ + /// + /// 扫描配置数据(顶层聚合,对应完整 INI 文件) + /// + public class ScanConfigData + { + public ProjectInfo ProjectInfo { get; set; } = new(); + public XRayConfig XRay { get; set; } = new(); + public DetectorConfig Detector { get; set; } = new(); + public MoveControlConfig MoveControl { get; set; } = new(); + public ScanSettings ScanSettings { get; set; } = new(); + public CorrectionConfig Correction { get; set; } = new(); + } +} +``` + +#### [Project_Information] + +```csharp +public class ProjectInfo +{ + /// 图像保存路径 + public string FileSave { get; set; } = string.Empty; + + /// 滤波片1 + public string Filter1 { get; set; } = "None"; + + /// 滤波片2 + public string Filter2 { get; set; } = "None"; + + /// 项目名称 + public string Project { get; set; } = string.Empty; + + /// 样品编号 + public string SampleNo { get; set; } = string.Empty; + + /// 扫描模式名称 + public string ScanMode { get; set; } = "QuickScan"; +} +``` + +#### [XRay] + +```csharp +public class XRayConfig +{ + /// 管电流 (μA) + public int Current_uA { get; set; } + + /// 焦点尺寸 + public string Focus { get; set; } = string.Empty; + + /// 管电压 (kV) + public int Voltage_kV { get; set; } +} +``` + +#### [Detector] + +```csharp +public class DetectorConfig +{ + /// 帧合并数 + public int Det_Avg_Frames { get; set; } = 1; + + /// Binning 模式 + public string Det_Binning { get; set; } = "1*1"; + + /// 帧率 + public int Det_Frame_rate { get; set; } = 2; + + /// 增益 (PGA) + public int Det_PGA { get; set; } = 6; + + // ROI 参数 + public int Image_ROI_Height { get; set; } + public int Image_ROI_Width { get; set; } + public int Image_ROI_xStart { get; set; } + public int Image_ROI_xEnd { get; set; } + public int Image_ROI_yStart { get; set; } + public int Image_ROI_yEnd { get; set; } + public int Image_ROI_zStart { get; set; } + public int Image_ROI_zEnd { get; set; } + + // 图像尺寸 + public int Image_Size_Height { get; set; } + public int Image_Size_Width { get; set; } + + // 物理尺寸 (mm) + public double Physical_Size_X { get; set; } + public double Physical_Size_Y { get; set; } + + // 像素尺寸 (mm) + public double Pixel_X { get; set; } + public double Pixel_Y { get; set; } +} +``` + +#### [Move_Control] + +```csharp +public class MoveControlConfig +{ + /// 探测器 X 位置 (mm) + public double DetX { get; set; } + + /// 探测器 Y 位置 (mm) + public double DetY { get; set; } + + /// 探测器 Z 位置 (mm) + public double DetZ { get; set; } + + /// 旋转台角度 (°) + public double Rotation { get; set; } + + /// 样品台 X 位置 (mm) — 即 SOD + public double X { get; set; } + + /// 射线源 Z 位置 (mm) + public double XRAYZ { get; set; } + + /// 样品台 Y 位置 (mm) + public double Y { get; set; } +} +``` + +#### [Scan_Config] + +```csharp +public class ScanSettings +{ + /// 采集张数 + public int AcquiresNums { get; set; } + + /// 旋转角度 (°) + public double RotateDegree { get; set; } + + /// 扫描模式描述 + public string ScanMode { get; set; } = string.Empty; + + /// SDD — 射线源到探测器距离 (mm) + public double SDD { get; set; } + + /// SOD — 射线源到样品距离 (mm) + public double SOD { get; set; } +} +``` + +#### [Correction_Config] + +```csharp +public class CorrectionConfig +{ + /// 探测器水平偏移 (mm) + public double Detector_Horizontal_Offset { get; set; } + + /// 探测器旋转偏移 (°) + public double Detector_Rotation_Offset { get; set; } +} +``` + +## 4. INI 序列化服务设计 + +### 4.1 接口 + +```csharp +namespace XP.Scan.Services +{ + public interface IScanConfigSerializer + { + /// 将配置数据序列化为 INI 格式字符串 + string Serialize(ScanConfigData config); + + /// 将配置数据写入 INI 文件 + void SaveToFile(ScanConfigData config, string filePath); + + /// 从 INI 文件读取配置数据 + ScanConfigData LoadFromFile(string filePath); + } +} +``` + +### 4.2 实现方案 + +不引入第三方 INI 库,手写轻量级序列化,原因: +- INI 结构简单固定(6 个 Section,字段已知) +- 避免额外 NuGet 依赖 +- 完全可控,格式与立式CT兼容 + +核心思路:用 `[IniSection("Section_Name")]` 和 `[IniKey("Key_Name")]` 特性标注模型属性,序列化时通过反射自动生成 INI 内容。 + +### 4.3 特性定义 + +```csharp +/// 标记 INI Section 名称 +[AttributeUsage(AttributeTargets.Class)] +public class IniSectionAttribute : Attribute +{ + public string SectionName { get; } + public IniSectionAttribute(string sectionName) => SectionName = sectionName; +} + +/// 标记 INI Key 名称(可选,默认用属性名) +[AttributeUsage(AttributeTargets.Property)] +public class IniKeyAttribute : Attribute +{ + public string KeyName { get; } + public IniKeyAttribute(string keyName) => KeyName = keyName; +} +``` + +### 4.4 模型标注示例 + +```csharp +[IniSection("Project_Information")] +public class ProjectInfo +{ + [IniKey("fileSave")] + public string FileSave { get; set; } = string.Empty; + + [IniKey("filter1")] + public string Filter1 { get; set; } = "None"; + + // ... +} + +[IniSection("XRay")] +public class XRayConfig +{ + [IniKey("Current_uA")] + public int Current_uA { get; set; } + + // ... +} +``` + +### 4.5 序列化输出示例 + +```ini +[Project_Information] +fileSave=D:\HexagonCTData\Test_2026-04-21\Image +filter1=Cu 0.2mm +filter2=None +Project=Test +SampleNo= +ScanMode=QuickScan + +[XRay] +Current_uA=1000 +Focus=450um +Voltage_kV=450 + +[Detector] +Det_Avg_Frames=1 +Det_Binning=1*1 +... +``` + +## 5. 文件结构规划 + +``` +XP.Scan/ +├── Models/ +│ ├── ScanConfigData.cs # 顶层聚合类 +│ ├── ProjectInfo.cs # [Project_Information] +│ ├── XRayConfig.cs # [XRay] +│ ├── DetectorConfig.cs # [Detector] +│ ├── MoveControlConfig.cs # [Move_Control] +│ ├── ScanSettings.cs # [Scan_Config] +│ └── CorrectionConfig.cs # [Correction_Config] +│ +├── Attributes/ +│ ├── IniSectionAttribute.cs # Section 特性 +│ └── IniKeyAttribute.cs # Key 特性 +│ +├── Services/ +│ ├── IScanConfigSerializer.cs # 序列化接口 +│ └── ScanConfigSerializer.cs # 序列化实现(反射 + 手写 INI) +│ +└── ... +``` + +## 6. 使用流程 + +``` +1. 扫描开始前,从各硬件服务收集参数 → 填充 ScanConfigData + + var config = new ScanConfigData(); + config.XRay.Voltage_kV = raySourceService.CurrentVoltage; + config.XRay.Current_uA = raySourceService.CurrentCurrent; + config.Detector.Det_Avg_Frames = detectorService.AvgFrames; + config.MoveControl.X = motionService.GetPosition(AxisId.StageX); + config.ScanSettings.AcquiresNums = acquisitionCount; + // ... + +2. 序列化为 INI 文件 + + var serializer = new ScanConfigSerializer(); + serializer.SaveToFile(config, @"D:\HexagonCTData\Test\ScanConfig.ini"); + +3. 传递给重构电脑(文件拷贝或网络传输) +``` + +## 7. 设计决策 + +| 决策 | 选择 | 理由 | +|------|------|------| +| 数据模型 | 分组类(每个 Section 一个类) | 职责清晰,属性有类型安全 | +| 序列化方式 | 自定义特性 + 反射 | 轻量,无第三方依赖,格式完全可控 | +| INI Key 映射 | `[IniKey]` 特性 | 属性名可以用 C# 命名规范,INI Key 保持与立式CT兼容 | +| 数值格式 | `InvariantCulture` | 避免不同系统区域设置导致小数点格式不一致 | +| 文件编码 | UTF-8 无 BOM | 兼容性最好 | + +## 8. 扩展性 + +- 新增 Section:创建新模型类 + 标注 `[IniSection]` + 在 `ScanConfigData` 中添加属性 +- 新增字段:在对应模型类中添加属性 + 标注 `[IniKey]` +- 反序列化:`LoadFromFile` 支持从 INI 文件回读配置(用于加载历史扫描参数) +- 验证:可在模型类中添加 `Validate()` 方法,检查参数范围合法性 + +--- + +**版本:** 1.0 +**最后更新:** 2026-04-21 diff --git a/XP.Scan/Models/CorrectionConfig.cs b/XP.Scan/Models/CorrectionConfig.cs new file mode 100644 index 0000000..ecbe9ea --- /dev/null +++ b/XP.Scan/Models/CorrectionConfig.cs @@ -0,0 +1,20 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 校正配置 | Correction configuration + /// 对应 INI [Correction_Config] Section + /// + [IniSection("Correction_Config")] + public class CorrectionConfig + { + /// 探测器水平偏移 (mm) | Detector horizontal offset (mm) + [IniKey("Detector_Horizontal_Offset")] + public double DetectorHorizontalOffset { get; set; } + + /// 探测器旋转偏移 (°) | Detector rotation offset (°) + [IniKey("Detector_Rotation_Offset")] + public double DetectorRotationOffset { get; set; } + } +} diff --git a/XP.Scan/Models/DetectorConfig.cs b/XP.Scan/Models/DetectorConfig.cs new file mode 100644 index 0000000..7e07a85 --- /dev/null +++ b/XP.Scan/Models/DetectorConfig.cs @@ -0,0 +1,84 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 探测器配置 | Detector configuration + /// 对应 INI [Detector] Section + /// + [IniSection("Detector")] + public class DetectorConfig + { + /// 帧合并数 | Average frames + [IniKey("Det_Avg_Frames")] + public int DetAvgFrames { get; set; } = 1; + + /// Binning 模式 | Binning mode + [IniKey("Det_Binning")] + public string DetBinning { get; set; } = "1*1"; + + /// 帧率 | Frame rate + [IniKey("Det_Frame_rate")] + public int DetFrameRate { get; set; } = 2; + + /// 增益 (PGA) | Gain (PGA) + [IniKey("Det_PGA")] + public int DetPGA { get; set; } = 6; + + /// ROI 高度 | ROI height + [IniKey("Image_ROI_Height")] + public int ImageROIHeight { get; set; } + + /// ROI 宽度 | ROI width + [IniKey("Image_ROI_Width")] + public int ImageROIWidth { get; set; } + + /// ROI X 起始 | ROI X start + [IniKey("Image_ROI_xStart")] + public int ImageROIxStart { get; set; } + + /// ROI X 结束 | ROI X end + [IniKey("Image_ROI_xEnd")] + public int ImageROIxEnd { get; set; } + + /// ROI Y 起始 | ROI Y start + [IniKey("Image_ROI_yStart")] + public int ImageROIyStart { get; set; } + + /// ROI Y 结束 | ROI Y end + [IniKey("Image_ROI_yEnd")] + public int ImageROIyEnd { get; set; } + + /// ROI Z 起始 | ROI Z start + [IniKey("Image_ROI_zStart")] + public int ImageROIzStart { get; set; } + + /// ROI Z 结束 | ROI Z end + [IniKey("Image_ROI_zEnd")] + public int ImageROIzEnd { get; set; } + + /// 图像高度 | Image height + [IniKey("Image_Size_Height")] + public int ImageSizeHeight { get; set; } + + /// 图像宽度 | Image width + [IniKey("Image_Size_Width")] + public int ImageSizeWidth { get; set; } + + /// 物理尺寸 X (mm) | Physical size X (mm) + [IniKey("Physical_Size_X")] + public double PhysicalSizeX { get; set; } + + /// 物理尺寸 Y (mm) | Physical size Y (mm) + [IniKey("Physical_Size_Y")] + public double PhysicalSizeY { get; set; } + + /// 像素尺寸 X (mm) | Pixel size X (mm) + [IniKey("Pixel_X")] + public double PixelX { get; set; } + + /// 像素尺寸 Y (mm) | Pixel size Y (mm) + [IniKey("Pixel_Y")] + public double PixelY { get; set; } + } +} diff --git a/XP.Scan/Models/MoveControlConfig.cs b/XP.Scan/Models/MoveControlConfig.cs new file mode 100644 index 0000000..edaaada --- /dev/null +++ b/XP.Scan/Models/MoveControlConfig.cs @@ -0,0 +1,40 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 运动控制配置 | Motion control configuration + /// 对应 INI [Move_Control] Section + /// + [IniSection("Move_Control")] + public class MoveControlConfig + { + /// 探测器 X 位置 (mm) | Detector X position (mm) + [IniKey("DetX")] + public double DetX { get; set; } + + /// 探测器 Y 位置 (mm) | Detector Y position (mm) + [IniKey("DetY")] + public double DetY { get; set; } + + /// 探测器 Z 位置 (mm) | Detector Z position (mm) + [IniKey("DetZ")] + public double DetZ { get; set; } + + /// 旋转台角度 (°) | Rotation angle (°) + [IniKey("Rotation")] + public double Rotation { get; set; } + + /// 样品台 X 位置 (mm),即 SOD | Stage X position (mm), i.e. SOD + [IniKey("X")] + public double X { get; set; } + + /// 射线源 Z 位置 (mm) | X-Ray source Z position (mm) + [IniKey("XRAYZ")] + public double XRAYZ { get; set; } + + /// 样品台 Y 位置 (mm) | Stage Y position (mm) + [IniKey("Y")] + public double Y { get; set; } + } +} diff --git a/XP.Scan/Models/ProjectInfo.cs b/XP.Scan/Models/ProjectInfo.cs new file mode 100644 index 0000000..50437bb --- /dev/null +++ b/XP.Scan/Models/ProjectInfo.cs @@ -0,0 +1,36 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 项目信息 | Project information + /// 对应 INI [Project_Information] Section + /// + [IniSection("Project_Information")] + public class ProjectInfo + { + /// 图像保存路径 | Image save path + [IniKey("fileSave")] + public string FileSave { get; set; } = string.Empty; + + /// 滤波片1 | Filter 1 + [IniKey("filter1")] + public string Filter1 { get; set; } = "None"; + + /// 滤波片2 | Filter 2 + [IniKey("filter2")] + public string Filter2 { get; set; } = "None"; + + /// 项目名称 | Project name + [IniKey("Project")] + public string Project { get; set; } = string.Empty; + + /// 样品编号 | Sample number + [IniKey("SampleNo")] + public string SampleNo { get; set; } = string.Empty; + + /// 扫描模式名称 | Scan mode name + [IniKey("ScanMode")] + public string ScanMode { get; set; } = "QuickScan"; + } +} diff --git a/XP.Scan/Models/ScanConfigData.cs b/XP.Scan/Models/ScanConfigData.cs new file mode 100644 index 0000000..1d714f4 --- /dev/null +++ b/XP.Scan/Models/ScanConfigData.cs @@ -0,0 +1,27 @@ +namespace XP.Scan.Models +{ + /// + /// 扫描配置数据(顶层聚合,对应完整 INI 文件) + /// Scan configuration data (top-level aggregate, corresponds to complete INI file) + /// + public class ScanConfigData + { + /// 项目信息 → [Project_Information] + public ProjectInfo ProjectInfo { get; set; } = new(); + + /// 射线源配置 → [XRay] + public XRayConfig XRay { get; set; } = new(); + + /// 探测器配置 → [Detector] + public DetectorConfig Detector { get; set; } = new(); + + /// 运动控制配置 → [Move_Control] + public MoveControlConfig MoveControl { get; set; } = new(); + + /// 扫描配置 → [Scan_Config] + public ScanSettings ScanSettings { get; set; } = new(); + + /// 校正配置 → [Correction_Config] + public CorrectionConfig Correction { get; set; } = new(); + } +} diff --git a/XP.Scan/Models/ScanSettings.cs b/XP.Scan/Models/ScanSettings.cs new file mode 100644 index 0000000..54b49f6 --- /dev/null +++ b/XP.Scan/Models/ScanSettings.cs @@ -0,0 +1,32 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 扫描配置 | Scan configuration + /// 对应 INI [Scan_Config] Section + /// + [IniSection("Scan_Config")] + public class ScanSettings + { + /// 采集张数 | Number of acquisitions + [IniKey("AcquiresNums")] + public int AcquiresNums { get; set; } + + /// 旋转角度 (°) | Rotation degree (°) + [IniKey("RotateDegree")] + public double RotateDegree { get; set; } + + /// 扫描模式描述 | Scan mode description + [IniKey("ScanMode")] + public string ScanMode { get; set; } = string.Empty; + + /// SDD — 射线源到探测器距离 (mm) | Source to detector distance (mm) + [IniKey("SDD")] + public double SDD { get; set; } + + /// SOD — 射线源到样品距离 (mm) | Source to object distance (mm) + [IniKey("SOD")] + public double SOD { get; set; } + } +} diff --git a/XP.Scan/Models/XRayConfig.cs b/XP.Scan/Models/XRayConfig.cs new file mode 100644 index 0000000..03d8cc3 --- /dev/null +++ b/XP.Scan/Models/XRayConfig.cs @@ -0,0 +1,24 @@ +using XP.Scan.Attributes; + +namespace XP.Scan.Models +{ + /// + /// 射线源配置 | X-Ray source configuration + /// 对应 INI [XRay] Section + /// + [IniSection("XRay")] + public class XRayConfig + { + /// 管电流 (μA) | Tube current (μA) + [IniKey("Current_uA")] + public int CurrentUA { get; set; } + + /// 焦点尺寸 | Focus size + [IniKey("Focus")] + public string Focus { get; set; } = string.Empty; + + /// 管电压 (kV) | Tube voltage (kV) + [IniKey("Voltage_kV")] + public int VoltageKV { get; set; } + } +} diff --git a/XP.Scan/Services/IScanConfigSerializer.cs b/XP.Scan/Services/IScanConfigSerializer.cs new file mode 100644 index 0000000..deed1cb --- /dev/null +++ b/XP.Scan/Services/IScanConfigSerializer.cs @@ -0,0 +1,19 @@ +using XP.Scan.Models; + +namespace XP.Scan.Services +{ + /// + /// 扫描配置 INI 序列化接口 | Scan config INI serialization interface + /// + public interface IScanConfigSerializer + { + /// 将配置数据序列化为 INI 格式字符串 | Serialize config to INI string + string Serialize(ScanConfigData config); + + /// 将配置数据写入 INI 文件 | Save config to INI file + void SaveToFile(ScanConfigData config, string filePath); + + /// 从 INI 文件读取配置数据 | Load config from INI file + ScanConfigData LoadFromFile(string filePath); + } +} diff --git a/XP.Scan/Services/ScanConfigSerializer.cs b/XP.Scan/Services/ScanConfigSerializer.cs new file mode 100644 index 0000000..5a76558 --- /dev/null +++ b/XP.Scan/Services/ScanConfigSerializer.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using XP.Scan.Attributes; +using XP.Scan.Models; + +namespace XP.Scan.Services +{ + /// + /// 扫描配置 INI 序列化实现 | Scan config INI serialization implementation + /// 通过反射 + 自定义特性自动生成/解析 INI 内容 + /// + public class ScanConfigSerializer : IScanConfigSerializer + { + /// + /// 将配置数据序列化为 INI 格式字符串 + /// + public string Serialize(ScanConfigData config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + + var sb = new StringBuilder(); + var sections = GetSectionObjects(config); + + foreach (var (sectionName, sectionObj) in sections) + { + sb.AppendLine($"[{sectionName}]"); + SerializeSection(sb, sectionObj); + sb.AppendLine(); + } + + return sb.ToString().TrimEnd(); + } + + /// + /// 将配置数据写入 INI 文件 + /// + public void SaveToFile(ScanConfigData config, string filePath) + { + if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); + + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var content = Serialize(config); + File.WriteAllText(filePath, content, new UTF8Encoding(false)); + } + + /// + /// 从 INI 文件读取配置数据 + /// + public ScanConfigData LoadFromFile(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"INI file not found: {filePath}", filePath); + + var lines = File.ReadAllLines(filePath, Encoding.UTF8); + var iniData = ParseIniLines(lines); + + var config = new ScanConfigData(); + DeserializeSection(iniData, config.ProjectInfo); + DeserializeSection(iniData, config.XRay); + DeserializeSection(iniData, config.Detector); + DeserializeSection(iniData, config.MoveControl); + DeserializeSection(iniData, config.ScanSettings); + DeserializeSection(iniData, config.Correction); + + return config; + } + + #region 序列化辅助方法 + + /// + /// 获取 ScanConfigData 中所有标注了 [IniSection] 的子对象 + /// + private List<(string SectionName, object SectionObj)> GetSectionObjects(ScanConfigData config) + { + var result = new List<(string, object)>(); + + foreach (var prop in typeof(ScanConfigData).GetProperties()) + { + var obj = prop.GetValue(config); + if (obj == null) continue; + + var sectionAttr = obj.GetType().GetCustomAttribute(); + if (sectionAttr != null) + { + result.Add((sectionAttr.SectionName, obj)); + } + } + + return result; + } + + /// + /// 序列化单个 Section 的所有属性为 key=value 行 + /// + private void SerializeSection(StringBuilder sb, object sectionObj) + { + foreach (var prop in sectionObj.GetType().GetProperties()) + { + var keyAttr = prop.GetCustomAttribute(); + var keyName = keyAttr?.KeyName ?? prop.Name; + var value = prop.GetValue(sectionObj); + var valueStr = FormatValue(value); + + sb.AppendLine($"{keyName}={valueStr}"); + } + } + + /// + /// 格式化属性值为 INI 字符串(使用 InvariantCulture) + /// + private string FormatValue(object? value) + { + if (value == null) return string.Empty; + + return value switch + { + double d => d.ToString(CultureInfo.InvariantCulture), + float f => f.ToString(CultureInfo.InvariantCulture), + _ => value.ToString() ?? string.Empty + }; + } + + #endregion + + #region 反序列化辅助方法 + + /// + /// 解析 INI 文件行为 Section → Key/Value 字典 + /// + private Dictionary> ParseIniLines(string[] lines) + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + string currentSection = string.Empty; + + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + + // 跳过空行和注释 + if (string.IsNullOrEmpty(line) || line.StartsWith(";") || line.StartsWith("#")) + continue; + + // Section 头 + if (line.StartsWith("[") && line.EndsWith("]")) + { + currentSection = line.Substring(1, line.Length - 2).Trim(); + if (!result.ContainsKey(currentSection)) + { + result[currentSection] = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + continue; + } + + // Key=Value + var eqIndex = line.IndexOf('='); + if (eqIndex > 0 && !string.IsNullOrEmpty(currentSection)) + { + var key = line.Substring(0, eqIndex).Trim(); + var val = line.Substring(eqIndex + 1).Trim(); + result[currentSection][key] = val; + } + } + + return result; + } + + /// + /// 将 INI 数据反序列化到模型对象 + /// + private void DeserializeSection(Dictionary> iniData, object sectionObj) + { + var sectionAttr = sectionObj.GetType().GetCustomAttribute(); + if (sectionAttr == null) return; + + if (!iniData.TryGetValue(sectionAttr.SectionName, out var sectionData)) + return; + + foreach (var prop in sectionObj.GetType().GetProperties()) + { + var keyAttr = prop.GetCustomAttribute(); + var keyName = keyAttr?.KeyName ?? prop.Name; + + if (!sectionData.TryGetValue(keyName, out var valueStr)) + continue; + + try + { + var convertedValue = ConvertValue(valueStr, prop.PropertyType); + if (convertedValue != null) + { + prop.SetValue(sectionObj, convertedValue); + } + } + catch + { + // 转换失败时保留默认值 + } + } + } + + /// + /// 将字符串值转换为目标类型 + /// + private object? ConvertValue(string value, Type targetType) + { + if (targetType == typeof(string)) + return value; + + if (targetType == typeof(int)) + return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var i) ? i : null; + + if (targetType == typeof(double)) + return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : null; + + if (targetType == typeof(float)) + return float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var f) ? f : null; + + if (targetType == typeof(bool)) + return bool.TryParse(value, out var b) ? b : null; + + return null; + } + + #endregion + } +}