Files
XplorePlane/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs
T

619 lines
24 KiB
C#

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
/// 演示如何使用 IReportService 生成 PDF 报告
/// Demonstrates how to use IReportService to generate PDF reports
/// </summary>
public class ReportDemoViewModel : BindableBase
{
private readonly IReportService _reportService;
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(
IReportService reportService,
IPdfViewerService pdfViewerService,
IPdfPrintService pdfPrintService,
ILoggerService logger,
ReportIdGenerator reportIdGenerator,
ReportConfig reportConfig)
{
_reportService = reportService ?? throw new ArgumentNullException(nameof(reportService));
_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(
$"是否将报告输出到默认位置?\r\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: Build report request
progressWindow.UpdateProgress("正在组装报告数据...", 30);
await Task.Delay(200);
var request = new ReportRequest
{
ProcessorOutputs = processorOutputs,
Metadata = new ReportMetadata
{
ReportId = fileNameParams["ReportId"],
InspectionDate = DateTime.Now,
SampleName = ProductName,
OperatorName = OperatorName,
Description = Description
},
OutputFilePath = outputPath,
FileNameParameters = fileNameParams
};
// 注入工件整体图片 | Inject workpiece overview image
var workpieceImagePath = System.IO.Path.Combine(MockImageDirectory, "OverView.png");
if (File.Exists(workpieceImagePath))
{
request.AdditionalImages["workpieceImage"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = workpieceImagePath
};
}
// 步骤 3:调用报告服务生成(在后台线程执行,避免阻塞 UI)
// Step 3: Call report service (on background thread to avoid blocking UI)
progressWindow.UpdateProgress("正在生成 PDF...", 60);
await Task.Delay(200);
var genResult = await Task.Run(() => _reportService.GenerateAsync(request));
// 步骤 4:处理结果 | Step 4: Handle result
progressWindow.UpdateProgress("正在完成...", 95);
await Task.Delay(200);
if (genResult.IsSuccess)
{
_lastOutputPath = genResult.OutputFilePath;
StatusMessage = $"报告生成成功:{genResult.OutputFilePath}";
_logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", genResult.OutputFilePath);
progressWindow.UpdateProgress("报告生成完成!", 100);
await Task.Delay(500);
// 根据配置自动打开 PDF 阅读器 | Auto-open PDF viewer based on config
if (_reportConfig.AutoOpenAfterGenerate)
{
try
{
_pdfViewerService.OpenViewer(genResult.OutputFilePath);
}
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()
{
// 生成 64 组 BGA 球模拟数据 | Generate 64 BGA ball mock data
var bgaBalls = new List<Dictionary<string, object>>();
var random = new Random(42); // 固定种子确保可重复 | Fixed seed for reproducibility
var voidLimit = 0.05;
for (int i = 1; i <= 64; i++)
{
// 大部分合格,少数不合格 | Most pass, a few fail
double voidRate;
if (i == 12 || i == 27 || i == 41 || i == 58)
{
// 这几个球不合格 | These balls fail
voidRate = Math.Round(0.05 + random.NextDouble() * 0.03, 4);
}
else
{
voidRate = Math.Round(random.NextDouble() * 0.045, 4);
}
var area = Math.Round(190.0 + random.NextDouble() * 15.0, 1);
var classification = voidRate > voidLimit ? "Fail" : "Pass";
bgaBalls.Add(new Dictionary<string, object>
{
["Index"] = i,
["VoidRate"] = voidRate,
["Area"] = area,
["Classification"] = classification
});
}
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"] = bgaBalls
},
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
};
}
#endregion
}
}