using System; 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; 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) /// 信号量:M103 /// private void ReadForm1Data(byte slaveId) { try { // 读取D200寄存器(时间数据) // 假设有5个试样,每个试样1个时间值 ushort[] registers = _modbusMaster.ReadHoldingRegisters(slaveId, 200, 5); // 使用反射获取Form1的私有字段 var sampleDataTableField = form1Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField != null) { DataTable dataTable = sampleDataTableField.GetValue(form1Instance) as DataTable; if (dataTable != null) { // 清除旧的平均值行 var avgRows = dataTable.Select("序号 = '平均时间(s)'"); foreach (var row in avgRows) { dataTable.Rows.Remove(row); } // 创建新数据行 DataRow dataRow = dataTable.NewRow(); dataRow["序号"] = "时间(s)"; // 填充时间数据(将寄存器值转换为实际时间,假设寄存器值为整数秒*10) for (int i = 0; i < Math.Min(registers.Length, 5); i++) { double timeValue = registers[i] / 10.0; // 转换为秒 dataRow[$"试样{i + 1}"] = timeValue; } dataTable.Rows.Add(dataRow); // 计算并添加平均值行 double totalAvg = 0; int count = 0; for (int i = 0; i < Math.Min(registers.Length, 5); i++) { totalAvg += registers[i] / 10.0; count++; } if (count > 0) { DataRow avgRow = dataTable.NewRow(); avgRow["序号"] = "平均时间(s)"; avgRow["试样1"] = totalAvg / count; for (int i = 2; i <= 5; i++) { avgRow[$"试样{i}"] = DBNull.Value; } dataTable.Rows.Add(avgRow); } // 刷新显示 dataTable.AcceptChanges(); this.Invoke(new Action(() => { // 触发Form1的界面刷新 var refreshMethod = form1Instance.GetType() .GetMethod("RefreshDataGridView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); refreshMethod?.Invoke(form1Instance, null); })); } } System.Diagnostics.Debug.WriteLine($"Form1数据读取成功:{registers.Length}个寄存器"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form1数据失败:{ex.Message}"); } } /// /// 读取Form2数据(液体吸收量) /// PLC地址:D420 - 初始重量, D422 - 浸润后重量, D402 - 浸润时间, D406 - 悬挂时间, D310 - 运行速度 /// 信号量:M252 /// private void ReadForm2Data(byte slaveId) { try { // 读取初始重量(D420开始,5个试样) ushort[] initialWeights = _modbusMaster.ReadHoldingRegisters(slaveId, 420, 5); // 读取浸润后重量(D422开始,5个试样) ushort[] afterWeights = _modbusMaster.ReadHoldingRegisters(slaveId, 422, 5); // 读取浸润时间(D402开始,5个试样) ushort[] soakTimes = _modbusMaster.ReadHoldingRegisters(slaveId, 402, 5); // 读取悬挂时间(D406开始,5个试样) ushort[] hangTimes = _modbusMaster.ReadHoldingRegisters(slaveId, 406, 5); // 读取运行速度(D310开始,5个试样) ushort[] runSpeeds = _modbusMaster.ReadHoldingRegisters(slaveId, 310, 5); // 使用反射获取Form2的私有字段 var sampleDataTableField = form2Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField != null) { DataTable dataTable = sampleDataTableField.GetValue(form2Instance) as DataTable; if (dataTable != null) { dataTable.Clear(); int count = Math.Min(initialWeights.Length, 5); // 转换数据(假设寄存器值为实际值*100) double[] initialWeightValues = new double[count]; double[] afterWeightValues = new double[count]; for (int i = 0; i < count; i++) { initialWeightValues[i] = initialWeights[i] / 100.0; afterWeightValues[i] = afterWeights[i] / 100.0; } // 1. 初始重量行 DataRow initialRow = dataTable.NewRow(); initialRow["序号"] = "初始重量(g)"; for (int i = 0; i < count; i++) { initialRow[$"试样{i + 1}"] = initialWeightValues[i]; } dataTable.Rows.Add(initialRow); // 2. 浸润后重量行 DataRow afterRow = dataTable.NewRow(); afterRow["序号"] = "浸润后重量(g)"; for (int i = 0; i < count; i++) { afterRow[$"试样{i + 1}"] = afterWeightValues[i]; } dataTable.Rows.Add(afterRow); // 3. 液体吸收量行 (%) DataRow absorptionRow = dataTable.NewRow(); absorptionRow["序号"] = "液体吸收量(%)"; double[] absorptions = new double[count]; for (int i = 0; i < count; i++) { absorptions[i] = ((afterWeightValues[i] - initialWeightValues[i]) / initialWeightValues[i]) * 100; 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]}s"; } dataTable.Rows.Add(soakTimeRow); // 5. 悬挂时间行(每个试样独立的悬挂时间) DataRow hangTimeRow = dataTable.NewRow(); hangTimeRow["序号"] = "悬挂时间"; for (int i = 0; i < count; i++) { hangTimeRow[$"试样{i + 1}"] = $"{hangTimes[i]}s"; } dataTable.Rows.Add(hangTimeRow); // 6. 运行速度行(每个试样独立的运行速度) DataRow runSpeedRow = dataTable.NewRow(); runSpeedRow["序号"] = "运行速度"; for (int i = 0; i < count; i++) { runSpeedRow[$"试样{i + 1}"] = $"{runSpeeds[i]}mm/min"; } dataTable.Rows.Add(runSpeedRow); // 7. 液体吸收量平均值行 DataRow avgRow = dataTable.NewRow(); avgRow["序号"] = "液体吸收量平均值(%)"; double avgAbsorption = absorptions.Average(); 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 = absorptions.Max(); 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 = CalculateStandardDeviation(absorptions); stdDevRow["试样1"] = stdDev; for (int i = 2; i <= count; 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数据读取成功"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form2数据失败:{ex.Message}"); } } /// /// 读取Form3数据(液体芯吸速率) /// PLC地址:D212 - 吸水时间(s) /// 信号量:M310 /// private void ReadForm3Data(byte slaveId) { try { // 读取吸水时间(D212开始,5个试样 * 3次测试 = 15个寄存器) ushort[] wickingTimes = _modbusMaster.ReadHoldingRegisters(slaveId, 212, 15); // 使用反射获取Form3的私有字段 var sampleDataTableField = form3Instance.GetType() .GetField("sampleDataTable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (sampleDataTableField != null) { DataTable dataTable = sampleDataTableField.GetValue(form3Instance) as DataTable; if (dataTable != null && dataTable.Rows.Count > 0) { // 获取第一行(吸水时间行) DataRow timeRow = dataTable.Rows[0]; // 填充吸水时间数据(假设寄存器值为实际时间*10) int registerIndex = 0; for (int i = 1; i <= 5; i++) { if (registerIndex < wickingTimes.Length) { timeRow[$"试样{i}_1"] = wickingTimes[registerIndex++] / 10.0; } if (registerIndex < wickingTimes.Length) { timeRow[$"试样{i}_2"] = wickingTimes[registerIndex++] / 10.0; } if (registerIndex < wickingTimes.Length) { timeRow[$"试样{i}_3"] = wickingTimes[registerIndex++] / 10.0; } } // 调用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数据读取成功"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"读取Form3数据失败:{ex.Message}"); } } /// /// 计算标准偏差 /// 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)); return Math.Sqrt(sumOfSquares / (values.Length - 1)); } /// /// 显示错误消息 /// 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; } } }