using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Windows.Forms; using System.IO; using System.IO.Ports; using System.Linq; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.SS.Util; using NPOIBorderStyle = NPOI.SS.UserModel.BorderStyle; using NPOIHorizontalAlignment = NPOI.SS.UserModel.HorizontalAlignment; using Modbus.Device; namespace WindowsFormsApp6 { public partial class MainForm : Form { private Form1 form1Instance; private Form2 form2Instance; private Form3 form3Instance; private System.Windows.Forms.Timer timeUpdateTimer; // 串口和Modbus相关 private SerialPort _serialPort; private IModbusMaster _modbusMaster; private System.Windows.Forms.Timer _readTimer; // Form1历史数据缓存(用于累积显示) private List form1TimeValues = new List(); // Form2历史数据缓存(用于累积显示) private List form2SampleDataList = new List(); // Form2单个试样数据结构 private class Form2SampleData { public double InitialWeight { get; set; } public double AfterWeight { get; set; } public int SoakTime { get; set; } public int HangTime { get; set; } public int RunSpeed { get; set; } } public MainForm() { InitializeComponent(); InitializeTabControl(); InitializeEmbeddedForms(); InitializeTimeUpdate(); InitializeSerialPortAndModbus(); CenterInfoControls(); } private void InitializeTimeUpdate() { // 初始化时间显示 textBox7.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); textBox7.ReadOnly = true; // 设置为只读 // 创建计时器,每秒更新一次 timeUpdateTimer = new System.Windows.Forms.Timer(); timeUpdateTimer.Interval = 1000; timeUpdateTimer.Tick += (s, e) => textBox7.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); timeUpdateTimer.Start(); } /// /// 初始化串口和Modbus通信 /// private void InitializeSerialPortAndModbus() { // 初始化读取定时器(信号量触发模式) _readTimer = new System.Windows.Forms.Timer(); _readTimer.Interval = 100; // 100ms检查一次信号量 _readTimer.Tick += ReadTimer_Tick; // 尝试打开默认串口 if (!TryOpenSerialPort("COM2")) { // 如果默认串口失败,弹出端口选择对话框 ShowPortSelectionDialog(); } } /// /// 尝试打开指定的串口 /// private bool TryOpenSerialPort(string portName) { try { // 如果已有串口打开,先关闭 if (_serialPort != null && _serialPort.IsOpen) { _serialPort.Close(); _serialPort.Dispose(); } // 配置串口参数 _serialPort = new SerialPort { PortName = portName, BaudRate = 19200, DataBits = 8, Parity = Parity.None, StopBits = StopBits.One, ReadTimeout = 500, WriteTimeout = 500, RtsEnable = false, DtrEnable = false }; // 打开串口 _serialPort.Open(); // 创建Modbus RTU主站 _modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort); _modbusMaster.Transport.WaitToRetryMilliseconds = 200; _modbusMaster.Transport.Retries = 1; _modbusMaster.Transport.ReadTimeout = 500; // 启动定时读取 _readTimer.Start(); ShowInfoMsg($"串口 {portName} 初始化成功"); return true; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"打开串口 {portName} 失败:{ex.Message}"); return false; } } /// /// 显示端口选择对话框 /// private void ShowPortSelectionDialog() { // 获取当前计算机所有可用串口 string[] ports = SerialPort.GetPortNames(); if (ports == null || ports.Length == 0) { 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("未选择串口,数据采集功能将不可用。"); } } /// /// 定时读取Modbus数据(信号量触发模式) /// private void ReadTimer_Tick(object sender, EventArgs e) { if (_modbusMaster == null || _serialPort == null || !_serialPort.IsOpen) return; try { byte slaveId = 1; // PLC从站地址 // 读取三个信号量(M103, M252, M310) // Modbus地址:M103 = 103, M252 = 252, M310 = 310 // 使用ReadCoils读取线圈状态(M区) bool signalM103 = ReadCoil(slaveId, 103); // Form1信号量 bool signalM252 = ReadCoil(slaveId, 252); // Form2信号量 bool signalM310 = ReadCoil(slaveId, 310); // Form3信号量 // 根据信号量触发相应的数据读取 if (signalM103) { ReadForm1Data(slaveId); // 读取完成后清除信号量 WriteCoil(slaveId, 103, false); } if (signalM252) { ReadForm2Data(slaveId); // 读取完成后清除信号量 WriteCoil(slaveId, 252, false); } if (signalM310) { ReadForm3Data(slaveId); // 读取完成后清除信号量 WriteCoil(slaveId, 310, false); } } catch (Exception ex) { // 读取失败时不弹窗,避免频繁打扰用户 System.Diagnostics.Debug.WriteLine($"Modbus读取失败:{ex.Message}"); } } /// /// 读取单个线圈状态 /// private bool ReadCoil(byte slaveId, ushort address) { try { bool[] coils = _modbusMaster.ReadCoils(slaveId, address, 1); return coils[0]; } catch { return false; } } /// /// 写入单个线圈状态 /// private void WriteCoil(byte slaveId, ushort address, bool value) { try { _modbusMaster.WriteSingleCoil(slaveId, address, value); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"写入线圈失败:{ex.Message}"); } } /// /// 读取Form1数据(液体吸收时间) /// PLC地址:D200 - 时间(s),每次测试读取1个试样(2个字节) /// 信号量:M103 /// 每次测试动态添加一列显示,每5个试样计算一次平均值 /// private void ReadForm1Data(byte slaveId) { try { // 读取1个寄存器(2个字节)- D200 ushort[] registers = _modbusMaster.ReadHoldingRegisters(slaveId, 200, 1); // 使用反射获取Form1的私有字段 var sampleDataTableField = form1Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var currentSampleCountField = form1Instance.GetType() .GetField("currentSampleCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField == null || currentSampleCountField == null) { System.Diagnostics.Debug.WriteLine("无法获取Form1的私有字段"); return; } DataTable dataTable = sampleDataTableField.GetValue(form1Instance) as DataTable; int currentSampleCount = (int)currentSampleCountField.GetValue(form1Instance); if (dataTable == null) { System.Diagnostics.Debug.WriteLine("DataTable为空"); return; } // 转换寄存器值为实际时间(秒) double timeValue = registers[0] / 10.0; // 检查NaN if (double.IsNaN(timeValue) || double.IsInfinity(timeValue)) { System.Diagnostics.Debug.WriteLine("读取的时间值无效,跳过本次读取"); return; } // 添加到历史数据 form1TimeValues.Add(timeValue); // 新试样的索引 int newSampleIndex = form1TimeValues.Count; // 如果需要扩展列数 if (newSampleIndex > currentSampleCount) { // 添加新列到DataTable if (!dataTable.Columns.Contains($"试样{newSampleIndex}")) { dataTable.Columns.Add($"试样{newSampleIndex}", typeof(double)); } // 更新currentSampleCount currentSampleCount = newSampleIndex; currentSampleCountField.SetValue(form1Instance, currentSampleCount); // 重新初始化DataGridView以显示新列 this.Invoke(new Action(() => { var initMethod = form1Instance.GetType() .GetMethod("InitializeDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); initMethod?.Invoke(form1Instance, null); })); // 重新获取DataTable(可能已重新绑定) dataTable = sampleDataTableField.GetValue(form1Instance) as DataTable; } // 清空所有数据,重新构建 dataTable.Clear(); // 创建时间行 DataRow timeRow = dataTable.NewRow(); timeRow["序号"] = "时间(s)"; // 填充所有历史数据 for (int i = 0; i < form1TimeValues.Count; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { timeRow[$"试样{i + 1}"] = form1TimeValues[i]; } } // 其余列设为空 for (int i = form1TimeValues.Count + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { timeRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(timeRow); // 计算并添加平均值行:每5个试样一组 int totalSamples = form1TimeValues.Count; 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); // 计算该组的平均值 double groupSum = 0; int validCount = 0; for (int i = startSample; i <= endSample; i++) { groupSum += form1TimeValues[i - 1]; validCount++; } if (validCount > 0) { double groupAvg = groupSum / validCount; // 检查NaN if (double.IsNaN(groupAvg) || double.IsInfinity(groupAvg)) { groupAvg = 0; } // 创建平均值行 DataRow avgRow = dataTable.NewRow(); avgRow["序号"] = "平均时间(s)"; // 将平均值显示在该组的第一个试样列 avgRow[$"试样{startSample}"] = groupAvg; // 其他列设置为空 for (int i = 1; i <= currentSampleCount; i++) { if (i != startSample && dataTable.Columns.Contains($"试样{i}")) { avgRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(avgRow); } } // 刷新显示 dataTable.AcceptChanges(); this.Invoke(new Action(() => { var refreshMethod = form1Instance.GetType() .GetMethod("RefreshDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); refreshMethod?.Invoke(form1Instance, null); })); System.Diagnostics.Debug.WriteLine($"Form1数据读取成功:试样{newSampleIndex},时间={timeValue:F2}秒,累计{totalSamples}个试样"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form1数据失败:{ex.Message}"); } } /// /// 读取Form2数据(液体吸收量) /// PLC地址:D420 - 初始重量, D422 - 浸润后重量, D402 - 浸润时间, D406 - 悬挂时间, D310 - 运行速度 /// 信号量:M252 /// 每次测试动态添加一列显示,每5个试样计算平均值、最大值、标准偏差 /// private void ReadForm2Data(byte slaveId) { try { // 读取当前试样的数据(每次读取2个字节) // 读取初始重量(D420,2字节) ushort[] initialWeightReg = _modbusMaster.ReadHoldingRegisters(slaveId, 420, 2); double initialWeight = ConvertRegistersToDouble(initialWeightReg); // 读取浸润后重量(D422,2字节) ushort[] afterWeightReg = _modbusMaster.ReadHoldingRegisters(slaveId, 422, 2); double afterWeight = ConvertRegistersToDouble(afterWeightReg); // 读取浸润时间(D402,2字节) ushort[] soakTimeReg = _modbusMaster.ReadHoldingRegisters(slaveId, 402, 2); int soakTime = ConvertRegistersToInt(soakTimeReg); // 读取悬挂时间(D406,2字节) ushort[] hangTimeReg = _modbusMaster.ReadHoldingRegisters(slaveId, 406, 2); int hangTime = ConvertRegistersToInt(hangTimeReg); // 读取运行速度(D310,2字节) ushort[] runSpeedReg = _modbusMaster.ReadHoldingRegisters(slaveId, 310, 2); int runSpeed = ConvertRegistersToInt(runSpeedReg); // 使用反射获取Form2的私有字段 var sampleDataTableField = form2Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var currentSampleCountField = form2Instance.GetType() .GetField("currentSampleCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField == null || currentSampleCountField == null) { System.Diagnostics.Debug.WriteLine("无法获取Form2的私有字段"); return; } DataTable dataTable = sampleDataTableField.GetValue(form2Instance) as DataTable; int currentSampleCount = (int)currentSampleCountField.GetValue(form2Instance); if (dataTable == null) { System.Diagnostics.Debug.WriteLine("DataTable为空"); return; } // 添加到历史数据 form2SampleDataList.Add(new Form2SampleData { InitialWeight = initialWeight, AfterWeight = afterWeight, SoakTime = soakTime, HangTime = hangTime, RunSpeed = runSpeed }); // 新试样的索引 int newSampleIndex = form2SampleDataList.Count; // 如果需要扩展列数 if (newSampleIndex > currentSampleCount) { // 添加新列到DataTable if (!dataTable.Columns.Contains($"试样{newSampleIndex}")) { dataTable.Columns.Add($"试样{newSampleIndex}", typeof(object)); } // 更新currentSampleCount currentSampleCount = newSampleIndex; currentSampleCountField.SetValue(form2Instance, currentSampleCount); // 重新初始化DataGridView以显示新列 this.Invoke(new Action(() => { var initMethod = form2Instance.GetType() .GetMethod("InitializeDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); initMethod?.Invoke(form2Instance, null); })); // 重新获取DataTable(可能已重新绑定) dataTable = sampleDataTableField.GetValue(form2Instance) as DataTable; } // 清空所有数据,重新构建 dataTable.Clear(); int totalSamples = form2SampleDataList.Count; // 1. 初始重量行 DataRow initialRow = dataTable.NewRow(); initialRow["序号"] = "初始重量(g)"; for (int i = 0; i < totalSamples; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { initialRow[$"试样{i + 1}"] = form2SampleDataList[i].InitialWeight; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { initialRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(initialRow); // 2. 浸润后重量行 DataRow afterRow = dataTable.NewRow(); afterRow["序号"] = "浸润后重量(g)"; for (int i = 0; i < totalSamples; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { afterRow[$"试样{i + 1}"] = form2SampleDataList[i].AfterWeight; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { afterRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(afterRow); // 3. 液体吸收量行 (%) DataRow absorptionRow = dataTable.NewRow(); absorptionRow["序号"] = "液体吸收量(%)"; double[] absorptions = new double[totalSamples]; for (int i = 0; i < totalSamples; i++) { if (form2SampleDataList[i].InitialWeight > 0) { 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; } } else { absorptions[i] = 0; } if (dataTable.Columns.Contains($"试样{i + 1}")) { absorptionRow[$"试样{i + 1}"] = absorptions[i]; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { absorptionRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(absorptionRow); // 4. 浸润时间行 DataRow soakTimeRow = dataTable.NewRow(); soakTimeRow["序号"] = "浸润时间"; for (int i = 0; i < totalSamples; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { soakTimeRow[$"试样{i + 1}"] = $"{form2SampleDataList[i].SoakTime}s"; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { soakTimeRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(soakTimeRow); // 5. 悬挂时间行 DataRow hangTimeRow = dataTable.NewRow(); hangTimeRow["序号"] = "悬挂时间"; for (int i = 0; i < totalSamples; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { hangTimeRow[$"试样{i + 1}"] = $"{form2SampleDataList[i].HangTime}s"; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { hangTimeRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(hangTimeRow); // 6. 运行速度行 DataRow runSpeedRow = dataTable.NewRow(); runSpeedRow["序号"] = "运行速度"; for (int i = 0; i < totalSamples; i++) { if (dataTable.Columns.Contains($"试样{i + 1}")) { runSpeedRow[$"试样{i + 1}"] = $"{form2SampleDataList[i].RunSpeed}mm/min"; } } for (int i = totalSamples + 1; i <= currentSampleCount; i++) { if (dataTable.Columns.Contains($"试样{i}")) { runSpeedRow[$"试样{i}"] = DBNull.Value; } } 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["序号"] = "液体吸收量平均值(%)"; double avgAbsorption = groupAbsorptions.Average(); // 检查NaN if (double.IsNaN(avgAbsorption) || double.IsInfinity(avgAbsorption)) { avgAbsorption = 0; } avgRow[$"试样{startSample}"] = avgAbsorption; for (int i = 1; i <= currentSampleCount; i++) { if (i != startSample && dataTable.Columns.Contains($"试样{i}")) { avgRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(avgRow); // 8. 液体吸收量最大值行 DataRow maxRow = dataTable.NewRow(); maxRow["序号"] = "液体吸收量最大值(%)"; double maxAbsorption = groupAbsorptions.Max(); // 检查NaN if (double.IsNaN(maxAbsorption) || double.IsInfinity(maxAbsorption)) { maxAbsorption = 0; } maxRow[$"试样{startSample}"] = maxAbsorption; for (int i = 1; i <= currentSampleCount; i++) { if (i != startSample && dataTable.Columns.Contains($"试样{i}")) { maxRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(maxRow); // 9. 标准偏差行 DataRow stdDevRow = dataTable.NewRow(); stdDevRow["序号"] = "标准偏差"; double stdDev = groupAbsorptions.Count > 1 ? CalculateStandardDeviation(groupAbsorptions.ToArray()) : 0; // 检查NaN if (double.IsNaN(stdDev) || double.IsInfinity(stdDev)) { stdDev = 0; } stdDevRow[$"试样{startSample}"] = stdDev; for (int i = 1; i <= currentSampleCount; i++) { if (i != startSample && dataTable.Columns.Contains($"试样{i}")) { stdDevRow[$"试样{i}"] = DBNull.Value; } } dataTable.Rows.Add(stdDevRow); } } // 刷新显示 dataTable.AcceptChanges(); this.Invoke(new Action(() => { var refreshMethod = form2Instance.GetType() .GetMethod("RefreshDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); refreshMethod?.Invoke(form2Instance, null); })); System.Diagnostics.Debug.WriteLine($"Form2数据读取成功:试样{newSampleIndex},初始重量={initialWeight:F2}g,浸润后重量={afterWeight:F2}g,累计{totalSamples}个试样"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form2数据失败:{ex.Message}"); } } /// /// 将寄存器数据转换为double(假设寄存器值为实际值*100) /// private double ConvertRegistersToDouble(ushort[] registers) { if (registers == null || registers.Length < 2) return 0.0; // 组合两个寄存器为32位整数(高位在前) uint value = ((uint)registers[0] << 16) | registers[1]; double result = value / 100.0; // 检查NaN if (double.IsNaN(result) || double.IsInfinity(result)) { return 0.0; } return result; } /// /// 将寄存器数据转换为int /// private int ConvertRegistersToInt(ushort[] registers) { if (registers == null || registers.Length < 2) return 0; // 组合两个寄存器为32位整数(高位在前) return (registers[0] << 16) | registers[1]; } /// /// 获取下一个试样索引 /// private int GetNextSampleIndex(DataTable dataTable, int currentSampleCount) { if (dataTable.Rows.Count == 0) return 0; // 查找初始重量行 var initialRow = dataTable.AsEnumerable() .FirstOrDefault(r => r.Field("序号") == "初始重量(g)"); if (initialRow == null) return 0; // 查找第一个空的试样列 for (int i = 0; i < currentSampleCount; i++) { var value = initialRow[$"试样{i + 1}"]; if (value == null || value == DBNull.Value || (value is double d && d == 0.0)) { return i; } } // 所有列都有数据,返回下一个索引 return currentSampleCount; } /// /// 从现有DataTable中提取已有的试样数据 /// private void ExtractExistingData(DataTable dataTable, double[] initialWeights, double[] afterWeights, string[] soakTimes, string[] hangTimes, string[] runSpeeds, int excludeIndex) { if (dataTable.Rows.Count == 0) return; var initialRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field("序号") == "初始重量(g)"); var afterRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field("序号") == "浸润后重量(g)"); var soakRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field("序号") == "浸润时间"); var hangRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field("序号") == "悬挂时间"); var speedRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field("序号") == "运行速度"); for (int i = 0; i < initialWeights.Length; i++) { if (i == excludeIndex) continue; // 跳过当前要更新的索引 string colName = $"试样{i + 1}"; if (initialRow != null && initialRow.Table.Columns.Contains(colName)) { var val = initialRow[colName]; initialWeights[i] = (val != null && val != DBNull.Value) ? Convert.ToDouble(val) : 0.0; } if (afterRow != null && afterRow.Table.Columns.Contains(colName)) { var val = afterRow[colName]; afterWeights[i] = (val != null && val != DBNull.Value) ? Convert.ToDouble(val) : 0.0; } if (soakRow != null && soakRow.Table.Columns.Contains(colName)) { var val = soakRow[colName]; soakTimes[i] = (val != null && val != DBNull.Value) ? val.ToString() : "0s"; } if (hangRow != null && hangRow.Table.Columns.Contains(colName)) { var val = hangRow[colName]; hangTimes[i] = (val != null && val != DBNull.Value) ? val.ToString() : "0s"; } if (speedRow != null && speedRow.Table.Columns.Contains(colName)) { var val = speedRow[colName]; runSpeeds[i] = (val != null && val != DBNull.Value) ? val.ToString() : "0mm/min"; } } } /// /// 更新Form2显示(精确匹配GenerateMockData的逻辑) /// private void UpdateForm2Display(DataTable dataTable, double[] initialWeights, double[] afterWeights, string[] soakTimes, string[] hangTimes, string[] runSpeeds, int count) { dataTable.Clear(); // 1. 初始重量行 DataRow initialRow = dataTable.NewRow(); initialRow["序号"] = "初始重量(g)"; for (int i = 0; i < count; i++) { initialRow[$"试样{i + 1}"] = initialWeights[i]; } dataTable.Rows.Add(initialRow); // 2. 浸润后重量行 DataRow afterRow = dataTable.NewRow(); afterRow["序号"] = "浸润后重量(g)"; for (int i = 0; i < count; i++) { afterRow[$"试样{i + 1}"] = afterWeights[i]; } dataTable.Rows.Add(afterRow); // 3. 液体吸收量行 (%) DataRow absorptionRow = dataTable.NewRow(); absorptionRow["序号"] = "液体吸收量(%)"; double[] absorptions = new double[count]; for (int i = 0; i < count; i++) { if (initialWeights[i] > 0) { absorptions[i] = ((afterWeights[i] - initialWeights[i]) / initialWeights[i]) * 100; // 检查NaN if (double.IsNaN(absorptions[i]) || double.IsInfinity(absorptions[i])) { absorptions[i] = 0; } } else { absorptions[i] = 0; } absorptionRow[$"试样{i + 1}"] = absorptions[i]; } dataTable.Rows.Add(absorptionRow); // 4. 浸润时间行 DataRow soakTimeRow = dataTable.NewRow(); soakTimeRow["序号"] = "浸润时间"; for (int i = 0; i < count; i++) { soakTimeRow[$"试样{i + 1}"] = soakTimes[i]; } dataTable.Rows.Add(soakTimeRow); // 5. 悬挂时间行 DataRow hangTimeRow = dataTable.NewRow(); hangTimeRow["序号"] = "悬挂时间"; for (int i = 0; i < count; i++) { hangTimeRow[$"试样{i + 1}"] = hangTimes[i]; } dataTable.Rows.Add(hangTimeRow); // 6. 运行速度行 DataRow runSpeedRow = dataTable.NewRow(); runSpeedRow["序号"] = "运行速度"; for (int i = 0; i < count; i++) { runSpeedRow[$"试样{i + 1}"] = runSpeeds[i]; } 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); dataTable.AcceptChanges(); } /// /// 读取Form3数据(液体芯吸速率) /// PLC地址:D200 - 吸水时间(s),D454 - 吸芯高度(mm),每次测试读取1个试样(各2个字节) /// 信号量:M310 /// 每次测试添加一个试样的一次测试数据,试样数量动态增长 /// private void ReadForm3Data(byte slaveId) { try { // 读取吸水时间(D200,2个字节) ushort[] timeRegisters = _modbusMaster.ReadHoldingRegisters(slaveId, 200, 2); // 读取吸芯高度(D454,2个字节) ushort[] heightRegisters = _modbusMaster.ReadHoldingRegisters(slaveId, 454, 2); // 使用反射获取Form3的私有字段 var sampleDataTableField = form3Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var currentSampleCountField = form3Instance.GetType() .GetField("currentSampleCount", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var isVerticalLayoutField = form3Instance.GetType() .GetField("isVerticalLayout", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField == null || currentSampleCountField == null || isVerticalLayoutField == null) { System.Diagnostics.Debug.WriteLine("无法获取Form3的私有字段"); return; } DataTable dataTable = sampleDataTableField.GetValue(form3Instance) as DataTable; int currentSampleCount = (int)currentSampleCountField.GetValue(form3Instance); bool isVerticalLayout = (bool)isVerticalLayoutField.GetValue(form3Instance); if (dataTable == null || dataTable.Rows.Count == 0) { System.Diagnostics.Debug.WriteLine("DataTable为空或没有行"); return; } // 将寄存器值转换为实际数据 double wickingTime = ConvertRegistersToDouble(timeRegisters); // 吸水时间(秒) double wickingHeight = ConvertRegistersToDouble(heightRegisters); // 吸芯高度(mm) // 确定当前要填充的位置(试样索引和测试次数) var position = GetNextForm3Position(dataTable, currentSampleCount, isVerticalLayout); int sampleIndex = position.Item1; // 试样索引(1-based) int testIndex = position.Item2; // 测试次数(1-3) // 如果需要扩展试样数量 if (sampleIndex > currentSampleCount) { currentSampleCount = sampleIndex; currentSampleCountField.SetValue(form3Instance, currentSampleCount); // 重新初始化DataTable和DataGridView this.Invoke(new Action(() => { var setSampleCountMethod = form3Instance.GetType() .GetMethod("SetSampleCount", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); setSampleCountMethod?.Invoke(form3Instance, new object[] { currentSampleCount }); })); // 重新获取更新后的DataTable dataTable = sampleDataTableField.GetValue(form3Instance) as DataTable; } // 根据布局方式填充数据(精确参考GenerateMockData的方式) if (isVerticalLayout) { // 纵向布局:第1行是吸水时间,第2行是吸芯高度 if (dataTable.Rows.Count >= 2) { DataRow timeRow = dataTable.Rows[0]; DataRow heightRow = dataTable.Rows[1]; string columnName = $"试样{sampleIndex}_{testIndex}"; // 填充吸水时间 if (timeRow.Table.Columns.Contains(columnName)) { timeRow[columnName] = wickingTime; } // 填充吸芯高度 if (heightRow.Table.Columns.Contains(columnName)) { heightRow[columnName] = wickingHeight; } } } else { // 横向布局:每行是一个试样的一次测试 // 计算行索引:(sampleIndex-1) * 3 + (testIndex-1) int rowIndex = (sampleIndex - 1) * 3 + (testIndex - 1); if (rowIndex < dataTable.Rows.Count) { DataRow row = dataTable.Rows[rowIndex]; string rowName = $"试样{sampleIndex}_{testIndex}"; // 确保序号正确 row["序号"] = rowName; // 填充吸水时间列 if (row.Table.Columns.Contains("吸水时间(s)")) { row["吸水时间(s)"] = wickingTime; } // 填充吸芯高度列 if (row.Table.Columns.Contains("吸芯高度(mm)")) { row["吸芯高度(mm)"] = wickingHeight; } } } // 调用Form3的计算方法(计算芯吸速率、平均值、标准偏差) this.Invoke(new Action(() => { var calculateMethod = form3Instance.GetType() .GetMethod("CalculateAllRows", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); calculateMethod?.Invoke(form3Instance, null); var refreshMethod = form3Instance.GetType() .GetMethod("RefreshDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); refreshMethod?.Invoke(form3Instance, null); })); System.Diagnostics.Debug.WriteLine($"Form3数据读取成功 - 试样{sampleIndex}第{testIndex}次测试,吸水时间:{wickingTime:F2}秒,吸芯高度:{wickingHeight:F2}mm"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form3数据失败:{ex.Message}"); } } /// /// 获取Form3下一个要填充的位置(试样索引和测试次数) /// 返回:(试样索引, 测试次数) - 都是1-based /// private Tuple GetNextForm3Position(DataTable dataTable, int currentSampleCount, bool isVerticalLayout) { if (isVerticalLayout) { // 纵向布局:第1行是吸水时间行 if (dataTable.Rows.Count > 0) { DataRow timeRow = dataTable.Rows[0]; // 遍历所有试样的所有测试次数,找到第一个空位 for (int i = 1; i <= currentSampleCount; i++) { for (int j = 1; j <= 3; j++) { string columnName = $"试样{i}_{j}"; 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)) { return Tuple.Create(i, j); } } } } // 所有位置都已填充,返回下一个试样的第1次测试 return Tuple.Create(currentSampleCount + 1, 1); } } else { // 横向布局:每行是一个试样的一次测试 for (int i = 1; i <= currentSampleCount; i++) { for (int j = 1; j <= 3; j++) { int rowIndex = (i - 1) * 3 + (j - 1); if (rowIndex < dataTable.Rows.Count) { DataRow row = dataTable.Rows[rowIndex]; if (row.Table.Columns.Contains("吸水时间(s)")) { var value = row["吸水时间(s)"]; if (value == null || value == DBNull.Value || (value is double d && Math.Abs(d) < 0.001)) { return Tuple.Create(i, j); } } } } } // 所有位置都已填充,返回下一个试样的第1次测试 return Tuple.Create(currentSampleCount + 1, 1); } // 默认返回第1个试样的第1次测试 return Tuple.Create(1, 1); } /// /// 计算标准偏差 /// private double CalculateStandardDeviation(double[] values) { if (values == null || values.Length <= 1) return 0; // 过滤掉NaN和Infinity值 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)); double result = Math.Sqrt(sumOfSquares / (validValues.Length - 1)); // 检查结果是否为NaN if (double.IsNaN(result) || double.IsInfinity(result)) { return 0; } return result; } /// /// 显示错误消息 /// private void ShowErrorMsg(string message) { MessageBox.Show(message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// /// 显示信息消息 /// private void ShowInfoMsg(string message) { MessageBox.Show(message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void InitializeTabControl() { tabControl1.SelectedIndexChanged += TabControl1_SelectedIndexChanged; } private void TabControl1_SelectedIndexChanged(object sender, EventArgs e) { UpdateTitleForCurrentTab(); // 根据选中的 Tab 显示/隐藏切换布局按钮 // 只有在 Form3 (Tab 2, index=2) 时显示切换按钮 buttonToggleLayout.Visible = (tabControl1.SelectedIndex == 2); // 更新切换按钮的文本 if (tabControl1.SelectedIndex == 2 && form3Instance != null) { UpdateToggleButtonText(); } } private void UpdateTitleForCurrentTab() { switch (tabControl1.SelectedIndex) { case 0: label1.Text = " 液体吸收时间测试报告"; break; case 1: label1.Text = " 液体吸收量测试报告"; break; case 2: label1.Text = " 液体芯吸速率测试报告"; break; } } private void InitializeEmbeddedForms() { form1Instance = new Form1(); form1Instance.TopLevel = false; form1Instance.FormBorderStyle = FormBorderStyle.None; form1Instance.Dock = DockStyle.Fill; Panel panel1 = new Panel(); panel1.Dock = DockStyle.Fill; panel1.Controls.Add(form1Instance.Controls["tableLayoutPanel1"].Controls["panel3"]); tabPage1.Controls.Add(panel1); form1Instance.Show(); form2Instance = new Form2(); form2Instance.TopLevel = false; form2Instance.FormBorderStyle = FormBorderStyle.None; form2Instance.Dock = DockStyle.Fill; Panel panel2 = new Panel(); panel2.Dock = DockStyle.Fill; panel2.Controls.Add(form2Instance.Controls["tableLayoutPanel1"].Controls["panel3"]); tabPage2.Controls.Add(panel2); form2Instance.Show(); form3Instance = new Form3(); form3Instance.TopLevel = false; form3Instance.FormBorderStyle = FormBorderStyle.None; form3Instance.Dock = DockStyle.Fill; Panel panel3 = new Panel(); panel3.Dock = DockStyle.Fill; panel3.Controls.Add(form3Instance.Controls["tableLayoutPanel1"].Controls["panel3"]); tabPage3.Controls.Add(panel3); form3Instance.Show(); } private void panelInfo_Resize(object sender, EventArgs e) { CenterInfoControls(); } private void CenterInfoControls() { // 每列的宽度定义 int col1LabelWidth = 75; // 样品名称/仪器 int col1InputWidth = 140; int col2LabelWidth = 75; // 物料编码/设备编号 int col2InputWidth = 140; int col3LabelWidth = 75; // 批号/操作人员(操作人员需要更宽) int col3InputWidth = 140; int col4LabelWidth = 75; // 测试时间/数据文件 int col4InputWidth = 140; int gap = 15; // 列之间的间距 // 计算总宽度 int totalWidth = col1LabelWidth + col1InputWidth + gap + col2LabelWidth + col2InputWidth + gap + col3LabelWidth + col3InputWidth + gap + col4LabelWidth + col4InputWidth; int startX = (panelInfo.Width - totalWidth) / 2; // 第一行 Y 坐标 int y1 = 10; int labelOffsetY = 3; // 标签相对输入框的垂直偏移 // 第一列:样品名称 int col1X = startX; label3.Location = new Point(col1X, y1 + labelOffsetY); textBox1.Location = new Point(col1X + col1LabelWidth, y1); // 第二列:物料编码 int col2X = col1X + col1LabelWidth + col1InputWidth + gap; label4.Location = new Point(col2X, y1 + labelOffsetY); textBox2.Location = new Point(col2X + col2LabelWidth, y1); // 第三列:批号 int col3X = col2X + col2LabelWidth + col2InputWidth + gap; label5.Location = new Point(col3X, y1 + labelOffsetY); textBox3.Location = new Point(col3X + col3LabelWidth, y1); // 第四列:测试时间 int col4X = col3X + col3LabelWidth + col3InputWidth + gap; label9.Location = new Point(col4X, y1 + labelOffsetY); textBox7.Location = new Point(col4X + col4LabelWidth, y1); // 第二行 Y 坐标 int y2 = 43; // 第一列:仪器 label6.Location = new Point(col1X, y2 + labelOffsetY); textBox4.Location = new Point(col1X + col1LabelWidth, y2); // 第二列:设备编号 label7.Location = new Point(col2X, y2 + labelOffsetY); textBox5.Location = new Point(col2X + col2LabelWidth, y2); // 第三列:操作人员 label8.Location = new Point(col3X, y2 + labelOffsetY); textBox6.Location = new Point(col3X + col3LabelWidth, y2); // 第四列:数据文件 label10.Location = new Point(col4X, y2 + labelOffsetY); textBox8.Location = new Point(col4X + col4LabelWidth, y2); } private void buttonPrint_Click(object sender, EventArgs e) { MessageBox.Show("打印功能开发中", "提示"); } private void buttonExport_Click(object sender, EventArgs e) { SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = "Excel 文件 (*.xlsx)|*.xlsx", FileName = $"测试报告_{DateTime.Now:yyyyMMdd_HHmmss}", Title = "导出整合报告" }; if (saveFileDialog.ShowDialog() == DialogResult.OK) { ExportIntegratedReport(saveFileDialog.FileName); } } private void button5_Click(object sender, EventArgs e) { switch (tabControl1.SelectedIndex) { case 0: form1Instance.GenerateMockData(); break; case 1: form2Instance.GenerateMockData(); break; case 2: form3Instance.GenerateMockData(); break; } } private void buttonToggleLayout_Click(object sender, EventArgs e) { if (form3Instance != null) { // 调用 Form3 的公共切换方法 form3Instance.ToggleTableLayout(); // 更新按钮文本 UpdateToggleButtonText(); } } /// /// 更新切换按钮的文本 /// private void UpdateToggleButtonText() { if (form3Instance != null) { // 使用公共方法获取布局状态 bool isVertical = form3Instance.IsVerticalLayout(); buttonToggleLayout.Text = isVertical ? "🔄 横向布局" : "🔄 纵向布局"; } } private void ExportIntegratedReport(string filePath) { try { IWorkbook workbook = new XSSFWorkbook(); // 创建单个整合的工作表 CreateIntegratedSheet(workbook); using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } MessageBox.Show($"导出成功:{filePath}", "成功"); } catch (Exception ex) { MessageBox.Show($"导出失败:{ex.Message}", "错误"); } } private void CreateIntegratedSheet(IWorkbook workbook) { 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; var dataTableField1 = form1Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); DataTable dataTable1 = dataTableField1?.GetValue(form1Instance) as DataTable; var dataTableField2 = form2Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); DataTable dataTable2 = dataTableField2?.GetValue(form2Instance) as DataTable; var dataTableField3 = form3Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); DataTable dataTable3 = dataTableField3?.GetValue(form3Instance) as DataTable; int currentRow = 0; // 1. 创建总标题 IRow titleRow = sheet.CreateRow(currentRow++); titleRow.Height = 800; ICell titleCell = titleRow.CreateCell(0); titleCell.SetCellValue("吸收性测试报告"); titleCell.CellStyle = styles.titleStyle; // 为合并区域的所有单元格设置样式(总标题不需要边框,但为了一致性也设置) for (int i = 1; i <= sampleCount + 5; i++) { titleRow.CreateCell(i).CellStyle = styles.titleStyle; } sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, sampleCount + 5)); currentRow++; // 空行 // 2. 创建信息输入区域 CreateInfoSection(sheet, ref currentRow, sampleCount, styles); currentRow++; // 空行 // 3. 创建液体吸收时间部分 CreateForm1Section(sheet, ref currentRow, sampleCount, dataTable1, styles); currentRow++; // 空行 // 4. 创建液体吸收量部分 CreateForm2Section(sheet, ref currentRow, sampleCount, dataTable2, styles); currentRow++; // 空行 // 5. 创建液体芯吸速率部分 CreateForm3Section(sheet, ref currentRow, sampleCount, dataTable3, styles); // 设置列宽 sheet.SetColumnWidth(0, 20 * 256); for (int i = 1; i <= sampleCount * 3; i++) { sheet.SetColumnWidth(i, 12 * 256); } } private void CreateInfoSection(ISheet sheet, ref int currentRow, int sampleCount, (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) { // 从界面获取实际数据 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"); // 第一行信息 IRow infoRow1 = sheet.CreateRow(currentRow++); infoRow1.Height = 400; // 样品名称 ICell cell1 = infoRow1.CreateCell(0); cell1.SetCellValue("样品名称:"); cell1.CellStyle = styles.dataStyle; ICell cell2 = infoRow1.CreateCell(1); cell2.SetCellValue(string.IsNullOrEmpty(sampleName) ? "" : sampleName); cell2.CellStyle = styles.yellowStyle; // 为合并区域的所有单元格设置样式 infoRow1.CreateCell(2).CellStyle = styles.yellowStyle; sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 1, 2)); // 物料编码 ICell cell3 = infoRow1.CreateCell(3); cell3.SetCellValue("物料编码:"); cell3.CellStyle = styles.dataStyle; ICell cell4 = infoRow1.CreateCell(4); cell4.SetCellValue(string.IsNullOrEmpty(materialCode) ? "" : materialCode); cell4.CellStyle = styles.yellowStyle; // 为合并区域的所有单元格设置样式 infoRow1.CreateCell(5).CellStyle = styles.yellowStyle; sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 4, 5)); // 批号 ICell cell5 = infoRow1.CreateCell(6); cell5.SetCellValue("批号:"); cell5.CellStyle = styles.dataStyle; ICell cell6 = infoRow1.CreateCell(7); cell6.SetCellValue(string.IsNullOrEmpty(batchNumber) ? "" : batchNumber); cell6.CellStyle = styles.yellowStyle; // 操作人员 ICell cell7 = infoRow1.CreateCell(8); cell7.SetCellValue("操作人员:"); cell7.CellStyle = styles.dataStyle; ICell cell8 = infoRow1.CreateCell(9); cell8.SetCellValue(string.IsNullOrEmpty(operatorName) ? "" : operatorName); cell8.CellStyle = styles.yellowStyle; // 第二行信息 IRow infoRow2 = sheet.CreateRow(currentRow++); infoRow2.Height = 400; // 测试时间 ICell cell21 = infoRow2.CreateCell(0); cell21.SetCellValue("测试时间:"); cell21.CellStyle = styles.dataStyle; ICell cell22 = infoRow2.CreateCell(1); cell22.SetCellValue(testTime); cell22.CellStyle = styles.yellowStyle; // 为合并区域的所有单元格设置样式 infoRow2.CreateCell(2).CellStyle = styles.yellowStyle; sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 1, 2)); // 仪器 ICell cell23 = infoRow2.CreateCell(3); cell23.SetCellValue("仪器:"); cell23.CellStyle = styles.dataStyle; ICell cell24 = infoRow2.CreateCell(4); cell24.SetCellValue(string.IsNullOrEmpty(instrument) ? "" : instrument); cell24.CellStyle = styles.yellowStyle; // 为合并区域的所有单元格设置样式 infoRow2.CreateCell(5).CellStyle = styles.yellowStyle; sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 4, 5)); // 设备编号 ICell cell25 = infoRow2.CreateCell(6); cell25.SetCellValue("设备编号:"); cell25.CellStyle = styles.dataStyle; ICell cell26 = infoRow2.CreateCell(7); cell26.SetCellValue(string.IsNullOrEmpty(deviceNumber) ? "" : deviceNumber); cell26.CellStyle = styles.yellowStyle; // 数据文件 ICell cell27 = infoRow2.CreateCell(8); cell27.SetCellValue("数据文件:"); cell27.CellStyle = styles.dataStyle; ICell cell28 = infoRow2.CreateCell(9); cell28.SetCellValue("(报告保存路径)"); cell28.CellStyle = styles.yellowStyle; } private void CreateForm1Section(ISheet sheet, ref int currentRow, int sampleCount, DataTable dataTable, (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) { // 子标题 IRow subtitleRow = sheet.CreateRow(currentRow++); subtitleRow.Height = 500; ICell subtitleCell = subtitleRow.CreateCell(0); subtitleCell.SetCellValue("液体吸收时间"); subtitleCell.CellStyle = styles.headerStyle; // 为合并区域的所有单元格设置样式 for (int i = 1; i <= sampleCount; i++) { subtitleRow.CreateCell(i).CellStyle = styles.headerStyle; } sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 0, sampleCount)); // 表头 IRow headerRow = sheet.CreateRow(currentRow++); headerRow.Height = 400; ICell seqCell = headerRow.CreateCell(0); seqCell.SetCellValue("序号"); seqCell.CellStyle = styles.headerStyle; for (int i = 1; i <= sampleCount; i++) { ICell cell = headerRow.CreateCell(i); cell.SetCellValue($"试样{i}"); cell.CellStyle = styles.headerStyle; } // 数据行 if (dataTable != null && dataTable.Rows.Count > 0) { foreach (DataRow dataRow in dataTable.Rows) { string rowName = dataRow["序号"]?.ToString() ?? ""; if (string.IsNullOrEmpty(rowName)) continue; IRow row = sheet.CreateRow(currentRow++); row.Height = 380; ICell nameCell = row.CreateCell(0); nameCell.SetCellValue(rowName); nameCell.CellStyle = styles.dataStyle; // 如果是平均时间行,只显示一个合并的单元格 if (rowName.Contains("平均")) { ICell avgCell = row.CreateCell(1); if (dataTable.Columns.Contains("试样1") && dataRow["试样1"] != DBNull.Value) { if (double.TryParse(dataRow["试样1"].ToString(), out double value)) { avgCell.SetCellValue(value); } else { avgCell.SetCellValue(""); } } else { avgCell.SetCellValue(""); } avgCell.CellStyle = styles.yellowStyle; // 为合并区域的其他单元格设置样式 for (int i = 2; i <= sampleCount; i++) { row.CreateCell(i).CellStyle = styles.yellowStyle; } // 合并平均时间单元格 sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 1, sampleCount)); } else { // 普通数据行,显示每个试样的数据 for (int i = 1; i <= sampleCount; i++) { ICell cell = row.CreateCell(i); if (dataTable.Columns.Contains($"试样{i}") && dataRow[$"试样{i}"] != DBNull.Value) { if (double.TryParse(dataRow[$"试样{i}"].ToString(), out double value)) { cell.SetCellValue(value); } else { cell.SetCellValue(""); } } else { cell.SetCellValue(""); } cell.CellStyle = styles.yellowStyle; } } } } } private void CreateForm2Section(ISheet sheet, ref int currentRow, int sampleCount, DataTable dataTable, (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) { // 子标题 IRow subtitleRow = sheet.CreateRow(currentRow++); subtitleRow.Height = 500; ICell subtitleCell = subtitleRow.CreateCell(0); subtitleCell.SetCellValue("液体吸收量"); subtitleCell.CellStyle = styles.headerStyle; // 为合并区域的所有单元格设置样式 for (int i = 1; i <= sampleCount; i++) { subtitleRow.CreateCell(i).CellStyle = styles.headerStyle; } sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 0, sampleCount)); // 表头 IRow headerRow = sheet.CreateRow(currentRow++); headerRow.Height = 400; ICell seqCell = headerRow.CreateCell(0); seqCell.SetCellValue("序号"); seqCell.CellStyle = styles.headerStyle; for (int i = 1; i <= sampleCount; i++) { ICell cell = headerRow.CreateCell(i); cell.SetCellValue($"试样{i}"); cell.CellStyle = styles.headerStyle; } // 数据行 if (dataTable != null && dataTable.Rows.Count > 0) { foreach (DataRow dataRow in dataTable.Rows) { string rowName = dataRow["序号"]?.ToString() ?? ""; if (string.IsNullOrEmpty(rowName)) continue; IRow row = sheet.CreateRow(currentRow++); row.Height = 380; ICell nameCell = row.CreateCell(0); nameCell.SetCellValue(rowName); nameCell.CellStyle = styles.dataStyle; // 判断是否是需要合并的行(平均值、最大值、标准偏差) bool isMergedRow = rowName.Contains("平均值") || rowName.Contains("最大值") || rowName.Contains("标准偏差"); if (isMergedRow) { // 合并为一列,只显示第一个试样的值 ICell valueCell = row.CreateCell(1); if (dataTable.Columns.Contains("试样1") && dataRow["试样1"] != DBNull.Value) { object value = dataRow["试样1"]; if (value != null && double.TryParse(value.ToString(), out double numValue)) { valueCell.SetCellValue(numValue); } else { valueCell.SetCellValue(""); } } else { valueCell.SetCellValue(""); } valueCell.CellStyle = styles.yellowStyle; // 为合并区域的其他单元格设置样式 for (int i = 2; i <= sampleCount; i++) { row.CreateCell(i).CellStyle = styles.yellowStyle; } // 合并单元格 sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 1, sampleCount)); } else { // 普通数据行,显示每个试样的数据 for (int i = 1; i <= sampleCount; i++) { ICell cell = row.CreateCell(i); if (dataTable.Columns.Contains($"试样{i}") && dataRow[$"试样{i}"] != DBNull.Value) { object value = dataRow[$"试样{i}"]; if (value != null) { // 尝试转换为 double if (double.TryParse(value.ToString(), out double numValue)) { cell.SetCellValue(numValue); } else { // 如果不是数字,直接显示字符串值 cell.SetCellValue(value.ToString()); } } else { cell.SetCellValue(""); } } else { cell.SetCellValue(""); } cell.CellStyle = styles.yellowStyle; } } } } } private void CreateForm3Section(ISheet sheet, ref int currentRow, int sampleCount, DataTable dataTable, (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) styles) { // 子标题 IRow subtitleRow = sheet.CreateRow(currentRow++); subtitleRow.Height = 500; ICell subtitleCell = subtitleRow.CreateCell(0); subtitleCell.SetCellValue("液体芯吸速率"); subtitleCell.CellStyle = styles.headerStyle; // 为合并区域的所有单元格设置样式 for (int i = 1; i <= sampleCount * 3; i++) { subtitleRow.CreateCell(i).CellStyle = styles.headerStyle; } sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 0, sampleCount * 3)); // 第一行表头(纵向/横向) IRow headerRow1 = sheet.CreateRow(currentRow); headerRow1.Height = 400; ICell seqCell1 = headerRow1.CreateCell(0); seqCell1.SetCellValue("序号"); seqCell1.CellStyle = styles.headerStyle; ICell dirCell = headerRow1.CreateCell(1); dirCell.SetCellValue("纵向/横向(选择按钮)"); dirCell.CellStyle = styles.headerStyle; // 为合并区域的所有单元格设置样式 for (int i = 2; i <= sampleCount * 3; i++) { headerRow1.CreateCell(i).CellStyle = styles.headerStyle; } sheet.AddMergedRegion(new CellRangeAddress(currentRow, currentRow, 1, sampleCount * 3)); currentRow++; // 第二行表头(试样1-N,每个3列) IRow headerRow2 = sheet.CreateRow(currentRow); headerRow2.Height = 400; // 序号列在第二行也需要创建并设置样式(用于合并) headerRow2.CreateCell(0).CellStyle = styles.headerStyle; int colIndex = 1; for (int i = 1; i <= sampleCount; i++) { ICell cell = headerRow2.CreateCell(colIndex); cell.SetCellValue($"试样{i}"); cell.CellStyle = styles.headerStyle; // 为合并区域的所有单元格设置样式 headerRow2.CreateCell(colIndex + 1).CellStyle = styles.headerStyle; headerRow2.CreateCell(colIndex + 2).CellStyle = styles.headerStyle; sheet.AddMergedRegion(new CellRangeAddress(currentRow, currentRow, colIndex, colIndex + 2)); colIndex += 3; } // 合并序号列(跨两行) sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow, 0, 0)); currentRow++; // 第三行表头(1、2、3) IRow headerRow3 = sheet.CreateRow(currentRow++); headerRow3.Height = 400; colIndex = 1; for (int i = 1; i <= sampleCount; i++) { for (int j = 1; j <= 3; j++) { ICell cell = headerRow3.CreateCell(colIndex++); cell.SetCellValue(j.ToString()); cell.CellStyle = styles.headerStyle; } } // 数据行 if (dataTable != null && dataTable.Rows.Count > 0) { foreach (DataRow dataRow in dataTable.Rows) { string rowName = dataRow["序号"]?.ToString() ?? ""; if (string.IsNullOrEmpty(rowName)) continue; IRow row = sheet.CreateRow(currentRow++); row.Height = 380; ICell nameCell = row.CreateCell(0); nameCell.SetCellValue(rowName); nameCell.CellStyle = styles.dataStyle; // 判断是否是标准偏差行(需要合并为一列) bool isStdDeviationRow = rowName.Contains("标准偏差"); if (isStdDeviationRow) { // 标准偏差行:合并为一列,只显示第一个值 ICell valueCell = row.CreateCell(1); string columnName = "试样1_1"; if (dataTable.Columns.Contains(columnName) && dataRow[columnName] != DBNull.Value) { if (double.TryParse(dataRow[columnName].ToString(), out double value)) { if (Math.Abs(value) >= 0.001) { valueCell.SetCellValue(value); } else { valueCell.SetCellValue(""); } } else { valueCell.SetCellValue(""); } } else { valueCell.SetCellValue(""); } valueCell.CellStyle = styles.yellowStyle; // 为合并区域的其他单元格设置样式 for (int i = 2; i <= sampleCount * 3; i++) { row.CreateCell(i).CellStyle = styles.yellowStyle; } // 合并单元格 sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 1, sampleCount * 3)); } else { // 普通数据行:显示每个试样的每次测试数据 colIndex = 1; for (int i = 1; i <= sampleCount; i++) { for (int j = 1; j <= 3; j++) { ICell cell = row.CreateCell(colIndex++); string columnName = $"试样{i}_{j}"; if (dataTable.Columns.Contains(columnName) && dataRow[columnName] != DBNull.Value) { if (double.TryParse(dataRow[columnName].ToString(), out double value)) { if (Math.Abs(value) >= 0.001) { cell.SetCellValue(value); } else { // 所有空数据都显示为空白 cell.SetCellValue(""); } } else { // 所有空数据都显示为空白 cell.SetCellValue(""); } } else { // 所有空数据都显示为空白 cell.SetCellValue(""); } cell.CellStyle = styles.yellowStyle; } } } } } } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { // 停止并释放定时器 timeUpdateTimer?.Stop(); timeUpdateTimer?.Dispose(); _readTimer?.Stop(); _readTimer?.Dispose(); // 关闭Modbus和串口 try { _modbusMaster?.Dispose(); if (_serialPort != null && _serialPort.IsOpen) { _serialPort.Close(); } _serialPort?.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"关闭串口失败:{ex.Message}"); } Application.Exit(); } private (ICellStyle titleStyle, ICellStyle headerStyle, ICellStyle dataStyle, ICellStyle yellowStyle) CreateReportStyles(IWorkbook workbook) { // 标题样式 ICellStyle titleStyle = workbook.CreateCellStyle(); IFont titleFont = workbook.CreateFont(); titleFont.FontHeightInPoints = 16; titleFont.IsBold = true; titleStyle.SetFont(titleFont); titleStyle.Alignment = NPOIHorizontalAlignment.Center; titleStyle.VerticalAlignment = VerticalAlignment.Center; // 表头样式(灰色背景) ICellStyle headerStyle = workbook.CreateCellStyle(); headerStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Grey25Percent.Index; headerStyle.FillPattern = FillPattern.SolidForeground; SetBorders(headerStyle); IFont headerFont = workbook.CreateFont(); headerFont.IsBold = true; headerStyle.SetFont(headerFont); headerStyle.Alignment = NPOIHorizontalAlignment.Center; headerStyle.VerticalAlignment = VerticalAlignment.Center; // 数据样式(白色背景) ICellStyle dataStyle = workbook.CreateCellStyle(); SetBorders(dataStyle); dataStyle.Alignment = NPOIHorizontalAlignment.Center; dataStyle.VerticalAlignment = VerticalAlignment.Center; // 黄色背景样式 ICellStyle yellowStyle = workbook.CreateCellStyle(); yellowStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.LightYellow.Index; yellowStyle.FillPattern = FillPattern.SolidForeground; SetBorders(yellowStyle); yellowStyle.Alignment = NPOIHorizontalAlignment.Center; yellowStyle.VerticalAlignment = VerticalAlignment.Center; return (titleStyle, headerStyle, dataStyle, yellowStyle); } private void SetBorders(ICellStyle style) { style.BorderBottom = NPOIBorderStyle.Thin; style.BorderTop = NPOIBorderStyle.Thin; style.BorderLeft = NPOIBorderStyle.Thin; style.BorderRight = NPOIBorderStyle.Thin; } } }