diff --git a/ViewModels/D7896ViewModel.cs b/ViewModels/D7896ViewModel.cs index 9900778..fb5451c 100644 --- a/ViewModels/D7896ViewModel.cs +++ b/ViewModels/D7896ViewModel.cs @@ -439,7 +439,7 @@ public partial class D7896ViewModel : ObservableObject // 测量间隔(即使舍弃也等待,让样品恢复) if (validCount < requiredCount && !_stopRequested && attemptCount < maxAttempts) { - try { await Task.Delay(_config.TestParameters.IntervalSeconds * 1000, _testCts.Token); } catch (OperationCanceledException) { break; } + try { await Task.Delay(_config.TestParameters.IntervalSeconds * 100, _testCts.Token); } catch (OperationCanceledException) { break; } } } @@ -667,34 +667,6 @@ public partial class D7896ViewModel : ObservableObject return timeArray.Length - 1; } - private void ExportMeasurementCsv(string path, double[] time, double[] ustd, double[] upt, double[] deltaT, int fitStart, int fitEnd) - { - using var sw = new StreamWriter(path, false, Encoding.UTF8); - sw.WriteLine("index,time,U_std,U_pt,deltaT,fitWindow"); - int n = Math.Min(Math.Min(time.Length, ustd.Length), deltaT.Length); - for (int i = 0; i < n; i++) - { - bool inFit = (i >= fitStart && i <= fitEnd); - sw.WriteLine($"{i},{time[i]:F6},{ustd[i]:F6},{upt[i]:F6},{(double.IsNaN(deltaT[i]) ? 0 : deltaT[i]):F6},{(inFit ? 1 : 0)}"); - } - } - - private void ExportCandidateWindowsCsv(string path, double[] time, double[] deltaT, int startIdx, int endIdx) - { - using var sw = new StreamWriter(path, false, Encoding.UTF8); - sw.WriteLine("s,e,t_start,t_end,meanDelta"); - int n = time.Length; - int minPts = 10; - for (int s = startIdx; s <= endIdx - minPts; s++) - { - for (int e = s + minPts - 1; e <= endIdx; e++) - { - double mean = deltaT.Skip(s).Take(e - s + 1).Where(x => !double.IsNaN(x)).DefaultIfEmpty(0).Average(); - sw.WriteLine($"{s},{e},{time[s]:F6},{time[e]:F6},{mean:F6}"); - } - } - } - ///// ///// 最小二乘法拟合斜率 (X轴为横坐标,Y轴为纵坐标) — 用于加热段 ln(t) vs ΔT ///// @@ -809,40 +781,6 @@ public partial class D7896ViewModel : ObservableObject TemperatureCurveModel = null; } - //[RelayCommand] - //private async Task GenerateReportAsync() - //{ - // if (Measurements.Count == 0) - // { - // MessageBox.Show("没有测试数据", "提示"); - // return; - // } - // try - // { - // var extraParams = new Dictionary - // { - // ["SampleVolume"] = SampleVolume, - // ["BubbleRemoved"] = BubbleRemoved, - // ["UsePressure"] = UsePressure, - // ["PressureValue"] = PressureValue, - // ["IsCleanConfirmed"] = IsCleanConfirmed, - // ["CleanerName"] = CleanerName, - // ["AmbientTemperature"] = AmbientTemperature, - // ["AmbientCalibrated"] = AmbientCalibrated, - // ["PlatinumCompatible"] = PlatinumCompatible, - // ["LiquidReactivityNote"] = LiquidReactivityNote, - // ["InitialResistance"] = PlatinumResistance - // }; - // string reportPath = await _reportService.GenerateReportAsync(SampleId, TestTemperature, Measurements.ToList(), - // AverageThermalConductivity, AverageThermalDiffusivity, AverageVolumetricHeatCapacity, - // _config.TestParameters, extraParams); - // MessageBox.Show($"报告已生成: {reportPath}", "成功"); - // } - // catch (Exception ex) - // { - // MessageBox.Show($"生成报告失败: {ex.Message}", "错误"); - // } - //} [RelayCommand] private async Task GenerateReportAsync() { @@ -866,8 +804,23 @@ public partial class D7896ViewModel : ObservableObject try { - // 生成 PDF - await Task.Run(() => GeneratePdfReport(pdfPath)); + // 先在 UI 线程导出曲线图为字节数组 + byte[] chartImageBytes = null; + await Application.Current.Dispatcher.InvokeAsync(() => + { + if (TemperatureCurveModel != null && TemperatureCurveModel.Series.Count > 0) + { + using (var stream = new MemoryStream()) + { + var exporter = new PngExporter { Width = 600, Height = 400 }; + exporter.Export(TemperatureCurveModel, stream); + chartImageBytes = stream.ToArray(); + } + } + }); + + // 然后在后台线程生成 PDF(避免阻塞 UI) + await Task.Run(() => GeneratePdfReport(pdfPath, chartImageBytes)); MessageBox.Show($"报表已生成: {pdfPath}", "成功"); } catch (Exception ex) @@ -879,103 +832,134 @@ public partial class D7896ViewModel : ObservableObject - private void GeneratePdfReport(string filePath) + private void GeneratePdfReport(string filePath, byte[] chartImageBytes) { - // 1. 创建文档 using (var document = new PdfDocument()) { - // 2. 添加页面 + // 创建第一个页面 var page = document.AddPage(); page.Width = XUnit.FromMillimeter(210); page.Height = XUnit.FromMillimeter(297); + var gfx = XGraphics.FromPdfPage(page); - // 3. 开始绘制 - using (var gfx = XGraphics.FromPdfPage(page)) + var titleFont = new XFont("SimHei", 16, XFontStyle.Bold); + var headerFont = new XFont("SimHei", 12, XFontStyle.Bold); + var normalFont = new XFont("SimHei", 10, XFontStyle.Regular); + double yPosition = 30; + + // 标题 + gfx.DrawString("ASTM D7896-19 瞬态热线法测试报告", titleFont, XBrushes.Black, + new XRect(0, yPosition, page.Width, 30), XStringFormats.TopCenter); + yPosition += 40; + + // 基础信息(只显示一次) + gfx.DrawString($"样品名称: {SampleId}", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"测试温度: {TestTemperature:F1} °C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"测试时间: {TestDateTime}", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"样品体积: {SampleVolume:F1} mL", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"样品密度: {SampleDensity:F0} kg/m^3", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"环境温度: {AmbientTemperature:F1} °C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"铂丝电阻温度系数: {AlphaPt:F4} /°C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"铂丝长度: {_config.TestParameters.PlatinumWireLength:F3} m", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; + gfx.DrawString($"铂丝直径: {_config.TestParameters.PlatinumWireDiameter * 1000:F2} mm", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 35; + + // 曲线图(如果有) + if (chartImageBytes != null) { - // 创建字体 - var titleFont = new XFont("Verdana", 16, XFontStyle.Bold); - var headerFont = new XFont("Verdana", 12, XFontStyle.Bold); - var normalFont = new XFont("Verdana", 10, XFontStyle.Regular); - double yPosition = 30; - - // ---------- 4. 添加标题 ---------- - gfx.DrawString("ASTM D7896-19 瞬态热线法测试报告", titleFont, XBrushes.Black, - new XRect(0, yPosition, page.Width, 30), XStringFormats.TopCenter); - yPosition += 40; - - // ---------- 5. 添加基础信息 ---------- - gfx.DrawString($"样品名称: {SampleId}", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 25; - gfx.DrawString($"测试温度: {TestTemperature:F1} °C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 25; - // ... 所有需要显示的基础信息 - - yPosition += 10; - - // ---------- 6. 插入温升曲线图 ---------- - if (TemperatureCurveModel != null) + using (var imgStream = new MemoryStream(chartImageBytes)) { - using (var stream = new MemoryStream()) - { - var exporter = new PngExporter { Width = 600, Height = 400 }; - exporter.Export(TemperatureCurveModel, stream); - stream.Position = 0; - var imgStream = new MemoryStream(stream.ToArray(), 0, (int)stream.Length, false, true); - var image = XImage.FromStream(() => imgStream); - gfx.DrawImage(image, 40, yPosition, 500, 330); - yPosition += 350; - } + var image = XImage.FromStream(() => new MemoryStream(imgStream.ToArray())); + gfx.DrawImage(image, 40, yPosition, 500, 330); + yPosition += 350; } + } - // ---------- 7. 创建表格 ---------- - gfx.DrawString("测量结果明细", headerFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 25; + // 表格标题 + gfx.DrawString("测量结果明细", headerFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 25; - // 表头 - string[] headers = { "序号", "热导率λ(W/(m·K))", "热扩散率α(10⁻⁷ m²/s)", "体积热容VHC(kJ/(m³·K))", "比热容Cp(J/(kg·K))" }; - double[] colWidths = { 50, 100, 100, 120, 100 }; - double startX = 40; - double currentRowY = yPosition; + // 表头(已替换特殊符号) + string[] headers = { "序号", "热导率λ(W/(m.K))", "热扩散率α(10^-7 m^2/s)", "体积热容VHC(kJ/(m^3.K))", "比热容Cp(J/(kg.K))" }; + double[] colWidths = { 50, 115, 125, 125, 125 }; + double startX = 40; - // 绘制表头 + // 辅助函数:绘制表头(用于新页) + void DrawHeader(XGraphics g, double y) + { for (int i = 0; i < headers.Length; i++) { double cellX = startX + colWidths.Take(i).Sum(); - gfx.DrawRectangle(XPens.Black, cellX, currentRowY, colWidths[i], 20); - var textRect = new XRect(cellX, currentRowY, colWidths[i], 20); - gfx.DrawString(headers[i], normalFont, XBrushes.Black, textRect, XStringFormats.Center); + g.DrawRectangle(XPens.Black, cellX, y, colWidths[i], 20); + var textRect = new XRect(cellX, y, colWidths[i], 20); + g.DrawString(headers[i], normalFont, XBrushes.Black, textRect, XStringFormats.Center); } - currentRowY += 20; - - // 填充数据行 (假设 Measurements 是列表) - for (int i = 0; i < Measurements.Count; i++) - { - var m = Measurements[i]; - string[] rowData = { - m.Index.ToString(), - m.ThermalConductivity.ToString("F6"), - (m.ThermalDiffusivity * 1e7).ToString("F3"), - m.VolumetricHeatCapacity.ToString("F2"), - m.SpecificHeatCapacity.ToString("F2") - }; - - for (int j = 0; j < rowData.Length; j++) - { - double cellX = startX + colWidths.Take(j).Sum(); - gfx.DrawRectangle(XPens.Black, cellX, currentRowY, colWidths[j], 20); - var textRect = new XRect(cellX, currentRowY, colWidths[j], 20); - gfx.DrawString(rowData[j], normalFont, XBrushes.Black, textRect, XStringFormats.Center); - } - currentRowY += 20; - } - yPosition = currentRowY + 10; - - // ---------- 8. 添加平均值 ---------- - gfx.DrawString($"平均热导率: {AverageThermalConductivity:F6} W/(m·K)", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 20; - // ... 其他平均值 - - // ---------- 9. 添加生成时间和页脚 ---------- - gfx.DrawString($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}", normalFont, XBrushes.Black, new XPoint(40, page.Height - 30)); } - // 10. 保存文档 + // 绘制第一页的表头 + DrawHeader(gfx, yPosition); + double currentRowY = yPosition + 20; // 表头高度20 + const double rowHeight = 20; + const double bottomMargin = 50; // 页面底部留白 + + // 遍历所有测量数据 + foreach (var m in Measurements) + { + // 检查是否需要换页 + if (currentRowY + rowHeight > page.Height - bottomMargin) + { + gfx.Dispose(); + page = document.AddPage(); + page.Width = XUnit.FromMillimeter(210); + page.Height = XUnit.FromMillimeter(297); + gfx = XGraphics.FromPdfPage(page); + gfx.DrawString("ASTM D7896-19 瞬态热线法测试报告(续)", headerFont, XBrushes.Black, + new XRect(0, 30, page.Width, 30), XStringFormats.TopCenter); + currentRowY = 80; + DrawHeader(gfx, currentRowY - 20); + } + + string[] rowData = { + m.Index.ToString(), + m.ThermalConductivity.ToString("F6"), + (m.ThermalDiffusivity * 1e7).ToString("F3"), + m.VolumetricHeatCapacity.ToString("F2"), + m.SpecificHeatCapacity.ToString("F2") + }; + for (int j = 0; j < rowData.Length; j++) + { + double cellX = startX + colWidths.Take(j).Sum(); + gfx.DrawRectangle(XPens.Black, cellX, currentRowY, colWidths[j], rowHeight); + var textRect = new XRect(cellX, currentRowY, colWidths[j], rowHeight); + gfx.DrawString(rowData[j], normalFont, XBrushes.Black, textRect, XStringFormats.Center); + } + currentRowY += rowHeight; + } + + // 添加空行,避免平均值与表格重叠 + currentRowY += 60; + + // 检查是否需要换页(平均值部分) + if (currentRowY + 80 > page.Height - bottomMargin) + { + gfx.Dispose(); + page = document.AddPage(); + page.Width = XUnit.FromMillimeter(210); + page.Height = XUnit.FromMillimeter(297); + gfx = XGraphics.FromPdfPage(page); + currentRowY = 50; + } + + // 平均值(单位已替换) + gfx.DrawString($"平均热导率: {AverageThermalConductivity:F6} W/(m.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; + gfx.DrawString($"平均热扩散率: {AverageThermalDiffusivity:E6} m^2/s", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; + gfx.DrawString($"平均体积热容: {AverageVolumetricHeatCapacity:F2} kJ/(m^3.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; + // 比热容转换:VHC 单位 kJ/(m^3.K) 先转为 J/(m^3.K) 再除以密度得到 J/(kg.K) + double avgCp_J_per_kgK = (AverageVolumetricHeatCapacity * 1000) / SampleDensity; + gfx.DrawString($"平均比热容: {avgCp_J_per_kgK:F0} J/(kg.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 40; + + // 页脚 + gfx.DrawString($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}", normalFont, XBrushes.Black, new XPoint(40, page.Height - 30)); + gfx.Dispose(); + document.Save(filePath); } }