修复字体文件在Terelik中无法pdf正确加载的问题,优化报告模板,新增丰富的测试i数据和图片,优化报告输出流程,根据优化修改文档和移除文件。

This commit is contained in:
QI Mingxuan
2026-05-12 21:18:23 +08:00
parent e20201c206
commit 6d4a662823
30 changed files with 1566 additions and 227 deletions
+207
View File
@@ -0,0 +1,207 @@
using System;
using System.Configuration;
using System.IO;
using XP.Common.Logging.Interfaces;
namespace XP.ReportEngine.Configs
{
/// <summary>
/// 报告引擎配置加载器(读取 App.config| Report engine configuration loader (reads from App.config)
/// </summary>
public class ConfigLoader
{
private readonly ILoggerService _logger;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="logger">日志服务 | Logger service</param>
public ConfigLoader(ILoggerService logger)
{
_logger = logger.ForModule<ConfigLoader>();
}
/// <summary>
/// 从 App.config 加载报告引擎配置 | Load report engine configuration from App.config
/// </summary>
/// <param name="prefix">配置前缀,默认为 "Report" | Configuration prefix, default is "Report"</param>
/// <returns>报告配置对象 | Report configuration object</returns>
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();
}
}
/// <summary>
/// 验证配置参数 | Validate configuration parameters
/// </summary>
/// <param name="config">报告配置对象 | Report configuration object</param>
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);
}
}
}
/// <summary>
/// 将边距值限制在有效范围内 | Clamp margin value to valid range
/// </summary>
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;
}
}
}
+187
View File
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace XP.ReportEngine.Configs
{
/// <summary>
/// 报告引擎配置模型 | Report engine configuration model
/// </summary>
public class ReportConfig
{
/// <summary>
/// 报告输出文件夹路径 | Report output directory path
/// </summary>
public string OutputDirectory { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"XplorePlane", "Reports");
/// <summary>
/// 报告模板文件路径(相对或绝对)| Report template file path (relative or absolute)
/// </summary>
public string TemplatePath { get; set; } = @"Templates\StandardReportTemplate.json";
/// <summary>
/// 输出文件名模式,支持占位符 | 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
/// </summary>
public string FileNamePattern { get; set; } = "{ReportId}";
/// <summary>
/// 文件名重复时是否自动累加序号 | Whether to auto-increment suffix when file name duplicates
/// true: 重复时生成 filename(1).pdf, filename(2).pdf ...
/// false: 直接覆盖同名文件
/// </summary>
public bool AutoIncrementOnDuplicate { get; set; } = true;
/// <summary>
/// 生成后是否自动打开 PDF 阅读器 | Whether to auto-open PDF viewer after generation
/// </summary>
public bool AutoOpenAfterGenerate { get; set; } = false;
/// <summary>
/// 默认页面尺寸 | Default page size
/// </summary>
public string DefaultPageSize { get; set; } = "A4";
/// <summary>
/// 默认页面方向(Portrait / Landscape| Default page orientation
/// </summary>
public string DefaultOrientation { get; set; } = "Portrait";
/// <summary>
/// 默认上边距(mm| Default top margin (mm)
/// </summary>
public float MarginTop { get; set; } = 20f;
/// <summary>
/// 默认下边距(mm| Default bottom margin (mm)
/// </summary>
public float MarginBottom { get; set; } = 20f;
/// <summary>
/// 默认左边距(mm| Default left margin (mm)
/// </summary>
public float MarginLeft { get; set; } = 20f;
/// <summary>
/// 默认右边距(mm| Default right margin (mm)
/// </summary>
public float MarginRight { get; set; } = 20f;
/// <summary>
/// 报告中显示的公司名称 | Company name displayed in report
/// </summary>
public string CompanyName { get; set; } = "海克斯康制造智能技术(青岛)有限公司";
/// <summary>
/// 公司 Logo 图片路径(可选,为空则不显示)| Company logo image path (optional, empty means no logo)
/// </summary>
public string CompanyLogo { get; set; } = string.Empty;
/// <summary>
/// 获取解析后的模板绝对路径 | Get resolved absolute template path
/// 如果 TemplatePath 是相对路径,则基于应用程序目录解析
/// If TemplatePath is relative, resolves based on application directory
/// </summary>
public string GetResolvedTemplatePath()
{
if (Path.IsPathRooted(TemplatePath))
{
return TemplatePath;
}
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplatePath);
}
/// <summary>
/// 根据文件名模式和上下文参数生成实际文件名 | Generate actual file name based on pattern and context parameters
/// </summary>
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
/// <returns>生成的文件名(不含扩展名)| Generated file name (without extension)</returns>
public string ResolveFileName(Dictionary<string, string> 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;
}
/// <summary>
/// 解析最终输出文件完整路径(含重复累加逻辑)| Resolve final output file full path (with duplicate increment logic)
/// </summary>
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
/// <param name="extension">文件扩展名(含点号,如 ".pdf"| File extension (with dot, e.g. ".pdf")</param>
/// <returns>最终输出文件完整路径 | Final output file full path</returns>
public string ResolveOutputFilePath(Dictionary<string, string> 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;
}
/// <summary>
/// 清理文件名中的非法字符 | Sanitize illegal characters in file name
/// </summary>
private static string SanitizeFileName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
foreach (var c in invalidChars)
{
name = name.Replace(c, '_');
}
return name;
}
}
}
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- ============================================================ -->
<!-- 报告引擎配置 | Report Engine Configuration -->
<!-- 前缀: Report -->
<!-- ============================================================ -->
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
<!-- 默认值: {我的文档}\XplorePlane\Reports | Default: {MyDocuments}\XplorePlane\Reports -->
<add key="Report:OutputDirectory" value="D:\Reports\XplorePlane" />
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
<!-- 默认值: Templates\StandardReportTemplate.json -->
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
<!--
输出文件名模式 | File name pattern
支持以下占位符 | Supported placeholders:
{ReportId} - 报告编号(如 RPT-20250512-001| Report ID (e.g. RPT-20250512-001)
{CncProgram} - CNC 程序名称 | CNC program name
{ProductName} - 产品名称 | Product name
{ProductCode} - 产品类型码 | Product type code
{WorkpieceSN} - 工件 SN 码 | Workpiece serial number
{DeviceId} - 检测设备编号(本机)| Inspection device ID (local machine)
{MachineId} - 生产机台号 | Production machine ID
{Date} - 日期(yyyyMMdd| Date (yyyyMMdd)
{Time} - 时间(HHmmss| Time (HHmmss)
{Result} - 综合检测结论(Pass/Fail| Overall inspection result (Pass/Fail)
示例 | Examples:
"{ReportId}" → RPT-20250512-001.pdf
"{Date}_{ProductName}_{WorkpieceSN}" → 20250512_PCB-A01_SN20250001.pdf
"{ProductCode}_{WorkpieceSN}_{Result}" → PCBA-X100_SN20250001_Pass.pdf
"{MachineId}_{CncProgram}_{Date}_{Time}" → MC01_Prog001_20250512_143025.pdf
"{DeviceId}_{Date}_{ReportId}" → XP-CT-001_20250512_RPT-20250512-001.pdf
-->
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
<!--
文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates
true: 重复时生成 filename(1).pdf, filename(2).pdf ... 不覆盖已有文件
false: 直接覆盖同名文件
-->
<add key="Report:AutoIncrementOnDuplicate" value="true" />
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
<add key="Report:AutoOpenAfterGenerate" value="false" />
<!-- ============================================================ -->
<!-- 页面布局配置 | Page Layout Configuration -->
<!-- ============================================================ -->
<!-- 默认页面尺寸 | Default page size (A4) -->
<add key="Report:DefaultPageSize" value="A4" />
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
<add key="Report:DefaultOrientation" value="Portrait" />
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
<add key="Report:MarginTop" value="20" />
<add key="Report:MarginBottom" value="20" />
<add key="Report:MarginLeft" value="20" />
<add key="Report:MarginRight" value="20" />
<!-- ============================================================ -->
<!-- 公司信息配置 | Company Information Configuration -->
<!-- ============================================================ -->
<!-- 报告中显示的公司名称 | Company name displayed in report -->
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
<!-- 支持相对路径(基于应用程序目录)或绝对路径 | Supports relative (based on app dir) or absolute path -->
<add key="Report:CompanyLogo" value="Resources\company_logo.png" />
</appSettings>
</configuration>
+25 -45
View File
@@ -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
<ItemGroup>
<EmbeddedResource Include="Fonts\*.otf" />
<EmbeddedResource Include="Fonts\*.ttf" />
</ItemGroup>
```
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()` 中扩展加载逻辑。
+3 -5
View File
@@ -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 容器调用
-1
View File
@@ -1 +0,0 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
+19
View File
@@ -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
/// </summary>
public void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册配置加载器(瞬态)| Register config loader (transient)
containerRegistry.Register<Configs.ConfigLoader>();
// 加载并注册配置为单例 | Load and register config as singleton
containerRegistry.RegisterSingleton<ReportConfig>(container =>
{
var logger = container.Resolve<ILoggerService>();
var loader = new Configs.ConfigLoader(logger);
return loader.LoadReportConfig();
});
// 注册报告生成器(瞬态)| Register report generator (transient)
containerRegistry.Register<IReportGenerator, PdfReportGenerator>();
@@ -65,6 +80,10 @@ namespace XP.ReportEngine
// 注册报告编号生成器(单例,维护每日计数器状态)| Register report ID generator (singleton, maintains daily counter state)
containerRegistry.RegisterSingleton<ReportIdGenerator>();
// 注册演示窗口 ViewModel 和 View | Register demo window ViewModel and View
containerRegistry.Register<ReportDemoViewModel>();
containerRegistry.Register<ReportDemoWindow>();
System.Console.WriteLine("[ReportEngineModule] 类型注册完成 | Type registration completed");
}
}
-1
View File
@@ -1 +0,0 @@
@@ -61,10 +61,46 @@ namespace XP.ReportEngine.Services
private void BindElement(TemplateElement element, ReportContext context)
{
if (string.IsNullOrEmpty(element.Content)) return;
// 文本内容绑定 | 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<Dictionary<string, object>> 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)
{
return ExpressionPattern.Replace(input, match =>
+69 -74
View File
@@ -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
/// </summary>
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<ITextPdfRenderer>() ?? 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,18 +86,16 @@ 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);
using (var pdfDocument = new PdfDocument(writer))
{
var pdfDocument = new PdfDocument(writer);
// 设置 A4 页面尺寸 | Set A4 page size
pdfDocument.SetDefaultPageSize(PageSize.A4);
using (var document = new Document(pdfDocument))
{
var document = new Document(pdfDocument);
// 设置默认边距(使用默认 20mm| Set default margins (20mm default)
float marginTop = 20f * MmToPoints;
float marginBottom = 20f * MmToPoints;
@@ -119,9 +117,10 @@ namespace XP.ReportEngine.Services
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;
@@ -252,97 +251,94 @@ namespace XP.ReportEngine.Services
#region 7.3 | Font management
/// <summary>
/// 初始化字体(加载嵌入资源字体)| Initialize fonts (load embedded resource fonts)
/// 确保字体已初始化(线程安全的延迟加载)| Ensure fonts are initialized (thread-safe lazy loading)
/// </summary>
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;
}
}
/// <summary>
/// 从嵌入资源加载字体 | 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
/// </summary>
/// <param name="resourceName">嵌入资源名称 | Embedded resource name</param>
/// <returns>PDF 字体对象 | PDF font object</returns>
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)
{
throw new FileNotFoundException($"嵌入资源未找到 | Embedded resource not found: {resourceName}");
_logger.Warn("微软雅黑加载失败,尝试后备字体 | Microsoft YaHei load failed, trying fallback: {Message}", ex.Message);
try
{
// 后备:宋体 | 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");
}
}
/// <summary>
/// 根据当前语言获取合适的字体 | Get appropriate font based on current language
/// zh-CN → 简体中文字体;zh-TW → 繁体中文字体en-US → 西文字体
/// zh-CN / zh-TW → 微软雅黑(支持简繁体)en-US → Arial
/// </summary>
/// <returns>PDF 字体 | PDF font</returns>
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
@@ -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);
}
/// <summary>
/// 根据处理器类型获取模板中对应的图像 dataKey | Get template image dataKey by processor type
/// </summary>
private static string GetImageDataKey(string processorType)
{
return processorType switch
{
LineMeasurementProcessor => "lineMeasurementImage",
BgaVoidRateProcessor => "bgaInspectionImage",
VoidMeasurementProcessor => "voidInspectionImage",
FillRateProcessor => "viaFillImage",
_ => $"{processorType}_image"
};
}
/// <summary>
/// 根据处理器类型获取模板中对应的表格 dataKey | Get template table dataKey by processor type
/// </summary>
private static string GetTableDataKey(string processorType)
{
return processorType switch
{
BgaVoidRateProcessor => "bgaBallsTable",
VoidMeasurementProcessor => "voidsTable",
_ => null
};
}
#endregion
}
}
-1
View File
@@ -1 +0,0 @@
@@ -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" },
@@ -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
{
/// <summary>
/// 报告生成演示窗口 ViewModel | Report generation demo window ViewModel
/// 演示如何使用 XP.ReportEngine 生成 PDF 报告
/// Demonstrates how to use XP.ReportEngine to generate PDF reports
/// </summary>
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
/// <summary>
/// 产品名称 | Product name
/// </summary>
public string ProductName
{
get => _productName;
set => SetProperty(ref _productName, value);
}
/// <summary>
/// 操作员 | Operator
/// </summary>
public string OperatorName
{
get => _operatorName;
set => SetProperty(ref _operatorName, value);
}
/// <summary>
/// 描述 | Description
/// </summary>
public string Description
{
get => _description;
set => SetProperty(ref _description, value);
}
/// <summary>
/// CNC 程序名称 | CNC program name
/// </summary>
public string CncProgram
{
get => _cncProgram;
set => SetProperty(ref _cncProgram, value);
}
/// <summary>
/// 产品类型码 | Product type code
/// </summary>
public string ProductCode
{
get => _productCode;
set => SetProperty(ref _productCode, value);
}
/// <summary>
/// 工件 SN 码 | Workpiece serial number
/// </summary>
public string WorkpieceSN
{
get => _workpieceSN;
set => SetProperty(ref _workpieceSN, value);
}
/// <summary>
/// 检测设备编号 | Inspection device ID
/// </summary>
public string DeviceId
{
get => _deviceId;
set => SetProperty(ref _deviceId, value);
}
/// <summary>
/// 生产机台号 | Production machine ID
/// </summary>
public string MachineId
{
get => _machineId;
set => SetProperty(ref _machineId, value);
}
/// <summary>
/// 状态信息 | Status message
/// </summary>
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
/// <summary>
/// 是否正在生成 | Whether generating
/// </summary>
public bool IsGenerating
{
get => _isGenerating;
set
{
if (SetProperty(ref _isGenerating, value))
{
GenerateReportCommand.RaiseCanExecuteChanged();
OpenViewerCommand.RaiseCanExecuteChanged();
PrintReportCommand.RaiseCanExecuteChanged();
}
}
}
#endregion
#region | Commands
/// <summary>
/// 生成报告命令 | Generate report command
/// </summary>
public DelegateCommand GenerateReportCommand { get; }
/// <summary>
/// 打开 PDF 阅读器命令 | Open PDF viewer command
/// </summary>
public DelegateCommand OpenViewerCommand { get; }
/// <summary>
/// 打印报告命令 | Print report command
/// </summary>
public DelegateCommand PrintReportCommand { get; }
#endregion
/// <summary>
/// 构造函数 | Constructor
/// </summary>
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<ReportDemoViewModel>() ?? 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));
}
/// <summary>
/// 生成报告(带进度条)| Generate report (with progress window)
/// </summary>
private async Task GenerateReportAsync()
{
IsGenerating = true;
StatusMessage = "正在生成报告...";
try
{
// 构建文件名占位符参数 | Build file name placeholder parameters
var fileNameParams = new Dictionary<string, string>
{
["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();
}
}
/// <summary>
/// 打开 PDF 阅读器 | Open PDF viewer
/// </summary>
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);
}
}
/// <summary>
/// 打印报告 | Print report
/// </summary>
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
/// <summary>
/// 模拟图像目录路径 | Mock image directory path
/// </summary>
private const string MockImageDirectory = @"D:\XplorePlane\DetectorImages";
/// <summary>
/// 创建模拟处理器输出数据(演示用,覆盖所有检测类型)
/// Create mock processor outputs (for demo, covers all inspection types)
/// </summary>
private List<ProcessorOutput> CreateMockProcessorOutputs()
{
return new List<ProcessorOutput>
{
CreateLineMeasurementOutput(),
CreateBgaVoidRateOutput(),
CreateVoidMeasurementOutput(),
CreateFillRateOutput()
};
}
/// <summary>
/// 创建线测量处理器模拟数据 | Create line measurement processor mock data
/// </summary>
private ProcessorOutput CreateLineMeasurementOutput()
{
return new ProcessorOutput
{
ProcessorType = "LineMeasurementProcessor",
OutputData = new Dictionary<string, object>
{
["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")
};
}
/// <summary>
/// 创建 BGA 气泡率处理器模拟数据 | Create BGA void rate processor mock data
/// </summary>
private ProcessorOutput CreateBgaVoidRateOutput()
{
return new ProcessorOutput
{
ProcessorType = "BgaVoidRateProcessor",
OutputData = new Dictionary<string, object>
{
["BgaCount"] = 64,
["VoidRate"] = 0.028,
["FillRate"] = 0.972,
["TotalBgaArea"] = 12500.5,
["TotalVoidArea"] = 350.2,
["Classification"] = "Pass",
["VoidLimit"] = 0.05,
["BgaBalls"] = new List<Dictionary<string, object>>
{
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")
};
}
/// <summary>
/// 创建空隙测量处理器模拟数据 | Create void measurement processor mock data
/// </summary>
private ProcessorOutput CreateVoidMeasurementOutput()
{
return new ProcessorOutput
{
ProcessorType = "VoidMeasurementProcessor",
OutputData = new Dictionary<string, object>
{
["RoiArea"] = 5000.0,
["TotalVoidArea"] = 125.8,
["VoidRate"] = 0.025,
["VoidLimit"] = 0.05,
["VoidCount"] = 5,
["MaxVoidArea"] = 65.2,
["Classification"] = "Pass",
["Voids"] = new List<Dictionary<string, object>>
{
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")
};
}
/// <summary>
/// 创建通孔填锡率处理器模拟数据 | Create via fill rate processor mock data
/// </summary>
private ProcessorOutput CreateFillRateOutput()
{
return new ProcessorOutput
{
ProcessorType = "FillRateProcessor",
OutputData = new Dictionary<string, object>
{
["FillRate"] = 0.85,
["VoidRate"] = 0.15,
["FullDistance"] = 1.60,
["FillDistance"] = 1.36,
["THTLimit"] = 0.75,
["Classification"] = "Pass",
["E1"] = new Dictionary<string, object>
{
["CenterX"] = 256.0, ["CenterY"] = 256.0,
["SemiAxisA"] = 120.5, ["SemiAxisB"] = 118.2, ["Angle"] = 2.3
},
["E2"] = new Dictionary<string, object>
{
["CenterX"] = 256.0, ["CenterY"] = 256.0,
["SemiAxisA"] = 95.8, ["SemiAxisB"] = 93.1, ["Angle"] = 2.3
},
["E3"] = new Dictionary<string, object>
{
["CenterX"] = 256.0, ["CenterY"] = 256.0,
["SemiAxisA"] = 70.2, ["SemiAxisB"] = 68.5, ["Angle"] = 1.8
},
["E4"] = new Dictionary<string, object>
{
["CenterX"] = 256.0, ["CenterY"] = 256.0,
["SemiAxisA"] = 45.0, ["SemiAxisB"] = 43.7, ["Angle"] = 1.5
}
},
AnnotatedImage = LoadMockImage("Void.png")
};
}
/// <summary>
/// 加载模拟图像文件 | Load mock image file
/// 从指定目录加载图像,文件不存在时返回 null
/// Loads image from specified directory, returns null if file not found
/// </summary>
/// <param name="fileName">图像文件名 | Image file name</param>
/// <returns>图像数据对象或 null | ImageData object or null</returns>
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
};
}
/// <summary>
/// 根据 ReportContext 中的结果分组生成首页汇总表数据
/// Generate homepage summary table data from ReportContext result groups
/// </summary>
/// <param name="context">报告上下文 | Report context</param>
/// <returns>汇总表行数据 | Summary table row data</returns>
private List<Dictionary<string, object>> CreateSummaryTableData(ReportContext context)
{
var rows = new List<Dictionary<string, object>>();
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<string, object>
{
["inspectionType"] = inspectionType,
["classification"] = classification,
["status"] = status
});
}
return rows;
}
#endregion
}
}
@@ -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";
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<Window x:Class="XP.ReportEngine.Views.ReportDemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="报告生成演示 | Report Generation Demo"
Width="560" Height="580"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 | Title -->
<TextBlock Grid.Row="0" Text="XP.ReportEngine 报告生成演示"
FontSize="18" FontWeight="Bold" Margin="0,0,0,12"/>
<!-- 输入表单 | Input form -->
<Grid Grid.Row="1" Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
</Grid.RowDefinitions>
<!-- 第一列 | Column 1 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="产品名称:" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ProductName, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="产品类型码:" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ProductCode, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="工件 SN 码:" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding WorkpieceSN, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="CNC 程序:" VerticalAlignment="Center"/>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CncProgram, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="操作员:" VerticalAlignment="Center"/>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<!-- 第二列 | Column 2 -->
<TextBlock Grid.Row="0" Grid.Column="3" Text="设备编号:" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="4" Text="{Binding DeviceId, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="3" Text="生产机台号:" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="4" Text="{Binding MachineId, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="3" Text="描述:" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="4" Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
</Grid>
<!-- 按钮区域 | Button area -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,12">
<Button Content="生成报告" Command="{Binding GenerateReportCommand}"
Width="100" Height="32" Margin="0,0,10,0"/>
<Button Content="查看 PDF" Command="{Binding OpenViewerCommand}"
Width="100" Height="32" Margin="0,0,10,0"/>
<Button Content="打印报告" Command="{Binding PrintReportCommand}"
Width="100" Height="32"/>
</StackPanel>
<!-- 说明区域 | Instructions -->
<Border Grid.Row="3" BorderBrush="#DDDDDD" BorderThickness="1"
Background="#F9F9F9" CornerRadius="4" Padding="10">
<TextBlock TextWrapping="Wrap" FontSize="12" Foreground="#555555">
<Run FontWeight="Bold">使用说明:</Run>
<LineBreak/>
<Run>1. 填写产品信息、设备信息和操作员</Run>
<LineBreak/>
<Run>2. 点击"生成报告",系统将使用模拟检测数据生成 PDF</Run>
<LineBreak/>
<Run>3. 生成完成后,可点击"查看 PDF"在阅读器中预览</Run>
<LineBreak/>
<Run>4. 点击"打印报告"可将 PDF 发送到打印机</Run>
<LineBreak/>
<LineBreak/>
<Run FontWeight="Bold">文件名模式(App.config 配置):</Run>
<LineBreak/>
<Run>{ReportId} {ProductName} {ProductCode} {WorkpieceSN}</Run>
<LineBreak/>
<Run>{CncProgram} {DeviceId} {MachineId} {Date} {Time} {Result}</Run>
<LineBreak/>
<LineBreak/>
<Run FontWeight="Bold">重复文件处理:</Run>
<Run>AutoIncrementOnDuplicate=true 时自动累加 (1)(2)...</Run>
</TextBlock>
</Border>
<!-- 状态栏 | Status bar -->
<Border Grid.Row="4" Background="#F0F0F0" Margin="0,10,0,0" Padding="8,5">
<TextBlock Text="{Binding StatusMessage}" FontSize="11" Foreground="#333333"
TextTrimming="CharacterEllipsis"/>
</Border>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
using XP.ReportEngine.ViewModels;
namespace XP.ReportEngine.Views
{
/// <summary>
/// 报告生成演示窗口 | Report generation demo window
/// </summary>
public partial class ReportDemoWindow : Window
{
/// <summary>
/// 构造函数(通过 DI 注入 ViewModel| Constructor (ViewModel injected via DI)
/// </summary>
/// <param name="viewModel">报告演示 ViewModel | Report demo ViewModel</param>
public ReportDemoWindow(ReportDemoViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}
-16
View File
@@ -1,16 +0,0 @@
<UserControl x:Class="XP.ReportEngine.Views.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XP.ReportEngine.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True" >
<Grid>
<TextBlock Text="{Binding Message}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</UserControl>
-28
View File
@@ -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
{
/// <summary>
/// Interaction logic for ViewA.xaml
/// </summary>
public partial class ViewA : UserControl
{
public ViewA()
{
InitializeComponent();
}
}
}
+8 -2
View File
@@ -16,12 +16,18 @@
<ItemGroup>
<Folder Include="Documents\" />
</ItemGroup>
<!-- 字体文件嵌入资源配置 | Font files embedded resource configuration -->
<!-- 注意:需要手动将实际字体文件添加到 Fonts/ 目录 | Note: Actual font files need to be added manually to Fonts/ directory -->
<!-- 字体目录保留备用,当前使用系统字体 | Fonts directory reserved, currently using system fonts -->
<!-- 如需添加自定义字体,取消以下注释 | Uncomment below to add custom fonts -->
<!--
<ItemGroup>
<None Remove="Fonts\*.otf" />
<None Remove="Fonts\*.ttf" />
<Resource Remove="Fonts\*.ttf" />
<Resource Remove="Fonts\*.otf" />
<EmbeddedResource Include="Fonts\*.otf" />
<EmbeddedResource Include="Fonts\*.ttf" />
</ItemGroup>
-->
<!-- 模板文件复制到输出目录 | Copy template files to output directory -->
<ItemGroup>
<Content Include="Templates\*.json">
+1 -1
View File
@@ -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
+28 -2
View File
@@ -8,7 +8,9 @@
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
<add key="Language" value="ZhCN" />
<add key="XpData:RootPath" value="D:\XPData" />
<add key="UserManual" value="D:\HMQProject\XplorePlane_CT\Code\XplorePlane\XP.App\bin\Debug\net8.0-windows7.0\UserManual.pdf" />
<add key="UserManual" value="UserManual.pdf" />
<add key="DeviceId" value="PlanerCT001" />
<!-- Serilog日志配置 -->
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
@@ -123,7 +125,6 @@
<!-- 自动重连 | Auto Reconnection -->
<add key="Plc:bReConnect" value="true" />
<!-- ==================== 运动控制配置 | Motion Control Configuration ==================== -->
<!-- 直线轴配置(单位:mm| Linear axis config (unit: mm) -->
<add key="MotionControl:SourceZ:Min" value="-500" />
<add key="MotionControl:SourceZ:Max" value="500" />
@@ -162,6 +163,31 @@
<!-- 运行参数 | Runtime parameters -->
<add key="MotionControl:PollingInterval" value="500" />
<add key="MotionControl:DefaultVelocity" value="500" />
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
<!-- 输出文件名模式 | File name pattern -->
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
<add key="Report:AutoIncrementOnDuplicate" value="true" />
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
<add key="Report:AutoOpenAfterGenerate" value="false" />
<!-- 默认页面尺寸 | Default page size (A4) -->
<add key="Report:DefaultPageSize" value="A4" />
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
<add key="Report:DefaultOrientation" value="Portrait" />
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
<add key="Report:MarginTop" value="20" />
<add key="Report:MarginBottom" value="20" />
<add key="Report:MarginLeft" value="20" />
<add key="Report:MarginRight" value="20" />
<!-- 报告中显示的公司名称 | Company name displayed in report -->
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
+2
View File
@@ -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<DetectorModule>();
moduleCatalog.AddModule<RaySourceModule>();
moduleCatalog.AddModule<MotionControlModule>();
moduleCatalog.AddModule<ReportEngineModule>();
base.ConfigureModuleCatalog(moduleCatalog);
}
@@ -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<XP.ReportEngine.ViewModels.ReportDemoViewModel>();
return new XP.ReportEngine.Views.ReportDemoWindow(viewModel);
}, "报告配置");
}
private void ExecuteLoadImage()
{
var dialog = new OpenFileDialog
+7
View File
@@ -472,6 +472,13 @@
Size="Medium"
SmallImage="/Assets/Icons/tools.png"
Text="PLC 地址" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开检测报告配置窗口"
telerik:ScreenTip.Title="报告配置"
Command="{Binding OpenReportConfigCommand}"
Size="Medium"
SmallImage="/Assets/Icons/message.png"
Text="报告配置" />
</StackPanel>
</telerik:RadRibbonGroup>
+1
View File
@@ -181,6 +181,7 @@
<ProjectReference Include="..\XP.ImageProcessing.Processors\XP.ImageProcessing.Processors.csproj" />
<ProjectReference Include="..\XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj" />
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
<ProjectReference Include="..\XP.ReportEngine\XP.ReportEngine.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="xcopy &quot;$(MSBuildProjectDirectory)\..\ExternalLibraries\*.*&quot; &quot;$(TargetDir)&quot; /d /y" />