前言
在有些比较消耗时间的业务场景中,比如生成报表等,如果没有在操作的过程中向用户反馈操作进度,会让用户以为程序 “死” 掉了,用户体验非常不好。
WinForm 桌面程序项目与 Console 项目不一样,如果 Console 项目,我们可以在处理业务的关键节点时,在控制台打印一些消息向用户报告进度,但是 WinForm 桌面程序项目出于线程安全的原因,是无法在处理业务的过程中同时显示 UI 控件的消息的,即你在处理业务的的关键节点时,向 UI 控件如 TextBox 写一些消息,但实际上 UI 是不会马上同时显示你写的消息的,只有在整个业务处理完后,UI 才会呈现你写的消息,但这时已经太迟了,没有太多意义。所以在 WinForm 桌面程序项目里,要在操作的过程中向用户反馈操作进度,只能通过多线程来处理,一个线程在后台处理业务,一个线程通过委托同步展示业务处理进度,不过手动写多线程代码比较麻烦,所以微软在 .NET 2.0 增加了 BackgroundWorker 类,专门用于处理这种情况。使用 BackgroundWorker 可以轻松地启动一个单独的线程上执行一些操作并进行管理,无需我们操心。
可以将它从 “工具箱” 的 “组件” 选项卡中拖到窗体上,也可以通过编程方式创建 BackgroundWorker 类,本文通过一个实际的详细例子演示如何通过编程方式使用 BackgroundWorker 类,大家可以照着做。
Step By Step 步骤
- 创建一个 WinForm 类型的项目
- 设计窗体 UI 内容,如图:
- 打开窗体如 FrmZqReportHandler 的代码
- 在顶部(构造方法之前)添加声明
BackgroundWorker
对象的代码
// 声明 BackgroundWorker 对象 private BackgroundWorker m_BackgroundWorker;
5.在构造方法中实例化及配置 BackgroundWorker
对象,(留意注释)
public FrmZqReportHandler() { InitializeComponent(); m_BackgroundWorker = new BackgroundWorker(); // 实例化 BackgroundWorker 对象 m_BackgroundWorker.WorkerReportsProgress = true; // 设置可以通告进度 m_BackgroundWorker.WorkerSupportsCancellation = false; // 设置不可以取消 // 声明 DoWork 事件 m_BackgroundWorker.DoWork += new DoWorkEventHandler(DoWork); // 声明 ProgressChanged 事件 m_BackgroundWorker.ProgressChanged += new ProgressChangedEventHandler(UpdateProgress); // 声明 RunWorkerCompleted 事件 m_BackgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(CompletedWork); }
6.在按钮的单击事件里启动 BackgroundWorker
,(留意注释)
private void btnSelectFile_Click(object sender, EventArgs e) { // 1. 选择文件 // 代码忽略 ...... // 2. 如果后台线程空闲,则启动后台操作 if (!m_BackgroundWorker.IsBusy) { // 启动后台操作,触发 DoWork 事件 // 参数可传可不传,暂时没什么有什么作用 m_BackgroundWorker.RunWorkerAsync(this); } }
7.编写 DoWork
事件,DoWork 事件中的代码就是后台线程要执行的业务处理方法,(留意注释)
void DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bw = sender as BackgroundWorker; // 1. 处理业务逻辑 01 // 处理业务逻辑代码 ...... // 传递进度,第一个参数是进度值,第二个参数是附加对象,此方法会触发 ProgressChanged 事件 bw.ReportProgress(10, obj); // ...... 处理业务逻辑代码 // 7. 处理业务逻辑 07,业务处理完成 // 处理业务逻辑代码 ...... // 传递进度,将进度值设置为进度条控件的最大值 bw.ReportProgress(100, fileUrl); }
8.编写 ProgressChanged
事件,注意:在这个事件里跟 UI 控件交互,(留意注释)
void UpdateProgress(object sender, ProgressChangedEventArgs e) { int progress = e.ProgressPercentage; // 根据 `DoWork` 事件传递过来的进度值不断更新进度条的值 pgBar.Value = progress; // 根据进度值向填写业务处理细节,提高用户体验 if (progress == 1) { txtPg.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} ......\r\n"); } else if (progress == 10) { txtPg.AppendText(...... } // ...... else if (progress == 100) { txtPg.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 数据处理后是 [{e.UserState}]\r\n"); } }
9.编写 RunWorkerCompleted
事件,此事件在后台操作已完成、被取消或引发异常时发生
void CompletedWork(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { txtPg.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 处理过程中出现错误:[{e.Error}]\r\n"); } else { txtPg.AppendText($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 处理完毕"); } }
10.至此,代码就完成了,运行效果如下:
总结
BackgroundWorker
类虽然是旧技术,但它在处理 WinForm 业务处理进度,提高用户体验上却是非常好用的,比直接用多线程方便很多,这可见,技术不分新旧,用在恰当的地方最重要。BackgroundWorker
类几个常用属性
- WorkerReportsProgress: 设置能否报告进度,通常设置为 true
- WorkerSupportsCancellation: 设置能否中途取消,按需设置
- IsBusy: 判断后台线程是否正在工作中,只读属性
- CancellationPending: 指示应用程序是否已请求取消后台操作,只读属性
BackgroundWorker
类几个常用方法
- RunWorkerAsync: 启动后台操作,此方法会触发 DoWork 事件
- CancelAsync: 请求取消挂起的后台操作,此方法只是将 CancellationPending 属性设置为 true,需要在 DoWork 事件中检查 CancellationPending 属性,来决定是否要继续取消后台操作
- ReportProgress: 报告进度,此方法会触发 ProgressChanged 事件
BackgroundWorker
类几个常用事件
- DoWork: 后台线程,一般在这里处理业务逻辑,不能操作 UI 控件
- ProgressChanged: 报告业务处理进度,可以操作 UI 控件
- RunWorkerCompleted: 后台操作已完成、被取消或引发异常时触发,可以操作 UI 控件
BackgroundWorker
类使用步骤:
- 新建 BackgroundWorder 对象
- 根据需求, 设置是否能取消、是否报告进度等
- 根据需求,设置好相关事件,DoWorker、ProgressChanged、RunWorkerCompleted
- 调用 RunWorkerAsyns() 方法,启动线程;
- DoWork 在需要取消的位置,判断 CancellationPending 的值,并做相关处理;
- DoWork 在适当的位置调用 ReportProgress方法,报告进度
我是老杨,一个奋斗在一线的资深研发老鸟,让我们一起聊聊技术,聊聊人生。
都看到这了,求个点赞、关注、在看三连呗,感谢支持。