winform控件篇-按钮

主要介绍按钮控件的相关使用方法和知识点,包括按钮的常用属性、事件使用、二次开发,以及多线程、异步之间的联动。

目录

  • [1. 属性](#1. 属性)
  • [2. 事件](#2. 事件)
    • [2.1 Click](#2.1 Click)
    • [2.2 MouseDown / MouseUp](#2.2 MouseDown / MouseUp)
    • [2.3 MouseEnter / MouseLeave](#2.3 MouseEnter / MouseLeave)
    • [2.4 动态绑定事件](#2.4 动态绑定事件)
    • [2.5 多个按钮共用同一个 Click 事件](#2.5 多个按钮共用同一个 Click 事件)
    • [2.6 自定义事件:给 Button 扩展自己的事件(重点)](#2.6 自定义事件:给 Button 扩展自己的事件(重点))
  • [3. 二次开发](#3. 二次开发)
  • [4. 相关案例](#4. 相关案例)
    • [4.1 案例 1:同一 Click 事件,多个按钮共用(sender 区分按钮)](#4.1 案例 1:同一 Click 事件,多个按钮共用(sender 区分按钮))
    • [4.2 案例 2:动态批量生成按钮](#4.2 案例 2:动态批量生成按钮)
    • [4.3 案例 3:按钮防重复点击(耗时操作,禁用按钮)](#4.3 案例 3:按钮防重复点击(耗时操作,禁用按钮))
    • [4.4 案例 4:按钮快捷键,Enter/ESC 绑定按钮](#4.4 案例 4:按钮快捷键,Enter/ESC 绑定按钮)
    • [4.5 案例 5:按钮实现打开/关闭子窗体,防止重复打开](#4.5 案例 5:按钮实现打开/关闭子窗体,防止重复打开)
    • [4.6 案例 6:窗体数据传送](#4.6 案例 6:窗体数据传送)
  • [5. 相关知识点](#5. 相关知识点)
    • [5.1 Invoke](#5.1 Invoke)
    • [5.2 Sender](#5.2 Sender)

1. 属性

属性 说明
Text 按钮显示文字。
Name 控件名称,代码中访问用(不要用 button1 默认名)。
Enabled true 启用;false 禁用(灰色不可点击)。
Visible 是否显示按钮,false 隐藏。
Size 按钮大小,如 new Size(120,40)
Location 按钮坐标位置
BackColor 背景颜色
ForeColor 文字颜色
Font 字体
FlatStyle 按钮样式:Flat、Popup、Standard、System;做扁平化按钮常用 FlatStyle=Flat
Image 按钮上显示图片
ImageAlign 图片对齐位置
csharp 复制代码
//代码设置示例
button1.Text = "保存";
button1.Enabled = true;
button1.Visible = true;
button1.Size = new Size(100,35);
button1.BackColor = Color.SteelBlue;
button1.ForeColor = Color.White;
button1.FlatStyle = FlatStyle.Flat;

2. 事件

2.1 Click

csharp 复制代码
private void button1_Click(object sender, EventArgs e)
{
    //sender:触发事件的控件对象,强转Button
    Button btn = sender as Button;
    MessageBox.Show($"点击了按钮:{btn.Text}");
}

2.2 MouseDown / MouseUp

csharp 复制代码
private void button1_MouseDown(object sender, MouseEventArgs e)
{
    //e.Button 判断左键/右键
    if(e.Button == MouseButtons.Left)
    {

    }
}

MouseEnter / MouseLeave

csharp 复制代码
private void button1_MouseEnter(object sender, EventArgs e)
{
    button1.BackColor = Color.LightBlue;
}
private void button1_MouseLeave(object sender, EventArgs e)
{
    button1.BackColor = Color.SteelBlue;
}

动态绑定事件

csharp 复制代码
//1.新建按钮
Button btnNew = new Button();
btnNew.Name = "btnTest";
btnNew.Text = "动态按钮";
btnNew.Size = new Size(100,30);
btnNew.Location = new Point(20,100);

//2.绑定Click事件,+= 订阅事件
btnNew.Click += BtnNew_Click;

//3.把按钮加到窗体上
this.Controls.Add(btnNew);

//事件处理方法
private void BtnNew_Click(object sender, EventArgs e)
{
    Button btn = sender as Button;
    MessageBox.Show($"动态按钮被点击:{btn.Name}");
}

多个按钮共用同一个 Click 事件

csharp 复制代码
button1.Click += CommonButton_Click;
button2.Click += CommonButton_Click;
button3.Click += CommonButton_Click;

private void CommonButton_Click(object sender, EventArgs e)
{
    Button btn = sender as Button;
    MessageBox.Show($"点击:{btn.Text}");
}

自定义事件:给 Button 扩展自己的事件(重点)

原生只有 Click,我们想要自定义事件,比如:按钮双击事件、按钮前置校验事件,点击按钮先触发自定义事件,再执行原有逻辑。

方式1:继承 Button,自定义控件增加自定义事件

此方法常用于对控件进行二次开发,而且并不仅仅是自定义事件,也可以自定义一些属性或者方法等。

csharp 复制代码
//对按钮控件进行二次开发,增加一个事件
using System;
using System.Windows.Forms;

namespace WinFormDemo
{
    public class MyButton : Button
    {
        //【1】声明自定义事件
        //事件名:BeforeClick,点击按钮前触发
        public event EventHandler BeforeClick;

        //重写OnClick,系统原生点击逻辑
        protected override void OnClick(EventArgs e)
        {
            //先触发我们自定义的 BeforeClick 事件
            BeforeClick?.Invoke(this, EventArgs.Empty);

            //再执行原生Click事件
            base.OnClick(e);
        }
    }
}

//实例化自定义的按钮控件,并调用事件,对事件进行重写
MyButton myBtn = new MyButton();
myBtn.Text = "自定义按钮";
myBtn.Size = new Size(120,40);
myBtn.Location = new Point(20, 150);

//订阅自定义事件 BeforeClick
myBtn.BeforeClick += MyBtn_BeforeClick;
//订阅原生Click
myBtn.Click += MyBtn_Click;

this.Controls.Add(myBtn);

//自定义事件处理方法
private void MyBtn_BeforeClick(object sender, EventArgs e)
{
    MessageBox.Show("【自定义事件】点击按钮之前执行!");
}

private void MyBtn_Click(object sender, EventArgs e)
{
    MessageBox.Show("【原生Click】按钮点击完成");
}


//订阅事件
myBtn.BeforeClickEx += (s, args) =>
{
    MessageBox.Show(args.Msg);
    //args.IsCancel = true; //打开注释就会取消按钮点击
};

二次开发

在开发过程中,有时候考虑到需求,经常会对控件进行二次开发,增加一些新的属性或者事件方法。

案例1:带点击计数的按钮

csharp 复制代码
public partial class CountButton : Button
{
    private int _clickCount = 0;

    public CountButton()
    {
        // 可在构造函数中设置默认样式
        this.Text = $"我被点击了 {_clickCount} 次";
    }

    // 重写 OnClick 方法来处理点击逻辑
    protected override void OnClick(EventArgs e)
    {
        _clickCount++;
        this.Text = $"我被点击了 {_clickCount} 次"; // 更新显示文本
        // 调用 base.OnClick(e) 确保 Click 事件仍能被外部订阅
        base.OnClick(e); 
    }

    // 重写 OnPaint 方法来自定义绘制(可选)
    protected override void OnPaint(PaintEventArgs pe)
    {
        base.OnPaint(pe);
        // 在这里可以添加更复杂的自定义绘制代码
        // 例如,在按钮上绘制一个自定义图标
    }
}

编译项目后,这个 CountButton 就会出现在工具箱中,你可以像使用普通按钮一样拖拽使用,而它的点击计数功能已经内置其中。

案例 2:按钮增加审计追踪相关功能。在 MES 或者 WMS 系统中,经常会用到审计追踪功能。接下来就此功能进行一个案例演示。

增加的功能:

  1. 增加自定义属性:操作编号、操作描述、操作人、是否开启审计
  2. 增加自定义事件:审计记录触发事件
  3. 新增内部方法:生成审计日志、保存审计记录
  4. 点击按钮自动捕获:点击时间、按钮名称、操作编号、操作描述、操作人、窗体名、结果状态
  5. 审计日志可以内存保存,也可以输出文本文件;外部窗体可订阅审计事件拿到审计数据。

项目架构

复制代码
AuditButtonDemo
├─ CustomControl
│    ├─ AuditButton.cs      // 二次开发自定义按钮
│    └─ AuditLog.cs         // 审计日志实体类
└─ FormMain.cs              // 主窗体,测试使用自定义按钮

代码

AuditLog.cs

复制代码
using System;

namespace AuditButtonDemo.CustomControl
{
    /// <summary>
    /// 审计追踪日志实体
    /// </summary>
    public class AuditLog
    {
        /// <summary>日志唯一编号</summary>
        public long LogId { get; set; }
        /// <summary>操作时间</summary>
        public DateTime OperateTime { get; set; }
        /// <summary>操作人</summary>
        public string Operator { get; set; }
        /// <summary>按钮控件名(Name)</summary>
        public string ControlName { get; set; }
        /// <summary>按钮文本</summary>
        public string ButtonText { get; set; }
        /// <summary>业务操作编号</summary>
        public string OperateCode { get; set; }
        /// <summary>操作描述</summary>
        public string OperateDesc { get; set; }
        /// <summary>所在窗体名称</summary>
        public string FormName { get; set; }
        /// <summary>操作结果:成功/失败</summary>
        public string Result { get; set; }

        public override string ToString()
        {
            return $"[{OperateTime:yyyy-MM-dd HH:mm:ss}] 用户:{Operator} | {OperateCode} | {OperateDesc} |窗体:{FormName} |控件:{ControlName} |结果:{Result}";
        }
    }
}

AuditButton.cs

继承 Button,新增属性、自定义事件、审计业务方法;重写 OnClick,点击自动生成审计记录。

csharp 复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows.Forms;

namespace AuditButtonDemo.CustomControl
{
    public class AuditButton : Button
    {
        #region ========== 自定义扩展属性【VS属性面板可见】 ==========
        [Category("审计追踪配置")]
        [Description("是否开启审计日志记录")]
        public bool EnableAudit { get; set; } = true;

        [Category("审计追踪配置")]
        [Description("业务操作编码,如 OP_DELETE、OP_SAVE")]
        public string OperateCode { get; set; } = string.Empty;

        [Category("审计追踪配置")]
        [Description("操作描述,如【删除工单】【下发设备指令】")]
        public string OperateDesc { get; set; } = string.Empty;

        [Category("审计追踪配置")]
        [Description("当前登录操作人")]
        public string CurrentOperator { get; set; } = "匿名用户";

        [Category("审计追踪配置")]
        [Description("审计日志文件输出路径,为空则不写文件")]
        public string AuditLogFilePath { get; set; } = "audit_log.txt";
        #endregion

        #region ========== 内存缓存全部审计记录 ==========
        /// <summary>内存缓存本按钮产生的所有审计日志</summary>
        [Browsable(false)] //属性窗口隐藏
        public List<AuditLog> AuditLogList { get; private set; }

        public AuditButton()
        {
            AuditLogList = new List<AuditLog>();
        }
        #endregion

        #region ========== 自定义事件:审计记录完成后向外触发 ==========
        /// <summary>审计日志生成完成事件,外部窗体订阅拿到审计对象</summary>
        public event EventHandler<AuditLogEventArgs> AuditLogCompleted;

        protected virtual void OnAuditLogCompleted(AuditLog log)
        {
            AuditLogCompleted?.Invoke(this, new AuditLogEventArgs(log));
        }
        #endregion

        #region ========== 内部审计核心方法 ==========
        /// <summary>生成一条审计日志对象</summary>
        private AuditLog CreateAuditLog(string result)
        {
            var log = new AuditLog
            {
                LogId = DateTime.Now.Ticks,
                OperateTime = DateTime.Now,
                Operator = CurrentOperator,
                ControlName = this.Name,
                ButtonText = this.Text,
                OperateCode = OperateCode,
                OperateDesc = OperateDesc,
                Result = result
            };
            //获取父窗体名称
            if (this.FindForm() != null)
            {
                log.FormName = this.FindForm().Text;
            }
            return log;
        }

        /// <summary>保存日志到本地文本文件</summary>
        public void SaveAuditLogToFile(AuditLog log)
        {
            if (string.IsNullOrWhiteSpace(AuditLogFilePath)) return;
            try
            {
                string content = log.ToString() + Environment.NewLine;
                File.AppendAllText(AuditLogFilePath, content);
            }
            catch
            {
                //文件读写异常,项目中可完善异常处理
            }
        }

        /// <summary>公开方法:清空内存审计记录</summary>
        public void ClearMemoryAuditLog()
        {
            AuditLogList.Clear();
        }
        #endregion

        #region ========== 重写OnClick,拦截点击逻辑,插入审计追踪 ==========
        protected override void OnClick(EventArgs e)
        {
            //1.点击按钮先执行审计逻辑
            if (EnableAudit)
            {
                var log = CreateAuditLog("成功");
                AuditLogList.Add(log); //内存保存
                SaveAuditLogToFile(log); //写入文件
                OnAuditLogCompleted(log); //触发自定义事件,通知外部窗体
            }

            //2.执行原生按钮点击事件,不能省略
            base.OnClick(e);
        }
        #endregion
    }

    /// <summary>自定义事件参数,传递审计日志实体</summary>
    public class AuditLogEventArgs : EventArgs
    {
        public AuditLog AuditLog { get; }
        public AuditLogEventArgs(AuditLog log)
        {
            AuditLog = log;
        }
    }
}

主窗体 FormMain.cs

窗体界面简单设计

  • 2 个AuditButton(审计按钮)
  • 1 个 TextBox/Multiline=true,用来实时显示审计日志
csharp 复制代码
using System;
using System.Windows.Forms;
using AuditButtonDemo.CustomControl;

namespace AuditButtonDemo
{
    public partial class FormMain : Form
    {
        public FormMain()
        {
            InitializeComponent();

            //====动态创建审计按钮演示,也可以编译后工具箱拖拽AuditButton====
            AuditButton btnSave = new AuditButton();
            btnSave.Text = "保存工单【带审计】";
            btnSave.Size = new System.Drawing.Size(160, 40);
            btnSave.Location = new System.Drawing.Point(30,30);

            //给自定义属性赋值
            btnSave.EnableAudit = true;
            btnSave.OperateCode = "OP_WORKORDER_SAVE";
            btnSave.OperateDesc = "保存工单数据操作";
            btnSave.CurrentOperator = "管理员_001";
            btnSave.AuditLogFilePath = Application.StartupPath + "\\audit_log.txt";

            //订阅【自定义审计事件】
            btnSave.AuditLogCompleted += BtnSave_AuditLogCompleted;
            //原生Click事件
            btnSave.Click += (s, ev) =>
            {
                MessageBox.Show("执行业务逻辑:保存工单");
            };

            this.Controls.Add(btnSave);


            //第二个审计按钮:删除操作
            AuditButton btnDelete = new AuditButton();
            btnDelete.Text = "删除工单【高危操作审计】";
            btnDelete.Size = new System.Drawing.Size(160, 40);
            btnDelete.Location = new System.Drawing.Point(220,30);
            btnDelete.EnableAudit = true;
            btnDelete.OperateCode = "OP_WORKORDER_DELETE";
            btnDelete.OperateDesc = "删除工单记录";
            btnDelete.CurrentOperator = "管理员_001";
            btnDelete.AuditLogCompleted += BtnSave_AuditLogCompleted;
            btnDelete.Click += (s, ev) =>
            {
                MessageBox.Show("执行业务逻辑:删除工单");
            };
            this.Controls.Add(btnDelete);
        }

        /// <summary>接收自定义审计事件,拿到审计实体,显示到文本框</summary>
        private void BtnSave_AuditLogCompleted(object sender, AuditLogEventArgs e)
        {
            //sender就是触发的AuditButton按钮
            var log = e.AuditLog;
            textBox1.Invoke(new Action(() =>
            {
                textBox1.AppendText(log.ToString() + Environment.NewLine);
            }));
        }
    }
}

相关案例

案例 1:同一 Click 事件,多个按钮共用(sender 区分按钮)

界面上 btnAdd、btnEdit、btnDelete 共用同一个点击事件。

复制代码
private void CommonBtn_Click(object sender, EventArgs e)
{
    Button btn = sender as Button;
    if(btn == null) return;

    switch (btn.Name)
    {
        case "btnAdd":
            MessageBox.Show("新增");
            break;
        case "btnEdit":
            MessageBox.Show("编辑");
            break;
        case "btnDelete":
            MessageBox.Show("删除");
            break;
    }
}

案例 2:动态批量生成按钮

复制代码
private void CreateButtons()
{
    for(int i = 0; i < 5; i++)
    {
        Button btn = new Button();
        btn.Name = $"btn_{i}";
        btn.Text = $"按钮{i}";
        btn.Size = new Size(80,30);
        btn.Location = new Point(20, 20 + i*40);
        btn.Click += BtnDynamic_Click;
        this.Controls.Add(btn);
    }
}

private void BtnDynamic_Click(object sender, EventArgs e)
{
    Button btn = sender as Button;
    MessageBox.Show($"点击动态按钮:{btn.Text}");
}

案例 3:按钮防重复点击(耗时操作,禁用按钮)

点击按钮执行数据库查询、文件读写,防止用户多次点击。

复制代码
private async void btnQuery_Click(object sender, EventArgs e)
{
    Button btn = sender as Button;
    btn.Enabled = false;
    try
    {
        // 模拟耗时任务
        await Task.Delay(2000);
        MessageBox.Show("查询完成");
    }
    catch(Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    finally
    {
        btn.Enabled = true; // 无论成功失败恢复按钮
    }
}

注意:耗时操作不要直接写同步阻塞 UI,尽量用 async/await,否则界面卡死。

案例 4:按钮快捷键,Enter/ESC 绑定按钮

设置窗体的 AcceptButtonCancelButton

复制代码
// Form构造函数
public Form1()
{
    InitializeComponent();
    // 按Enter触发btnOk
    this.AcceptButton = btnOk;
    // 按ESC触发btnCancel
    this.CancelButton = btnCancel;
}

案例 5:按钮实现打开/关闭子窗体,防止重复打开

复制代码
Form2 _form2 = null;
private void btnOpenChild_Click(object sender, EventArgs e)
{
    if(_form2 == null || _form2.IsDisposed)
    {
        _form2 = new Form2();
        _form2.Show();
    }
    else
    {
        _form2.BringToFront(); // 已经打开则前置激活
    }
}

案例6:窗体数据传送

  • FormA:发布者窗体,内部有按钮,点击按钮触发自定义事件向外发送消息;
  • FormB:订阅者窗体,打开 FormA,订阅 FormA 的事件,接收消息更新自己界面;

谁有数据,谁定义事件;谁要数据,谁订阅事件。

公共事件参数类(新建文件 CommonEventArgs.cs
复制代码
using System;

namespace WinFormEventDemo
{
    /// <summary>
    /// 自定义事件参数,传递业务数据,推荐用这种模式,不直接传string
    /// </summary>
    public class CommonMsgEventArgs : EventArgs
    {
        public string Message { get; set; }
        public DateTime SendTime { get; set; }
        public int OperateCode { get; set; }
    }
}
FormA.cs(发布窗体,事件在这里定义)

相当于前面的"窗体 A",只负责触发事件,完全不认识 FormB

复制代码
using System;
using System.Windows.Forms;

namespace WinFormEventDemo
{
    public partial class FormA : Form
    {
        #region 1、定义事件(发布)
        //方式:使用泛型 EventHandler&lt;T&gt;,不用手动写 delegate 委托(现代 WinForms 推荐)
        public event EventHandler<CommonMsgEventArgs> MessageNotify;

        //虚方法,用于触发事件,规范写法
        protected virtual void OnMessageNotify(CommonMsgEventArgs e)
        {
            //有订阅者才触发
            MessageNotify?.Invoke(this, e);
        }
        #endregion

        public FormA()
        {
            InitializeComponent();
        }

        //FormA 上按钮点击
        private void btnSendMsg_Click(object sender, EventArgs e)
        {
            //组装要发送的数据
            var args = new CommonMsgEventArgs
            {
                Message = "FormA下发操作通知",
                SendTime = DateTime.Now,
                OperateCode = 1001
            };

            //向外广播事件
            OnMessageNotify(args);
        }
    }
}

FormA 界面简单放一个按钮:Name=btnSendMsg,Text="发送事件消息"

FormB.cs(订阅窗体,打开 FormA、订阅事件、关闭取消订阅)

FormB 界面控件:

  1. btnOpenA:按钮,文本:打开 FormA

  2. textBox1:Multiline = true,ScrollBars = Vertical,用来显示收到消息

  3. 给 FormB 绑定 FormClosing 事件,关联上面 FormB_FormClosing

    using System;
    using System.Windows.Forms;

    namespace WinFormEventDemo
    {
    public partial class FormB : Form
    {
    // 保存 FormA 实例引用,用于取消订阅
    private FormA _formA = null;

    复制代码
         public FormB()
         {
             InitializeComponent();
         }
    
         // 打开 FormA 按钮
         private void btnOpenA_Click(object sender, EventArgs e)
         {
             if (_formA != null && !_formA.IsDisposed)
             {
                 _formA.BringToFront();
                 return;
             }
    
             _formA = new FormA();
    
             // ===== 订阅 FormA 的事件 =====
             _formA.MessageNotify += FormA_MessageNotify;
    
             _formA.Show();
         }
    
         /// <summary>
         /// 事件回调:接收 FormA 发过来的数据
         /// </summary>
         private void FormA_MessageNotify(object sender, CommonMsgEventArgs e)
         {
             //回调有可能非UI线程,必须Invoke更新控件
             if (textBox1.InvokeRequired)
             {
                 textBox1.Invoke(new Action(() => FormA_MessageNotify(sender, e)));
                 return;
             }
    
             textBox1.AppendText($"[{e.SendTime:HH:mm:ss}] 编码:{e.OperateCode} 消息:{e.Message}{Environment.NewLine}");
         }
    
         /// <summary>
         /// ✅关键:FormB关闭的时候,取消事件订阅,防止内存泄漏
         /// </summary>
         private void FormB_FormClosing(object sender, FormClosingEventArgs e)
         {
             if (_formA != null)
             {
                 //取消订阅 -=
                 _formA.MessageNotify -= FormA_MessageNotify;
    
                 if (!_formA.IsDisposed)
                 {
                     _formA.Close();
                 }
                 _formA = null;
             }
         }
     }

    }

Program.cs 入口,启动 FormB
复制代码
using System;
using System.Windows.Forms;

namespace WinFormEventDemo
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new FormB());
        }
    }
}
运行流程
  1. 程序启动打开 FormB;
  2. FormB 点击【打开 FormA】,弹出 FormA;同时 += 订阅 FormA.MessageNotify;
  3. 在 FormA 点击【发送事件消息】,触发事件;
  4. FormB 的 FormA_MessageNotify 被执行,消息打印到 textBox;
  5. 关闭 FormB 窗体 → 触发 FormClosing,执行 -= 取消事件订阅,再关闭 FormA,释放对象

相关知识点

Invoke

在 WinForm 中,Invoke 常用有以下几种:

1、EventHandler.Invoke():事件委托的 Invoke,用来手动触发事件

2、Control.Invoke() :WinForm 控件的 Invoke,跨线程更新 UI(Control 类的方法)

委托 / 事件的 Invoke(触发事件)

属于多播委托event 事件名.Invoke(参数),手动执行绑定到事件的所有方法。

csharp 复制代码
// 触发事件,如果不为null就调用
BtnCommon_Click?.Invoke(this, EventArgs.Empty);
  • ?. 空条件运算符:事件没有订阅者时不会报错。
  • 第一个参数 sender:一般传 this(当前窗体对象)。
  • 第二个参数 e:事件参数。

示例代码:

复制代码
using System;

class Demo
{
    // 定义一个事件
    public event EventHandler MyEvent;

    void MethodA(object sender, EventArgs e)
    {
        Console.WriteLine("方法A执行");
    }
    void MethodB(object sender, EventArgs e)
    {
        Console.WriteLine("方法B执行");
    }

    public void Run()
    {
        // 把两个方法绑定订阅到同一个事件上
        MyEvent += MethodA;
        MyEvent += MethodB;

        Console.WriteLine("准备调用Invoke");
        // 手动触发事件:MyEvent上绑定的MethodA、MethodB全部执行
        MyEvent?.Invoke(this, EventArgs.Empty);
    }
}

//调用
// var d = new Demo();
// d.Run();
/*输出:
准备调用Invoke
方法A执行
方法B执行
*/

跨线程更新 UI

WinForm UI 控件只能由创建它的主线程访问;后台线程直接操作控件会抛异常:跨线程操作无效,不是创建控件的线程访问它。

Control.Invoke():把委托代码封送回 UI 主线程执行,同步等待执行完成。Control.BeginInvoke():异步提交,不等待,直接返回。

复制代码
// 同步:后台线程调用,切回 UI 线程,等待执行完毕再返回
public object Invoke(Delegate method);

// 异步:提交任务就返回,不等执行结果
public IAsyncResult BeginInvoke(Delegate method);

示例代码

复制代码
private void button1_Click(object sender, EventArgs e)
{
    // 开启后台工作线程
    Task.Run(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            // ❌ 错误:后台线程直接操作UI
            // label1.Text = i.ToString();

            // ✅ 使用Control.Invoke切回UI线程
            label1.Invoke(new Action(() =>
            {
                label1.Text = i.ToString();
            }));
            
             //使用lambda隐式转换
             label1.Invoke(()=>
             {
                 label1.Text = i.ToString();
             });

            Thread.Sleep(300);
        }
    });
}

也可通过增加 InvokeRequired 判断是否需要 Invoke。if(label1.InvokeRequired)

  • InvokeRequired=true:当前不是 UI 线程,必须调用 Invoke 切线程

  • false:已经在 UI 线程,可以直接操作控件

    private void button1_Click(object sender, EventArgs e)
    {
    // 开启后台工作线程
    Task.Run(() =>
    {
    for (int i = 0; i < 10; i++)
    {
    // ❌ 错误:后台线程直接操作UI
    // label1.Text = i.ToString();

    复制代码
              // ✅ 使用Control.Invoke切回UI线程
              if(label1.InvokeRequired)
              {
               label1.Invoke(new Action(() =>
              {
                  label1.Text = i.ToString();
              }));
              
               //使用lambda隐式转换
               label1.Invoke(()=>
               {
                   label1.Text = i.ToString();
               });
              
              }
             
    
              Thread.Sleep(300);
          }
      });

    }

两者的区别

项目 事件委托 事件.Invoke() 控件 Control.Invoke()
所属对象 event / Delegate Control(Form、Button、Label 都继承 Control)
作用 手动触发事件,执行订阅的回调 把委托代码调度到 UI 主线程执行
线程行为 就在当前线程执行,不切换线程 强制切回 UI 线程
等待逻辑 同步执行,等待全部回调跑完 同步,等待 UI 执行完才返回
配套异步 BeginInvoke()(委托异步调用) Control.BeginInvoke() 提交不等待

多线程场景

如果在多线程情况下,事件 .Invoke() 的操作场景如下:

事件的 .Invoke() 不会切换线程!在哪个线程调用 事件?.Invoke(),所有订阅的回调方法就在哪个线程执行。

场景 1:UI 主线程调用事件 Invoke
复制代码
// 在UI主线程执行
btnAdd.Click?.Invoke(btnAdd, EventArgs.Empty);
  • 当前线程 = UI 主线程
  • 所有绑定的回调方法(CommonBtnHandler)跑在 UI 线程
  • 可以直接操作控件、弹窗,不会报跨线程异常
场景 2:后台线程 Task.Run/Thread 里面调用事件 Invoke
复制代码
public event EventHandler MyEvent;

private void Form1_Load(object sender, EventArgs e)
{
    MyEvent += Func1;
    MyEvent += Func2;

    Task.Run(() =>
    {
        Console.WriteLine($"后台线程开始,线程id:{Thread.CurrentThread.ManagedThreadId}");

        MyEvent?.Invoke(null, EventArgs.Empty); 
        // ⛔这里后台线程会卡住!等待Func1、Func2全部执行完毕,才往下走

        Console.WriteLine("Invoke执行完毕,后台线程继续往下跑");
    });
}

void Func1(object s, EventArgs e)
{
    Console.WriteLine($"Func1 线程id:{Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(2000); //模拟耗时2秒
}
void Func2(object s, EventArgs e)
{
    Console.WriteLine($"Func2 线程id:{Thread.CurrentThread.ManagedThreadId}");
}


//输出顺序
后台线程开始,线程id:4
Func1 线程id:4
//等待2秒
Func2 线程id:4
Invoke执行完毕,后台线程继续往下跑

这里强调一点,后台线程其实可以理解为独立于 UI 线程执行的,但是在后台线程执行的代码,默认是同步顺序执行

Sender

object 类型 ,代表触发当前这个事件方法的控件/对象实例

主要作用

  1. 实现多个控件共用同一个事件处理方法,不需要为每一个按钮写单独的 Click 函数。
  2. 在事件内部拿到触发源控件实例 ,读取它的 Name、Text、Enabled、Tag 等属性,也可以修改控件状态。
  3. 动态创建的控件,绑定同一个事件,靠 sender 区分来源。

常见使用场景

1、多个按钮共用同一个 Click 事件
csharp 复制代码
using System.Reflection.Metadata.Ecma335;

namespace FireStar10
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        //public event EventHandler BtnCommon_Click;
        private void BtnCommon_Click(object sender,EventArgs e)
        {
            Button btn = (Button)sender;
            if (btn == null) return;
            switch(btn.Name)
            {
                case nameof(btnAdd):
                    MessageBox.Show("执行新增");
                    break;
                case nameof(btnEdit):
                    MessageBox.Show("执行编辑");
                    break;
                case nameof(btnDelete):
                    MessageBox.Show("执行删除");
                    break;
            }
        }
    }
}

相关推荐
DevOpenClub5 小时前
Markdown、HTML 和 PPT 如何稳定交付:文档转换任务的幂等发布流程
开发语言·前端·c#·html·powerpoint
软件黑马王子5 小时前
3.单例模式问题1:构造函数
开发语言·单例模式·c#
软件黑马王子6 小时前
4.单例模式问题2:重复挂载
开发语言·单例模式·c#
夏霞7 小时前
c# 不支持的目标框架 解决方案
开发语言·c#
软件黑马王子8 小时前
5.单例模式问题3:多线程问题
单例模式·前端框架·c#
geovindu8 小时前
CSharp: Template Method Pattern
开发语言·后端·c#·.net·模板方法模式·行为模式
CSDN_RTKLIB8 小时前
SetImpliedSelection
c#
软件黑马王子8 小时前
1.non-MonoBehaviour 单例模式泛型基类
unity·前端框架·c#
高彬9 小时前
SAP ECC6.0 对接 C# web Services服务
开发语言·前端·c#·sap