本文纯理论 + 标准案例,脱离你之前的业务代码,从委托、事件原理 → 单参数传值 → 多参数传值 → 一对多传值 → 跨窗口常见坑 完整讲解,适合系统学习 WinForm 窗口通信。


一、先搞懂核心概念

1. 什么是委托(Delegate)

委托 = 类型安全的方法容器 / 方法指针 作用:可以把一个方法当成参数、变量来传递。 简单理解:委托规定了「方法的格式」(返回值、参数列表),只有格式匹配的方法,才能赋值给这个委托。

2. 什么是事件(Event)

事件是委托的封装版本,专门用来做「发布 - 订阅」:

  • 发布者:定义事件,满足条件时触发事件(向外发数据)
  • 订阅者:绑定事件,事件触发时执行自己的方法(接收数据)

3. 窗口传值整体思路

  1. 子窗口(发布方):定义委托 + 事件
  2. 子窗口内部:收集要传递的数据,在合适时机触发事件,把数据丢出去;
  3. 主窗口 / 其他窗口(订阅方):打开子窗口前,订阅子窗口的事件
  4. 事件触发后,订阅方的方法自动执行,拿到传递的数据。

优势: 两个窗口互不依赖,不需要互相持有对方窗体引用,耦合度极低,是 WinForm 多窗口传值最标准、工业项目最常用方案。


二、前置知识:系统内置委托

.NET 提供了两套通用委托,99% 传值场景直接用,不用自己定义委托:

  1. Action 系列:无返回值,只用来执行方法 / 传数据(传值首选)
    • Action:无参数、无返回值
    • Action<T>:1 个参数
    • Action<T1,T2>:2 个参数
    • Action<T1,T2,T3>:多参数,依次类推
  2. Func 系列:有返回值,适合需要回调结果的场景

窗口传值几乎全部使用 Action,下面案例统一使用内置 Action


三、案例 1:基础版 两个窗口 + 传递单个字符串

场景

  • Form1:主窗口,有按钮「打开子窗口」、文本框(用来显示收到的值)
  • Form2:子窗口,有输入框、按钮「发送数据给主窗口」
  • 流程:打开子窗口 → 子窗口输入内容 → 点击发送 → 主窗口接收并显示。

第一步:创建项目与两个窗口

  1. 新建 Windows 窗体应用 (.NET Framework)
  2. 自动生成 Form1,手动添加新窗体 Form2

1. 设计 Form2(子窗口:发布者)

界面控件:

  • TextBox → 命名:txtInput(输入要传递的内容)
  • Button → 命名:btnSend(发送按钮)

Form2.cs 完整代码(核心:定义事件 + 触发事件)

using System;
using System.Windows.Forms;

namespace WinFormDelegateDemo
{
    public partial class Form2 : Form
    {
        // ========== 核心1:定义事件(使用内置 Action<string> 委托) ==========
        // 含义:这个事件可以传递 1 个 string 类型数据
        public event Action<string> SendDataEvent;

        public Form2()
        {
            InitializeComponent();
        }

        // 发送按钮点击事件
        private void btnSend_Click(object sender, EventArgs e)
        {
            // 1. 获取子窗口输入框内容
            string msg = txtInput.Text.Trim();

            // 2. ========== 核心2:触发事件,向外传递数据 ==========
            // ?.Invoke 语法:如果没有任何窗口订阅事件,不会报错(空安全)
            SendDataEvent?.Invoke(msg);

            // 可选:发送后关闭子窗口
            this.Close();
        }
    }
}

2. 设计 Form1(主窗口:订阅者)

界面控件:

  • Button → 命名:btnOpenForm2(打开子窗口)
  • TextBox → 命名:txtShow(显示接收到的数据)

Form1.cs 完整代码(核心:订阅事件 + 接收数据)

using System;
using System.Windows.Forms;

namespace WinFormDelegateDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // 打开子窗口按钮
        private void btnOpenForm2_Click(object sender, EventArgs e)
        {
            // 1. 实例化子窗口
            Form2 form2 = new Form2();

            // ========== 核心3:订阅子窗口的事件 ==========
            // 当 form2 的 SendDataEvent 触发时,执行后面的方法
            form2.SendDataEvent += ReceiveData;

            // 模态打开子窗口(阻塞主窗口,子窗口关闭后主窗口继续执行)
            form2.ShowDialog();

            // ========== 可选:取消订阅(规范写法,防止内存泄漏) ==========
            form2.SendDataEvent -= ReceiveData;
        }

        // ========== 核心4:接收数据的回调方法 ==========
        // 方法签名必须和事件委托一致:参数 string,无返回值
        private void ReceiveData(string content)
        {
            // 把收到的数据显示到主窗口文本框
            txtShow.Text = content;
        }
    }
}

运行流程拆解(逐行逻辑)

  1. 点击 Form1「打开子窗口」→ 创建 Form2 对象;
  2. Form1 订阅 Form2SendDataEvent 事件;
  3. 弹出 Form2,用户在输入框写内容;
  4. 点击 Form2「发送」→ 执行 SendDataEvent?.Invoke(内容)触发事件
  5. 所有订阅了该事件的方法自动执行 → Form1ReceiveData 被调用,拿到字符串;
  6. 数据展示在主窗口。

四、案例 2:进阶版 传递多个不同类型参数

实际开发经常需要传多个值(文本、数字、布尔等),分两种实现方式:

方式 A:直接使用多参数 Action(简单推荐)

场景

子窗口传递 姓名 (string) + 年龄 (int) 两个数据给主窗口。

1. 修改 Form2(子窗口)

只改事件定义和触发部分:

public partial class Form2 : Form
{
    // 定义事件:传递 string + int 两个参数
    public event Action<string, int> SendMultiDataEvent;

    private void btnSend_Click(object sender, EventArgs e)
    {
        string name = txtName.Text.Trim();
        // 简单转换(实际项目建议加 TryParse 防报错)
        int age = int.Parse(txtAge.Text.Trim());

        // 触发事件,传递两个参数
        SendMultiDataEvent?.Invoke(name, age);
        this.Close();
    }
}

2. 修改 Form1(主窗口)

订阅事件 + 编写多参数回调方法:

private void btnOpenForm2_Click(object sender, EventArgs e)
{
    Form2 form2 = new Form2();
    // 订阅多参数事件
    form2.SendMultiDataEvent += ReceiveMultiData;
    form2.ShowDialog();
    form2.SendMultiDataEvent -= ReceiveMultiData;
}

// 回调方法:参数列表必须和事件完全一致
private void ReceiveMultiData(string name, int age)
{
    txtShow.Text = $"姓名:{name},年龄:{age}";
}

方式 B:自定义实体类传复杂数据

如果参数超过 3 个、或数据结构复杂(多个字段),推荐封装实体类,可读性最强。

1. 新建实体类(单独文件)

namespace WinFormDelegateDemo
{
    // 自定义数据实体,封装要传递的所有字段
    public class UserInfo
    {
        public string UserName { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }
    }
}

2. Form2 中定义事件(传实体对象)

public partial class Form2 : Form
{
    // 事件:传递一个 UserInfo 对象
    public event Action<UserInfo> SendUserInfoEvent;

    private void btnSend_Click(object sender, EventArgs e)
    {
        // 封装数据到实体类
        UserInfo info = new UserInfo
        {
            UserName = txtName.Text.Trim(),
            Age = int.Parse(txtAge.Text.Trim()),
            Address = txtAddr.Text.Trim()
        };

        // 触发事件,传递整个实体
        SendUserInfoEvent?.Invoke(info);
        this.Close();
    }
}

3. Form1 接收实体数据

private void btnOpenForm2_Click(object sender, EventArgs e)
{
    Form2 form2 = new Form2();
    form2.SendUserInfoEvent += ReceiveUserInfo;
    form2.ShowDialog();
    form2.SendUserInfoEvent -= ReceiveUserInfo;
}

private void ReceiveUserInfo(UserInfo info)
{
    txtShow.Text = $"姓名:{info.UserName},年龄:{info.Age},地址:{info.Address}";
}

五、案例 3:一对多传值(一个子窗口 → 多个窗口接收)

事件本质是多播委托,支持多个窗口同时订阅同一个事件,实现一对多广播。

场景

  • Form1(主窗口)、Form3(日志窗口)同时订阅 Form2 的事件;
  • 子窗口发送一次数据,两个窗口同时收到。

1. Form3(新增第三个窗口,作为第二个订阅者)

using System;
using System.Windows.Forms;

namespace WinFormDelegateDemo
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        // 接收数据的方法
        public void LogData(string msg)
        {
            txtLog.AppendText($"收到消息:{msg}\r\n");
        }
    }
}

2. Form1 同时打开 Form2 + Form3,并双双订阅事件

private void btnOpenForm2_Click(object sender, EventArgs e)
{
    Form2 form2 = new Form2();
    Form3 form3 = new Form3();
    form3.Show(); // 打开日志窗口

    // 订阅1:主窗口接收数据
    form2.SendDataEvent += ReceiveData;
    // 订阅2:日志窗口接收数据
    form2.SendDataEvent += form3.LogData;

    form2.ShowDialog();

    // 取消订阅
    form2.SendDataEvent -= ReceiveData;
    form2.SendDataEvent -= form3.LogData;
}

效果:子窗口发送一条数据,Form1、Form3 同时收到


六、两种订阅写法:方法组 / Lambda 表达式

上面案例用的是「方法组订阅」,实际开发中还有常用的 Lambda 写法,两种等价。

1. 传统方法组(适合逻辑复杂)

form2.SendDataEvent += ReceiveData;

2. Lambda 表达式(适合简单逻辑,代码紧凑)

不用单独写回调方法,直接在行内处理:

private void btnOpenForm2_Click(object sender, EventArgs e)
{
    Form2 form2 = new Form2();

    // Lambda 直接接收参数并处理
    form2.SendDataEvent += (content) =>
    {
        txtShow.Text = content;
    };

    form2.ShowDialog();
}

区别:简单赋值、简短逻辑用 Lambda;复杂业务逻辑、需要复用的方法用传统方法组。


七、WinForm 委托传值 核心避坑

坑 1:触发事件必须用 ?.Invoke

// ✅ 正确:如果没有任何订阅者,不会报错
SendDataEvent?.Invoke(msg);

// ❌ 错误:无订阅者时,事件为 null,直接抛 空引用异常
SendDataEvent.Invoke(msg);

坑 2:一定要「取消订阅」,防止内存泄漏

WinForm 窗体托管在内存中,如果只订阅、不取消: 事件会持有窗体方法的引用,导致窗体关闭后无法被 GC 回收,长期运行程序内存越来越高。

规范写法:

form2.XXXEvent += 方法;  // 订阅
form2.ShowDialog();
form2.XXXEvent -= 方法;  // 取消订阅

坑 3:跨线程 UI 异常

如果子窗口是异步线程、后台线程触发事件,在回调方法里直接操作主窗口控件,会报错: “线程间操作无效:从不是创建控件的线程访问它”

解决方案:使用 Invoke 回到 UI 线程

通用模板:

private void ReceiveData(string content)
{
    // 判断是否需要跨线程
    if (txtShow.InvokeRequired)
    {
        // 回到UI线程执行
        txtShow.Invoke(new Action(() =>
        {
            txtShow.Text = content;
        }));
    }
    else
    {
        txtShow.Text = content;
    }
}

说明:ShowDialog() 模态窗口一般不会触发跨线程问题;Show() 非模态、后台线程调用必须加 Invoke

坑 4:事件定义位置错误

事件必须定义在「数据来源窗口」(子窗口),不能定义在接收窗口。 规则:谁发数据,谁定义事件


八、总结:标准开发流程

1. 发布方(发数据的窗口)

  1. 使用 Action 委托定义 public 事件
  2. 收集本地数据;
  3. 使用 事件?.Invoke(数据) 触发事件。

2. 订阅方(收数据的窗口)

  1. 实例化目标窗口;
  2. 窗口.事件 += 回调方法 完成订阅;
  3. 打开窗口;
  4. 窗口关闭后 窗口.事件 -= 回调方法 取消订阅;
  5. 回调方法中接收数据,如需跨线程则使用 Invoke

3. 选型建议

  • 1~2 个简单参数:直接用 Action<T> / Action<T1,T2>
  • 3 个以上 / 复杂结构:封装实体类 + Action<实体类>
  • 一对多广播:直接利用事件多播特性,多次 += 订阅即可

更多推荐