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
+ }
+}