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