利用 C# 的委托和事件实现工控上位机中窗体间的传值,这是上位机开发中非常常见的需求(比如:参数设置窗体修改值后同步到主监控窗体、报警弹窗将确认状态传回主窗体)。委托 + 事件是窗体间传值的最优方案,能避免窗体间直接耦合,符合工控软件 “模块化、易维护” 的设计原则。
核心思路
窗体间传值的本质是:数据发送方(子窗体)定义事件,数据接收方(主窗体)订阅事件,子窗体触发事件时将数据传递给主窗体,全程无需子窗体知道主窗体的存在,彻底解耦。
下面以工控上位机最典型的两个场景为例,提供完整可运行的代码示例:
场景 1:子窗体(参数设置)→ 主窗体(实时监控)传值
这是最常见的场景:操作员在子窗体修改 PLC 的阈值参数,修改后同步到主窗体显示并生效。
步骤 1:定义通用委托(或直接用内置泛型委托)
为了复用,我们先定义一个带参数的委托(也可以直接用 .NET 内置的 Action<T> 泛型委托):
// 定义传递数值参数的委托(工控场景:PLC阈值、设备参数等)
public delegate void ParameterChangedDelegate(string plcAddress, float newThreshold);
步骤 2:子窗体(参数设置窗体)代码
子窗体作为数据发送方,定义事件并在用户确认修改时触发事件:
using System;
using System.Windows.Forms;
namespace HmiFormCommunication
{
public partial class FrmParameterSetting : Form
{
// 1. 定义事件(基于自定义委托)
public event ParameterChangedDelegate ParameterChanged;
// 构造函数
public FrmParameterSetting()
{
InitializeComponent();
// 初始化控件(模拟PLC地址和当前阈值)
txtPlcAddress.Text = "D100";
txtThreshold.Text = "40.0";
}
// 确认按钮点击事件(触发参数变更事件)
private void btnConfirm_Click(object sender, EventArgs e)
{
// 1. 校验输入(工控软件必须做数据校验)
if (!float.TryParse(txtThreshold.Text, out float newThreshold))
{
MessageBox.Show("请输入有效的数值!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (string.IsNullOrWhiteSpace(txtPlcAddress.Text))
{
MessageBox.Show("PLC地址不能为空!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 2. 触发事件,传递新参数(空值保护:避免无订阅者时报错)
ParameterChanged?.Invoke(txtPlcAddress.Text, newThreshold);
// 3. 提示并关闭子窗体
MessageBox.Show("参数设置成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
this.Close();
}
// 取消按钮
private void btnCancel_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
步骤 3:主窗体(监控窗体)代码
主窗体作为数据接收方,创建子窗体时订阅事件,接收并处理传递过来的参数:
using System;
using System.Windows.Forms;
namespace HmiFormCommunication
{
public partial class FrmMainMonitor : Form
{
// 当前PLC阈值(主窗体维护的核心参数)
private float _currentThreshold = 40.0f;
private string _plcAddress = "D100";
public FrmMainMonitor()
{
InitializeComponent();
// 初始化UI显示
lblCurrentAddress.Text = $"PLC地址:{_plcAddress}";
lblCurrentThreshold.Text = $"当前阈值:{_currentThreshold}℃";
}
// 打开参数设置窗体按钮
private void btnSetParameter_Click(object sender, EventArgs e)
{
// 1. 创建子窗体实例
FrmParameterSetting frmSetting = new FrmParameterSetting();
// 2. 订阅子窗体的事件(核心:接收子窗体传递的数据)
frmSetting.ParameterChanged += FrmSetting_ParameterChanged;
// 3. 显示子窗体(模态显示,阻止主窗体操作)
frmSetting.ShowDialog();
// 4. 子窗体关闭后,可取消订阅(避免内存泄漏,工控软件必做)
frmSetting.ParameterChanged -= FrmSetting_ParameterChanged;
}
// 事件处理方法:接收子窗体传递的参数并更新主窗体
private void FrmSetting_ParameterChanged(string plcAddress, float newThreshold)
{
// 1. 更新主窗体的参数
_plcAddress = plcAddress;
_currentThreshold = newThreshold;
// 2. 更新UI显示
lblCurrentAddress.Text = $"PLC地址:{_plcAddress}";
lblCurrentThreshold.Text = $"当前阈值:{_currentThreshold}℃";
// 3. 工控场景:同步更新PLC的实际阈值(此处模拟)
UpdatePlcThreshold(plcAddress, newThreshold);
}
// 模拟更新PLC阈值(实际为Modbus/OPC写操作)
private void UpdatePlcThreshold(string address, float value)
{
lblStatus.Text = $"[{DateTime.Now:HH:mm:ss}] 已同步PLC地址{address}阈值为:{value}℃";
}
}
}
场景 2:主窗体→子窗体传初始值 + 子窗体回传确认状态
进阶场景:主窗体打开报警确认弹窗时,先传递报警信息给子窗体,子窗体确认 / 取消后将状态传回主窗体。
关键修改(子窗体):
// 1. 定义接收初始值的构造函数
public FrmAlarmConfirm(string alarmId, string alarmMsg)
{
InitializeComponent();
// 显示主窗体传递的初始报警信息
lblAlarmId.Text = $"报警ID:{alarmId}";
lblAlarmMsg.Text = $"报警信息:{alarmMsg}";
}
// 2. 定义回传确认状态的事件
public event Action<bool> AlarmConfirmed; // 用内置Action泛型委托,简化代码
// 确认按钮
private void btnConfirm_Click(object sender, EventArgs e)
{
AlarmConfirmed?.Invoke(true); // 传回“已确认”
this.Close();
}
// 取消按钮
private void btnCancel_Click(object sender, EventArgs e)
{
AlarmConfirmed?.Invoke(false); // 传回“取消”
this.Close();
}
关键修改(主窗体):
// 打开报警确认弹窗
private void btnShowAlarm_Click(object sender, EventArgs e)
{
// 1. 主窗体传递初始报警信息给子窗体
FrmAlarmConfirm frmAlarm = new FrmAlarmConfirm("TEMP001", "烘箱温度超限(85℃)");
// 2. 订阅子窗体的确认状态事件
frmAlarm.AlarmConfirmed += FrmAlarm_AlarmConfirmed;
// 3. 显示子窗体
frmAlarm.ShowDialog();
// 4. 取消订阅
frmAlarm.AlarmConfirmed -= FrmAlarm_AlarmConfirmed;
}
// 接收子窗体的确认状态
private void FrmAlarm_AlarmConfirmed(bool isConfirmed)
{
if (isConfirmed)
{
lblStatus.Text = $"[{DateTime.Now:HH:mm:ss}] 报警已确认,正在清除报警...";
// 实际逻辑:发送清除报警指令给PLC
}
else
{
lblStatus.Text = $"[{DateTime.Now:HH:mm:ss}] 用户取消了报警确认";
}
}
关键细节(工控上位机必注意)
- 1. 事件订阅 / 取消订阅:子窗体关闭后必须用
-= 取消订阅,否则会导致窗体对象无法释放,工控软件 7*24 运行时极易引发内存泄漏; - 2. 线程安全:如果传值后需要更新 UI,若事件触发在非 UI 线程(如通信线程),需用
Invoke 跨线程更新:
// 主窗体事件处理方法中加线程安全判断
private void FrmSetting_ParameterChanged(string plcAddress, float newThreshold)
{
if (this.InvokeRequired)
{
this.Invoke(new ParameterChangedDelegate(FrmSetting_ParameterChanged), plcAddress, newThreshold);
return;
}
// 后续更新UI逻辑...
}
- 3. 数据校验:工控场景下传递的参数(如 PLC 地址、阈值)必须做严格校验,避免非法值写入设备;
- 4. 泛型委托复用:优先使用
.NET 内置的 Action<T>/Func<T> 泛型委托,减少自定义委托的冗余代码。
总结
委托和事件实现窗体间传值的核心要点:
- 1. 解耦设计:发送方(子窗体)只定义事件,不依赖接收方(主窗体);接收方订阅事件,接收并处理数据;
- 2. 双向传值:主→子可通过构造函数 / 属性传初始值,子→主通过事件回传结果;
- 3. 工控适配:必须注意线程安全、内存泄漏(取消订阅)、数据校验,适配 7*24 小时运行的场景;
- 4. 简化实现:优先使用内置泛型委托(
Action<T>/Func<T>),减少自定义委托的代码量。
这种方式相比直接引用窗体对象、静态变量传值更安全、更易维护,是工控上位机窗体间通信的标准最佳实践。
阅读原文:https://mp.weixin.qq.com/s/nZLtBzhaoT50CH3TBowO_A
该文章在 2026/1/24 10:07:15 编辑过