From a766061e77c4ed59ab09affe7cb1e52549806ab8 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Wed, 6 May 2026 16:06:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WindowsFormsApp6/Form1.cs | 328 ++++----- WindowsFormsApp6/Form2.cs | 341 ++++----- WindowsFormsApp6/Form3.cs | 348 ++++++--- WindowsFormsApp6/MainForm.cs | 900 +++++++++++++---------- WindowsFormsApp6/WindowsFormsApp6.csproj | 5 +- WindowsFormsApp6/packages.config | 3 +- 6 files changed, 1025 insertions(+), 900 deletions(-) diff --git a/WindowsFormsApp6/Form1.cs b/WindowsFormsApp6/Form1.cs index 5d599ee..eb62c21 100644 --- a/WindowsFormsApp6/Form1.cs +++ b/WindowsFormsApp6/Form1.cs @@ -321,14 +321,11 @@ namespace WindowsFormsApp6 /// private void RemoveAverageRows() { - var avgRows1 = sampleDataTable.Select($"序号 = '平均时间(s)试样1-5'"); - var avgRows2 = sampleDataTable.Select($"序号 = '平均时间(s)试样6-10'"); - - foreach (var row in avgRows1) - { - sampleDataTable.Rows.Remove(row); - } - foreach (var row in avgRows2) + var avgRows = sampleDataTable.AsEnumerable() + .Where(row => IsAverageTimeRow(row.Field("序号"))) + .ToList(); + + foreach (var row in avgRows) { sampleDataTable.Rows.Remove(row); } @@ -422,14 +419,11 @@ namespace WindowsFormsApp6 private void UpdateAverageRow(double[] averages) { // 移除所有平均值行 - var avgRows1 = sampleDataTable.Select($"序号 = '平均时间(s)试样1-5'"); - var avgRows2 = sampleDataTable.Select($"序号 = '平均时间(s)试样6-10'"); - - foreach (var row in avgRows1) - { - sampleDataTable.Rows.Remove(row); - } - foreach (var row in avgRows2) + var avgRows = sampleDataTable.AsEnumerable() + .Where(row => IsAverageTimeRow(row.Field("序号"))) + .ToList(); + + foreach (var row in avgRows) { sampleDataTable.Rows.Remove(row); } @@ -441,78 +435,61 @@ namespace WindowsFormsApp6 if (!timeRows.Any()) return; - // 计算试样1-5的平均值 - int group1Count = Math.Min(5, currentSampleCount); - if (group1Count > 0) + int groupCount = (int)Math.Ceiling(currentSampleCount / 5.0); + for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { - double sum1 = 0; - int count1 = 0; - + int startSample = groupIndex * 5 + 1; + int endSample = Math.Min(startSample + 4, currentSampleCount); + double sum = 0; + int count = 0; + foreach (var row in timeRows) { - for (int i = 1; i <= group1Count; i++) + for (int i = startSample; i <= endSample; i++) { - sum1 += row.Field($"试样{i}"); - count1++; + if (TryGetDouble(row[$"试样{i}"], out double value)) + { + sum += value; + count++; + } } } - - double avg1 = count1 > 0 ? sum1 / count1 : 0; - - // 添加试样1-5平均值行 - 只在第一列显示平均值 - DataRow avgRow1 = sampleDataTable.NewRow(); - avgRow1["序号"] = "平均时间(s)试样1-5"; - - // 只在试样1列显示平均值 - avgRow1["试样1"] = avg1; - - // 其他列设置为DBNull(不显示) - for (int i = 2; i <= currentSampleCount; i++) + + DataRow avgRow = sampleDataTable.NewRow(); + avgRow["序号"] = $"平均时间(s)试样{startSample}-{endSample}"; + + for (int i = 1; i <= currentSampleCount; i++) { - avgRow1[$"试样{i}"] = DBNull.Value; + avgRow[$"试样{i}"] = i == startSample && count > 0 ? (object)(sum / count) : DBNull.Value; } - - sampleDataTable.Rows.Add(avgRow1); + + sampleDataTable.Rows.Add(avgRow); } - - // 计算试样6-10的平均值(仅当试样数量>5时) - if (currentSampleCount > 5) + } + + private bool IsAverageTimeRow(string rowName) + { + return !string.IsNullOrEmpty(rowName) && rowName.StartsWith("平均时间(s)"); + } + + private bool TryGetDouble(object value, out double result) + { + result = 0; + if (value == null || value == DBNull.Value) { - int group2Count = Math.Min(5, currentSampleCount - 5); - double sum2 = 0; - int count2 = 0; - - foreach (var row in timeRows) - { - for (int i = 6; i <= 5 + group2Count; i++) - { - sum2 += row.Field($"试样{i}"); - count2++; - } - } - - double avg2 = count2 > 0 ? sum2 / count2 : 0; - - // 添加试样6-10平均值行 - 只在第6列显示平均值 - DataRow avgRow2 = sampleDataTable.NewRow(); - avgRow2["序号"] = "平均时间(s)试样6-10"; - - // 前5列为DBNull(不显示) - for (int i = 1; i <= 5; i++) - { - avgRow2[$"试样{i}"] = DBNull.Value; - } - - // 只在试样6列显示平均值 - avgRow2["试样6"] = avg2; - - // 其他列为DBNull(不显示) - for (int i = 7; i <= currentSampleCount; i++) - { - avgRow2[$"试样{i}"] = DBNull.Value; - } - - sampleDataTable.Rows.Add(avgRow2); + return false; + } + + return double.TryParse(value.ToString(), out result) && + !double.IsNaN(result) && + !double.IsInfinity(result); + } + + private IEnumerable<(int Start, int End)> GetSampleGroups(int sampleCount) + { + for (int start = 1; start <= sampleCount; start += 5) + { + yield return (start, Math.Min(start + 4, sampleCount)); } } @@ -595,6 +572,8 @@ namespace WindowsFormsApp6 /// private (ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle avgStyle) CreateExcelStyles(IWorkbook workbook) { + short numberFormat = workbook.CreateDataFormat().GetFormat("0.00"); + // 标题样式 ICellStyle headerStyle = workbook.CreateCellStyle(); headerStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Yellow.Index; @@ -605,11 +584,13 @@ namespace WindowsFormsApp6 // 数据样式 ICellStyle dataStyle = workbook.CreateCellStyle(); + dataStyle.DataFormat = numberFormat; // 平均值样式 ICellStyle avgStyle = workbook.CreateCellStyle(); avgStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Yellow.Index; avgStyle.FillPattern = FillPattern.SolidForeground; + avgStyle.DataFormat = numberFormat; return (headerStyle, dataStyle, avgStyle); } @@ -643,7 +624,7 @@ namespace WindowsFormsApp6 int rowIndex = 1; foreach (DataRow dataRow in sampleDataTable.Rows) { - if (dataRow["序号"].ToString() == AVG_ROW_LABEL) + if (IsAverageTimeRow(dataRow["序号"].ToString())) continue; IRow row = sheet.CreateRow(rowIndex++); @@ -662,81 +643,53 @@ namespace WindowsFormsApp6 private void AddAverageRow(ISheet sheet, ICellStyle avgStyle) { var dataRows = sampleDataTable.AsEnumerable() - .Where(r => r.Field("序号") != "平均时间(s)试样1-5" && - r.Field("序号") != "平均时间(s)试样6-10"); + .Where(r => !IsAverageTimeRow(r.Field("序号"))) + .ToList(); if (!dataRows.Any()) return; int rowIndex = sheet.LastRowNum + 1; - - // 计算试样1-5的平均值 - int group1Count = Math.Min(5, currentSampleCount); - if (group1Count > 0) + + foreach (var group in GetSampleGroups(currentSampleCount)) { - double sum1 = 0; - int count1 = 0; - - foreach (var row in dataRows) + double sum = 0; + int count = 0; + + foreach (var dataRow in dataRows) { - for (int i = 1; i <= group1Count; i++) + for (int i = group.Start; i <= group.End; i++) { - sum1 += row.Field($"试样{i}"); - count1++; + if (TryGetDouble(dataRow[$"试样{i}"], out double value)) + { + sum += value; + count++; + } } } - - double avg1 = count1 > 0 ? sum1 / count1 : 0; - - // 添加试样1-5平均值行 - IRow avgRow1 = sheet.CreateRow(rowIndex++); - ICell labelCell1 = avgRow1.CreateCell(0); - labelCell1.SetCellValue("平均时间(s)试样1-5"); - labelCell1.CellStyle = avgStyle; - - ICell avgCell1 = avgRow1.CreateCell(1); - avgCell1.SetCellValue(avg1); - avgCell1.CellStyle = avgStyle; - - // 合并试样1-5列 - if (group1Count > 1) + + if (count == 0) { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, 1, group1Count)); + continue; } - } - - // 计算试样6-10的平均值(仅当试样数量>5时) - if (currentSampleCount > 5) - { - int group2Count = Math.Min(5, currentSampleCount - 5); - double sum2 = 0; - int count2 = 0; - - foreach (var row in dataRows) + + IRow avgRow = sheet.CreateRow(rowIndex++); + ICell labelCell = avgRow.CreateCell(0); + labelCell.SetCellValue($"平均时间(s)试样{group.Start}-{group.End}"); + labelCell.CellStyle = avgStyle; + + ICell avgCell = avgRow.CreateCell(group.Start); + avgCell.SetCellValue(sum / count); + avgCell.CellStyle = avgStyle; + + for (int i = group.Start + 1; i <= group.End; i++) { - for (int i = 6; i <= 5 + group2Count; i++) - { - sum2 += row.Field($"试样{i}"); - count2++; - } + avgRow.CreateCell(i).CellStyle = avgStyle; } - - double avg2 = count2 > 0 ? sum2 / count2 : 0; - - // 添加试样6-10平均值行 - IRow avgRow2 = sheet.CreateRow(rowIndex); - ICell labelCell2 = avgRow2.CreateCell(0); - labelCell2.SetCellValue("平均时间(s)试样6-10"); - labelCell2.CellStyle = avgStyle; - - ICell avgCell2 = avgRow2.CreateCell(6); - avgCell2.SetCellValue(avg2); - avgCell2.CellStyle = avgStyle; - - // 合并试样6-10列 - if (group2Count > 1) + + if (group.End > group.Start) { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 6, 5 + group2Count)); + sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, group.Start, group.End)); } } } @@ -805,6 +758,8 @@ namespace WindowsFormsApp6 private (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) CreateReportStyles(IWorkbook workbook) { + short numberFormat = workbook.CreateDataFormat().GetFormat("0.00"); + // 标题样式 ICellStyle titleStyle = workbook.CreateCellStyle(); IFont titleFont = workbook.CreateFont(); @@ -827,12 +782,14 @@ namespace WindowsFormsApp6 ICellStyle dataStyle = workbook.CreateCellStyle(); SetBorders(dataStyle); dataStyle.Alignment = NPOIHorizontalAlignment.Center; + dataStyle.DataFormat = numberFormat; // 黄色背景样式 ICellStyle yellowStyle = workbook.CreateCellStyle(); yellowStyle.CloneStyleFrom(dataStyle); yellowStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Yellow.Index; yellowStyle.FillPattern = FillPattern.SolidForeground; + yellowStyle.DataFormat = numberFormat; return (titleStyle, headerStyle, dataStyle, yellowStyle); } @@ -912,73 +869,50 @@ namespace WindowsFormsApp6 } } - // 计算试样1-5的平均值 - int group1Count = Math.Min(5, currentSampleCount); - if (group1Count > 0 && timeRows.Any()) + if (!timeRows.Any()) { - double sum1 = 0; - int count1 = 0; - - foreach (var row in timeRows) - { - for (int i = 1; i <= group1Count; i++) - { - sum1 += row.Field($"试样{i}"); - count1++; - } - } - - double avg1 = count1 > 0 ? sum1 / count1 : 0; - - // 添加试样1-5平均值行 - IRow avgRow1 = sheet.CreateRow(rowIndex++); - ICell avgLabelCell1 = avgRow1.CreateCell(0); - avgLabelCell1.SetCellValue("平均时间(s)试样1-5"); - avgLabelCell1.CellStyle = dataStyle; - - ICell avgCell1 = avgRow1.CreateCell(1); - avgCell1.SetCellValue(avg1); - avgCell1.CellStyle = yellowStyle; - - // 合并试样1-5列 - if (group1Count > 1) - { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, 1, group1Count)); - } + return; } - - // 计算试样6-10的平均值(仅当试样数量>5时) - if (currentSampleCount > 5 && timeRows.Any()) + + foreach (var group in GetSampleGroups(currentSampleCount)) { - int group2Count = Math.Min(5, currentSampleCount - 5); - double sum2 = 0; - int count2 = 0; - + double sum = 0; + int count = 0; + foreach (var row in timeRows) { - for (int i = 6; i <= 5 + group2Count; i++) + for (int i = group.Start; i <= group.End; i++) { - sum2 += row.Field($"试样{i}"); - count2++; + if (TryGetDouble(row[$"试样{i}"], out double value)) + { + sum += value; + count++; + } } } - - double avg2 = count2 > 0 ? sum2 / count2 : 0; - - // 添加试样6-10平均值行 - IRow avgRow2 = sheet.CreateRow(rowIndex); - ICell avgLabelCell2 = avgRow2.CreateCell(0); - avgLabelCell2.SetCellValue("平均时间(s)试样6-10"); - avgLabelCell2.CellStyle = dataStyle; - - ICell avgCell2 = avgRow2.CreateCell(6); - avgCell2.SetCellValue(avg2); - avgCell2.CellStyle = yellowStyle; - - // 合并试样6-10列 - if (group2Count > 1) + + if (count == 0) { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 6, 5 + group2Count)); + continue; + } + + IRow avgRow = sheet.CreateRow(rowIndex++); + ICell avgLabelCell = avgRow.CreateCell(0); + avgLabelCell.SetCellValue($"平均时间(s)试样{group.Start}-{group.End}"); + avgLabelCell.CellStyle = dataStyle; + + ICell avgCell = avgRow.CreateCell(group.Start); + avgCell.SetCellValue(sum / count); + avgCell.CellStyle = yellowStyle; + + for (int i = group.Start + 1; i <= group.End; i++) + { + avgRow.CreateCell(i).CellStyle = yellowStyle; + } + + if (group.End > group.Start) + { + sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, group.Start, group.End)); } } } diff --git a/WindowsFormsApp6/Form2.cs b/WindowsFormsApp6/Form2.cs index 63fd3a3..2acc06f 100644 --- a/WindowsFormsApp6/Form2.cs +++ b/WindowsFormsApp6/Form2.cs @@ -304,15 +304,7 @@ namespace WindowsFormsApp6 r.Field("序号") == ROW_SOAK_TIME || r.Field("序号") == ROW_HANG_TIME || r.Field("序号") == ROW_RUN_SPEED || - r.Field("序号") == ROW_AVG_ABSORPTION || - r.Field("序号") == ROW_MAX_ABSORPTION || - r.Field("序号") == ROW_STD_DEVIATION || - r.Field("序号") == "液体吸收量平均值(%)试样1-5" || - r.Field("序号") == "液体吸收量平均值(%)试样6-10" || - r.Field("序号") == "液体吸收量最大值(%)试样1-5" || - r.Field("序号") == "液体吸收量最大值(%)试样6-10" || - r.Field("序号") == "标准偏差试样1-5" || - r.Field("序号") == "标准偏差试样6-10") + IsAbsorptionStatRow(r.Field("序号"))) .ToList(); foreach (var row in rowsToRemove) @@ -412,11 +404,18 @@ namespace WindowsFormsApp6 // 3. 液体吸收量行 (%) DataRow absorptionRow = sampleDataTable.NewRow(); absorptionRow["序号"] = ROW_ABSORPTION; - double[] absorptions = new double[count]; + double?[] absorptions = new double?[count]; for (int i = 0; i < count; i++) { - absorptions[i] = ((afterWeights[i] - initialWeights[i]) / initialWeights[i]) * 100; - absorptionRow[$"试样{i + 1}"] = absorptions[i]; + if (TryCalculateAbsorption(initialWeights[i], afterWeights[i], out double absorption)) + { + absorptions[i] = absorption; + absorptionRow[$"试样{i + 1}"] = absorption; + } + else + { + absorptionRow[$"试样{i + 1}"] = DBNull.Value; + } } sampleDataTable.Rows.Add(absorptionRow); @@ -453,157 +452,7 @@ namespace WindowsFormsApp6 } sampleDataTable.Rows.Add(runSpeedRow); - // 7. 液体吸收量平均值行(试样1-5) - DataRow avgRow1 = sampleDataTable.NewRow(); - avgRow1["序号"] = "液体吸收量平均值(%)试样1-5"; - - // 计算试样1-5的平均值 - int group1Count = Math.Min(5, count); - if (group1Count > 0) - { - double avgAbsorption1 = absorptions.Take(group1Count).Average(); - avgRow1["试样1"] = avgAbsorption1; - } - - // 试样2-5列为空(只设置到第5列或count,取较小值) - int maxCol1 = Math.Min(5, count); - for (int i = 2; i <= maxCol1; i++) - { - avgRow1[$"试样{i}"] = DBNull.Value; - } - - // 如果有试样6-10,这些列也设置为空 - for (int i = 6; i <= count; i++) - { - avgRow1[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(avgRow1); - - // 8. 液体吸收量平均值行(试样6-10)- 仅当试样数量>5时添加 - if (count > 5) - { - DataRow avgRow2 = sampleDataTable.NewRow(); - avgRow2["序号"] = "液体吸收量平均值(%)试样6-10"; - - // 计算试样6-10的平均值 - int group2Count = Math.Min(5, count - 5); - if (group2Count > 0) - { - double avgAbsorption2 = absorptions.Skip(5).Take(group2Count).Average(); - avgRow2["试样6"] = avgAbsorption2; - } - - // 其他列为空 - for (int i = 1; i <= 5; i++) - { - avgRow2[$"试样{i}"] = DBNull.Value; - } - for (int i = 7; i <= count; i++) - { - avgRow2[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(avgRow2); - } - - // 9. 液体吸收量最大值行(试样1-5) - DataRow maxRow1 = sampleDataTable.NewRow(); - maxRow1["序号"] = "液体吸收量最大值(%)试样1-5"; - - // 计算试样1-5的最大值 - if (group1Count > 0) - { - double maxAbsorption1 = absorptions.Take(group1Count).Max(); - maxRow1["试样1"] = maxAbsorption1; - } - - // 试样2-5列为空 - for (int i = 2; i <= maxCol1; i++) - { - maxRow1[$"试样{i}"] = DBNull.Value; - } - - // 如果有试样6-10,这些列也设置为空 - for (int i = 6; i <= count; i++) - { - maxRow1[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(maxRow1); - - // 10. 液体吸收量最大值行(试样6-10)- 仅当试样数量>5时添加 - if (count > 5) - { - DataRow maxRow2 = sampleDataTable.NewRow(); - maxRow2["序号"] = "液体吸收量最大值(%)试样6-10"; - - // 计算试样6-10的最大值 - int group2Count = Math.Min(5, count - 5); - if (group2Count > 0) - { - double maxAbsorption2 = absorptions.Skip(5).Take(group2Count).Max(); - maxRow2["试样6"] = maxAbsorption2; - } - - // 其他列为空 - for (int i = 1; i <= 5; i++) - { - maxRow2[$"试样{i}"] = DBNull.Value; - } - for (int i = 7; i <= count; i++) - { - maxRow2[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(maxRow2); - } - - // 11. 标准偏差行(试样1-5) - DataRow stdDevRow1 = sampleDataTable.NewRow(); - stdDevRow1["序号"] = "标准偏差试样1-5"; - - // 计算试样1-5的标准偏差 - if (group1Count > 1) - { - double stdDev1 = CalculateStandardDeviation(absorptions.Take(group1Count).ToArray()); - stdDevRow1["试样1"] = stdDev1; - } - - // 试样2-5列为空 - for (int i = 2; i <= maxCol1; i++) - { - stdDevRow1[$"试样{i}"] = DBNull.Value; - } - - // 如果有试样6-10,这些列也设置为空 - for (int i = 6; i <= count; i++) - { - stdDevRow1[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(stdDevRow1); - - // 12. 标准偏差行(试样6-10)- 仅当试样数量>5时添加 - if (count > 5) - { - DataRow stdDevRow2 = sampleDataTable.NewRow(); - stdDevRow2["序号"] = "标准偏差试样6-10"; - - // 计算试样6-10的标准偏差 - int group2Count = Math.Min(5, count - 5); - if (group2Count > 1) - { - double stdDev2 = CalculateStandardDeviation(absorptions.Skip(5).Take(group2Count).ToArray()); - stdDevRow2["试样6"] = stdDev2; - } - - // 其他列为空 - for (int i = 1; i <= 5; i++) - { - stdDevRow2[$"试样{i}"] = DBNull.Value; - } - for (int i = 7; i <= count; i++) - { - stdDevRow2[$"试样{i}"] = DBNull.Value; - } - sampleDataTable.Rows.Add(stdDevRow2); - } + AddAbsorptionStatRows(sampleDataTable, absorptions, count); // 刷新界面 RefreshDataGridView(); @@ -615,11 +464,99 @@ namespace WindowsFormsApp6 private double CalculateStandardDeviation(double[] values) { if (values == null || values.Length <= 1) return 0; - - double avg = values.Average(); - double sumOfSquares = values.Sum(val => Math.Pow(val - avg, 2)); + + var validValues = values + .Where(v => !double.IsNaN(v) && !double.IsInfinity(v)) + .ToArray(); + + if (validValues.Length <= 1) return 0; + + double avg = validValues.Average(); + double sumOfSquares = validValues.Sum(val => Math.Pow(val - avg, 2)); // 使用样本标准偏差公式:除以 (n-1) - return Math.Sqrt(sumOfSquares / (values.Length - 1)); + return Math.Sqrt(sumOfSquares / (validValues.Length - 1)); + } + + private bool TryCalculateAbsorption(double initialWeight, double afterWeight, out double absorption) + { + absorption = 0; + if (initialWeight <= 0 || + double.IsNaN(initialWeight) || + double.IsInfinity(initialWeight) || + double.IsNaN(afterWeight) || + double.IsInfinity(afterWeight)) + { + return false; + } + + absorption = ((afterWeight - initialWeight) / initialWeight) * 100; + return !double.IsNaN(absorption) && !double.IsInfinity(absorption); + } + + private bool IsAbsorptionStatRow(string rowName) + { + return !string.IsNullOrEmpty(rowName) && + (rowName.StartsWith(ROW_AVG_ABSORPTION) || + rowName.StartsWith(ROW_MAX_ABSORPTION) || + rowName.StartsWith(ROW_STD_DEVIATION)); + } + + private IEnumerable<(int Start, int End)> GetSampleGroups(int sampleCount) + { + for (int start = 1; start <= sampleCount; start += 5) + { + yield return (start, Math.Min(start + 4, sampleCount)); + } + } + + private void AddAbsorptionStatRows(DataTable dataTable, double?[] absorptions, int sampleCount) + { + foreach (var group in GetSampleGroups(sampleCount)) + { + double[] groupValues = Enumerable.Range(group.Start, group.End - group.Start + 1) + .Select(index => absorptions[index - 1]) + .Where(value => value.HasValue) + .Select(value => value.Value) + .ToArray(); + + AddAbsorptionStatRow(dataTable, $"{ROW_AVG_ABSORPTION}试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 0 ? (object)groupValues.Average() : DBNull.Value); + + AddAbsorptionStatRow(dataTable, $"{ROW_MAX_ABSORPTION}试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 0 ? (object)groupValues.Max() : DBNull.Value); + + AddAbsorptionStatRow(dataTable, $"{ROW_STD_DEVIATION}试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 1 ? (object)CalculateStandardDeviation(groupValues) : DBNull.Value); + } + } + + private void AddAbsorptionStatRow(DataTable dataTable, string rowName, int startSample, int sampleCount, object statValue) + { + DataRow row = dataTable.NewRow(); + row["序号"] = rowName; + + for (int i = 1; i <= sampleCount; i++) + { + row[$"试样{i}"] = i == startSample ? statValue : DBNull.Value; + } + + dataTable.Rows.Add(row); + } + + private int FindStatStartSample(DataRow row) + { + for (int i = 1; i <= currentSampleCount; i++) + { + string columnName = $"试样{i}"; + if (row.Table.Columns.Contains(columnName) && + row[columnName] != null && + row[columnName] != DBNull.Value) + { + return i; + } + } + + return 0; } /// @@ -692,6 +629,8 @@ namespace WindowsFormsApp6 private (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) CreateReportStyles(IWorkbook workbook) { + short numberFormat = workbook.CreateDataFormat().GetFormat("0.00"); + // 标题样式 ICellStyle titleStyle = workbook.CreateCellStyle(); IFont titleFont = workbook.CreateFont(); @@ -714,12 +653,14 @@ namespace WindowsFormsApp6 ICellStyle dataStyle = workbook.CreateCellStyle(); SetBorders(dataStyle); dataStyle.Alignment = NPOIHorizontalAlignment.Center; + dataStyle.DataFormat = numberFormat; // 黄色背景样式 ICellStyle yellowStyle = workbook.CreateCellStyle(); yellowStyle.CloneStyleFrom(dataStyle); yellowStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Yellow.Index; yellowStyle.FillPattern = FillPattern.SolidForeground; + yellowStyle.DataFormat = numberFormat; return (titleStyle, headerStyle, dataStyle, yellowStyle); } @@ -779,10 +720,8 @@ namespace WindowsFormsApp6 { string rowName = dataRow["序号"].ToString(); - // 处理试样1-5分组行的合并单元格 - if (rowName == "液体吸收量平均值(%)试样1-5" || - rowName == "液体吸收量最大值(%)试样1-5" || - rowName == "标准偏差试样1-5") + // 处理每5个试样一组的统计行 + if (IsAbsorptionStatRow(rowName)) { IRow row = sheet.CreateRow(rowIndex++); @@ -790,48 +729,32 @@ namespace WindowsFormsApp6 ICell nameCell = row.CreateCell(0); nameCell.SetCellValue(rowName); nameCell.CellStyle = dataStyle; - - // 获取试样1的值 - var value = dataRow["试样1"]; - if (value != null && value != DBNull.Value) + + int startSample = FindStatStartSample(dataRow); + int endSample = startSample > 0 ? Math.Min(startSample + 4, currentSampleCount) : startSample; + + if (startSample > 0) { - ICell cell = row.CreateCell(1); - cell.SetCellValue(Convert.ToDouble(value)); - cell.CellStyle = yellowStyle; - - // 合并试样1-5列 - int group1Count = Math.Min(5, currentSampleCount); - if (group1Count > 1) + ICell cell = row.CreateCell(startSample); + object value = dataRow[$"试样{startSample}"]; + if (value != null && value != DBNull.Value && double.TryParse(value.ToString(), out double numValue)) { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, 1, group1Count)); + cell.SetCellValue(numValue); } - } - } - // 处理试样6-10分组行的合并单元格 - else if (rowName == "液体吸收量平均值(%)试样6-10" || - rowName == "液体吸收量最大值(%)试样6-10" || - rowName == "标准偏差试样6-10") - { - IRow row = sheet.CreateRow(rowIndex++); - - // 序号列 - ICell nameCell = row.CreateCell(0); - nameCell.SetCellValue(rowName); - nameCell.CellStyle = dataStyle; - - // 获取试样6的值 - var value = dataRow["试样6"]; - if (value != null && value != DBNull.Value) - { - ICell cell = row.CreateCell(6); - cell.SetCellValue(Convert.ToDouble(value)); - cell.CellStyle = yellowStyle; - - // 合并试样6-10列 - int group2Count = Math.Min(5, currentSampleCount - 5); - if (group2Count > 1) + else { - sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, 6, 5 + group2Count)); + cell.SetCellValue(""); + } + cell.CellStyle = yellowStyle; + + for (int i = startSample + 1; i <= endSample; i++) + { + row.CreateCell(i).CellStyle = yellowStyle; + } + + if (endSample > startSample) + { + sheet.AddMergedRegion(new CellRangeAddress(rowIndex - 1, rowIndex - 1, startSample, endSample)); } } } diff --git a/WindowsFormsApp6/Form3.cs b/WindowsFormsApp6/Form3.cs index d364867..7ebd930 100644 --- a/WindowsFormsApp6/Form3.cs +++ b/WindowsFormsApp6/Form3.cs @@ -331,11 +331,11 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { - // 纵向布局:检查芯吸速率行(第3行,索引2) - if (sampleDataTable.Rows.Count <= 2) + // 纵向布局:检查芯吸速率行 + DataRow rateRow = FindDataRow(ROW_WICKING_RATE); + if (rateRow == null) return false; - - DataRow rateRow = sampleDataTable.Rows[2]; + for (int i = 1; i <= currentSampleCount; i++) { for (int j = 1; j <= 3; j++) @@ -497,7 +497,10 @@ namespace WindowsFormsApp6 string colName = $"试样{i}_{j}"; if (row.Table.Columns.Contains(colName)) { - rowData[colName] = ConvertToDouble(row[colName]); + if (TryGetExistingDouble(row[colName], out double value)) + { + rowData[colName] = value; + } } } } @@ -507,7 +510,7 @@ namespace WindowsFormsApp6 else { // 从横向布局保存:需要转换数据结构 - // 初始化5个数据行 + // 初始化数据行 data[ROW_WICKING_TIME] = new Dictionary(); data[ROW_WICKING_HEIGHT] = new Dictionary(); data[ROW_WICKING_RATE] = new Dictionary(); @@ -525,20 +528,25 @@ namespace WindowsFormsApp6 DataRow row = sampleDataTable.Rows[rowIndex]; string colName = $"试样{i}_{j}"; - if (row.Table.Columns.Contains(ROW_WICKING_TIME)) - data[ROW_WICKING_TIME][colName] = ConvertToDouble(row[ROW_WICKING_TIME]); + if (row.Table.Columns.Contains(ROW_WICKING_TIME) && + TryGetExistingDouble(row[ROW_WICKING_TIME], out double wickingTime)) + data[ROW_WICKING_TIME][colName] = wickingTime; - if (row.Table.Columns.Contains(ROW_WICKING_HEIGHT)) - data[ROW_WICKING_HEIGHT][colName] = ConvertToDouble(row[ROW_WICKING_HEIGHT]); + if (row.Table.Columns.Contains(ROW_WICKING_HEIGHT) && + TryGetExistingDouble(row[ROW_WICKING_HEIGHT], out double wickingHeight)) + data[ROW_WICKING_HEIGHT][colName] = wickingHeight; - if (row.Table.Columns.Contains(ROW_WICKING_RATE)) - data[ROW_WICKING_RATE][colName] = ConvertToDouble(row[ROW_WICKING_RATE]); + if (row.Table.Columns.Contains(ROW_WICKING_RATE) && + TryGetExistingDouble(row[ROW_WICKING_RATE], out double wickingRate)) + data[ROW_WICKING_RATE][colName] = wickingRate; - if (row.Table.Columns.Contains(ROW_AVG_WICKING_RATE)) - data[ROW_AVG_WICKING_RATE][colName] = ConvertToDouble(row[ROW_AVG_WICKING_RATE]); + if (row.Table.Columns.Contains(ROW_AVG_WICKING_RATE) && + TryGetExistingDouble(row[ROW_AVG_WICKING_RATE], out double avgWickingRate)) + data[ROW_AVG_WICKING_RATE][colName] = avgWickingRate; - if (row.Table.Columns.Contains(ROW_STD_DEVIATION)) - data[ROW_STD_DEVIATION][colName] = ConvertToDouble(row[ROW_STD_DEVIATION]); + if (row.Table.Columns.Contains(ROW_STD_DEVIATION) && + TryGetExistingDouble(row[ROW_STD_DEVIATION], out double stdDeviation)) + data[ROW_STD_DEVIATION][colName] = stdDeviation; rowIndex++; } @@ -615,6 +623,19 @@ namespace WindowsFormsApp6 } } + private bool TryGetExistingDouble(object value, out double result) + { + result = 0; + if (value == null || value == DBNull.Value) + { + return false; + } + + return double.TryParse(value.ToString(), out result) && + !double.IsNaN(result) && + !double.IsInfinity(result); + } + /// /// 连接设备按钮点击事件 /// @@ -713,7 +734,7 @@ namespace WindowsFormsApp6 sampleDataTable.Columns.Add($"试样{i}_3", typeof(double)); } - // 初始化5行数据 + // 初始化数据行 AddDataRow(ROW_WICKING_TIME); AddDataRow(ROW_WICKING_HEIGHT); AddDataRow(ROW_WICKING_RATE); @@ -737,11 +758,11 @@ namespace WindowsFormsApp6 { DataRow row = sampleDataTable.NewRow(); row["序号"] = $"试样{i}_{j}"; - row[ROW_WICKING_TIME] = 0.0; - row[ROW_WICKING_HEIGHT] = 0.0; - row[ROW_WICKING_RATE] = 0.0; - row[ROW_AVG_WICKING_RATE] = 0.0; - row[ROW_STD_DEVIATION] = 0.0; + row[ROW_WICKING_TIME] = DBNull.Value; + row[ROW_WICKING_HEIGHT] = DBNull.Value; + row[ROW_WICKING_RATE] = DBNull.Value; + row[ROW_AVG_WICKING_RATE] = DBNull.Value; + row[ROW_STD_DEVIATION] = DBNull.Value; sampleDataTable.Rows.Add(row); } } @@ -756,17 +777,35 @@ namespace WindowsFormsApp6 DataRow row = sampleDataTable.NewRow(); row["序号"] = rowName; - // 初始化所有数值列为0 + // 未采集的数据保持为空,避免报表把空数据导出为0。 for (int i = 1; i <= currentSampleCount; i++) { - row[$"试样{i}_1"] = 0.0; - row[$"试样{i}_2"] = 0.0; - row[$"试样{i}_3"] = 0.0; + row[$"试样{i}_1"] = DBNull.Value; + row[$"试样{i}_2"] = DBNull.Value; + row[$"试样{i}_3"] = DBNull.Value; } sampleDataTable.Rows.Add(row); } + private DataRow FindDataRow(string rowName) + { + if (sampleDataTable == null) + { + return null; + } + + foreach (DataRow row in sampleDataTable.Rows) + { + if (string.Equals(row["序号"]?.ToString(), rowName, StringComparison.Ordinal)) + { + return row; + } + } + + return null; + } + /// /// 初始化定时器用于模拟寄存器数据读取 /// @@ -785,7 +824,12 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局:第1行读取吸水时间 - DataRow timeRow = sampleDataTable.Rows[0]; + DataRow timeRow = FindDataRow(ROW_WICKING_TIME); + if (timeRow == null) + { + return; + } + for (int i = 1; i <= currentSampleCount; i++) { double time1 = ReadRegisterData((i - 1) * 3); @@ -839,14 +883,19 @@ namespace WindowsFormsApp6 { for (int colIndex = 1; colIndex < sampleDataTable.Columns.Count; colIndex++) { - row[colIndex] = 0.0; + row[colIndex] = DBNull.Value; } } if (isVerticalLayout) { // 纵向布局:第1行生成吸水时间 - DataRow timeRow = sampleDataTable.Rows[0]; + DataRow timeRow = FindDataRow(ROW_WICKING_TIME); + if (timeRow == null) + { + return; + } + for (int i = 1; i <= currentSampleCount; i++) { double baseTime = 31 + random.NextDouble() * 2; @@ -935,9 +984,13 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局 - DataRow timeRow = sampleDataTable.Rows[0]; - DataRow heightRow = sampleDataTable.Rows[1]; - DataRow rateRow = sampleDataTable.Rows[2]; + DataRow timeRow = FindDataRow(ROW_WICKING_TIME); + DataRow heightRow = FindDataRow(ROW_WICKING_HEIGHT); + DataRow rateRow = FindDataRow(ROW_WICKING_RATE); + if (timeRow == null || heightRow == null || rateRow == null) + { + return; + } for (int i = 1; i <= currentSampleCount; i++) { @@ -949,13 +1002,9 @@ namespace WindowsFormsApp6 double height2 = ConvertToDouble(heightRow[$"试样{i}_2"]); double height3 = ConvertToDouble(heightRow[$"试样{i}_3"]); - double rate1 = (time1 > 0 && height1 > 0) ? height1 / (time1 / 60.0) : 0; - double rate2 = (time2 > 0 && height2 > 0) ? height2 / (time2 / 60.0) : 0; - double rate3 = (time3 > 0 && height3 > 0) ? height3 / (time3 / 60.0) : 0; - - rateRow[$"试样{i}_1"] = Math.Round(rate1, 2); - rateRow[$"试样{i}_2"] = Math.Round(rate2, 2); - rateRow[$"试样{i}_3"] = Math.Round(rate3, 2); + rateRow[$"试样{i}_1"] = (time1 > 0 && height1 > 0) ? (object)(height1 / (time1 / 60.0)) : DBNull.Value; + rateRow[$"试样{i}_2"] = (time2 > 0 && height2 > 0) ? (object)(height2 / (time2 / 60.0)) : DBNull.Value; + rateRow[$"试样{i}_3"] = (time3 > 0 && height3 > 0) ? (object)(height3 / (time3 / 60.0)) : DBNull.Value; } } else @@ -965,8 +1014,7 @@ namespace WindowsFormsApp6 { double time = ConvertToDouble(row[ROW_WICKING_TIME]); double height = ConvertToDouble(row[ROW_WICKING_HEIGHT]); - double rate = (time > 0 && height > 0) ? height / (time / 60.0) : 0; - row[ROW_WICKING_RATE] = Math.Round(rate, 2); + row[ROW_WICKING_RATE] = (time > 0 && height > 0) ? (object)(height / (time / 60.0)) : DBNull.Value; } } } @@ -1000,15 +1048,19 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局 - DataRow rateRow = sampleDataTable.Rows[2]; - DataRow avgRow = sampleDataTable.Rows[3]; + DataRow rateRow = FindDataRow(ROW_WICKING_RATE); + DataRow avgRow = FindDataRow(ROW_AVG_WICKING_RATE); + if (rateRow == null || avgRow == null) + { + return; + } // 清空平均值行的所有数据 for (int i = 1; i <= currentSampleCount; i++) { - avgRow[$"试样{i}_1"] = 0.0; - avgRow[$"试样{i}_2"] = 0.0; - avgRow[$"试样{i}_3"] = 0.0; + avgRow[$"试样{i}_1"] = DBNull.Value; + avgRow[$"试样{i}_2"] = DBNull.Value; + avgRow[$"试样{i}_3"] = DBNull.Value; } // 计算每个试样的平均值,并填充到该试样的第1列(合并显示效果) @@ -1024,11 +1076,11 @@ namespace WindowsFormsApp6 if (rate3 > 0) groupRates.Add(rate3); // 计算该试样的平均值 - double groupAvg = groupRates.Count > 0 ? groupRates.Average() : 0; - - // 只填充到该试样的第1列,实现合并显示效果 - avgRow[$"试样{i}_1"] = Math.Round(groupAvg, 2); - // 第2、3列保持为0(显示为空白) + if (groupRates.Count > 0) + { + // 只填充到该试样的第1列,实现合并显示效果 + avgRow[$"试样{i}_1"] = groupRates.Average(); + } } } else @@ -1037,7 +1089,7 @@ namespace WindowsFormsApp6 // 清空所有行的平均值列 foreach (DataRow row in sampleDataTable.Rows) { - row[ROW_AVG_WICKING_RATE] = 0.0; + row[ROW_AVG_WICKING_RATE] = DBNull.Value; } // 计算每个试样的平均值 @@ -1056,13 +1108,14 @@ namespace WindowsFormsApp6 } // 计算该试样的平均值 - double groupAvg = groupRates.Count > 0 ? groupRates.Average() : 0; - - // 填充到该试样的第3次测试行(合并显示效果) - int targetRowIndex = (i - 1) * 3 + 2; // 第3次测试的行索引 - if (targetRowIndex < sampleDataTable.Rows.Count) + if (groupRates.Count > 0) { - sampleDataTable.Rows[targetRowIndex][ROW_AVG_WICKING_RATE] = Math.Round(groupAvg, 2); + // 填充到该试样的第3次测试行(合并显示效果) + int targetRowIndex = (i - 1) * 3 + 2; // 第3次测试的行索引 + if (targetRowIndex < sampleDataTable.Rows.Count) + { + sampleDataTable.Rows[targetRowIndex][ROW_AVG_WICKING_RATE] = groupRates.Average(); + } } } } @@ -1083,8 +1136,12 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局 - DataRow rateRow = sampleDataTable.Rows[2]; - DataRow stdRow = sampleDataTable.Rows[4]; + DataRow rateRow = FindDataRow(ROW_WICKING_RATE); + DataRow stdRow = FindDataRow(ROW_STD_DEVIATION); + if (rateRow == null || stdRow == null) + { + return; + } // 清空标准偏差行的所有数据(设置为DBNull,不显示) for (int i = 1; i <= currentSampleCount; i++) @@ -1124,17 +1181,14 @@ namespace WindowsFormsApp6 } } - // 基于该组试样的平均值计算标准偏差 - double stdDev = 0; if (groupAverages.Count > 1) { double overallAverage = groupAverages.Average(); double sumOfSquares = groupAverages.Sum(avg => Math.Pow(avg - overallAverage, 2)); - stdDev = Math.Sqrt(sumOfSquares / (groupAverages.Count - 1)); + double stdDev = Math.Sqrt(sumOfSquares / (groupAverages.Count - 1)); + // 只在该组第1个试样的第1列显示标准偏差,其他列保持DBNull(不显示) + stdRow[$"试样{startSample}_1"] = stdDev; } - - // 只在该组第1个试样的第1列显示标准偏差,其他列保持DBNull(不显示) - stdRow[$"试样{startSample}_1"] = Math.Round(stdDev, 2); } } else @@ -1179,20 +1233,17 @@ namespace WindowsFormsApp6 } } - // 基于该组试样的平均值计算标准偏差 - double stdDev = 0; if (groupAverages.Count > 1) { double overallAverage = groupAverages.Average(); double sumOfSquares = groupAverages.Sum(avg => Math.Pow(avg - overallAverage, 2)); - stdDev = Math.Sqrt(sumOfSquares / (groupAverages.Count - 1)); - } - - // 只在该组第1个试样的第1次测试行显示标准偏差,其他行保持DBNull(不显示) - int targetRowIndex = (startSample - 1) * 3; // 第1次测试的行索引 - if (targetRowIndex < sampleDataTable.Rows.Count) - { - sampleDataTable.Rows[targetRowIndex][ROW_STD_DEVIATION] = Math.Round(stdDev, 2); + double stdDev = Math.Sqrt(sumOfSquares / (groupAverages.Count - 1)); + // 只在该组第1个试样的第1次测试行显示标准偏差,其他行保持DBNull(不显示) + int targetRowIndex = (startSample - 1) * 3; // 第1次测试的行索引 + if (targetRowIndex < sampleDataTable.Rows.Count) + { + sampleDataTable.Rows[targetRowIndex][ROW_STD_DEVIATION] = stdDev; + } } } } @@ -1256,6 +1307,8 @@ namespace WindowsFormsApp6 private (ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) CreateReportStyles(IWorkbook workbook) { + short numberFormat = workbook.CreateDataFormat().GetFormat("0.00"); + // 表头样式 ICellStyle headerStyle = workbook.CreateCellStyle(); headerStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Grey25Percent.Index; @@ -1272,6 +1325,7 @@ namespace WindowsFormsApp6 SetBorders(dataStyle); dataStyle.Alignment = NPOIHorizontalAlignment.Center; dataStyle.VerticalAlignment = VerticalAlignment.Center; + dataStyle.DataFormat = numberFormat; // 黄色背景样式 ICellStyle yellowStyle = workbook.CreateCellStyle(); @@ -1280,6 +1334,7 @@ namespace WindowsFormsApp6 SetBorders(yellowStyle); yellowStyle.Alignment = NPOIHorizontalAlignment.Center; yellowStyle.VerticalAlignment = VerticalAlignment.Center; + yellowStyle.DataFormat = numberFormat; return (headerStyle, dataStyle, yellowStyle); } @@ -1300,6 +1355,29 @@ namespace WindowsFormsApp6 /// private void CreateReportHeader(ISheet sheet, ICellStyle headerStyle) { + if (!isVerticalLayout) + { + string[] headers = + { + "序号", + ROW_WICKING_TIME, + ROW_WICKING_HEIGHT, + ROW_WICKING_RATE, + ROW_AVG_WICKING_RATE, + ROW_STD_DEVIATION + }; + + IRow headerRow = sheet.CreateRow(0); + headerRow.Height = 400; + for (int i = 0; i < headers.Length; i++) + { + ICell cell = headerRow.CreateCell(i); + cell.SetCellValue(headers[i]); + cell.CellStyle = headerStyle; + } + return; + } + // 创建第一行表头(试样1-N) IRow headerRow1 = sheet.CreateRow(0); headerRow1.Height = 400; @@ -1345,6 +1423,12 @@ namespace WindowsFormsApp6 /// private void FillReportData(ISheet sheet, ICellStyle dataStyle, ICellStyle yellowStyle) { + if (!isVerticalLayout) + { + FillHorizontalReportData(sheet, dataStyle, yellowStyle); + return; + } + int rowIndex = 2; int dataRowIndex = 0; @@ -1362,33 +1446,24 @@ namespace WindowsFormsApp6 cellSeq.CellStyle = rowStyle; int colIndex = 1; + string rowName = dataRow["序号"].ToString(); + bool sourceRow = rowName == ROW_WICKING_TIME || + rowName == ROW_WICKING_HEIGHT; for (int i = 1; i <= currentSampleCount; i++) { // 试样次数1 ICell cell1 = row.CreateCell(colIndex++); - double val1 = ConvertToDouble(dataRow[$"试样{i}_1"]); - if (Math.Abs(val1) >= 0.001) // 只有非0值才显示 - { - cell1.SetCellValue(val1); - } + SetForm3ReportCell(cell1, dataRow[$"试样{i}_1"], sourceRow); cell1.CellStyle = rowStyle; // 试样次数2 ICell cell2 = row.CreateCell(colIndex++); - double val2 = ConvertToDouble(dataRow[$"试样{i}_2"]); - if (Math.Abs(val2) >= 0.001) // 只有非0值才显示 - { - cell2.SetCellValue(val2); - } + SetForm3ReportCell(cell2, dataRow[$"试样{i}_2"], sourceRow); cell2.CellStyle = rowStyle; // 试样次数3 ICell cell3 = row.CreateCell(colIndex++); - double val3 = ConvertToDouble(dataRow[$"试样{i}_3"]); - if (Math.Abs(val3) >= 0.001) // 只有非0值才显示 - { - cell3.SetCellValue(val3); - } + SetForm3ReportCell(cell3, dataRow[$"试样{i}_3"], sourceRow); cell3.CellStyle = rowStyle; } @@ -1397,12 +1472,103 @@ namespace WindowsFormsApp6 } } + private void FillHorizontalReportData(ISheet sheet, ICellStyle dataStyle, ICellStyle yellowStyle) + { + string[] columns = + { + "序号", + ROW_WICKING_TIME, + ROW_WICKING_HEIGHT, + ROW_WICKING_RATE, + ROW_AVG_WICKING_RATE, + ROW_STD_DEVIATION + }; + + int rowIndex = 1; + foreach (DataRow dataRow in sampleDataTable.Rows) + { + IRow row = sheet.CreateRow(rowIndex++); + row.Height = 380; + + for (int i = 0; i < columns.Length; i++) + { + ICell cell = row.CreateCell(i); + string columnName = columns[i]; + if (sampleDataTable.Columns.Contains(columnName) && + dataRow[columnName] != null && + dataRow[columnName] != DBNull.Value) + { + if (i == 0) + { + cell.SetCellValue(dataRow[columnName].ToString()); + } + else if (double.TryParse(dataRow[columnName].ToString(), out double value)) + { + if ((columnName == ROW_WICKING_TIME || + columnName == ROW_WICKING_HEIGHT) && + Math.Abs(value) < 0.001) + { + cell.SetCellValue(""); + } + else + { + cell.SetCellValue(value); + } + } + else + { + cell.SetCellValue(""); + } + } + else + { + cell.SetCellValue(""); + } + + cell.CellStyle = i == 0 ? dataStyle : yellowStyle; + } + } + } + + private void SetForm3ReportCell(ICell cell, object value, bool blankZeroSourceValue) + { + if (value == null || value == DBNull.Value) + { + cell.SetCellValue(""); + return; + } + + if (!double.TryParse(value.ToString(), out double number)) + { + cell.SetCellValue(""); + return; + } + + if (blankZeroSourceValue && Math.Abs(number) < 0.001) + { + cell.SetCellValue(""); + return; + } + + cell.SetCellValue(number); + } + /// /// 设置报表列宽 /// private void SetReportColumnWidths(ISheet sheet) { sheet.SetColumnWidth(0, 20 * 256); // 序号列 + if (!isVerticalLayout) + { + sheet.SetColumnWidth(1, 16 * 256); + sheet.SetColumnWidth(2, 16 * 256); + sheet.SetColumnWidth(3, 18 * 256); + sheet.SetColumnWidth(4, 22 * 256); + sheet.SetColumnWidth(5, 14 * 256); + return; + } + int totalColumns = currentSampleCount * 3; for (int i = 1; i <= totalColumns; i++) { diff --git a/WindowsFormsApp6/MainForm.cs b/WindowsFormsApp6/MainForm.cs index 0c20ee1..de3ee1a 100644 --- a/WindowsFormsApp6/MainForm.cs +++ b/WindowsFormsApp6/MainForm.cs @@ -4,8 +4,8 @@ using System.Data; using System.Drawing; using System.Windows.Forms; using System.IO; -using System.IO.Ports; using System.Linq; +using System.Net.Sockets; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.SS.Util; @@ -22,10 +22,19 @@ namespace WindowsFormsApp6 private Form3 form3Instance; private System.Windows.Forms.Timer timeUpdateTimer; - // 串口和Modbus相关 - private SerialPort _serialPort; + // TCP和Modbus相关 + private const string ModbusTcpHost = "192.168.1.10"; + private const int ModbusTcpPort = 502; + private const int ModbusTcpConnectTimeoutMilliseconds = 1000; + private const int ModbusTcpReconnectIntervalMilliseconds = 2000; + private const string FORM3_ROW_WICKING_TIME = "吸水时间(s)"; + private const string FORM3_ROW_WICKING_HEIGHT = "吸芯高度(mm)"; + + private TcpClient _tcpClient; private IModbusMaster _modbusMaster; private System.Windows.Forms.Timer _readTimer; + private System.Windows.Forms.Timer _reconnectTimer; + private bool _isConnecting; // Form1历史数据缓存(用于累积显示) private List form1TimeValues = new List(); @@ -40,7 +49,7 @@ namespace WindowsFormsApp6 public double AfterWeight { get; set; } public int SoakTime { get; set; } public int HangTime { get; set; } - public int RunSpeed { get; set; } + public double RunSpeed { get; set; } } public MainForm() @@ -49,7 +58,7 @@ namespace WindowsFormsApp6 InitializeTabControl(); InitializeEmbeddedForms(); InitializeTimeUpdate(); - InitializeSerialPortAndModbus(); + InitializeTcpModbus(); CenterInfoControls(); CenterBottomButtons(); @@ -71,168 +80,89 @@ namespace WindowsFormsApp6 } /// - /// 初始化串口和Modbus通信 + /// 初始化TCP和Modbus通信 /// - private void InitializeSerialPortAndModbus() + private void InitializeTcpModbus() { // 初始化读取定时器(信号量触发模式) _readTimer = new System.Windows.Forms.Timer(); _readTimer.Interval = 100; // 100ms检查一次信号量 _readTimer.Tick += ReadTimer_Tick; - // 尝试打开默认串口 - if (!TryOpenSerialPort("COM14")) + _reconnectTimer = new System.Windows.Forms.Timer(); + _reconnectTimer.Interval = ModbusTcpReconnectIntervalMilliseconds; + _reconnectTimer.Tick += ReconnectTimer_Tick; + + if (!TryConnectTcpModbus()) { - // 如果默认串口失败,弹出端口选择对话框 - ShowPortSelectionDialog(); + _reconnectTimer.Start(); } } /// - /// 尝试打开指定的串口 + /// 重连定时器 /// - private bool TryOpenSerialPort(string portName) + private void ReconnectTimer_Tick(object sender, EventArgs e) { + if (_modbusMaster != null && IsTcpConnected()) + { + _reconnectTimer.Stop(); + return; + } + + TryConnectTcpModbus(); + } + + /// + /// 尝试连接Modbus TCP设备 + /// + private bool TryConnectTcpModbus() + { + if (_isConnecting) + { + return false; + } + + _isConnecting = true; try { - // 如果已有串口打开,先关闭 - if (_serialPort != null && _serialPort.IsOpen) + DisposeModbusConnection(); + + var tcpClient = new TcpClient(); + IAsyncResult connectResult = tcpClient.BeginConnect(ModbusTcpHost, ModbusTcpPort, null, null); + bool connected = connectResult.AsyncWaitHandle.WaitOne(ModbusTcpConnectTimeoutMilliseconds); + connectResult.AsyncWaitHandle.Close(); + + if (!connected) { - _serialPort.Close(); - _serialPort.Dispose(); + tcpClient.Close(); + throw new TimeoutException($"连接 {ModbusTcpHost}:{ModbusTcpPort} 超时"); } - // 配置串口参数 - _serialPort = new SerialPort - { - PortName = portName, - BaudRate = 19200, - DataBits = 8, - Parity = Parity.None, - StopBits = StopBits.One, - ReadTimeout = 500, - WriteTimeout = 500, - RtsEnable = false, - DtrEnable = false - }; + tcpClient.EndConnect(connectResult); + tcpClient.ReceiveTimeout = 500; + tcpClient.SendTimeout = 500; - // 打开串口 - _serialPort.Open(); - - // 创建Modbus RTU主站 - _modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort); + _tcpClient = tcpClient; + _modbusMaster = ModbusIpMaster.CreateIp(_tcpClient); _modbusMaster.Transport.WaitToRetryMilliseconds = 200; _modbusMaster.Transport.Retries = 1; _modbusMaster.Transport.ReadTimeout = 500; - // 启动定时读取 + _reconnectTimer.Stop(); _readTimer.Start(); - ShowInfoMsg($"串口 {portName} 初始化成功"); + System.Diagnostics.Debug.WriteLine($"Modbus TCP连接成功:{ModbusTcpHost}:{ModbusTcpPort}"); return true; } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"打开串口 {portName} 失败:{ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Modbus TCP连接失败:{ex.Message}"); return false; } - } - - /// - /// 显示端口选择对话框 - /// - private void ShowPortSelectionDialog() - { - // 获取当前计算机所有可用串口 - string[] ports = SerialPort.GetPortNames(); - - if (ports == null || ports.Length == 0) + finally { - ShowErrorMsg("未检测到可用的串口,请检查串口连接。"); - return; - } - - // 创建端口选择对话框 - Form portDialog = new Form - { - Text = "选择串口", - Width = 350, - Height = 200, - StartPosition = FormStartPosition.CenterParent, - FormBorderStyle = FormBorderStyle.FixedDialog, - MaximizeBox = false, - MinimizeBox = false - }; - - Label label = new Label - { - Text = "串口初始化失败,请选择可用的串口:", - Location = new Point(20, 20), - AutoSize = true - }; - - ComboBox comboBox = new ComboBox - { - Location = new Point(20, 50), - Width = 290, - DropDownStyle = ComboBoxStyle.DropDownList - }; - comboBox.Items.AddRange(ports); - if (comboBox.Items.Count > 0) - { - comboBox.SelectedIndex = 0; - } - - Button btnOK = new Button - { - Text = "确定", - DialogResult = DialogResult.OK, - Location = new Point(140, 100), - Width = 80 - }; - - Button btnCancel = new Button - { - Text = "取消", - DialogResult = DialogResult.Cancel, - Location = new Point(230, 100), - Width = 80 - }; - - portDialog.Controls.AddRange(new Control[] { label, comboBox, btnOK, btnCancel }); - portDialog.AcceptButton = btnOK; - portDialog.CancelButton = btnCancel; - - // 显示对话框 - if (portDialog.ShowDialog(this) == DialogResult.OK) - { - string selectedPort = comboBox.SelectedItem?.ToString(); - if (!string.IsNullOrEmpty(selectedPort)) - { - if (TryOpenSerialPort(selectedPort)) - { - // 成功打开 - return; - } - else - { - // 选择的端口也失败,询问是否重新选择 - var result = MessageBox.Show( - $"打开串口 {selectedPort} 失败,是否重新选择?", - "错误", - MessageBoxButtons.YesNo, - MessageBoxIcon.Error); - - if (result == DialogResult.Yes) - { - ShowPortSelectionDialog(); // 递归调用,重新选择 - } - } - } - } - else - { - ShowErrorMsg("未选择串口,数据采集功能将不可用。"); + _isConnecting = false; } } @@ -241,8 +171,11 @@ namespace WindowsFormsApp6 /// private void ReadTimer_Tick(object sender, EventArgs e) { - if (_modbusMaster == null || _serialPort == null || !_serialPort.IsOpen) + if (_modbusMaster == null || !IsTcpConnected()) + { + HandleModbusDisconnected("Modbus TCP连接不可用"); return; + } try { @@ -285,6 +218,7 @@ namespace WindowsFormsApp6 { // 读取失败时不弹窗,避免频繁打扰用户 System.Diagnostics.Debug.WriteLine($"Modbus读取失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); } } @@ -298,8 +232,10 @@ namespace WindowsFormsApp6 bool[] coils = _modbusMaster.ReadCoils(slaveId, address, 1); return coils[0]; } - catch + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"读取线圈失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); return false; } } @@ -316,6 +252,70 @@ namespace WindowsFormsApp6 catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"写入线圈失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); + } + } + + /// + /// 检查TCP连接状态 + /// + private bool IsTcpConnected() + { + try + { + return _tcpClient != null && _tcpClient.Client != null && _tcpClient.Connected; + } + catch + { + return false; + } + } + + /// + /// 释放当前Modbus TCP连接 + /// + private void DisposeModbusConnection() + { + try + { + _modbusMaster?.Dispose(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"释放Modbus主站失败:{ex.Message}"); + } + finally + { + _modbusMaster = null; + } + + try + { + _tcpClient?.Close(); + _tcpClient?.Dispose(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"关闭TCP连接失败:{ex.Message}"); + } + finally + { + _tcpClient = null; + } + } + + /// + /// 进入断线重连流程 + /// + private void HandleModbusDisconnected(string reason) + { + System.Diagnostics.Debug.WriteLine($"Modbus TCP断开,准备重连:{reason}"); + _readTimer?.Stop(); + DisposeModbusConnection(); + + if (_reconnectTimer != null && !_reconnectTimer.Enabled) + { + _reconnectTimer.Start(); } } @@ -490,12 +490,13 @@ namespace WindowsFormsApp6 catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form1数据失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); } } /// /// 读取Form2数据(液体吸收量) - /// PLC地址:D420 - 初始重量, D422 - 浸润后重量, D402 - 浸润时间, D406 - 悬挂时间, D410 - 运行速度 + /// PLC地址:D420 - 初始重量, D422 - 浸润后重量, D402 - 浸润时间, D406 - 悬挂时间, D310 - 运行速度 /// 信号量:M252 /// 每次测试动态添加一列显示,每5个试样计算平均值、最大值、标准偏差 /// @@ -521,7 +522,7 @@ namespace WindowsFormsApp6 ushort[] hangTimeReg = _modbusMaster.ReadHoldingRegisters(slaveId, 406, 1); int hangTime = ConvertSingleRegisterToInt(hangTimeReg[0]); - // 读取运行速度(D410,1个寄存器,整数,单位:mm/min) + // 读取运行速度(D310,2个寄存器,浮点数,单位:mm/min) ushort[] runSpeedReg = _modbusMaster.ReadHoldingRegisters(slaveId, 310, 2); double runSpeed = ConvertRegistersToDouble(runSpeedReg); runSpeed = ModbusUshortToFloat(runSpeedReg[1], runSpeedReg[0]); @@ -555,7 +556,7 @@ namespace WindowsFormsApp6 AfterWeight = afterWeight, SoakTime = soakTime, HangTime = hangTime, - RunSpeed = (int)runSpeed + RunSpeed = runSpeed }); // 新试样的索引 @@ -632,27 +633,17 @@ namespace WindowsFormsApp6 // 3. 液体吸收量行 (%) DataRow absorptionRow = dataTable.NewRow(); absorptionRow["序号"] = "液体吸收量(%)"; - double[] absorptions = new double[totalSamples]; + double?[] absorptions = new double?[totalSamples]; for (int i = 0; i < totalSamples; i++) { - if (form2SampleDataList[i].InitialWeight > 0) + if (TryCalculateAbsorption(form2SampleDataList[i].InitialWeight, form2SampleDataList[i].AfterWeight, out double absorption)) { - absorptions[i] = ((form2SampleDataList[i].AfterWeight - form2SampleDataList[i].InitialWeight) - / form2SampleDataList[i].InitialWeight) * 100; - - // 检查NaN - if (double.IsNaN(absorptions[i]) || double.IsInfinity(absorptions[i])) - { - absorptions[i] = 0; - } + absorptions[i] = absorption; + absorptionRow[$"试样{i + 1}"] = absorption; } else { - absorptions[i] = 0; - } - if (dataTable.Columns.Contains($"试样{i + 1}")) - { - absorptionRow[$"试样{i + 1}"] = absorptions[i]; + absorptionRow[$"试样{i + 1}"] = DBNull.Value; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) @@ -709,7 +700,7 @@ namespace WindowsFormsApp6 { if (dataTable.Columns.Contains($"试样{i + 1}")) { - runSpeedRow[$"试样{i + 1}"] = $"{form2SampleDataList[i].RunSpeed}mm/min"; + runSpeedRow[$"试样{i + 1}"] = $"{form2SampleDataList[i].RunSpeed:0.##}mm/min"; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) @@ -721,111 +712,7 @@ namespace WindowsFormsApp6 } dataTable.Rows.Add(runSpeedRow); - // 计算并添加统计行:每5个试样一组 - int groupCount = (int)Math.Ceiling((double)totalSamples / 5.0); - - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) - { - int startSample = groupIndex * 5 + 1; - int endSample = Math.Min(startSample + 4, totalSamples); - - // 获取该组的吸收量数据 - List groupAbsorptions = new List(); - for (int i = startSample - 1; i < endSample; i++) - { - groupAbsorptions.Add(absorptions[i]); - } - - if (groupAbsorptions.Count > 0) - { - // 7. 液体吸收量平均值行 - DataRow avgRow = dataTable.NewRow(); - avgRow["序号"] = $"液体吸收量平均值(%) 试样{startSample}-{endSample}"; - double avgAbsorption = groupAbsorptions.Average(); - - // 检查NaN - if (double.IsNaN(avgAbsorption) || double.IsInfinity(avgAbsorption)) - { - avgAbsorption = 0; - } - - // 只在该组第一个试样列显示平均值,其他列设置为DBNull - for (int i = 1; i <= currentSampleCount; i++) - { - if (dataTable.Columns.Contains($"试样{i}")) - { - // 只在该组第一个试样列显示平均值 - if (i == startSample) - { - avgRow[$"试样{i}"] = avgAbsorption; - } - else - { - avgRow[$"试样{i}"] = DBNull.Value; - } - } - } - dataTable.Rows.Add(avgRow); - - // 8. 液体吸收量最大值行 - DataRow maxRow = dataTable.NewRow(); - maxRow["序号"] = $"液体吸收量最大值(%) 试样{startSample}-{endSample}"; - double maxAbsorption = groupAbsorptions.Max(); - - // 检查NaN - if (double.IsNaN(maxAbsorption) || double.IsInfinity(maxAbsorption)) - { - maxAbsorption = 0; - } - - // 只在该组第一个试样列显示最大值,其他列设置为DBNull - for (int i = 1; i <= currentSampleCount; i++) - { - if (dataTable.Columns.Contains($"试样{i}")) - { - // 只在该组第一个试样列显示最大值 - if (i == startSample) - { - maxRow[$"试样{i}"] = maxAbsorption; - } - else - { - maxRow[$"试样{i}"] = DBNull.Value; - } - } - } - dataTable.Rows.Add(maxRow); - - // 9. 标准偏差行 - DataRow stdDevRow = dataTable.NewRow(); - stdDevRow["序号"] = $"标准偏差 试样{startSample}-{endSample}"; - double stdDev = groupAbsorptions.Count > 1 ? CalculateStandardDeviation(groupAbsorptions.ToArray()) : 0; - - // 检查NaN - if (double.IsNaN(stdDev) || double.IsInfinity(stdDev)) - { - stdDev = 0; - } - - // 只在该组第一个试样列显示标准偏差,其他列设置为DBNull - for (int i = 1; i <= currentSampleCount; i++) - { - if (dataTable.Columns.Contains($"试样{i}")) - { - // 只在该组第一个试样列显示标准偏差 - if (i == startSample) - { - stdDevRow[$"试样{i}"] = stdDev; - } - else - { - stdDevRow[$"试样{i}"] = DBNull.Value; - } - } - } - dataTable.Rows.Add(stdDevRow); - } - } + AddAbsorptionStatRows(dataTable, absorptions, totalSamples); // 刷新显示 dataTable.AcceptChanges(); @@ -841,6 +728,7 @@ namespace WindowsFormsApp6 catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form2数据失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); } } @@ -992,24 +880,18 @@ namespace WindowsFormsApp6 // 3. 液体吸收量行 (%) DataRow absorptionRow = dataTable.NewRow(); absorptionRow["序号"] = "液体吸收量(%)"; - double[] absorptions = new double[count]; + double?[] absorptions = new double?[count]; for (int i = 0; i < count; i++) { - if (initialWeights[i] > 0) + if (TryCalculateAbsorption(initialWeights[i], afterWeights[i], out double absorption)) { - absorptions[i] = ((afterWeights[i] - initialWeights[i]) / initialWeights[i]) * 100; - - // 检查NaN - if (double.IsNaN(absorptions[i]) || double.IsInfinity(absorptions[i])) - { - absorptions[i] = 0; - } + absorptions[i] = absorption; + absorptionRow[$"试样{i + 1}"] = absorption; } else { - absorptions[i] = 0; + absorptionRow[$"试样{i + 1}"] = DBNull.Value; } - absorptionRow[$"试样{i + 1}"] = absorptions[i]; } dataTable.Rows.Add(absorptionRow); @@ -1040,58 +922,7 @@ namespace WindowsFormsApp6 } dataTable.Rows.Add(runSpeedRow); - // 计算有效试样数量(初始重量>0的试样,且吸收量不是NaN) - var validAbsorptions = absorptions - .Where((a, idx) => initialWeights[idx] > 0 && !double.IsNaN(a) && !double.IsInfinity(a)) - .ToArray(); - int validCount = validAbsorptions.Length; - - // 7. 液体吸收量平均值行 - DataRow avgRow = dataTable.NewRow(); - avgRow["序号"] = "液体吸收量平均值(%)"; - double avgAbsorption = validCount > 0 ? validAbsorptions.Average() : 0; - - // 检查NaN - if (double.IsNaN(avgAbsorption) || double.IsInfinity(avgAbsorption)) - { - avgAbsorption = 0; - } - - avgRow["试样1"] = avgAbsorption; - for (int i = 2; i <= count; i++) - { - avgRow[$"试样{i}"] = DBNull.Value; - } - dataTable.Rows.Add(avgRow); - - // 8. 液体吸收量最大值行 - DataRow maxRow = dataTable.NewRow(); - maxRow["序号"] = "液体吸收量最大值(%)"; - double maxAbsorption = validCount > 0 ? validAbsorptions.Max() : 0; - - // 检查NaN - if (double.IsNaN(maxAbsorption) || double.IsInfinity(maxAbsorption)) - { - maxAbsorption = 0; - } - - maxRow["试样1"] = maxAbsorption; - for (int i = 2; i <= count; i++) - { - maxRow[$"试样{i}"] = DBNull.Value; - } - dataTable.Rows.Add(maxRow); - - // 9. 标准偏差行 - DataRow stdDevRow = dataTable.NewRow(); - stdDevRow["序号"] = "标准偏差"; - double stdDev = validCount > 1 ? CalculateStandardDeviation(validAbsorptions) : 0; - stdDevRow["试样1"] = stdDev; - for (int i = 2; i <= count; i++) - { - stdDevRow[$"试样{i}"] = DBNull.Value; - } - dataTable.Rows.Add(stdDevRow); + AddAbsorptionStatRows(dataTable, absorptions, count); dataTable.AcceptChanges(); } @@ -1201,10 +1032,10 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局:第1行是吸水时间,第2行是吸芯高度 - if (dataTable.Rows.Count >= 2) + DataRow timeRow = FindDataRow(dataTable, FORM3_ROW_WICKING_TIME); + DataRow heightRow = FindDataRow(dataTable, FORM3_ROW_WICKING_HEIGHT); + if (timeRow != null && heightRow != null) { - DataRow timeRow = dataTable.Rows[0]; - DataRow heightRow = dataTable.Rows[1]; string columnName = $"试样{sampleIndex}_{testIndex}"; // 填充吸水时间 @@ -1235,15 +1066,15 @@ namespace WindowsFormsApp6 row["序号"] = rowName; // 填充吸水时间列 - if (row.Table.Columns.Contains("吸水时间(s)")) + if (row.Table.Columns.Contains(FORM3_ROW_WICKING_TIME)) { - row["吸水时间(s)"] = wickingTime; + row[FORM3_ROW_WICKING_TIME] = wickingTime; } // 填充吸芯高度列 - if (row.Table.Columns.Contains("吸芯高度(mm)")) + if (row.Table.Columns.Contains(FORM3_ROW_WICKING_HEIGHT)) { - row["吸芯高度(mm)"] = wickingHeight; + row[FORM3_ROW_WICKING_HEIGHT] = wickingHeight; } } } @@ -1265,6 +1096,7 @@ namespace WindowsFormsApp6 catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form3数据失败:{ex.Message}"); + HandleModbusDisconnected(ex.Message); } } @@ -1310,10 +1142,9 @@ namespace WindowsFormsApp6 if (isVerticalLayout) { // 纵向布局:第1行是吸水时间行 - if (dataTable.Rows.Count > 0) + DataRow timeRow = FindDataRow(dataTable, FORM3_ROW_WICKING_TIME); + if (timeRow != null) { - DataRow timeRow = dataTable.Rows[0]; - // 遍历所有试样的所有测试次数,找到第一个空位 for (int i = 1; i <= currentSampleCount; i++) { @@ -1323,8 +1154,7 @@ namespace WindowsFormsApp6 if (timeRow.Table.Columns.Contains(columnName)) { var value = timeRow[columnName]; - if (value == null || value == DBNull.Value || - (value is double d && Math.Abs(d) < 0.001)) + if (IsEmptyForm3Value(value)) { return Tuple.Create(i, j); } @@ -1349,11 +1179,10 @@ namespace WindowsFormsApp6 { DataRow row = dataTable.Rows[rowIndex]; - if (row.Table.Columns.Contains("吸水时间(s)")) + if (row.Table.Columns.Contains(FORM3_ROW_WICKING_TIME)) { - var value = row["吸水时间(s)"]; - if (value == null || value == DBNull.Value || - (value is double d && Math.Abs(d) < 0.001)) + var value = row[FORM3_ROW_WICKING_TIME]; + if (IsEmptyForm3Value(value)) { return Tuple.Create(i, j); } @@ -1370,6 +1199,39 @@ namespace WindowsFormsApp6 return Tuple.Create(1, 1); } + private DataRow FindDataRow(DataTable dataTable, string rowName) + { + if (dataTable == null) + { + return null; + } + + foreach (DataRow row in dataTable.Rows) + { + if (string.Equals(row["序号"]?.ToString(), rowName, StringComparison.Ordinal)) + { + return row; + } + } + + return null; + } + + private bool IsEmptyForm3Value(object value) + { + if (value == null || value == DBNull.Value) + { + return true; + } + + if (double.TryParse(value.ToString(), out double number)) + { + return Math.Abs(number) < 0.001; + } + + return false; + } + /// /// 计算标准偏差 /// @@ -1395,6 +1257,135 @@ namespace WindowsFormsApp6 return result; } + private bool TryCalculateAbsorption(double initialWeight, double afterWeight, out double absorption) + { + absorption = 0; + if (initialWeight <= 0 || + double.IsNaN(initialWeight) || + double.IsInfinity(initialWeight) || + double.IsNaN(afterWeight) || + double.IsInfinity(afterWeight)) + { + return false; + } + + absorption = ((afterWeight - initialWeight) / initialWeight) * 100; + return !double.IsNaN(absorption) && !double.IsInfinity(absorption); + } + + private IEnumerable<(int Start, int End)> GetSampleGroups(int sampleCount) + { + for (int start = 1; start <= sampleCount; start += 5) + { + yield return (start, Math.Min(start + 4, sampleCount)); + } + } + + private bool IsAbsorptionStatRow(string rowName) + { + return !string.IsNullOrEmpty(rowName) && + (rowName.StartsWith("液体吸收量平均值(%)") || + rowName.StartsWith("液体吸收量最大值(%)") || + rowName.StartsWith("标准偏差")); + } + + private void AddAbsorptionStatRows(DataTable dataTable, double?[] absorptions, int sampleCount) + { + foreach (var group in GetSampleGroups(sampleCount)) + { + double[] groupValues = Enumerable.Range(group.Start, group.End - group.Start + 1) + .Where(index => index - 1 < absorptions.Length) + .Select(index => absorptions[index - 1]) + .Where(value => value.HasValue) + .Select(value => value.Value) + .ToArray(); + + AddAbsorptionStatRow(dataTable, $"液体吸收量平均值(%) 试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 0 ? (object)groupValues.Average() : DBNull.Value); + + AddAbsorptionStatRow(dataTable, $"液体吸收量最大值(%) 试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 0 ? (object)groupValues.Max() : DBNull.Value); + + AddAbsorptionStatRow(dataTable, $"标准偏差 试样{group.Start}-{group.End}", + group.Start, sampleCount, groupValues.Length > 1 ? (object)CalculateStandardDeviation(groupValues) : DBNull.Value); + } + } + + private void AddAbsorptionStatRow(DataTable dataTable, string rowName, int startSample, int sampleCount, object statValue) + { + DataRow row = dataTable.NewRow(); + row["序号"] = rowName; + + for (int i = 1; i <= sampleCount; i++) + { + if (dataTable.Columns.Contains($"试样{i}")) + { + row[$"试样{i}"] = i == startSample ? statValue : DBNull.Value; + } + } + + dataTable.Rows.Add(row); + } + + private int FindStatStartSample(DataRow row, int sampleCount) + { + for (int i = 1; i <= sampleCount; i++) + { + string columnName = $"试样{i}"; + if (row.Table.Columns.Contains(columnName) && + row[columnName] != null && + row[columnName] != DBNull.Value) + { + return i; + } + } + + string rowName = row["序号"]?.ToString(); + int parsedStart = ParseStatStartSample(rowName, sampleCount); + if (parsedStart > 0) + { + return parsedStart; + } + + return 0; + } + + private int ParseStatStartSample(string rowName, int sampleCount) + { + if (string.IsNullOrWhiteSpace(rowName)) + { + return 0; + } + + int markerIndex = rowName.IndexOf("试样", StringComparison.Ordinal); + if (markerIndex < 0) + { + return 0; + } + + int numberStart = markerIndex + "试样".Length; + int numberEnd = numberStart; + while (numberEnd < rowName.Length && char.IsDigit(rowName[numberEnd])) + { + numberEnd++; + } + + if (numberEnd == numberStart) + { + return 0; + } + + string startText = rowName.Substring(numberStart, numberEnd - numberStart); + if (int.TryParse(startText, out int startSample) && + startSample >= 1 && + startSample <= sampleCount) + { + return startSample; + } + + return 0; + } + /// /// 显示错误消息 /// @@ -1414,6 +1405,7 @@ namespace WindowsFormsApp6 private void InitializeTabControl() { tabControl1.SelectedIndexChanged += TabControl1_SelectedIndexChanged; + panelBottom.Resize += (s, e) => CenterBottomButtons(); } private void TabControl1_SelectedIndexChanged(object sender, EventArgs e) @@ -1429,6 +1421,8 @@ namespace WindowsFormsApp6 { UpdateToggleButtonText(); } + + CenterBottomButtons(); } private void UpdateTitleForCurrentTab() @@ -1570,21 +1564,25 @@ namespace WindowsFormsApp6 if (visibleButtons.Count == 0) return; // 按钮参数 - int buttonWidth = 140; int buttonGap = 20; // 按钮之间的间距 - int buttonY = 17; + int buttonY = (panelBottom.ClientSize.Height - visibleButtons[0].Height) / 2; // 计算总宽度 - int totalWidth = visibleButtons.Count * buttonWidth + (visibleButtons.Count - 1) * buttonGap; + int totalWidth = visibleButtons.Sum(button => button.Width) + (visibleButtons.Count - 1) * buttonGap; // 计算起始X坐标(居中) int startX = (panelBottom.Width - totalWidth) / 2; + if (startX < panelBottom.Padding.Left) + { + startX = panelBottom.Padding.Left; + } // 依次设置每个按钮的位置 + int currentX = startX; for (int i = 0; i < visibleButtons.Count; i++) { - int x = startX + i * (buttonWidth + buttonGap); - visibleButtons[i].Location = new Point(x, buttonY); + visibleButtons[i].Location = new Point(currentX, buttonY); + currentX += visibleButtons[i].Width + buttonGap; } } @@ -1612,6 +1610,7 @@ namespace WindowsFormsApp6 if (saveFileDialog.ShowDialog() == DialogResult.OK) { + textBox8.Text = saveFileDialog.FileName; ExportIntegratedReport(saveFileDialog.FileName, exportOptions); } } @@ -1753,6 +1752,7 @@ namespace WindowsFormsApp6 // 更新按钮文本 UpdateToggleButtonText(); + CenterBottomButtons(); } } @@ -1766,6 +1766,7 @@ namespace WindowsFormsApp6 // 使用公共方法获取布局状态 bool isVertical = form3Instance.IsVerticalLayout(); buttonToggleLayout.Text = isVertical ? "🔄 横向布局" : "🔄 纵向布局"; + CenterBottomButtons(); } } @@ -1776,7 +1777,7 @@ namespace WindowsFormsApp6 IWorkbook workbook = new XSSFWorkbook(); // 创建单个整合的工作表 - CreateIntegratedSheet(workbook, options); + CreateIntegratedSheet(workbook, options, filePath); using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { @@ -1791,15 +1792,15 @@ namespace WindowsFormsApp6 } } - private void CreateIntegratedSheet(IWorkbook workbook, ExportOptions options) + private void CreateIntegratedSheet(IWorkbook workbook, ExportOptions options, string filePath) { ISheet sheet = workbook.CreateSheet("吸收性测试报告"); var styles = CreateReportStyles(workbook); // 获取三个表单的数据 - var sampleCountField1 = form1Instance.GetType() - .GetField("currentSampleCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - int sampleCount = sampleCountField1 != null ? (int)sampleCountField1.GetValue(form1Instance) : 5; + int form1SampleCount = GetCurrentSampleCount(form1Instance, 5); + int form2SampleCount = GetCurrentSampleCount(form2Instance, 5); + int form3SampleCount = GetCurrentSampleCount(form3Instance, 5); var dataTableField1 = form1Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); @@ -1813,6 +1814,16 @@ namespace WindowsFormsApp6 .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); DataTable dataTable3 = dataTableField3?.GetValue(form3Instance) as DataTable; + int maxDataColumn = 0; + if (options.IncludeAbsorptionTime) maxDataColumn = Math.Max(maxDataColumn, form1SampleCount); + if (options.IncludeAbsorptionAmount) maxDataColumn = Math.Max(maxDataColumn, form2SampleCount); + if (options.IncludeWickingRate) + { + maxDataColumn = Math.Max(maxDataColumn, + dataTable3 != null && dataTable3.Columns.Contains(FORM3_ROW_WICKING_TIME) ? 5 : form3SampleCount * 3); + } + int lastColumn = Math.Max(9, maxDataColumn); + int currentRow = 0; // 1. 创建总标题 @@ -1822,58 +1833,81 @@ namespace WindowsFormsApp6 titleCell.SetCellValue("吸收性测试报告"); titleCell.CellStyle = styles.titleStyle; // 为合并区域的所有单元格设置样式(总标题不需要边框,但为了一致性也设置) - for (int i = 1; i <= sampleCount + 5; i++) + for (int i = 1; i <= lastColumn; i++) { titleRow.CreateCell(i).CellStyle = styles.titleStyle; } - sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, sampleCount + 5)); + sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, lastColumn)); currentRow++; // 空行 // 2. 创建信息输入区域 - CreateInfoSection(sheet, ref currentRow, sampleCount, styles); + CreateInfoSection(sheet, ref currentRow, lastColumn, styles, filePath); currentRow++; // 空行 // 3. 创建液体吸收时间部分(根据选项) if (options.IncludeAbsorptionTime) { - CreateForm1Section(sheet, ref currentRow, sampleCount, dataTable1, styles); + CreateForm1Section(sheet, ref currentRow, form1SampleCount, dataTable1, styles); currentRow++; // 空行 } // 4. 创建液体吸收量部分(根据选项) if (options.IncludeAbsorptionAmount) { - CreateForm2Section(sheet, ref currentRow, sampleCount, dataTable2, styles); + CreateForm2Section(sheet, ref currentRow, form2SampleCount, dataTable2, styles); currentRow++; // 空行 } // 5. 创建液体芯吸速率部分(根据选项) if (options.IncludeWickingRate) { - CreateForm3Section(sheet, ref currentRow, sampleCount, dataTable3, styles); + CreateForm3Section(sheet, ref currentRow, form3SampleCount, dataTable3, styles); } // 设置列宽 sheet.SetColumnWidth(0, 20 * 256); - for (int i = 1; i <= sampleCount * 3; i++) + for (int i = 1; i <= lastColumn; i++) { sheet.SetColumnWidth(i, 12 * 256); } } + private int GetCurrentSampleCount(Form form, int defaultValue) + { + if (form == null) + { + return defaultValue; + } + + var sampleCountField = form.GetType() + .GetField("currentSampleCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (sampleCountField == null) + { + return defaultValue; + } + + object value = sampleCountField.GetValue(form); + return value is int count && count > 0 ? count : defaultValue; + } + private void CreateInfoSection(ISheet sheet, ref int currentRow, int sampleCount, - (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) + (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles, + string filePath) { // 从界面获取实际数据 string sampleName = textBox1.Text.Trim(); string materialCode = textBox2.Text.Trim(); string batchNumber = textBox3.Text.Trim(); - string operatorName = textBox4.Text.Trim(); - string instrument = textBox5.Text.Trim(); - string deviceNumber = textBox6.Text.Trim(); - string testTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + string instrument = textBox4.Text.Trim(); + string deviceNumber = textBox5.Text.Trim(); + string operatorName = textBox6.Text.Trim(); + string testTime = string.IsNullOrWhiteSpace(textBox7.Text) + ? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + : textBox7.Text.Trim(); + string dataFile = string.IsNullOrWhiteSpace(filePath) ? textBox8.Text.Trim() : filePath; // 第一行信息 IRow infoRow1 = sheet.CreateRow(currentRow++); @@ -1964,7 +1998,7 @@ namespace WindowsFormsApp6 cell27.CellStyle = styles.dataStyle; ICell cell28 = infoRow2.CreateCell(9); - cell28.SetCellValue(""); + cell28.SetCellValue(string.IsNullOrEmpty(dataFile) ? "" : dataFile); cell28.CellStyle = styles.yellowStyle; } @@ -2016,16 +2050,12 @@ namespace WindowsFormsApp6 // 如果是平均时间行,按每5个试样一组显示 if (rowName.Contains("平均")) { - // 计算有多少组(每5个试样一组) - int groupCount = (int)Math.Ceiling((double)sampleCount / 5.0); - - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) + int startSample = FindStatStartSample(dataRow, sampleCount); + if (startSample > 0) { - int startSample = groupIndex * 5 + 1; int endSample = Math.Min(startSample + 4, sampleCount); - - // 查找该组的平均值(存储在该组第一个试样的列中) ICell avgCell = row.CreateCell(startSample); + if (dataTable.Columns.Contains($"试样{startSample}") && dataRow[$"试样{startSample}"] != DBNull.Value) { if (double.TryParse(dataRow[$"试样{startSample}"].ToString(), out double value)) @@ -2134,16 +2164,12 @@ namespace WindowsFormsApp6 if (isMergedRow) { - // 按每5个试样一组显示 - int groupCount = (int)Math.Ceiling((double)sampleCount / 5.0); - - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) + int startSample = FindStatStartSample(dataRow, sampleCount); + if (startSample > 0) { - int startSample = groupIndex * 5 + 1; int endSample = Math.Min(startSample + 4, sampleCount); - - // 查找该组的统计值(存储在该组第一个试样的列中) ICell valueCell = row.CreateCell(startSample); + if (dataTable.Columns.Contains($"试样{startSample}") && dataRow[$"试样{startSample}"] != DBNull.Value) { object value = dataRow[$"试样{startSample}"]; @@ -2216,6 +2242,12 @@ namespace WindowsFormsApp6 private void CreateForm3Section(ISheet sheet, ref int currentRow, int sampleCount, DataTable dataTable, (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) { + if (dataTable != null && dataTable.Columns.Contains(FORM3_ROW_WICKING_TIME)) + { + CreateForm3HorizontalSection(sheet, ref currentRow, dataTable, styles); + return; + } + // 子标题 IRow subtitleRow = sheet.CreateRow(currentRow++); subtitleRow.Height = 500; @@ -2315,14 +2347,7 @@ namespace WindowsFormsApp6 { if (double.TryParse(dataRow[columnName].ToString(), out double value)) { - if (Math.Abs(value) >= 0.001) - { - valueCell.SetCellValue(value); - } - else - { - valueCell.SetCellValue(""); - } + valueCell.SetCellValue(value); } else { @@ -2358,14 +2383,15 @@ namespace WindowsFormsApp6 { if (double.TryParse(dataRow[columnName].ToString(), out double value)) { - if (Math.Abs(value) >= 0.001) + bool sourceRow = rowName == FORM3_ROW_WICKING_TIME || + rowName == FORM3_ROW_WICKING_HEIGHT; + if (sourceRow && Math.Abs(value) < 0.001) { - cell.SetCellValue(value); + cell.SetCellValue(""); } else { - // 所有空数据都显示为空白 - cell.SetCellValue(""); + cell.SetCellValue(value); } } else @@ -2387,6 +2413,85 @@ namespace WindowsFormsApp6 } } + private void CreateForm3HorizontalSection(ISheet sheet, ref int currentRow, DataTable dataTable, + (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) + { + string[] headers = + { + "序号", + FORM3_ROW_WICKING_TIME, + FORM3_ROW_WICKING_HEIGHT, + "芯吸速率(mm/min)", + "平均芯吸速率(mm/min)", + "标准偏差" + }; + + IRow subtitleRow = sheet.CreateRow(currentRow++); + subtitleRow.Height = 500; + ICell subtitleCell = subtitleRow.CreateCell(0); + subtitleCell.SetCellValue("液体芯吸速率"); + subtitleCell.CellStyle = styles.headerStyle; + for (int i = 1; i < headers.Length; i++) + { + subtitleRow.CreateCell(i).CellStyle = styles.headerStyle; + } + sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 0, headers.Length - 1)); + + IRow headerRow = sheet.CreateRow(currentRow++); + headerRow.Height = 400; + for (int i = 0; i < headers.Length; i++) + { + ICell cell = headerRow.CreateCell(i); + cell.SetCellValue(headers[i]); + cell.CellStyle = styles.headerStyle; + } + + if (dataTable == null || dataTable.Rows.Count == 0) + { + return; + } + + foreach (DataRow dataRow in dataTable.Rows) + { + IRow row = sheet.CreateRow(currentRow++); + row.Height = 380; + + for (int i = 0; i < headers.Length; i++) + { + ICell cell = row.CreateCell(i); + string columnName = headers[i]; + + if (dataTable.Columns.Contains(columnName) && dataRow[columnName] != DBNull.Value) + { + object value = dataRow[columnName]; + if (double.TryParse(value.ToString(), out double numValue)) + { + if ((columnName == FORM3_ROW_WICKING_TIME || + columnName == FORM3_ROW_WICKING_HEIGHT) && + Math.Abs(numValue) < 0.001) + { + cell.SetCellValue(""); + } + else + { + cell.SetCellValue(numValue); + } + } + else + { + cell.SetCellValue(value?.ToString() ?? ""); + } + } + else + { + cell.SetCellValue(""); + } + + cell.CellStyle = i == 0 ? styles.dataStyle : styles.yellowStyle; + } + } + } + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { // 停止并释放定时器 @@ -2396,20 +2501,17 @@ namespace WindowsFormsApp6 _readTimer?.Stop(); _readTimer?.Dispose(); - // 关闭Modbus和串口 + _reconnectTimer?.Stop(); + _reconnectTimer?.Dispose(); + + // 关闭Modbus TCP连接 try { - _modbusMaster?.Dispose(); - - if (_serialPort != null && _serialPort.IsOpen) - { - _serialPort.Close(); - } - _serialPort?.Dispose(); + DisposeModbusConnection(); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"关闭串口失败:{ex.Message}"); + System.Diagnostics.Debug.WriteLine($"关闭Modbus TCP失败:{ex.Message}"); } Application.Exit(); @@ -2418,6 +2520,8 @@ namespace WindowsFormsApp6 private (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) CreateReportStyles(IWorkbook workbook) { + short numberFormat = workbook.CreateDataFormat().GetFormat("0.00"); + // 标题样式 ICellStyle titleStyle = workbook.CreateCellStyle(); IFont titleFont = workbook.CreateFont(); @@ -2443,6 +2547,7 @@ namespace WindowsFormsApp6 SetBorders(dataStyle); dataStyle.Alignment = NPOIHorizontalAlignment.Center; dataStyle.VerticalAlignment = VerticalAlignment.Center; + dataStyle.DataFormat = numberFormat; // 黄色背景样式 ICellStyle yellowStyle = workbook.CreateCellStyle(); @@ -2451,6 +2556,7 @@ namespace WindowsFormsApp6 SetBorders(yellowStyle); yellowStyle.Alignment = NPOIHorizontalAlignment.Center; yellowStyle.VerticalAlignment = VerticalAlignment.Center; + yellowStyle.DataFormat = numberFormat; return (titleStyle, headerStyle, dataStyle, yellowStyle); } diff --git a/WindowsFormsApp6/WindowsFormsApp6.csproj b/WindowsFormsApp6/WindowsFormsApp6.csproj index 9892743..fee8be4 100644 --- a/WindowsFormsApp6/WindowsFormsApp6.csproj +++ b/WindowsFormsApp6/WindowsFormsApp6.csproj @@ -102,9 +102,6 @@ - - ..\packages\System.IO.Ports.8.0.0\lib\net462\System.IO.Ports.dll - ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll @@ -215,4 +212,4 @@ - \ No newline at end of file + diff --git a/WindowsFormsApp6/packages.config b/WindowsFormsApp6/packages.config index b8446d8..3f49b9f 100644 --- a/WindowsFormsApp6/packages.config +++ b/WindowsFormsApp6/packages.config @@ -18,7 +18,6 @@ - @@ -27,4 +26,4 @@ - \ No newline at end of file +