分享一个在 WinForm 桌面程序中使用进度条展示报表处理进度的例子,提升用户体验

简介: 分享一个在 WinForm 桌面程序中使用进度条展示报表处理进度的例子,提升用户体验

前言

在有些比较消耗时间的业务场景中,比如生成报表等,如果没有在操作的过程中向用户反馈操作进度,会让用户以为程序 “死” 掉了,用户体验非常不好。

WinForm 桌面程序项目与 Console 项目不一样,如果 Console 项目,我们可以在处理业务的关键节点时,在控制台打印一些消息向用户报告进度,但是 WinForm 桌面程序项目出于线程安全的原因,是无法在处理业务的过程中同时显示 UI 控件的消息的,即你在处理业务的的关键节点时,向 UI 控件如 TextBox 写一些消息,但实际上 UI 是不会马上同时显示你写的消息的,只有在整个业务处理完后,UI 才会呈现你写的消息,但这时已经太迟了,没有太多意义。所以在 WinForm 桌面程序项目里,要在操作的过程中向用户反馈操作进度,只能通过多线程来处理,一个线程在后台处理业务,一个线程通过委托同步展示业务处理进度,不过手动写多线程代码比较麻烦,所以微软在 .NET 2.0 增加了 BackgroundWorker 类,专门用于处理这种情况。使用 BackgroundWorker 可以轻松地启动一个单独的线程上执行一些操作并进行管理,无需我们操心。

可以将它从 “工具箱” 的 “组件” 选项卡中拖到窗体上,也可以通过编程方式创建 BackgroundWorker 类,本文通过一个实际的详细例子演示如何通过编程方式使用 BackgroundWorker 类,大家可以照着做。

Step By Step 步骤

  1. 创建一个 WinForm 类型的项目
  2. 设计窗体 UI 内容,如图:

  3. 打开窗体如 FrmZqReportHandler 的代码
  4. 在顶部(构造方法之前)添加声明 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.至此,代码就完成了,运行效果如下:

总结

  1. BackgroundWorker 类虽然是旧技术,但它在处理 WinForm 业务处理进度,提高用户体验上却是非常好用的,比直接用多线程方便很多,这可见,技术不分新旧,用在恰当的地方最重要。
  2. BackgroundWorker类几个常用属性
  1. WorkerReportsProgress: 设置能否报告进度,通常设置为 true
  2. WorkerSupportsCancellation: 设置能否中途取消,按需设置
  3. IsBusy: 判断后台线程是否正在工作中,只读属性
  4. CancellationPending: 指示应用程序是否已请求取消后台操作,只读属性
  1. BackgroundWorker类几个常用方法
  1. RunWorkerAsync: 启动后台操作,此方法会触发 DoWork 事件
  2. CancelAsync: 请求取消挂起的后台操作,此方法只是将 CancellationPending 属性设置为 true,需要在 DoWork 事件中检查 CancellationPending 属性,来决定是否要继续取消后台操作
  3. ReportProgress: 报告进度,此方法会触发 ProgressChanged 事件
  1. BackgroundWorker类几个常用事件
  1. DoWork: 后台线程,一般在这里处理业务逻辑,不能操作 UI 控件
  2. ProgressChanged: 报告业务处理进度,可以操作 UI 控件
  3. RunWorkerCompleted: 后台操作已完成、被取消或引发异常时触发,可以操作 UI 控件
  1. BackgroundWorker类使用步骤:
  1. 新建 BackgroundWorder 对象
  2. 根据需求, 设置是否能取消、是否报告进度等
  3. 根据需求,设置好相关事件,DoWorker、ProgressChanged、RunWorkerCompleted
  4. 调用 RunWorkerAsyns() 方法,启动线程;
  5. DoWork 在需要取消的位置,判断 CancellationPending 的值,并做相关处理;
  6. DoWork 在适当的位置调用 ReportProgress方法,报告进度

我是老杨,一个奋斗在一线的资深研发老鸟,让我们一起聊聊技术,聊聊人生。

都看到这了,求个点赞、关注、在看三连呗,感谢支持。


相关文章
|
测试技术 程序员 C++
C++单元测试GoogleTest和GoogleMock十分钟快速上手(gtest&gmock)
gtest是Google开源的一个跨平台的(Liunx、Mac OS X、Windows等)的 C++ 单元测试框架,可以帮助程序员测试 C++ 程序的结果预期。它提供了丰富的断言、致命和非致命判断、参数化、”死亡测试”等等。另一方面,gmock并不是一个独立的测试框架,而是gtest的辅助框架,主要用于模拟没有实现的类的操作,以便在没有完整类的情况下进行测试。通过配合使用gtest和gmock,开发者可以编写出更为复杂且健壮的C++单元测试。
1835 0
|
7月前
|
IDE Shell 开发工具
灵码使用体验
上周使用了通义灵码三天,分享一下体验。相较于Trae、VSCode和CodeBuddy,灵码存在一些不足:响应速度较慢,生成代码效率低;汉化不够完善,菜单仍为英文;纠错能力弱,无法有效提示代码问题;Shell集成效果差,终端命令错误处理不佳;MCP工具集成不如Trae便捷。不过,灵码也有亮点:支持超长上下文输入,有助于精确开发;Qwen3推理能力强,能较好理解用户意图并编辑代码。希望后续更新能优化这些问题,提升用户体验。
983 0
|
SQL 数据库 数据库管理
SQLite数据库操作
【7月更文挑战第31天】SQLite数据库操作
279 6
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
325 4
|
设计模式 前端开发 C#
WPF/C#:理解与实现WPF中的MVVM模式
WPF/C#:理解与实现WPF中的MVVM模式
1365 0
|
存储 C++
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
|
机器学习/深度学习 自然语言处理 索引
深入了解 Transformers – Part 1: 介绍 Transformer 模型
深入了解 Transformers – Part 1: 介绍 Transformer 模型
1993 1
|
测试技术
公路(最小生成树)
描述 岛屿国家Flatopia是完全平坦的。不幸的是,Flatopia没有公共的公路。所以Flatopia的交通很困难。Flatopian政府意识到这个问题。
1257 154
Visual Studio(VS2017/VS2019) C++ 配置 CPLEX 教程
Visual Studio(VS2017/VS2019) C++ 配置 CPLEX 教程
Visual Studio(VS2017/VS2019) C++ 配置 CPLEX 教程
matlab2020a编译环境 MCR 安装步骤(非常实用)
matlab2020a编译环境 MCR 安装步骤(非常实用)