diff --git a/XP.ReportEngine/Configs/ConfigLoader.cs b/XP.ReportEngine/Configs/ConfigLoader.cs
new file mode 100644
index 0000000..7b08002
--- /dev/null
+++ b/XP.ReportEngine/Configs/ConfigLoader.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Configuration;
+using System.IO;
+using XP.Common.Logging.Interfaces;
+
+namespace XP.ReportEngine.Configs
+{
+ ///
+ /// 报告引擎配置加载器(读取 App.config)| Report engine configuration loader (reads from App.config)
+ ///
+ public class ConfigLoader
+ {
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 日志服务 | Logger service
+ public ConfigLoader(ILoggerService logger)
+ {
+ _logger = logger.ForModule();
+ }
+
+ ///
+ /// 从 App.config 加载报告引擎配置 | Load report engine configuration from App.config
+ ///
+ /// 配置前缀,默认为 "Report" | Configuration prefix, default is "Report"
+ /// 报告配置对象 | Report configuration object
+ public ReportConfig LoadReportConfig(string prefix = "Report")
+ {
+ try
+ {
+ _logger.Info("开始从 App.config 加载报告引擎配置,前缀: {Prefix} | Loading report config from App.config, prefix: {Prefix}", prefix);
+
+ var config = new ReportConfig();
+
+ // 读取输出目录 | Read output directory
+ var outputDirectory = ConfigurationManager.AppSettings[$"{prefix}:OutputDirectory"];
+ if (!string.IsNullOrEmpty(outputDirectory))
+ {
+ config.OutputDirectory = outputDirectory;
+ }
+
+ // 读取模板路径 | Read template path
+ var templatePath = ConfigurationManager.AppSettings[$"{prefix}:TemplatePath"];
+ if (!string.IsNullOrEmpty(templatePath))
+ {
+ config.TemplatePath = templatePath;
+ }
+
+ // 读取文件名模式 | Read file name pattern
+ var fileNamePattern = ConfigurationManager.AppSettings[$"{prefix}:FileNamePattern"];
+ if (!string.IsNullOrEmpty(fileNamePattern))
+ {
+ config.FileNamePattern = fileNamePattern;
+ }
+
+ // 读取重复文件名自动累加设置 | Read auto-increment on duplicate setting
+ var autoIncrement = ConfigurationManager.AppSettings[$"{prefix}:AutoIncrementOnDuplicate"];
+ if (bool.TryParse(autoIncrement, out var autoIncrementValue))
+ {
+ config.AutoIncrementOnDuplicate = autoIncrementValue;
+ }
+
+ // 读取自动打开设置 | Read auto-open setting
+ var autoOpen = ConfigurationManager.AppSettings[$"{prefix}:AutoOpenAfterGenerate"];
+ if (bool.TryParse(autoOpen, out var autoOpenValue))
+ {
+ config.AutoOpenAfterGenerate = autoOpenValue;
+ }
+
+ // 读取页面尺寸 | Read page size
+ var pageSize = ConfigurationManager.AppSettings[$"{prefix}:DefaultPageSize"];
+ if (!string.IsNullOrEmpty(pageSize))
+ {
+ config.DefaultPageSize = pageSize;
+ }
+
+ // 读取页面方向 | Read page orientation
+ var orientation = ConfigurationManager.AppSettings[$"{prefix}:DefaultOrientation"];
+ if (!string.IsNullOrEmpty(orientation))
+ {
+ config.DefaultOrientation = orientation;
+ }
+
+ // 读取边距配置 | Read margin configuration
+ var marginTop = ConfigurationManager.AppSettings[$"{prefix}:MarginTop"];
+ if (float.TryParse(marginTop, out var marginTopValue))
+ {
+ config.MarginTop = marginTopValue;
+ }
+
+ var marginBottom = ConfigurationManager.AppSettings[$"{prefix}:MarginBottom"];
+ if (float.TryParse(marginBottom, out var marginBottomValue))
+ {
+ config.MarginBottom = marginBottomValue;
+ }
+
+ var marginLeft = ConfigurationManager.AppSettings[$"{prefix}:MarginLeft"];
+ if (float.TryParse(marginLeft, out var marginLeftValue))
+ {
+ config.MarginLeft = marginLeftValue;
+ }
+
+ var marginRight = ConfigurationManager.AppSettings[$"{prefix}:MarginRight"];
+ if (float.TryParse(marginRight, out var marginRightValue))
+ {
+ config.MarginRight = marginRightValue;
+ }
+
+ // 读取公司名称 | Read company name
+ var companyName = ConfigurationManager.AppSettings[$"{prefix}:CompanyName"];
+ if (!string.IsNullOrEmpty(companyName))
+ {
+ config.CompanyName = companyName;
+ }
+
+ // 读取公司 Logo 路径 | Read company logo path
+ var companyLogo = ConfigurationManager.AppSettings[$"{prefix}:CompanyLogo"];
+ if (!string.IsNullOrEmpty(companyLogo))
+ {
+ config.CompanyLogo = companyLogo;
+ }
+
+ // 验证配置 | Validate configuration
+ ValidateConfig(config);
+
+ _logger.Info("报告引擎配置加载成功:输出目录={OutputDir}, 模板={Template} | Report config loaded: OutputDir={OutputDir}, Template={Template}",
+ config.OutputDirectory, config.TemplatePath);
+
+ return config;
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("加载报告引擎配置失败,使用默认配置 | Failed to load report config, using defaults: {Message}", ex.Message);
+ return new ReportConfig();
+ }
+ }
+
+ ///
+ /// 验证配置参数 | Validate configuration parameters
+ ///
+ /// 报告配置对象 | Report configuration object
+ private void ValidateConfig(ReportConfig config)
+ {
+ // 验证输出目录(不存在则尝试创建)| Validate output directory (create if not exists)
+ if (string.IsNullOrWhiteSpace(config.OutputDirectory))
+ {
+ _logger.Warn("OutputDirectory 为空,使用默认值 | OutputDirectory is empty, using default");
+ config.OutputDirectory = new ReportConfig().OutputDirectory;
+ }
+
+ // 验证模板路径 | Validate template path
+ if (string.IsNullOrWhiteSpace(config.TemplatePath))
+ {
+ _logger.Warn("TemplatePath 为空,使用默认值 | TemplatePath is empty, using default");
+ config.TemplatePath = @"Templates\StandardReportTemplate.json";
+ }
+
+ // 验证文件名模式 | Validate file name pattern
+ if (string.IsNullOrWhiteSpace(config.FileNamePattern))
+ {
+ _logger.Warn("FileNamePattern 为空,使用默认值 | FileNamePattern is empty, using default");
+ config.FileNamePattern = "{ReportId}";
+ }
+
+ // 验证页面方向 | Validate page orientation
+ if (config.DefaultOrientation != "Portrait" && config.DefaultOrientation != "Landscape")
+ {
+ _logger.Warn("DefaultOrientation 无效: {Value},使用默认值 Portrait | DefaultOrientation invalid: {Value}, using default Portrait", config.DefaultOrientation);
+ config.DefaultOrientation = "Portrait";
+ }
+
+ // 验证边距范围(0-100mm)| Validate margin range (0-100mm)
+ config.MarginTop = ClampMargin(config.MarginTop, "MarginTop");
+ config.MarginBottom = ClampMargin(config.MarginBottom, "MarginBottom");
+ config.MarginLeft = ClampMargin(config.MarginLeft, "MarginLeft");
+ config.MarginRight = ClampMargin(config.MarginRight, "MarginRight");
+
+ // 验证 Logo 路径(如果配置了则检查文件是否存在)| Validate logo path (check file exists if configured)
+ if (!string.IsNullOrEmpty(config.CompanyLogo))
+ {
+ var logoPath = Path.IsPathRooted(config.CompanyLogo)
+ ? config.CompanyLogo
+ : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, config.CompanyLogo);
+
+ if (!File.Exists(logoPath))
+ {
+ _logger.Warn("公司 Logo 文件不存在: {Path},将不显示 Logo | Company logo file not found: {Path}, logo will not be displayed", logoPath);
+ }
+ }
+ }
+
+ ///
+ /// 将边距值限制在有效范围内 | Clamp margin value to valid range
+ ///
+ private float ClampMargin(float value, string name)
+ {
+ if (value < 0 || value > 100)
+ {
+ _logger.Warn("{Name} 超出有效范围 [0, 100]: {Value},使用默认值 20 | {Name} out of valid range [0, 100]: {Value}, using default 20", name, value);
+ return 20f;
+ }
+ return value;
+ }
+ }
+}
diff --git a/XP.ReportEngine/Configs/ReportConfig.cs b/XP.ReportEngine/Configs/ReportConfig.cs
new file mode 100644
index 0000000..8198b6e
--- /dev/null
+++ b/XP.ReportEngine/Configs/ReportConfig.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace XP.ReportEngine.Configs
+{
+ ///
+ /// 报告引擎配置模型 | Report engine configuration model
+ ///
+ public class ReportConfig
+ {
+ ///
+ /// 报告输出文件夹路径 | Report output directory path
+ ///
+ public string OutputDirectory { get; set; } = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "XplorePlane", "Reports");
+
+ ///
+ /// 报告模板文件路径(相对或绝对)| Report template file path (relative or absolute)
+ ///
+ public string TemplatePath { get; set; } = @"Templates\StandardReportTemplate.json";
+
+ ///
+ /// 输出文件名模式,支持占位符 | Output file name pattern, supports placeholders
+ /// 支持的占位符 | Supported placeholders:
+ /// {ReportId} - 报告编号(如 RPT-20250512-001)
+ /// {CncProgram} - CNC 程序名称
+ /// {ProductName} - 产品名称
+ /// {ProductCode} - 产品类型码
+ /// {WorkpieceSN} - 工件 SN 码
+ /// {DeviceId} - 检测设备编号(本机)
+ /// {MachineId} - 生产机台号
+ /// {Date} - 日期(yyyyMMdd)
+ /// {Time} - 时间(HHmmss)
+ /// {Result} - 综合检测结论(Pass/Fail)
+ ///
+ public string FileNamePattern { get; set; } = "{ReportId}";
+
+ ///
+ /// 文件名重复时是否自动累加序号 | Whether to auto-increment suffix when file name duplicates
+ /// true: 重复时生成 filename(1).pdf, filename(2).pdf ...
+ /// false: 直接覆盖同名文件
+ ///
+ public bool AutoIncrementOnDuplicate { get; set; } = true;
+
+ ///
+ /// 生成后是否自动打开 PDF 阅读器 | Whether to auto-open PDF viewer after generation
+ ///
+ public bool AutoOpenAfterGenerate { get; set; } = false;
+
+ ///
+ /// 默认页面尺寸 | Default page size
+ ///
+ public string DefaultPageSize { get; set; } = "A4";
+
+ ///
+ /// 默认页面方向(Portrait / Landscape)| Default page orientation
+ ///
+ public string DefaultOrientation { get; set; } = "Portrait";
+
+ ///
+ /// 默认上边距(mm)| Default top margin (mm)
+ ///
+ public float MarginTop { get; set; } = 20f;
+
+ ///
+ /// 默认下边距(mm)| Default bottom margin (mm)
+ ///
+ public float MarginBottom { get; set; } = 20f;
+
+ ///
+ /// 默认左边距(mm)| Default left margin (mm)
+ ///
+ public float MarginLeft { get; set; } = 20f;
+
+ ///
+ /// 默认右边距(mm)| Default right margin (mm)
+ ///
+ public float MarginRight { get; set; } = 20f;
+
+ ///
+ /// 报告中显示的公司名称 | Company name displayed in report
+ ///
+ public string CompanyName { get; set; } = "海克斯康制造智能技术(青岛)有限公司";
+
+ ///
+ /// 公司 Logo 图片路径(可选,为空则不显示)| Company logo image path (optional, empty means no logo)
+ ///
+ public string CompanyLogo { get; set; } = string.Empty;
+
+ ///
+ /// 获取解析后的模板绝对路径 | Get resolved absolute template path
+ /// 如果 TemplatePath 是相对路径,则基于应用程序目录解析
+ /// If TemplatePath is relative, resolves based on application directory
+ ///
+ public string GetResolvedTemplatePath()
+ {
+ if (Path.IsPathRooted(TemplatePath))
+ {
+ return TemplatePath;
+ }
+ return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplatePath);
+ }
+
+ ///
+ /// 根据文件名模式和上下文参数生成实际文件名 | Generate actual file name based on pattern and context parameters
+ ///
+ /// 占位符参数字典 | Placeholder parameter dictionary
+ /// 生成的文件名(不含扩展名)| Generated file name (without extension)
+ public string ResolveFileName(Dictionary parameters)
+ {
+ var fileName = FileNamePattern;
+
+ // 替换所有已知占位符 | Replace all known placeholders
+ if (parameters != null)
+ {
+ foreach (var kvp in parameters)
+ {
+ fileName = fileName.Replace($"{{{kvp.Key}}}", SanitizeFileName(kvp.Value ?? ""));
+ }
+ }
+
+ // 替换日期和时间(始终可用)| Replace date and time (always available)
+ fileName = fileName.Replace("{Date}", DateTime.Now.ToString("yyyyMMdd"));
+ fileName = fileName.Replace("{Time}", DateTime.Now.ToString("HHmmss"));
+
+ // 清理未被替换的占位符(替换为空)| Clean up unreplaced placeholders
+ fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"\{[^}]+\}", "");
+
+ // 移除连续的分隔符 | Remove consecutive separators
+ fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"[_\-]{2,}", "_");
+ fileName = fileName.Trim('_', '-');
+
+ return string.IsNullOrWhiteSpace(fileName) ? "Report" : fileName;
+ }
+
+ ///
+ /// 解析最终输出文件完整路径(含重复累加逻辑)| Resolve final output file full path (with duplicate increment logic)
+ ///
+ /// 占位符参数字典 | Placeholder parameter dictionary
+ /// 文件扩展名(含点号,如 ".pdf")| File extension (with dot, e.g. ".pdf")
+ /// 最终输出文件完整路径 | Final output file full path
+ public string ResolveOutputFilePath(Dictionary parameters, string extension = ".pdf")
+ {
+ var baseName = ResolveFileName(parameters);
+ var outputDir = OutputDirectory;
+
+ // 确保输出目录存在 | Ensure output directory exists
+ if (!Directory.Exists(outputDir))
+ {
+ Directory.CreateDirectory(outputDir);
+ }
+
+ var filePath = Path.Combine(outputDir, baseName + extension);
+
+ // 重复累加逻辑 | Duplicate increment logic
+ if (AutoIncrementOnDuplicate && File.Exists(filePath))
+ {
+ int counter = 1;
+ string newPath;
+ do
+ {
+ newPath = Path.Combine(outputDir, $"{baseName}({counter}){extension}");
+ counter++;
+ } while (File.Exists(newPath));
+
+ filePath = newPath;
+ }
+
+ return filePath;
+ }
+
+ ///
+ /// 清理文件名中的非法字符 | Sanitize illegal characters in file name
+ ///
+ private static string SanitizeFileName(string name)
+ {
+ var invalidChars = Path.GetInvalidFileNameChars();
+ foreach (var c in invalidChars)
+ {
+ name = name.Replace(c, '_');
+ }
+ return name;
+ }
+ }
+}
diff --git a/XP.ReportEngine/Documents/App.config.example b/XP.ReportEngine/Documents/App.config.example
new file mode 100644
index 0000000..45247d9
--- /dev/null
+++ b/XP.ReportEngine/Documents/App.config.example
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.ReportEngine/Documents/FontFilesGuidance.md b/XP.ReportEngine/Documents/FontFilesGuidance.md
index 767ae02..ce0850d 100644
--- a/XP.ReportEngine/Documents/FontFilesGuidance.md
+++ b/XP.ReportEngine/Documents/FontFilesGuidance.md
@@ -1,54 +1,34 @@
-# 字体文件目录 | Font Files Directory
+# 字体方案说明 | Font Strategy
-## 所需字体文件 | Required Font Files
+## 当前方案 | Current Approach
-请将以下字体文件手动添加到 `XP.ReportEngine/Fonts/` 目录:
+XP.ReportEngine 使用 **Windows 系统自带字体** 生成 PDF,无需额外下载或嵌入字体文件。
-1. **NotoSansCJKsc-Regular.otf** — 中文字体(CJK 字符支持)| Chinese font (CJK character support)
-2. **NotoSans-Regular.ttf** — 西文字体 | Western font
+| 语言 | 字体 | 文件 | 说明 |
+|------|------|------|------|
+| 简体中文 / 繁体中文 | 微软雅黑 | `C:\Windows\Fonts\msyh.ttc` | Windows 10+ 自带,支持简繁体 |
+| 西文(英文等) | Arial | `C:\Windows\Fonts\arial.ttf` | Windows 自带 |
-## 下载地址 | Download Links
+## 为什么使用系统字体 | Why System Fonts
-- Noto Sans CJK: https://github.com/googlefonts/noto-cjk/releases
- - 下载 `NotoSansCJKsc-Regular.otf`(简体中文版本)
- - 文件大小约 16MB
-- Noto Sans: https://fonts.google.com/noto/specimen/Noto+Sans
- - 下载 Regular 字重的 TTF 文件
- - 文件大小约 500KB
-
-## 下载步骤 | Download Steps
-
-### NotoSansCJKsc-Regular.otf
-
-1. 访问 https://github.com/googlefonts/noto-cjk/releases
-2. 找到最新 Release,展开 Assets
-3. 下载 `NotoSansCJKsc-Regular.otf`(或从 Sans 包中提取)
-4. 将文件放入 `XP.ReportEngine/Fonts/` 目录
-
-### NotoSans-Regular.ttf
-
-1. 访问 https://fonts.google.com/noto/specimen/Noto+Sans
-2. 点击 "Download family"
-3. 解压后找到 `NotoSans-Regular.ttf`
-4. 将文件放入 `XP.ReportEngine/Fonts/` 目录
-
-## 配置说明 | Configuration Notes
-
-字体文件已在 `.csproj` 中配置为嵌入资源(EmbeddedResource),添加文件后无需额外配置。
-Font files are configured as EmbeddedResource in .csproj, no additional configuration needed after adding files.
-
-```xml
-
-
-
-
-```
+1. **Telerik RadPdfViewer 兼容性** — 系统字体生成的 PDF 在 Telerik RadPdfViewer 中显示正常,不会出现中文乱码
+2. **无需额外部署** — 不需要随应用程序分发字体文件,减小安装包体积
+3. **跨阅读器兼容** — 字体以子集嵌入 PDF 中(`PREFER_EMBEDDED`),在任何 PDF 阅读器中都能正确显示
## 后备机制 | Fallback Mechanism
-如果字体文件缺失,ITextPdfRenderer 会按以下顺序降级:
-1. 尝试加载嵌入的 CJK 字体和西文字体
-2. 加载失败时记录警告日志
-3. 最终使用 iText 内置 Helvetica 字体(不支持中文字符)
+ITextPdfRenderer 按以下顺序加载字体:
-因此,如果需要生成包含中文内容的报告,**必须**添加 NotoSansCJKsc-Regular.otf 字体文件。
+1. 微软雅黑(`msyh.ttc`)→ 加载失败时尝试宋体(`simsun.ttc`)
+2. Arial(`arial.ttf`)→ 加载失败时使用 iText 内置 Helvetica
+3. 最终后备:Helvetica(不支持中文字符)
+
+## 系统要求 | System Requirements
+
+- Windows 10 或更高版本(微软雅黑为系统预装字体)
+- 如果在精简版 Windows 上运行,需确保系统已安装微软雅黑字体
+
+## Fonts 目录 | Fonts Directory
+
+`XP.ReportEngine/Fonts/` 目录当前为空,保留用于未来可能的自定义字体扩展。
+如需添加自定义字体,可在 `ITextPdfRenderer.InitializeFonts()` 中扩展加载逻辑。
diff --git a/XP.ReportEngine/Documents/README.md b/XP.ReportEngine/Documents/README.md
index f4c63ac..ecb8bb8 100644
--- a/XP.ReportEngine/Documents/README.md
+++ b/XP.ReportEngine/Documents/README.md
@@ -47,7 +47,7 @@ XP.ReportEngine/
│ ├── ITextPdfRenderer.cs # iText 7 PDF 渲染
│ ├── ProcessorDataAdapter.cs # 处理器数据适配
│ └── ReportIdGenerator.cs # 报告编号生成
-├── Fonts/ # 嵌入字体文件(需手动添加)
+├── Fonts/ # 字体目录(当前使用系统字体,目录保留备用)
├── Templates/ # JSON 报告模板
│ └── StandardReportTemplate.json
├── Resources/ # 多语言资源文件
@@ -62,11 +62,9 @@ XP.ReportEngine/
## 快速开始
-### 1. 添加字体文件
+### 1. 字体说明
-将以下字体文件放入 `Fonts/` 目录(详见 `Documents/字体文件说明.md`):
-- `NotoSansCJKsc-Regular.otf` — 中文 CJK 字体
-- `NotoSans-Regular.ttf` — 西文字体
+模块使用 Windows 系统自带字体(微软雅黑 + Arial),无需额外添加字体文件。详见 `Documents/FontFilesGuidance.md`。
### 2. 通过 DI 容器调用
diff --git a/XP.ReportEngine/Fonts/.gitkeep b/XP.ReportEngine/Fonts/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/XP.ReportEngine/Fonts/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/XP.ReportEngine/Fonts/NotoSans-Regular.ttf b/XP.ReportEngine/Fonts/NotoSans-Regular.ttf
deleted file mode 100644
index 4bac02f..0000000
Binary files a/XP.ReportEngine/Fonts/NotoSans-Regular.ttf and /dev/null differ
diff --git a/XP.ReportEngine/Fonts/NotoSerifCJKtc-Regular.otf b/XP.ReportEngine/Fonts/NotoSerifCJKtc-Regular.otf
deleted file mode 100644
index a59de8d..0000000
Binary files a/XP.ReportEngine/Fonts/NotoSerifCJKtc-Regular.otf and /dev/null differ
diff --git a/XP.ReportEngine/Fonts/NotoSerifSC-Regular.otf b/XP.ReportEngine/Fonts/NotoSerifSC-Regular.otf
deleted file mode 100644
index be55fbd..0000000
Binary files a/XP.ReportEngine/Fonts/NotoSerifSC-Regular.otf and /dev/null differ
diff --git a/XP.ReportEngine/Module/.gitkeep b/XP.ReportEngine/Module/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/XP.ReportEngine/Module/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/XP.ReportEngine/ReportEngineModule.cs b/XP.ReportEngine/ReportEngineModule.cs
index aae3f36..79701d8 100644
--- a/XP.ReportEngine/ReportEngineModule.cs
+++ b/XP.ReportEngine/ReportEngineModule.cs
@@ -3,8 +3,12 @@ using Prism.Modularity;
using System.Resources;
using XP.Common.Localization;
using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+using XP.ReportEngine.Configs;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Services;
+using XP.ReportEngine.ViewModels;
+using XP.ReportEngine.Views;
namespace XP.ReportEngine
{
@@ -41,6 +45,17 @@ namespace XP.ReportEngine
///
public void RegisterTypes(IContainerRegistry containerRegistry)
{
+ // 注册配置加载器(瞬态)| Register config loader (transient)
+ containerRegistry.Register();
+
+ // 加载并注册配置为单例 | Load and register config as singleton
+ containerRegistry.RegisterSingleton(container =>
+ {
+ var logger = container.Resolve();
+ var loader = new Configs.ConfigLoader(logger);
+ return loader.LoadReportConfig();
+ });
+
// 注册报告生成器(瞬态)| Register report generator (transient)
containerRegistry.Register();
@@ -65,6 +80,10 @@ namespace XP.ReportEngine
// 注册报告编号生成器(单例,维护每日计数器状态)| Register report ID generator (singleton, maintains daily counter state)
containerRegistry.RegisterSingleton();
+ // 注册演示窗口 ViewModel 和 View | Register demo window ViewModel and View
+ containerRegistry.Register();
+ containerRegistry.Register();
+
System.Console.WriteLine("[ReportEngineModule] 类型注册完成 | Type registration completed");
}
}
diff --git a/XP.ReportEngine/Services/.gitkeep b/XP.ReportEngine/Services/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/XP.ReportEngine/Services/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/XP.ReportEngine/Services/ExpressionDataBinder.cs b/XP.ReportEngine/Services/ExpressionDataBinder.cs
index 4c58dcb..fc33d4f 100644
--- a/XP.ReportEngine/Services/ExpressionDataBinder.cs
+++ b/XP.ReportEngine/Services/ExpressionDataBinder.cs
@@ -61,8 +61,44 @@ namespace XP.ReportEngine.Services
private void BindElement(TemplateElement element, ReportContext context)
{
- if (string.IsNullOrEmpty(element.Content)) return;
- element.Content = ResolveAllExpressions(element.Content, context);
+ // 文本内容绑定 | Text content binding
+ if (!string.IsNullOrEmpty(element.Content))
+ {
+ element.Content = ResolveAllExpressions(element.Content, context);
+ }
+
+ // 图像元素绑定:通过 DataKey 从 ReportContext.Images 获取 ImageData
+ // Image element binding: get ImageData from ReportContext.Images via DataKey
+ if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(element.DataKey)
+ && context.Images != null
+ && context.Images.TryGetValue(element.DataKey, out var imageData))
+ {
+ element.ImageData = imageData;
+ }
+
+ // 表格数据绑定:通过 DataKey 从 ReportContext.Properties 获取表格行数据
+ // Table data binding: get table row data from ReportContext.Properties via DataKey
+ if (string.Equals(element.Type, "table", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(element.DataKey)
+ && context.Properties != null
+ && context.Properties.TryGetValue(element.DataKey, out var tableValue)
+ && tableValue is List> tableRows)
+ {
+ element.TableData = tableRows;
+ }
+
+ // 表格列头绑定 | Table column header binding
+ if (element.Columns != null)
+ {
+ foreach (var column in element.Columns)
+ {
+ if (!string.IsNullOrEmpty(column.Header))
+ {
+ column.Header = ResolveAllExpressions(column.Header, context);
+ }
+ }
+ }
}
private string ResolveAllExpressions(string input, ReportContext context)
diff --git a/XP.ReportEngine/Services/ITextPdfRenderer.cs b/XP.ReportEngine/Services/ITextPdfRenderer.cs
index 87a0ebd..4e4892b 100644
--- a/XP.ReportEngine/Services/ITextPdfRenderer.cs
+++ b/XP.ReportEngine/Services/ITextPdfRenderer.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Reflection;
using System.Windows.Media.Imaging;
using iText.IO.Font;
using iText.Kernel.Colors;
@@ -59,15 +58,16 @@ namespace XP.ReportEngine.Services
///
private const string EvenRowBackgroundColor = "#F5F5F5";
- private PdfFont _cjkScFont;
- private PdfFont _cjkTcFont;
+ private PdfFont _cjkFont;
private PdfFont _westernFont;
+ private bool _fontsInitialized;
+ private readonly object _fontLock = new();
public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
{
_logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
- InitializeFonts();
+ // 字体延迟加载,不在构造函数中阻塞 | Fonts loaded lazily, not blocking in constructor
}
#region 7.1 基础 PDF 文档创建 | Basic PDF document creation
@@ -86,43 +86,42 @@ namespace XP.ReportEngine.Services
try
{
- using (var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true)))
+ var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true));
+ // 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
+ writer.SetCloseStream(false);
+
+ var pdfDocument = new PdfDocument(writer);
+ // 设置 A4 页面尺寸 | Set A4 page size
+ pdfDocument.SetDefaultPageSize(PageSize.A4);
+
+ var document = new Document(pdfDocument);
+
+ // 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
+ float marginTop = 20f * MmToPoints;
+ float marginBottom = 20f * MmToPoints;
+ float marginLeft = 20f * MmToPoints;
+ float marginRight = 20f * MmToPoints;
+
+ document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
+
+ if (pages != null && pages.Count > 0)
{
- // 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
- writer.SetCloseStream(false);
-
- using (var pdfDocument = new PdfDocument(writer))
+ for (int i = 0; i < pages.Count; i++)
{
- // 设置 A4 页面尺寸 | Set A4 page size
- pdfDocument.SetDefaultPageSize(PageSize.A4);
-
- using (var document = new Document(pdfDocument))
+ if (i > 0)
{
- // 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
- float marginTop = 20f * MmToPoints;
- float marginBottom = 20f * MmToPoints;
- float marginLeft = 20f * MmToPoints;
- float marginRight = 20f * MmToPoints;
-
- document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
-
- if (pages != null && pages.Count > 0)
- {
- for (int i = 0; i < pages.Count; i++)
- {
- if (i > 0)
- {
- // 添加新页面 | Add new page
- document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
- }
-
- RenderPage(document, pages[i]);
- }
- }
+ // 添加新页面 | Add new page
+ document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
}
+
+ RenderPage(document, pages[i]);
}
}
+ // 仅关闭 Document(它会级联关闭 PdfDocument 和 PdfWriter)
+ // Only close Document (it cascades to PdfDocument and PdfWriter)
+ document.Close();
+
// 重置流位置以便后续读取 | Reset stream position for subsequent reading
memoryStream.Position = 0;
_logger.Info("PDF 渲染完成 | PDF rendering completed");
@@ -252,97 +251,94 @@ namespace XP.ReportEngine.Services
#region 7.3 字体管理 | Font management
///
- /// 初始化字体(加载嵌入资源字体)| Initialize fonts (load embedded resource fonts)
+ /// 确保字体已初始化(线程安全的延迟加载)| Ensure fonts are initialized (thread-safe lazy loading)
///
- private void InitializeFonts()
+ private void EnsureFontsInitialized()
{
- // 加载简体中文字体 | Load Simplified Chinese font
- try
+ if (_fontsInitialized) return;
+ lock (_fontLock)
{
- _cjkScFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSerifSC-Regular.otf");
- _logger.Info("简体中文字体加载成功 | Simplified Chinese font loaded successfully");
- }
- catch (Exception ex)
- {
- _logger.Warn("简体中文嵌入字体加载失败,将使用后备字体 | SC embedded font load failed, will use fallback: {Message}", ex.Message);
- _cjkScFont = null;
- }
-
- // 加载繁体中文字体 | Load Traditional Chinese font
- try
- {
- _cjkTcFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSerifCJKtc-Regular.otf");
- _logger.Info("繁体中文字体加载成功 | Traditional Chinese font loaded successfully");
- }
- catch (Exception ex)
- {
- _logger.Warn("繁体中文嵌入字体加载失败,将使用后备字体 | TC embedded font load failed, will use fallback: {Message}", ex.Message);
- _cjkTcFont = null;
- }
-
- // 加载西文字体 | Load Western font
- try
- {
- _westernFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSans-Regular.ttf");
- _logger.Info("西文字体加载成功 | Western font loaded successfully");
- }
- catch (Exception ex)
- {
- _logger.Warn("西文嵌入字体加载失败,将使用后备字体 | Western embedded font load failed, will use fallback: {Message}", ex.Message);
- _westernFont = null;
- }
-
- // 如果嵌入字体都不可用,使用 iText 内置 Helvetica | If embedded fonts unavailable, use built-in Helvetica
- if (_cjkScFont == null && _cjkTcFont == null && _westernFont == null)
- {
- _logger.Warn("所有嵌入字体不可用,使用 Helvetica 后备字体 | All embedded fonts unavailable, using Helvetica fallback");
+ if (_fontsInitialized) return;
+ InitializeFonts();
+ _fontsInitialized = true;
}
}
///
- /// 从嵌入资源加载字体 | Load font from embedded resource
+ /// 初始化字体(从系统字体目录加载)| Initialize fonts (load from system fonts directory)
+ /// 使用 Windows 系统自带字体,确保 Telerik RadPdfViewer 兼容性
+ /// Uses Windows built-in fonts to ensure Telerik RadPdfViewer compatibility
///
- /// 嵌入资源名称 | Embedded resource name
- /// PDF 字体对象 | PDF font object
- private PdfFont LoadEmbeddedFont(string resourceName)
+ private void InitializeFonts()
{
- var assembly = Assembly.GetExecutingAssembly();
- using (var stream = assembly.GetManifestResourceStream(resourceName))
+ var fontsDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
+
+ // 加载微软雅黑(支持简体中文、繁体中文)| Load Microsoft YaHei (supports Simplified & Traditional Chinese)
+ try
{
- if (stream == null)
+ var msyhPath = System.IO.Path.Combine(fontsDir, "msyh.ttc");
+ _cjkFont = PdfFontFactory.CreateFont(msyhPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
+ _logger.Info("中文字体加载成功(微软雅黑)| Chinese font loaded successfully (Microsoft YaHei)");
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("微软雅黑加载失败,尝试后备字体 | Microsoft YaHei load failed, trying fallback: {Message}", ex.Message);
+ try
{
- throw new FileNotFoundException($"嵌入资源未找到 | Embedded resource not found: {resourceName}");
+ // 后备:宋体 | Fallback: SimSun
+ var simsunPath = System.IO.Path.Combine(fontsDir, "simsun.ttc");
+ _cjkFont = PdfFontFactory.CreateFont(simsunPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
+ _logger.Info("中文后备字体加载成功(宋体)| Chinese fallback font loaded successfully (SimSun)");
}
+ catch (Exception ex2)
+ {
+ _logger.Warn("宋体加载失败 | SimSun load failed: {Message}", ex2.Message);
+ _cjkFont = null;
+ }
+ }
- var fontBytes = new byte[stream.Length];
- stream.Read(fontBytes, 0, fontBytes.Length);
+ // 加载 Arial(西文字体)| Load Arial (Western font)
+ try
+ {
+ var arialPath = System.IO.Path.Combine(fontsDir, "arial.ttf");
+ _westernFont = PdfFontFactory.CreateFont(arialPath, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
+ _logger.Info("西文字体加载成功(Arial)| Western font loaded successfully (Arial)");
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Arial 加载失败,使用 Helvetica 后备 | Arial load failed, using Helvetica fallback: {Message}", ex.Message);
+ _westernFont = null;
+ }
- var fontProgram = FontProgramFactory.CreateFont(fontBytes);
- return PdfFontFactory.CreateFont(fontProgram, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
+ // 如果系统字体都不可用,使用 iText 内置 Helvetica | If system fonts unavailable, use built-in Helvetica
+ if (_cjkFont == null && _westernFont == null)
+ {
+ _logger.Warn("所有系统字体不可用,使用 Helvetica 后备字体 | All system fonts unavailable, using Helvetica fallback");
}
}
///
/// 根据当前语言获取合适的字体 | Get appropriate font based on current language
- /// zh-CN → 简体中文字体;zh-TW → 繁体中文字体;en-US → 西文字体
+ /// zh-CN / zh-TW → 微软雅黑(支持简繁体);en-US → Arial
///
/// PDF 字体 | PDF font
private PdfFont GetFontForCurrentLanguage()
{
+ // 延迟初始化字体 | Lazy initialize fonts
+ EnsureFontsInitialized();
+
var language = _localizationService.CurrentLanguage;
PdfFont selectedFont;
switch (language)
{
case SupportedLanguage.ZhCN:
- selectedFont = _cjkScFont;
- break;
case SupportedLanguage.ZhTW:
- selectedFont = _cjkTcFont ?? _cjkScFont; // 繁体优先,简体后备 | TC preferred, SC fallback
+ selectedFont = _cjkFont;
break;
case SupportedLanguage.EnUS:
default:
- selectedFont = _westernFont;
+ selectedFont = _westernFont ?? _cjkFont; // 西文优先,中文后备(微软雅黑也支持西文)| Western preferred, CJK fallback (YaHei supports Western too)
break;
}
@@ -352,9 +348,8 @@ namespace XP.ReportEngine.Services
return selectedFont;
}
- // 尝试使用其他嵌入字体 | Try other embedded fonts
- if (_cjkScFont != null) return _cjkScFont;
- if (_cjkTcFont != null) return _cjkTcFont;
+ // 尝试使用其他字体 | Try other fonts
+ if (_cjkFont != null) return _cjkFont;
if (_westernFont != null) return _westernFont;
// 最终后备:使用 iText 内置 Helvetica | Final fallback: use built-in Helvetica
diff --git a/XP.ReportEngine/Services/ProcessorDataAdapter.cs b/XP.ReportEngine/Services/ProcessorDataAdapter.cs
index b525767..a5fe73c 100644
--- a/XP.ReportEngine/Services/ProcessorDataAdapter.cs
+++ b/XP.ReportEngine/Services/ProcessorDataAdapter.cs
@@ -59,11 +59,43 @@ namespace XP.ReportEngine.Services
var group = AdaptProcessorOutput(output, i);
context.ResultGroups.Add(group);
- // 关联标注图像 | Associate annotated image
+ // 将结果数据扁平化到 Properties(供模板 ${key} 表达式绑定)
+ // Flatten result data to Properties (for template ${key} expression binding)
+ if (group.Data != null)
+ {
+ foreach (var kvp in group.Data)
+ {
+ context.Properties[kvp.Key] = kvp.Value;
+ }
+ }
+
+ // 将 Classification 也放入 Properties | Also put Classification into Properties
+ if (!string.IsNullOrEmpty(group.Classification))
+ {
+ context.Properties["classification"] = group.Classification;
+ }
+
+ // 将表格数据以模板期望的 dataKey 存入 Properties
+ // Store table data with template-expected dataKey into Properties
+ if (group.TableRows != null && group.TableRows.Count > 0)
+ {
+ var tableKey = GetTableDataKey(output.ProcessorType);
+ if (!string.IsNullOrEmpty(tableKey))
+ {
+ context.Properties[tableKey] = group.TableRows;
+ }
+ }
+
+ // 关联标注图像(使用模板期望的 dataKey)
+ // Associate annotated image (using template-expected dataKey)
if (output.AnnotatedImage != null)
{
- var imageKey = $"{output.ProcessorType}_{i}_annotated";
+ var imageKey = GetImageDataKey(output.ProcessorType);
context.Images[imageKey] = output.AnnotatedImage;
+
+ // 同时保留原始键名以兼容其他调用方 | Also keep original key for other callers
+ var originalKey = $"{output.ProcessorType}_{i}_annotated";
+ context.Images[originalKey] = output.AnnotatedImage;
}
}
@@ -467,6 +499,34 @@ namespace XP.ReportEngine.Services
return char.ToLowerInvariant(input[0]) + input.Substring(1);
}
+ ///
+ /// 根据处理器类型获取模板中对应的图像 dataKey | Get template image dataKey by processor type
+ ///
+ private static string GetImageDataKey(string processorType)
+ {
+ return processorType switch
+ {
+ LineMeasurementProcessor => "lineMeasurementImage",
+ BgaVoidRateProcessor => "bgaInspectionImage",
+ VoidMeasurementProcessor => "voidInspectionImage",
+ FillRateProcessor => "viaFillImage",
+ _ => $"{processorType}_image"
+ };
+ }
+
+ ///
+ /// 根据处理器类型获取模板中对应的表格 dataKey | Get template table dataKey by processor type
+ ///
+ private static string GetTableDataKey(string processorType)
+ {
+ return processorType switch
+ {
+ BgaVoidRateProcessor => "bgaBallsTable",
+ VoidMeasurementProcessor => "voidsTable",
+ _ => null
+ };
+ }
+
#endregion
}
}
diff --git a/XP.ReportEngine/Templates/.gitkeep b/XP.ReportEngine/Templates/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/XP.ReportEngine/Templates/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/XP.ReportEngine/Templates/StandardReportTemplate.json b/XP.ReportEngine/Templates/StandardReportTemplate.json
index 414249b..c93a758 100644
--- a/XP.ReportEngine/Templates/StandardReportTemplate.json
+++ b/XP.ReportEngine/Templates/StandardReportTemplate.json
@@ -8,6 +8,8 @@
{
"type": "homepage",
"elements": [
+ { "type": "image", "dataKey": "companyLogo", "position": [0, 0], "size": [30, 15], "positioning": "absolute" },
+ { "type": "text", "content": "${CompanyName}", "style": "body", "position": [32, 3], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "position": [60, 40], "positioning": "absolute" },
{ "type": "divider", "position": [0, 60], "size": [170, 1], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Id}:${metadata.reportId}", "style": "body", "position": [0, 75], "positioning": "absolute" },
diff --git a/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs b/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs
new file mode 100644
index 0000000..56b3402
--- /dev/null
+++ b/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs
@@ -0,0 +1,664 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using Prism.Commands;
+using Prism.Mvvm;
+using XP.Common.GeneralForm.Views;
+using XP.Common.Logging.Interfaces;
+using XP.Common.PdfViewer.Interfaces;
+using XP.ReportEngine.Configs;
+using XP.ReportEngine.Interfaces;
+using XP.ReportEngine.Models;
+using XP.ReportEngine.Services;
+
+namespace XP.ReportEngine.ViewModels
+{
+ ///
+ /// 报告生成演示窗口 ViewModel | Report generation demo window ViewModel
+ /// 演示如何使用 XP.ReportEngine 生成 PDF 报告
+ /// Demonstrates how to use XP.ReportEngine to generate PDF reports
+ ///
+ public class ReportDemoViewModel : BindableBase
+ {
+ private readonly IReportGenerator _reportGenerator;
+ private readonly IReportDataAdapter _dataAdapter;
+ private readonly IPdfViewerService _pdfViewerService;
+ private readonly IPdfPrintService _pdfPrintService;
+ private readonly ILoggerService _logger;
+ private readonly ReportIdGenerator _reportIdGenerator;
+ private readonly ReportConfig _reportConfig;
+
+ private string _productName = "PCB-TEST-001";
+ private string _operatorName = "戚明轩 mingxuan.qi@hexagon.com";
+ private string _description = "BGA 焊球气泡率检测";
+ private string _cncProgram = "Prog001";
+ private string _productCode = "PCBA-X100";
+ private string _workpieceSN = "SN20250001";
+ private string _deviceId = "XP-CT-001";
+ private string _machineId = "MC01";
+ private string _statusMessage = "就绪";
+ private string _lastOutputPath;
+ private bool _isGenerating;
+
+ #region 属性 | Properties
+
+ ///
+ /// 产品名称 | Product name
+ ///
+ public string ProductName
+ {
+ get => _productName;
+ set => SetProperty(ref _productName, value);
+ }
+
+ ///
+ /// 操作员 | Operator
+ ///
+ public string OperatorName
+ {
+ get => _operatorName;
+ set => SetProperty(ref _operatorName, value);
+ }
+
+ ///
+ /// 描述 | Description
+ ///
+ public string Description
+ {
+ get => _description;
+ set => SetProperty(ref _description, value);
+ }
+
+ ///
+ /// CNC 程序名称 | CNC program name
+ ///
+ public string CncProgram
+ {
+ get => _cncProgram;
+ set => SetProperty(ref _cncProgram, value);
+ }
+
+ ///
+ /// 产品类型码 | Product type code
+ ///
+ public string ProductCode
+ {
+ get => _productCode;
+ set => SetProperty(ref _productCode, value);
+ }
+
+ ///
+ /// 工件 SN 码 | Workpiece serial number
+ ///
+ public string WorkpieceSN
+ {
+ get => _workpieceSN;
+ set => SetProperty(ref _workpieceSN, value);
+ }
+
+ ///
+ /// 检测设备编号 | Inspection device ID
+ ///
+ public string DeviceId
+ {
+ get => _deviceId;
+ set => SetProperty(ref _deviceId, value);
+ }
+
+ ///
+ /// 生产机台号 | Production machine ID
+ ///
+ public string MachineId
+ {
+ get => _machineId;
+ set => SetProperty(ref _machineId, value);
+ }
+
+ ///
+ /// 状态信息 | Status message
+ ///
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetProperty(ref _statusMessage, value);
+ }
+
+ ///
+ /// 是否正在生成 | Whether generating
+ ///
+ public bool IsGenerating
+ {
+ get => _isGenerating;
+ set
+ {
+ if (SetProperty(ref _isGenerating, value))
+ {
+ GenerateReportCommand.RaiseCanExecuteChanged();
+ OpenViewerCommand.RaiseCanExecuteChanged();
+ PrintReportCommand.RaiseCanExecuteChanged();
+ }
+ }
+ }
+
+ #endregion
+
+ #region 命令 | Commands
+
+ ///
+ /// 生成报告命令 | Generate report command
+ ///
+ public DelegateCommand GenerateReportCommand { get; }
+
+ ///
+ /// 打开 PDF 阅读器命令 | Open PDF viewer command
+ ///
+ public DelegateCommand OpenViewerCommand { get; }
+
+ ///
+ /// 打印报告命令 | Print report command
+ ///
+ public DelegateCommand PrintReportCommand { get; }
+
+ #endregion
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ public ReportDemoViewModel(
+ IReportGenerator reportGenerator,
+ IReportDataAdapter dataAdapter,
+ IPdfViewerService pdfViewerService,
+ IPdfPrintService pdfPrintService,
+ ILoggerService logger,
+ ReportIdGenerator reportIdGenerator,
+ ReportConfig reportConfig)
+ {
+ _reportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator));
+ _dataAdapter = dataAdapter ?? throw new ArgumentNullException(nameof(dataAdapter));
+ _pdfViewerService = pdfViewerService ?? throw new ArgumentNullException(nameof(pdfViewerService));
+ _pdfPrintService = pdfPrintService ?? throw new ArgumentNullException(nameof(pdfPrintService));
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ _reportIdGenerator = reportIdGenerator ?? throw new ArgumentNullException(nameof(reportIdGenerator));
+ _reportConfig = reportConfig ?? throw new ArgumentNullException(nameof(reportConfig));
+
+ GenerateReportCommand = new DelegateCommand(async () => await GenerateReportAsync(), () => !IsGenerating);
+ OpenViewerCommand = new DelegateCommand(OpenViewer, () => !IsGenerating && !string.IsNullOrEmpty(_lastOutputPath));
+ PrintReportCommand = new DelegateCommand(PrintReport, () => !IsGenerating && !string.IsNullOrEmpty(_lastOutputPath));
+ }
+
+ ///
+ /// 生成报告(带进度条)| Generate report (with progress window)
+ ///
+ private async Task GenerateReportAsync()
+ {
+ IsGenerating = true;
+ StatusMessage = "正在生成报告...";
+
+ try
+ {
+ // 构建文件名占位符参数 | Build file name placeholder parameters
+ var fileNameParams = new Dictionary
+ {
+ ["ReportId"] = _reportIdGenerator.GenerateNext(),
+ ["ProductName"] = ProductName,
+ ["CncProgram"] = CncProgram,
+ ["ProductCode"] = ProductCode,
+ ["WorkpieceSN"] = WorkpieceSN,
+ ["DeviceId"] = DeviceId,
+ ["MachineId"] = MachineId,
+ ["Result"] = "Pass"
+ };
+
+ // 确定输出路径:提示用户是否使用默认位置 | Determine output path: ask user whether to use default location
+ var defaultOutputPath = _reportConfig.ResolveOutputFilePath(fileNameParams);
+ var defaultFileName = System.IO.Path.GetFileName(defaultOutputPath);
+
+ var result = MessageBox.Show(
+ $"是否将报告输出到默认位置?\n\n{defaultOutputPath}",
+ "输出位置确认",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Cancel)
+ {
+ StatusMessage = "已取消生成";
+ IsGenerating = false;
+ return;
+ }
+
+ string outputPath;
+ if (result == MessageBoxResult.No)
+ {
+ // 用户选择自定义位置 | User chooses custom location
+ var saveDialog = new Microsoft.Win32.SaveFileDialog
+ {
+ Title = "选择报告保存位置",
+ Filter = "PDF 文件 (*.pdf)|*.pdf",
+ FileName = defaultFileName,
+ DefaultExt = ".pdf",
+ InitialDirectory = System.IO.Path.GetDirectoryName(defaultOutputPath)
+ };
+
+ if (saveDialog.ShowDialog() != true)
+ {
+ StatusMessage = "已取消生成";
+ IsGenerating = false;
+ return;
+ }
+
+ outputPath = saveDialog.FileName;
+ }
+ else
+ {
+ // 使用默认路径 | Use default path
+ outputPath = defaultOutputPath;
+ }
+
+ // 创建进度条窗口 | Create progress window
+ var progressWindow = new ProgressWindow(
+ title: "报告生成中",
+ message: "正在准备数据...",
+ isCancelable: false,
+ logger: _logger);
+
+ progressWindow.Owner = Application.Current.MainWindow;
+ progressWindow.Show();
+
+ try
+ {
+ // 步骤 1:准备模拟数据 | Step 1: Prepare mock data
+ progressWindow.UpdateProgress("正在准备检测数据...", 10);
+ await Task.Delay(300); // 模拟耗时 | Simulate delay
+
+ var processorOutputs = CreateMockProcessorOutputs();
+
+ // 步骤 2:数据适配 | Step 2: Data adaptation
+ progressWindow.UpdateProgress("正在适配数据...", 30);
+ await Task.Delay(200);
+
+ var metadata = new ReportMetadata
+ {
+ ReportId = fileNameParams["ReportId"],
+ InspectionDate = DateTime.Now,
+ SampleName = ProductName,
+ OperatorName = OperatorName,
+ Description = Description
+ };
+
+ var context = _dataAdapter.Adapt(processorOutputs, metadata);
+
+ // 注入公司 Logo 图像数据 | Inject company logo image data
+ if (!string.IsNullOrEmpty(_reportConfig.CompanyLogo))
+ {
+ var logoPath = System.IO.Path.IsPathRooted(_reportConfig.CompanyLogo)
+ ? _reportConfig.CompanyLogo
+ : System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.CompanyLogo);
+
+ if (File.Exists(logoPath))
+ {
+ context.Images["companyLogo"] = new ImageData
+ {
+ SourceType = ImageSourceType.FilePath,
+ FilePath = logoPath
+ };
+ _logger.Info("公司 Logo 已加载:{Path} | Company logo loaded: {Path}", logoPath);
+ }
+ else
+ {
+ _logger.Warn("公司 Logo 文件不存在:{Path} | Company logo file not found: {Path}", logoPath);
+ }
+ }
+
+ // 注入公司名称到上下文属性 | Inject company name into context properties
+ if (!string.IsNullOrEmpty(_reportConfig.CompanyName))
+ {
+ context.Properties["CompanyName"] = _reportConfig.CompanyName;
+ }
+
+ // 注入首页汇总表数据 | Inject homepage summary table data
+ context.Properties["summaryTable"] = CreateSummaryTableData(context);
+
+ // 步骤 3:确定模板 | Step 3: Determine template
+ progressWindow.UpdateProgress("正在加载模板...", 50);
+ await Task.Delay(200);
+
+ var templatePath = _reportConfig.GetResolvedTemplatePath();
+
+ var options = new ReportGenerationOptions
+ {
+ TemplatePath = templatePath,
+ OutputFilePath = outputPath,
+ Format = ReportOutputFormat.Pdf
+ };
+
+ // 步骤 4:生成 PDF | Step 4: Generate PDF
+ progressWindow.UpdateProgress("正在生成 PDF...", 70);
+
+ var genResult = await _reportGenerator.GenerateAsync(context, options);
+
+ // 步骤 5:处理结果 | Step 5: Handle result
+ progressWindow.UpdateProgress("正在完成...", 95);
+ await Task.Delay(200);
+
+ if (genResult.IsSuccess)
+ {
+ _lastOutputPath = outputPath;
+ StatusMessage = $"报告生成成功:{outputPath}";
+ _logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", outputPath);
+
+ progressWindow.UpdateProgress("报告生成完成!", 100);
+ await Task.Delay(500);
+
+ // 根据配置自动打开 PDF 阅读器 | Auto-open PDF viewer based on config
+ if (_reportConfig.AutoOpenAfterGenerate)
+ {
+ try
+ {
+ _pdfViewerService.OpenViewer(outputPath);
+ }
+ catch (Exception viewerEx)
+ {
+ _logger.Warn("自动打开 PDF 失败 | Auto-open PDF failed: {Message}", viewerEx.Message);
+ }
+ }
+ }
+ else
+ {
+ StatusMessage = $"报告生成失败:{genResult.ErrorMessage}";
+ _logger.Error(null, "报告生成失败:{Message} | Report generation failed: {Message}", genResult.ErrorMessage);
+
+ MessageBox.Show(
+ $"报告生成失败:\n{genResult.ErrorMessage}",
+ "错误",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+ finally
+ {
+ progressWindow.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ StatusMessage = $"报告生成异常:{ex.Message}";
+ _logger.Error(ex, "报告生成异常 | Report generation exception: {Message}", ex.Message);
+
+ MessageBox.Show(
+ $"报告生成过程中发生异常:\n{ex.Message}",
+ "错误",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ finally
+ {
+ IsGenerating = false;
+ OpenViewerCommand.RaiseCanExecuteChanged();
+ PrintReportCommand.RaiseCanExecuteChanged();
+ }
+ }
+
+ ///
+ /// 打开 PDF 阅读器 | Open PDF viewer
+ ///
+ private void OpenViewer()
+ {
+ if (string.IsNullOrEmpty(_lastOutputPath) || !File.Exists(_lastOutputPath))
+ {
+ MessageBox.Show("PDF 文件不存在,请先生成报告。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ try
+ {
+ // 使用 XP.Common PDF 阅读器打开 | Open with XP.Common PDF viewer
+ _pdfViewerService.OpenViewer(_lastOutputPath);
+ _logger.Info("使用内置 PDF 阅读器打开:{Path} | Opened with built-in PDF viewer: {Path}", _lastOutputPath);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "打开 PDF 失败 | Failed to open PDF: {Message}", ex.Message);
+ MessageBox.Show($"打开 PDF 失败:\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ ///
+ /// 打印报告 | Print report
+ ///
+ private void PrintReport()
+ {
+ if (string.IsNullOrEmpty(_lastOutputPath) || !File.Exists(_lastOutputPath))
+ {
+ MessageBox.Show("PDF 文件不存在,请先生成报告。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ try
+ {
+ var confirmed = _pdfPrintService.PrintWithDialog(_lastOutputPath);
+ if (confirmed)
+ {
+ _logger.Info("报告已发送到打印机:{Path} | Report sent to printer: {Path}", _lastOutputPath);
+ StatusMessage = "报告已发送到打印机";
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "打印报告失败 | Failed to print report: {Message}", ex.Message);
+ MessageBox.Show($"打印失败:\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ #region 模拟数据 | Mock Data
+
+ ///
+ /// 模拟图像目录路径 | Mock image directory path
+ ///
+ private const string MockImageDirectory = @"D:\XplorePlane\DetectorImages";
+
+ ///
+ /// 创建模拟处理器输出数据(演示用,覆盖所有检测类型)
+ /// Create mock processor outputs (for demo, covers all inspection types)
+ ///
+ private List CreateMockProcessorOutputs()
+ {
+ return new List
+ {
+ CreateLineMeasurementOutput(),
+ CreateBgaVoidRateOutput(),
+ CreateVoidMeasurementOutput(),
+ CreateFillRateOutput()
+ };
+ }
+
+ ///
+ /// 创建线测量处理器模拟数据 | Create line measurement processor mock data
+ ///
+ private ProcessorOutput CreateLineMeasurementOutput()
+ {
+ return new ProcessorOutput
+ {
+ ProcessorType = "LineMeasurementProcessor",
+ OutputData = new Dictionary
+ {
+ ["MeasurementType"] = "TwoPointDistance",
+ ["Point1"] = "X=125.32, Y=80.15",
+ ["Point2"] = "X=340.78, Y=80.15",
+ ["PixelDistance"] = 215.46,
+ ["ActualDistance"] = 3.256,
+ ["Unit"] = "mm",
+ ["Angle"] = 0.0
+ },
+ AnnotatedImage = LoadMockImage("BGA.png")
+ };
+ }
+
+ ///
+ /// 创建 BGA 气泡率处理器模拟数据 | Create BGA void rate processor mock data
+ ///
+ private ProcessorOutput CreateBgaVoidRateOutput()
+ {
+ return new ProcessorOutput
+ {
+ ProcessorType = "BgaVoidRateProcessor",
+ OutputData = new Dictionary
+ {
+ ["BgaCount"] = 64,
+ ["VoidRate"] = 0.028,
+ ["FillRate"] = 0.972,
+ ["TotalBgaArea"] = 12500.5,
+ ["TotalVoidArea"] = 350.2,
+ ["Classification"] = "Pass",
+ ["VoidLimit"] = 0.05,
+ ["BgaBalls"] = new List>
+ {
+ new() { ["Index"] = 1, ["VoidRate"] = 0.012, ["Area"] = 195.3, ["Classification"] = "Pass" },
+ new() { ["Index"] = 2, ["VoidRate"] = 0.035, ["Area"] = 198.1, ["Classification"] = "Pass" },
+ new() { ["Index"] = 3, ["VoidRate"] = 0.008, ["Area"] = 192.7, ["Classification"] = "Pass" },
+ new() { ["Index"] = 4, ["VoidRate"] = 0.042, ["Area"] = 201.0, ["Classification"] = "Pass" },
+ new() { ["Index"] = 5, ["VoidRate"] = 0.015, ["Area"] = 196.5, ["Classification"] = "Pass" },
+ new() { ["Index"] = 6, ["VoidRate"] = 0.028, ["Area"] = 199.8, ["Classification"] = "Pass" },
+ new() { ["Index"] = 7, ["VoidRate"] = 0.005, ["Area"] = 194.2, ["Classification"] = "Pass" },
+ new() { ["Index"] = 8, ["VoidRate"] = 0.048, ["Area"] = 200.5, ["Classification"] = "Pass" },
+ new() { ["Index"] = 9, ["VoidRate"] = 0.019, ["Area"] = 197.0, ["Classification"] = "Pass" },
+ new() { ["Index"] = 10, ["VoidRate"] = 0.031, ["Area"] = 196.1, ["Classification"] = "Pass" }
+ }
+ },
+ AnnotatedImage = LoadMockImage("BGA.png")
+ };
+ }
+
+ ///
+ /// 创建空隙测量处理器模拟数据 | Create void measurement processor mock data
+ ///
+ private ProcessorOutput CreateVoidMeasurementOutput()
+ {
+ return new ProcessorOutput
+ {
+ ProcessorType = "VoidMeasurementProcessor",
+ OutputData = new Dictionary
+ {
+ ["RoiArea"] = 5000.0,
+ ["TotalVoidArea"] = 125.8,
+ ["VoidRate"] = 0.025,
+ ["VoidLimit"] = 0.05,
+ ["VoidCount"] = 5,
+ ["MaxVoidArea"] = 65.2,
+ ["Classification"] = "Pass",
+ ["Voids"] = new List>
+ {
+ new() { ["Index"] = 1, ["Area"] = 65.2, ["AreaPercent"] = 1.30, ["CenterX"] = 120.5, ["CenterY"] = 85.3 },
+ new() { ["Index"] = 2, ["Area"] = 38.4, ["AreaPercent"] = 0.77, ["CenterX"] = 200.1, ["CenterY"] = 150.8 },
+ new() { ["Index"] = 3, ["Area"] = 22.2, ["AreaPercent"] = 0.44, ["CenterX"] = 80.0, ["CenterY"] = 220.5 },
+ new() { ["Index"] = 4, ["Area"] = 15.6, ["AreaPercent"] = 0.31, ["CenterX"] = 310.2, ["CenterY"] = 95.7 },
+ new() { ["Index"] = 5, ["Area"] = 8.9, ["AreaPercent"] = 0.18, ["CenterX"] = 155.8, ["CenterY"] = 280.1 }
+ }
+ },
+ AnnotatedImage = LoadMockImage("Void.png")
+ };
+ }
+
+ ///
+ /// 创建通孔填锡率处理器模拟数据 | Create via fill rate processor mock data
+ ///
+ private ProcessorOutput CreateFillRateOutput()
+ {
+ return new ProcessorOutput
+ {
+ ProcessorType = "FillRateProcessor",
+ OutputData = new Dictionary
+ {
+ ["FillRate"] = 0.85,
+ ["VoidRate"] = 0.15,
+ ["FullDistance"] = 1.60,
+ ["FillDistance"] = 1.36,
+ ["THTLimit"] = 0.75,
+ ["Classification"] = "Pass",
+ ["E1"] = new Dictionary
+ {
+ ["CenterX"] = 256.0, ["CenterY"] = 256.0,
+ ["SemiAxisA"] = 120.5, ["SemiAxisB"] = 118.2, ["Angle"] = 2.3
+ },
+ ["E2"] = new Dictionary
+ {
+ ["CenterX"] = 256.0, ["CenterY"] = 256.0,
+ ["SemiAxisA"] = 95.8, ["SemiAxisB"] = 93.1, ["Angle"] = 2.3
+ },
+ ["E3"] = new Dictionary
+ {
+ ["CenterX"] = 256.0, ["CenterY"] = 256.0,
+ ["SemiAxisA"] = 70.2, ["SemiAxisB"] = 68.5, ["Angle"] = 1.8
+ },
+ ["E4"] = new Dictionary
+ {
+ ["CenterX"] = 256.0, ["CenterY"] = 256.0,
+ ["SemiAxisA"] = 45.0, ["SemiAxisB"] = 43.7, ["Angle"] = 1.5
+ }
+ },
+ AnnotatedImage = LoadMockImage("Void.png")
+ };
+ }
+
+ ///
+ /// 加载模拟图像文件 | Load mock image file
+ /// 从指定目录加载图像,文件不存在时返回 null
+ /// Loads image from specified directory, returns null if file not found
+ ///
+ /// 图像文件名 | Image file name
+ /// 图像数据对象或 null | ImageData object or null
+ private ImageData LoadMockImage(string fileName)
+ {
+ var filePath = System.IO.Path.Combine(MockImageDirectory, fileName);
+ if (!File.Exists(filePath))
+ {
+ _logger.Warn("模拟图像文件不存在:{Path} | Mock image file not found: {Path}", filePath);
+ return null;
+ }
+
+ return new ImageData
+ {
+ SourceType = ImageSourceType.FilePath,
+ FilePath = filePath
+ };
+ }
+
+ ///
+ /// 根据 ReportContext 中的结果分组生成首页汇总表数据
+ /// Generate homepage summary table data from ReportContext result groups
+ ///
+ /// 报告上下文 | Report context
+ /// 汇总表行数据 | Summary table row data
+ private List> CreateSummaryTableData(ReportContext context)
+ {
+ var rows = new List>();
+
+ foreach (var group in context.ResultGroups)
+ {
+ var inspectionType = group.ProcessorType switch
+ {
+ "LineMeasurementProcessor" => "线测量 | Line Measurement",
+ "BgaVoidRateProcessor" => "BGA 气泡率检测 | BGA Void Rate",
+ "VoidMeasurementProcessor" => "空隙测量 | Void Measurement",
+ "FillRateProcessor" => "通孔填锡率 | Via Fill Rate",
+ _ => group.ProcessorType
+ };
+
+ var classification = string.IsNullOrEmpty(group.Classification) ? "N/A" : group.Classification;
+ var status = classification == "Pass" ? "✓ 合格" : classification == "Fail" ? "✗ 不合格" : "—";
+
+ rows.Add(new Dictionary
+ {
+ ["inspectionType"] = inspectionType,
+ ["classification"] = classification,
+ ["status"] = status
+ });
+ }
+
+ return rows;
+ }
+
+ #endregion
+ }
+}
diff --git a/XP.ReportEngine/ViewModels/ViewAViewModel.cs b/XP.ReportEngine/ViewModels/ViewAViewModel.cs
deleted file mode 100644
index 188146b..0000000
--- a/XP.ReportEngine/ViewModels/ViewAViewModel.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Prism.Commands;
-using Prism.Mvvm;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace XP.ReportEngine.ViewModels
-{
- public class ViewAViewModel : BindableBase
- {
- private string _message;
- public string Message
- {
- get { return _message; }
- set { SetProperty(ref _message, value); }
- }
-
- public ViewAViewModel()
- {
- Message = "View A from your Prism Module";
- }
- }
-}
diff --git a/XP.ReportEngine/Views/ReportDemoWindow.xaml b/XP.ReportEngine/Views/ReportDemoWindow.xaml
new file mode 100644
index 0000000..f0c0d97
--- /dev/null
+++ b/XP.ReportEngine/Views/ReportDemoWindow.xaml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用说明:
+
+ 1. 填写产品信息、设备信息和操作员
+
+ 2. 点击"生成报告",系统将使用模拟检测数据生成 PDF
+
+ 3. 生成完成后,可点击"查看 PDF"在阅读器中预览
+
+ 4. 点击"打印报告"可将 PDF 发送到打印机
+
+
+ 文件名模式(App.config 配置):
+
+ {ReportId} {ProductName} {ProductCode} {WorkpieceSN}
+
+ {CncProgram} {DeviceId} {MachineId} {Date} {Time} {Result}
+
+
+ 重复文件处理:
+ AutoIncrementOnDuplicate=true 时自动累加 (1)(2)...
+
+
+
+
+
+
+
+
+
diff --git a/XP.ReportEngine/Views/ReportDemoWindow.xaml.cs b/XP.ReportEngine/Views/ReportDemoWindow.xaml.cs
new file mode 100644
index 0000000..ed60a62
--- /dev/null
+++ b/XP.ReportEngine/Views/ReportDemoWindow.xaml.cs
@@ -0,0 +1,21 @@
+using System.Windows;
+using XP.ReportEngine.ViewModels;
+
+namespace XP.ReportEngine.Views
+{
+ ///
+ /// 报告生成演示窗口 | Report generation demo window
+ ///
+ public partial class ReportDemoWindow : Window
+ {
+ ///
+ /// 构造函数(通过 DI 注入 ViewModel)| Constructor (ViewModel injected via DI)
+ ///
+ /// 报告演示 ViewModel | Report demo ViewModel
+ public ReportDemoWindow(ReportDemoViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ }
+ }
+}
diff --git a/XP.ReportEngine/Views/ViewA.xaml b/XP.ReportEngine/Views/ViewA.xaml
deleted file mode 100644
index 124944c..0000000
--- a/XP.ReportEngine/Views/ViewA.xaml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
diff --git a/XP.ReportEngine/Views/ViewA.xaml.cs b/XP.ReportEngine/Views/ViewA.xaml.cs
deleted file mode 100644
index d31ba7a..0000000
--- a/XP.ReportEngine/Views/ViewA.xaml.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
-
-namespace XP.ReportEngine.Views
-{
- ///
- /// Interaction logic for ViewA.xaml
- ///
- public partial class ViewA : UserControl
- {
- public ViewA()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/XP.ReportEngine/XP.ReportEngine.csproj b/XP.ReportEngine/XP.ReportEngine.csproj
index 4e322d0..540ffef 100644
--- a/XP.ReportEngine/XP.ReportEngine.csproj
+++ b/XP.ReportEngine/XP.ReportEngine.csproj
@@ -16,12 +16,18 @@
-
-
+
+
+
diff --git a/XplorePlane.sln b/XplorePlane.sln
index 326513c..22c932e 100644
--- a/XplorePlane.sln
+++ b/XplorePlane.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.14.37203.1 d17.14
+VisualStudioVersion = 17.14.37203.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
EndProject
diff --git a/XplorePlane/App.config b/XplorePlane/App.config
index 70214f4..cedf1d3 100644
--- a/XplorePlane/App.config
+++ b/XplorePlane/App.config
@@ -8,7 +8,9 @@
-
+
+
+
@@ -123,7 +125,6 @@
-
@@ -162,6 +163,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs
index a0cf2c2..01a0955 100644
--- a/XplorePlane/App.xaml.cs
+++ b/XplorePlane/App.xaml.cs
@@ -31,6 +31,7 @@ using XP.Hardware.MotionControl.Module;
using XP.Hardware.PLC;
using XP.Hardware.RaySource.Module;
using XP.Hardware.RaySource.Services;
+using XP.ReportEngine;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Camera;
@@ -444,6 +445,7 @@ namespace XplorePlane
moduleCatalog.AddModule();
moduleCatalog.AddModule();
moduleCatalog.AddModule();
+ moduleCatalog.AddModule();
base.ConfigureModuleCatalog(moduleCatalog);
}
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index a61340f..aa4ef94 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -101,6 +101,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand OpenMotionDebugCommand { get; }
public DelegateCommand OpenPlcAddrConfigCommand { get; }
public DelegateCommand OpenRaySourceConfigCommand { get; }
+ public DelegateCommand OpenReportConfigCommand { get; }
public DelegateCommand WarmUpCommand { get; }
// 测量命令
@@ -195,6 +196,7 @@ namespace XplorePlane.ViewModels
private Window _settingsWindow;
private Window _toolboxWindow;
private Window _raySourceConfigWindow;
+ private Window _reportConfigWindow;
private object _imagePanelContent;
private GridLength _viewportPanelWidth = new(1, GridUnitType.Star);
private GridLength _imagePanelWidth = new(320);
@@ -308,6 +310,7 @@ namespace XplorePlane.ViewModels
OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug);
OpenPlcAddrConfigCommand = new DelegateCommand(ExecuteOpenPlcAddrConfig);
OpenRaySourceConfigCommand = new DelegateCommand(ExecuteOpenRaySourceConfig);
+ OpenReportConfigCommand = new DelegateCommand(ExecuteOpenReportConfig);
WarmUpCommand = new DelegateCommand(ExecuteWarmUp);
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
@@ -399,6 +402,9 @@ namespace XplorePlane.ViewModels
try
{
var manualPath = ConfigurationManager.AppSettings["UserManual"];
+ manualPath = Path.IsPathRooted(manualPath)
+ ? manualPath
+ : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, manualPath);
if (string.IsNullOrEmpty(manualPath))
{
_logger.Warn("User manual path is not configured.");
@@ -679,6 +685,16 @@ namespace XplorePlane.ViewModels
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
}
+ private void ExecuteOpenReportConfig()
+ {
+ ShowOrActivate(_reportConfigWindow, w => _reportConfigWindow = w,
+ () =>
+ {
+ var viewModel = _containerProvider.Resolve();
+ return new XP.ReportEngine.Views.ReportDemoWindow(viewModel);
+ }, "报告配置");
+ }
+
private void ExecuteLoadImage()
{
var dialog = new OpenFileDialog
diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml
index 7f8e53f..abf7f47 100644
--- a/XplorePlane/Views/Main/MainWindow.xaml
+++ b/XplorePlane/Views/Main/MainWindow.xaml
@@ -472,6 +472,13 @@
Size="Medium"
SmallImage="/Assets/Icons/tools.png"
Text="PLC 地址" />
+
diff --git a/XplorePlane/XplorePlane.csproj b/XplorePlane/XplorePlane.csproj
index ce47455..0f725dd 100644
--- a/XplorePlane/XplorePlane.csproj
+++ b/XplorePlane/XplorePlane.csproj
@@ -181,6 +181,7 @@
+