Files
WindowsFormsApp6/WindowsFormsApp6/MainForm.cs
GukSang.Jin a766061e77 更新
2026-05-06 16:06:11 +08:00

2573 lines
102 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Linq;
using System.Net.Sockets;
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;
// 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<double> form1TimeValues = new List<double>();
// Form2历史数据缓存用于累积显示
private List<Form2SampleData> form2SampleDataList = new List<Form2SampleData>();
// 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 double RunSpeed { get; set; }
}
public MainForm()
{
InitializeComponent();
InitializeTabControl();
InitializeEmbeddedForms();
InitializeTimeUpdate();
InitializeTcpModbus();
CenterInfoControls();
CenterBottomButtons();
// 监听窗体大小变化以重新居中按钮
this.Resize += (s, e) => CenterBottomButtons();
}
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();
}
/// <summary>
/// 初始化TCP和Modbus通信
/// </summary>
private void InitializeTcpModbus()
{
// 初始化读取定时器(信号量触发模式)
_readTimer = new System.Windows.Forms.Timer();
_readTimer.Interval = 100; // 100ms检查一次信号量
_readTimer.Tick += ReadTimer_Tick;
_reconnectTimer = new System.Windows.Forms.Timer();
_reconnectTimer.Interval = ModbusTcpReconnectIntervalMilliseconds;
_reconnectTimer.Tick += ReconnectTimer_Tick;
if (!TryConnectTcpModbus())
{
_reconnectTimer.Start();
}
}
/// <summary>
/// 重连定时器
/// </summary>
private void ReconnectTimer_Tick(object sender, EventArgs e)
{
if (_modbusMaster != null && IsTcpConnected())
{
_reconnectTimer.Stop();
return;
}
TryConnectTcpModbus();
}
/// <summary>
/// 尝试连接Modbus TCP设备
/// </summary>
private bool TryConnectTcpModbus()
{
if (_isConnecting)
{
return false;
}
_isConnecting = true;
try
{
DisposeModbusConnection();
var tcpClient = new TcpClient();
IAsyncResult connectResult = tcpClient.BeginConnect(ModbusTcpHost, ModbusTcpPort, null, null);
bool connected = connectResult.AsyncWaitHandle.WaitOne(ModbusTcpConnectTimeoutMilliseconds);
connectResult.AsyncWaitHandle.Close();
if (!connected)
{
tcpClient.Close();
throw new TimeoutException($"连接 {ModbusTcpHost}:{ModbusTcpPort} 超时");
}
tcpClient.EndConnect(connectResult);
tcpClient.ReceiveTimeout = 500;
tcpClient.SendTimeout = 500;
_tcpClient = tcpClient;
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
_modbusMaster.Transport.WaitToRetryMilliseconds = 200;
_modbusMaster.Transport.Retries = 1;
_modbusMaster.Transport.ReadTimeout = 500;
_reconnectTimer.Stop();
_readTimer.Start();
System.Diagnostics.Debug.WriteLine($"Modbus TCP连接成功{ModbusTcpHost}:{ModbusTcpPort}");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Modbus TCP连接失败{ex.Message}");
return false;
}
finally
{
_isConnecting = false;
}
}
/// <summary>
/// 定时读取Modbus数据信号量触发模式
/// </summary>
private void ReadTimer_Tick(object sender, EventArgs e)
{
if (_modbusMaster == null || !IsTcpConnected())
{
HandleModbusDisconnected("Modbus TCP连接不可用");
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 signalM253 = ReadCoil(slaveId, 253); // Form2 设备停止信号量
bool signalM310 = ReadCoil(slaveId, 310); // Form3信号量
// 根据信号量触发相应的数据读取
if (signalM103)
{
ReadForm1Data(slaveId);
// 读取完成后清除信号量
WriteCoil(slaveId, 103, false);
}
// Form2数据读取只有M252为true且M253为false时才读取数据
// M253为true表示设备停止防止误触发
if (signalM252 && !signalM253)
{
ReadForm2Data(slaveId);
// 读取完成后清除信号量
WriteCoil(slaveId, 252, false);
}
if (signalM310)
{
ReadForm3Data(slaveId);
// 读取完成后清除信号量
WriteCoil(slaveId, 310, false);
}
}
catch (Exception ex)
{
// 读取失败时不弹窗,避免频繁打扰用户
System.Diagnostics.Debug.WriteLine($"Modbus读取失败{ex.Message}");
HandleModbusDisconnected(ex.Message);
}
}
/// <summary>
/// 读取单个线圈状态
/// </summary>
private bool ReadCoil(byte slaveId, ushort address)
{
try
{
bool[] coils = _modbusMaster.ReadCoils(slaveId, address, 1);
return coils[0];
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"读取线圈失败:{ex.Message}");
HandleModbusDisconnected(ex.Message);
return false;
}
}
/// <summary>
/// 写入单个线圈状态
/// </summary>
private void WriteCoil(byte slaveId, ushort address, bool value)
{
try
{
_modbusMaster.WriteSingleCoil(slaveId, address, value);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"写入线圈失败:{ex.Message}");
HandleModbusDisconnected(ex.Message);
}
}
/// <summary>
/// 检查TCP连接状态
/// </summary>
private bool IsTcpConnected()
{
try
{
return _tcpClient != null && _tcpClient.Client != null && _tcpClient.Connected;
}
catch
{
return false;
}
}
/// <summary>
/// 释放当前Modbus TCP连接
/// </summary>
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;
}
}
/// <summary>
/// 进入断线重连流程
/// </summary>
private void HandleModbusDisconnected(string reason)
{
System.Diagnostics.Debug.WriteLine($"Modbus TCP断开准备重连{reason}");
_readTimer?.Stop();
DisposeModbusConnection();
if (_reconnectTimer != null && !_reconnectTimer.Enabled)
{
_reconnectTimer.Start();
}
}
/// <summary>
/// 读取Form1数据液体吸收时间
/// PLC地址D200 - 时间(s)每次测试读取1个试样2个字节
/// 信号量M103
/// 每次测试动态添加一列显示每5个试样计算一次平均值
/// </summary>
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];
// 检查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) 试样{startSample}-{endSample}";
// 只在该组第一个试样列显示平均值其他列设置为DBNull
for (int i = 1; i <= currentSampleCount; i++)
{
if (dataTable.Columns.Contains($"试样{i}"))
{
// 只在该组第一个试样列显示平均值
if (i == startSample)
{
avgRow[$"试样{i}"] = groupAvg;
}
else
{
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}");
HandleModbusDisconnected(ex.Message);
}
}
/// <summary>
/// 读取Form2数据液体吸收量
/// PLC地址D420 - 初始重量, D422 - 浸润后重量, D402 - 浸润时间, D406 - 悬挂时间, D310 - 运行速度
/// 信号量M252
/// 每次测试动态添加一列显示每5个试样计算平均值、最大值、标准偏差
/// </summary>
private void ReadForm2Data(byte slaveId)
{
try
{
// 读取初始重量D4202个寄存器浮点数单位g精度0.01g
ushort[] initialWeightReg = _modbusMaster.ReadHoldingRegisters(slaveId, 420, 2);
double initialWeight = ConvertRegistersToDouble(initialWeightReg);
initialWeight = ModbusUshortToFloat(initialWeightReg[1], initialWeightReg[0]);
// 读取浸润后重量D4222个寄存器浮点数单位g精度0.01g
ushort[] afterWeightReg = _modbusMaster.ReadHoldingRegisters(slaveId, 422, 2);
double afterWeight = ConvertRegistersToDouble(afterWeightReg);
afterWeight = ModbusUshortToFloat(afterWeightReg[1], afterWeightReg[0]);
// 读取浸润时间D4021个寄存器整数单位
ushort[] soakTimeReg = _modbusMaster.ReadHoldingRegisters(slaveId, 402, 1);
int soakTime = ConvertSingleRegisterToInt(soakTimeReg[0]);
// 读取悬挂时间D4061个寄存器整数单位
ushort[] hangTimeReg = _modbusMaster.ReadHoldingRegisters(slaveId, 406, 1);
int hangTime = ConvertSingleRegisterToInt(hangTimeReg[0]);
// 读取运行速度D3102个寄存器浮点数单位mm/min
ushort[] runSpeedReg = _modbusMaster.ReadHoldingRegisters(slaveId, 310, 2);
double runSpeed = ConvertRegistersToDouble(runSpeedReg);
runSpeed = ModbusUshortToFloat(runSpeedReg[1], runSpeedReg[0]);
// 使用反射获取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 (TryCalculateAbsorption(form2SampleDataList[i].InitialWeight, form2SampleDataList[i].AfterWeight, out double absorption))
{
absorptions[i] = absorption;
absorptionRow[$"试样{i + 1}"] = absorption;
}
else
{
absorptionRow[$"试样{i + 1}"] = DBNull.Value;
}
}
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:0.##}mm/min";
}
}
for (int i = totalSamples + 1; i <= currentSampleCount; i++)
{
if (dataTable.Columns.Contains($"试样{i}"))
{
runSpeedRow[$"试样{i}"] = DBNull.Value;
}
}
dataTable.Rows.Add(runSpeedRow);
AddAbsorptionStatRows(dataTable, absorptions, totalSamples);
// 刷新显示
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}");
HandleModbusDisconnected(ex.Message);
}
}
/// <summary>
/// 将寄存器数据转换为double用于重量数据假设寄存器值为实际值*100
/// </summary>
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;
}
/// <summary>
/// 将寄存器数据转换为int用于时间、速度等整数数据
/// </summary>
private int ConvertRegistersToInt(ushort[] registers)
{
if (registers == null || registers.Length < 2) return 0;
// 组合两个寄存器为32位整数高位在前
return (registers[0] << 16) | registers[1];
}
/// <summary>
/// 将单个寄存器数据转换为int用于时间等单寄存器整数数据
/// </summary>
private int ConvertSingleRegisterToInt(ushort register)
{
return register;
}
/// <summary>
/// 获取下一个试样索引
/// </summary>
private int GetNextSampleIndex(DataTable dataTable, int currentSampleCount)
{
if (dataTable.Rows.Count == 0) return 0;
// 查找初始重量行
var initialRow = dataTable.AsEnumerable()
.FirstOrDefault(r => r.Field<string>("序号") == "初始重量(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;
}
/// <summary>
/// 从现有DataTable中提取已有的试样数据
/// </summary>
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<string>("序号") == "初始重量(g)");
var afterRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field<string>("序号") == "浸润后重量(g)");
var soakRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field<string>("序号") == "浸润时间");
var hangRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field<string>("序号") == "悬挂时间");
var speedRow = dataTable.AsEnumerable().FirstOrDefault(r => r.Field<string>("序号") == "运行速度");
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";
}
}
}
/// <summary>
/// 更新Form2显示精确匹配GenerateMockData的逻辑
/// </summary>
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 (TryCalculateAbsorption(initialWeights[i], afterWeights[i], out double absorption))
{
absorptions[i] = absorption;
absorptionRow[$"试样{i + 1}"] = absorption;
}
else
{
absorptionRow[$"试样{i + 1}"] = DBNull.Value;
}
}
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);
AddAbsorptionStatRows(dataTable, absorptions, count);
dataTable.AcceptChanges();
}
/// <summary>
/// 读取Form3数据液体芯吸速率
/// PLC地址D200 - 吸水时间(s)D454 - 吸芯高度(mm)每次测试读取1个试样各1个寄存器用于时间2个寄存器用于高度
///
/// 数据结构说明:
/// - 每组试样包含3次测试数据试样1_1, 试样1_2, 试样1_3
/// - 每5个试样为一组显示5列
/// - 平均芯吸速率计算每个试样3次测试的平均值合并显示在该试样的第3列
/// - 标准偏差计算每组5个试样的标准偏差合并显示在第1列
/// - 试样数量动态增长
///
/// 信号量M310
/// </summary>
private void ReadForm3Data(byte slaveId)
{
try
{
// 读取吸水时间D2001个寄存器整数单位
ushort[] timeRegisters = _modbusMaster.ReadHoldingRegisters(slaveId, 212, 2);
// 读取吸芯高度D4542个寄存器浮点数单位mm精度0.01mm
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;
}
// 将寄存器值转换为实际数据
// 吸水时间:单个寄存器,整数,单位:秒
int wickingTime = timeRegisters[0];
//wickingTime = ModbusUshortToInt(timeRegisters[1], timeRegisters[0]);
// 吸芯高度2个寄存器浮点数单位mm精度0.01mm
double wickingHeight = ConvertRegistersToDouble(heightRegisters);
wickingHeight = ModbusUshortToFloat(heightRegisters[1], heightRegisters[0]);
// 检查数据有效性
if (wickingTime <= 0 || double.IsNaN(wickingHeight) || double.IsInfinity(wickingHeight))
{
System.Diagnostics.Debug.WriteLine("读取的数据无效,跳过本次读取");
return;
}
// 确定当前要填充的位置(试样索引和测试次数)
var position = GetNextForm3Position(dataTable, currentSampleCount, isVerticalLayout);
int sampleIndex = position.Item1; // 试样索引1-based
int testIndex = position.Item2; // 测试次数1-3
// Form3限制最多5组试样15次测试
const int MAX_FORM3_SAMPLES = 5;
// 如果超过最大试样数量,显示警告并停止
if (sampleIndex > MAX_FORM3_SAMPLES)
{
this.Invoke(new Action(() =>
{
MessageBox.Show($"Form3已达到最大试样数量限制{MAX_FORM3_SAMPLES}组,共{MAX_FORM3_SAMPLES * 3}次测试)\n" +
"无法继续添加数据。",
"数据已满", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}));
return;
}
// 如果需要扩展试样数量(动态增长,但不超过最大值)
if (sampleIndex > currentSampleCount && sampleIndex <= MAX_FORM3_SAMPLES)
{
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;
}
// 根据布局方式填充数据
if (isVerticalLayout)
{
// 纵向布局第1行是吸水时间第2行是吸芯高度
DataRow timeRow = FindDataRow(dataTable, FORM3_ROW_WICKING_TIME);
DataRow heightRow = FindDataRow(dataTable, FORM3_ROW_WICKING_HEIGHT);
if (timeRow != null && heightRow != null)
{
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(FORM3_ROW_WICKING_TIME))
{
row[FORM3_ROW_WICKING_TIME] = wickingTime;
}
// 填充吸芯高度列
if (row.Table.Columns.Contains(FORM3_ROW_WICKING_HEIGHT))
{
row[FORM3_ROW_WICKING_HEIGHT] = 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}");
HandleModbusDisconnected(ex.Message);
}
}
private float ModbusUshortToFloat(ushort highReg, ushort lowReg)
{
byte[] floatBytes = new byte[4];
// 高寄存器→高2字节低寄存器→低2字节与读取时的顺序一致
floatBytes[0] = (byte)(highReg >> 8);
floatBytes[1] = (byte)highReg;
floatBytes[2] = (byte)(lowReg >> 8);
floatBytes[3] = (byte)lowReg;
if (BitConverter.IsLittleEndian)
{
Array.Reverse(floatBytes);
}
return BitConverter.ToSingle(floatBytes, 0);
}
private int ModbusUshortToInt(ushort highReg, ushort lowReg)
{
byte[] floatBytes = new byte[4];
// 高寄存器→高2字节低寄存器→低2字节与读取时的顺序一致
floatBytes[0] = (byte)(highReg >> 8);
floatBytes[1] = (byte)highReg;
floatBytes[2] = (byte)(lowReg >> 8);
floatBytes[3] = (byte)lowReg;
if (BitConverter.IsLittleEndian)
{
Array.Reverse(floatBytes);
}
return BitConverter.ToInt32(floatBytes, 0);
}
/// <summary>
/// 获取Form3下一个要填充的位置试样索引和测试次数
/// 返回:(试样索引, 测试次数) - 都是1-based
/// </summary>
private Tuple<int, int> GetNextForm3Position(DataTable dataTable, int currentSampleCount, bool isVerticalLayout)
{
if (isVerticalLayout)
{
// 纵向布局第1行是吸水时间行
DataRow timeRow = FindDataRow(dataTable, FORM3_ROW_WICKING_TIME);
if (timeRow != null)
{
// 遍历所有试样的所有测试次数,找到第一个空位
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 (IsEmptyForm3Value(value))
{
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(FORM3_ROW_WICKING_TIME))
{
var value = row[FORM3_ROW_WICKING_TIME];
if (IsEmptyForm3Value(value))
{
return Tuple.Create(i, j);
}
}
}
}
}
// 所有位置都已填充返回下一个试样的第1次测试
return Tuple.Create(currentSampleCount + 1, 1);
}
// 默认返回第1个试样的第1次测试
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;
}
/// <summary>
/// 计算标准偏差
/// </summary>
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 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;
}
/// <summary>
/// 显示错误消息
/// </summary>
private void ShowErrorMsg(string message)
{
MessageBox.Show(message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
/// <summary>
/// 显示信息消息
/// </summary>
private void ShowInfoMsg(string message)
{
MessageBox.Show(message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void InitializeTabControl()
{
tabControl1.SelectedIndexChanged += TabControl1_SelectedIndexChanged;
panelBottom.Resize += (s, e) => CenterBottomButtons();
}
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();
}
CenterBottomButtons();
}
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);
}
/// <summary>
/// 动态居中底部按钮
/// </summary>
private void CenterBottomButtons()
{
// 收集所有可见的按钮
List<Button> visibleButtons = new List<Button>();
if (buttonToggleLayout.Visible) visibleButtons.Add(buttonToggleLayout);
if (button5.Visible) visibleButtons.Add(button5);
if (buttonPrint.Visible) visibleButtons.Add(buttonPrint);
if (buttonExport.Visible) visibleButtons.Add(buttonExport);
if (visibleButtons.Count == 0) return;
// 按钮参数
int buttonGap = 20; // 按钮之间的间距
int buttonY = (panelBottom.ClientSize.Height - visibleButtons[0].Height) / 2;
// 计算总宽度
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++)
{
visibleButtons[i].Location = new Point(currentX, buttonY);
currentX += visibleButtons[i].Width + buttonGap;
}
}
private void ButtonPrint_Click(object sender, EventArgs e)
{
MessageBox.Show("打印功能开发中", "提示");
}
private void ButtonExport_Click(object sender, EventArgs e)
{
// 显示导出选项对话框
var exportOptions = ShowExportOptionsDialog();
if (exportOptions == null)
{
// 用户取消了导出
return;
}
SaveFileDialog saveFileDialog = new SaveFileDialog
{
Filter = "Excel 文件 (*.xlsx)|*.xlsx",
FileName = $"测试报告_{DateTime.Now:yyyyMMdd_HHmmss}",
Title = "导出整合报告"
};
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
textBox8.Text = saveFileDialog.FileName;
ExportIntegratedReport(saveFileDialog.FileName, exportOptions);
}
}
/// <summary>
/// 显示导出选项对话框
/// </summary>
private ExportOptions ShowExportOptionsDialog()
{
// 创建对话框
Form optionsDialog = new Form
{
Text = "选择导出内容",
Width = 350,
Height = 250,
StartPosition = FormStartPosition.CenterParent,
FormBorderStyle = FormBorderStyle.FixedDialog,
MaximizeBox = false,
MinimizeBox = false
};
Label label = new Label
{
Text = "请选择要导出的测试项目:",
Location = new Point(20, 20),
AutoSize = true,
Font = new Font("微软雅黑", 10F)
};
CheckBox chkAbsorptionTime = new CheckBox
{
Text = "液体吸收时间",
Location = new Point(40, 60),
AutoSize = true,
Checked = true,
Font = new Font("微软雅黑", 9F)
};
CheckBox chkAbsorptionAmount = new CheckBox
{
Text = "液体吸收量",
Location = new Point(40, 90),
AutoSize = true,
Checked = true,
Font = new Font("微软雅黑", 9F)
};
CheckBox chkWickingRate = new CheckBox
{
Text = "液体芯吸速率",
Location = new Point(40, 120),
AutoSize = true,
Checked = true,
Font = new Font("微软雅黑", 9F)
};
Button btnOK = new Button
{
Text = "确定",
DialogResult = DialogResult.OK,
Location = new Point(100, 160),
Width = 80,
Height = 30
};
Button btnCancel = new Button
{
Text = "取消",
DialogResult = DialogResult.Cancel,
Location = new Point(190, 160),
Width = 80,
Height = 30
};
optionsDialog.Controls.AddRange(new Control[] {
label,
chkAbsorptionTime,
chkAbsorptionAmount,
chkWickingRate,
btnOK,
btnCancel
});
optionsDialog.AcceptButton = btnOK;
optionsDialog.CancelButton = btnCancel;
// 显示对话框
if (optionsDialog.ShowDialog(this) == DialogResult.OK)
{
// 检查是否至少选择了一项
if (!chkAbsorptionTime.Checked && !chkAbsorptionAmount.Checked && !chkWickingRate.Checked)
{
MessageBox.Show("请至少选择一个导出项目!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return null;
}
return new ExportOptions
{
IncludeAbsorptionTime = chkAbsorptionTime.Checked,
IncludeAbsorptionAmount = chkAbsorptionAmount.Checked,
IncludeWickingRate = chkWickingRate.Checked
};
}
return null;
}
/// <summary>
/// 导出选项类
/// </summary>
private class ExportOptions
{
public bool IncludeAbsorptionTime { get; set; }
public bool IncludeAbsorptionAmount { get; set; }
public bool IncludeWickingRate { get; set; }
}
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();
CenterBottomButtons();
}
}
/// <summary>
/// 更新切换按钮的文本
/// </summary>
private void UpdateToggleButtonText()
{
if (form3Instance != null)
{
// 使用公共方法获取布局状态
bool isVertical = form3Instance.IsVerticalLayout();
buttonToggleLayout.Text = isVertical ? "🔄 横向布局" : "🔄 纵向布局";
CenterBottomButtons();
}
}
private void ExportIntegratedReport(string filePath, ExportOptions options)
{
try
{
IWorkbook workbook = new XSSFWorkbook();
// 创建单个整合的工作表
CreateIntegratedSheet(workbook, options, filePath);
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, ExportOptions options, string filePath)
{
ISheet sheet = workbook.CreateSheet("吸收性测试报告");
var styles = CreateReportStyles(workbook);
// 获取三个表单的数据
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);
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 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. 创建总标题
IRow titleRow = sheet.CreateRow(currentRow++);
titleRow.Height = 800;
ICell titleCell = titleRow.CreateCell(0);
titleCell.SetCellValue("吸收性测试报告");
titleCell.CellStyle = styles.titleStyle;
// 为合并区域的所有单元格设置样式(总标题不需要边框,但为了一致性也设置)
for (int i = 1; i <= lastColumn; i++)
{
titleRow.CreateCell(i).CellStyle = styles.titleStyle;
}
sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, lastColumn));
currentRow++; // 空行
// 2. 创建信息输入区域
CreateInfoSection(sheet, ref currentRow, lastColumn, styles, filePath);
currentRow++; // 空行
// 3. 创建液体吸收时间部分(根据选项)
if (options.IncludeAbsorptionTime)
{
CreateForm1Section(sheet, ref currentRow, form1SampleCount, dataTable1, styles);
currentRow++; // 空行
}
// 4. 创建液体吸收量部分(根据选项)
if (options.IncludeAbsorptionAmount)
{
CreateForm2Section(sheet, ref currentRow, form2SampleCount, dataTable2, styles);
currentRow++; // 空行
}
// 5. 创建液体芯吸速率部分(根据选项)
if (options.IncludeWickingRate)
{
CreateForm3Section(sheet, ref currentRow, form3SampleCount, dataTable3, styles);
}
// 设置列宽
sheet.SetColumnWidth(0, 20 * 256);
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,
string filePath)
{
// 从界面获取实际数据
string sampleName = textBox1.Text.Trim();
string materialCode = textBox2.Text.Trim();
string batchNumber = textBox3.Text.Trim();
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++);
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(string.IsNullOrEmpty(dataFile) ? "" : dataFile);
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;
// 如果是平均时间行按每5个试样一组显示
if (rowName.Contains("平均"))
{
int startSample = FindStatStartSample(dataRow, sampleCount);
if (startSample > 0)
{
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))
{
avgCell.SetCellValue(value);
}
else
{
avgCell.SetCellValue("");
}
}
else
{
avgCell.SetCellValue("");
}
avgCell.CellStyle = styles.yellowStyle;
// 为该组的其他单元格设置样式
for (int i = startSample + 1; i <= endSample; i++)
{
row.CreateCell(i).CellStyle = styles.yellowStyle;
}
// 合并该组的单元格
if (endSample > startSample)
{
sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, startSample, endSample));
}
}
}
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)
{
int startSample = FindStatStartSample(dataRow, sampleCount);
if (startSample > 0)
{
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}"];
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 = startSample + 1; i <= endSample; i++)
{
row.CreateCell(i).CellStyle = styles.yellowStyle;
}
// 合并该组的单元格
if (endSample > startSample)
{
sheet.AddMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, startSample, endSample));
}
}
}
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)
{
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;
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))
{
valueCell.SetCellValue(value);
}
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))
{
bool sourceRow = rowName == FORM3_ROW_WICKING_TIME ||
rowName == FORM3_ROW_WICKING_HEIGHT;
if (sourceRow && Math.Abs(value) < 0.001)
{
cell.SetCellValue("");
}
else
{
cell.SetCellValue(value);
}
}
else
{
// 所有空数据都显示为空白
cell.SetCellValue("");
}
}
else
{
// 所有空数据都显示为空白
cell.SetCellValue("");
}
cell.CellStyle = styles.yellowStyle;
}
}
}
}
}
}
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)
{
// 停止并释放定时器
timeUpdateTimer?.Stop();
timeUpdateTimer?.Dispose();
_readTimer?.Stop();
_readTimer?.Dispose();
_reconnectTimer?.Stop();
_reconnectTimer?.Dispose();
// 关闭Modbus TCP连接
try
{
DisposeModbusConnection();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"关闭Modbus TCP失败{ex.Message}");
}
Application.Exit();
}
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();
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;
dataStyle.DataFormat = numberFormat;
// 黄色背景样式
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;
yellowStyle.DataFormat = numberFormat;
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;
}
}
}