主要介绍按钮控件的相关使用方法和知识点,包括按钮的常用属性、事件使用、二次开发,以及多线程、异步之间的联动。
目录
- [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 系统中,经常会用到审计追踪功能。接下来就此功能进行一个案例演示。
增加的功能:
- 增加自定义属性:操作编号、操作描述、操作人、是否开启审计
- 增加自定义事件:审计记录触发事件
- 新增内部方法:生成审计日志、保存审计记录
- 点击按钮自动捕获:点击时间、按钮名称、操作编号、操作描述、操作人、窗体名、结果状态
- 审计日志可以内存保存,也可以输出文本文件;外部窗体可订阅审计事件拿到审计数据。
项目架构
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 绑定按钮
设置窗体的 AcceptButton 和 CancelButton。
// 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<T>,不用手动写 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 界面控件:
-
btnOpenA:按钮,文本:打开 FormA -
textBox1:Multiline = true,ScrollBars = Vertical,用来显示收到消息 -
给 FormB 绑定
FormClosing事件,关联上面FormB_FormClosingusing 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());
}
}
}
运行流程
- 程序启动打开 FormB;
- FormB 点击【打开 FormA】,弹出 FormA;同时
+=订阅 FormA.MessageNotify; - 在 FormA 点击【发送事件消息】,触发事件;
- FormB 的
FormA_MessageNotify被执行,消息打印到 textBox; - 关闭 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 类型 ,代表触发当前这个事件方法的控件/对象实例。
主要作用
- 实现多个控件共用同一个事件处理方法,不需要为每一个按钮写单独的 Click 函数。
- 在事件内部拿到触发源控件实例 ,读取它的
Name、Text、Enabled、Tag等属性,也可以修改控件状态。 - 动态创建的控件,绑定同一个事件,靠 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;
}
}
}
}