1.[C# Winform]BackgroundWorker实现进度条的那点事儿

学习笔记01
公司前辈让我用C#winform结构来逐步实现一个他曾经写过的配置程序的批处理文件的所有功能。设计什么的就不说了,就是俩字简洁。
直接上过程。

小助手主页面

取消了最大化功能的小助手,如下。
其主要功能简单概括为对一批文件进行复制操作,一次配置过程大约耗时3~5分钟,为了能让使用者感受到程序的工作变化我决定加入进度条来显示。

右键项目-》添加-》窗体-》ConfigByProBarForm.cs
点击‘电脑一键配置’按钮就会跳转到ConfigByProBarForm
并在按钮中加入代码

private void button1_Click(object sender, EventArgs e)
        {
            ConfigByProBarForm proBarForm = new ConfigByProBarForm();
            proBarForm.Show();
        }

进度条

进度条的实现有两类,一种假一种真。
假进度条指让进度条进度随我们的想法直接变化,而不考虑实际程序工作过程,程序结束而后进度条直接满值进而结束。
真进度条则在每一步我们定义好的检查点进度增加,进而告诉使用者程序的工作进度。

我选择做一个真的并且一弹窗形式展现。
这里主要参考https://blog.csdn.net/feiyang5260/article/details/90272311中的方法四。

进度条页面主要设计

在这里插入图片描述

加入控件如下
在这里插入图片描述
这里说一下一开始我照抄(新手都这样QAQ)前面博客里的方法四,但是奈何那方法没写清楚并且我怎么也显示不成功,于是痛定思痛,我仔细反省了一下。
既然BackgroundWorker控件是异步线程实现进度条,虽然我同原作者一样将其加入主页面进度条放在弹出页面中,但无法实现,技术薄弱排查不出原因,因此干脆将控件都放在弹窗中,弹窗加载时就启动它,这样一来应该不影响使用。试了一下成功。
这里注意将后台任务控件的两个属性设置为true
它俩字面意思就是允许报告进度和支持取消操作
在这里插入图片描述
子窗体代码实现

public ConfigByProBarForm()
        {
            InitializeComponent();
			/*
			以下为参考MSDN中BackgroundWorker使用方式写出的,和原博客对比它没有加入对DoWork的引用
			并且主窗体和子窗体都写了一个RunWorkerCompleted,但只引用了子窗体的,这可能是代码运行失败
			的原因?若各位看客能看懂其实现原理烦请告知。
			*/
			
            //模拟完成程序功能的代码
            backgroundWorker1.DoWork += BackgroundWorker_DoWork;

            //绑定进度条改变事件
            backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;

            //绑定后台操作完成、取消、异常的事件
            backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
        }

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            for (int i = 1; i <= 100; i++)
            {
                //判断是否取消
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    Thread.Sleep(100);
                    //报告进度
                    worker.ReportProgress(i);
                }

            }
            //throw new NotImplementedException();

        }

        private void backgroundWorker1_ProgressChanged(object sender, 
        	ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;//获取异步任务的进度条
            label1.Text = e.ProgressPercentage.ToString();
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, 
        	RunWorkerCompletedEventArgs e)
        {
        	//这里弹窗提示还是按钮变化凭各位喜好
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message);
            }
            else if (e.Cancelled)
            {
                MessageBox.Show("It's Cancelled!");
            }
            else
            {
                //MessageBox.Show("Completed!");
                button1.Text = "完成!";
            }
        }

        //取消按钮
        private void button1_Click(object sender, EventArgs e)
        {
        	/*
			一开始认为进度值达到100才能算完成,不到则必定没完成,后来调试时发现有时候进度值可能
			没到100但是任务做完了,此时条件a就显得不够合理,因此改为检测按钮状态,也算是增加了程
			序的健壮性吧,哈哈。
			*/
            //if(progressBar1.Value != 100)      --------条件a
            if(button1.Text == "取消")
            {
                //请求取消挂起的后台操作
                backgroundWorker1.CancelAsync();
                button1.Enabled = false;
                Close();
            }
            else
            {
                Close();
            }
        }

		//加载子窗体时就开始执行后台任务进程
        private void ProgressForm_Load(object sender, EventArgs e)
        {
            backgroundWorker1.RunWorkerAsync();
        }
    }

运行效果
在这里插入图片描述
在这里插入图片描述

实际体验

我是先写好这个进度条的程序后,才把之前写好功能的源程序主要功能代码移植过来,美滋滋的想着实现功能。结果不出意料的出现各种问题…

先看一下原代码的DoWork

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            bool monitor = false;//监视变量
            int i = 1;
            int num = 24;//任务数量
            while (!monitor)
            {
                //判断是否取消
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                else if (i == num + 1)
                {
                    monitor = true;
                }
                else
                {
                    #region 任务1
                    //...
                    worker.ReportProgress(i * 100 / num);
                    i++;
                    #endregion
                    ...
                    ...
                    #region 任务24
                    //...
                    worker.ReportProgress(i * 100 / num);
                    i++;
                    #endregion
                }
            }
        }

之前写实验程序的时候使用的是 循环睡眠线程 来模拟任务的耗时操作,但是实际任务不可能每个都一样,我又要写一个真进度条,因此我分割了任务并且每执行一部分就报告一次进度,我又想当然的外套了一层while循环来检测取消和任务完成状态。
紧接着出现问题。

首先是 取消失效了,因为任务没执行结束,所以显然不会回头执行取消的判断部分。
其次我参考的几乎所有博客都使用循环来模拟任务进度,以及最开始子窗体建立过程中 “对象.方法 += 方法”的写法让我猜测…_DoWork,…_ReportProgress(…),…RunWorkerCompleted这三个方法在运行过程中被执行了几次。

换个思路重新调试

我决定重写DoWork的实现,并拆开模拟任务的循环来观察DoWork的执行过程,发现DoWork只被执行了一次,另外两个应该是同理并且是多线程一直监视我们放入的对象。这个想法启发了我,取消能不能也安排一个多线程来监视它呢?这样我就不用每个任务后面都加入判断了,那显然太蠢。

//在DoWork 的worker对象赋值之后加入Thread(ParameterizedThreadStart)的引用实现多线程,就一个程序
//也就不用考虑线程安全的问题了
//Thread cancel = new Thread(BackgroundWorker_Cancelled);

//同是新定义一个报告进度并可以使其取消的方法,加入任务数量参数count和任务标记数i,i自增来标记进度
private static void BackgroundWorker_ReportProgress(object worker, 
	DoWorkEventArgs e, int count,int i)
        {
        	//BackgroundWorker worker1 = (BackgroundWorker)worker;
            //判断是否取消
            if (worker1.CancellationPending)
            {
                e.Cancel = true;
            }
            //报告进度
            worker1.ReportProgress(100 / count * i);
            i++;
        }

此时我的DoWork代码如下

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            ///注意使用 Thread(ParameterizedThreadStart)构造方法的线程参数必须为object对象,且这种方式线程不安全
            Thread cancel = new Thread(BackgroundWorker_Cancelled);
            cancel.Start(worker);
            int count = 5;
			int i = 1;
            
            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count,i);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count,i);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count,i);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count,i);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count,i);
        }

老手显然能看出i的变量作用域在变化所以i的自增实现是失败的,并且Thread(ParameterizedThreadStart)构造方法的线程参数必须为object对象,且这种方式线程不安全
回头接着改报告方法传入的BackgroundWorker对象为object,新定义了一个BackgroundWorker然后接着调试,发现新的worker1对象默认的进度值为100,运行就会报错超出进度条的最大值。加个线程的方法被我放弃,又换了一个思路。
!========================================!

我决定仍然使用一开始就定义好的BackgroundWorker对象,并且静态化任务标记数 i,使其成为类属性。并且为了避免当任务数无法被100整除导致最终进度不满100就直接完成的尴尬状况(我将count调成7就出现了进度值98而任务完成的情况,且点击完成并不会关闭窗口,这也是我修改取消按钮判断状况的原因)。
将count定义为float,
代码如下

static int i = 1;
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            float count = 7;
            
            //多次重复模拟不同的任务处理
            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);

            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);
            
            Thread.Sleep(100);
            BackgroundWorker_ReportProgress(worker, e, count);
        }

        private static void BackgroundWorker_ReportProgress(BackgroundWorker worker,
        	 DoWorkEventArgs e, float count)
        {
            //判断是否取消
            if (worker.CancellationPending)
            {
                e.Cancel = true;
            }
            //报告进度
            worker.ReportProgress((int)(100 / count * i));
            i++;
        }

此时再调试效果如下
在这里插入图片描述
总算成功了。
简单的进度条让我踩了很多坑但最终还是完成了它,决定写个博客记录一下学习过程,第一次写,如有误烦请告知。

====================================================================
2021-1-18 更新
拖了几天终于想起这个博客,更新一下。
上面写的进度条在让我检查的时候发现了很多bug。
在我放弃使用循环采用完成不同任务并在每次任务后打标记进而增加进度的方式后,我选择了将判断取消的部分放在了报告进度变化的方法中,上述最近的代码块中就是。
在我检测的时候发现,取消是个假的。
当我把所有模拟任务耗时的操作Thread.Sleep(100);
改成了
MessageBox.Show(string.Format(“任务{0},i”));
问题就出现了,在我点击取消后,进度条窗口关闭,而任务进度弹窗依旧出现。

	MessageBox.Show(string.Format("任务{0}", i));
    BackgroundWorker_ReportProgress(worker, e, count);

	MessageBox.Show(string.Format("任务{0},{1}", i,backgroundWorker1.CancellationPending));
    BackgroundWorker_ReportProgress(worker, e, count);

在这里插入图片描述
此时点击取消,在这里插入图片描述
可以看到进度条弹窗关闭,在确定任务1,然后任务二依旧弹出
同时我加入了对backgroundWorker1.CancellationPending值的检测,要知道在前面代码的设计中,当点击取消后执行backgroundWorker1.CancelAsync()操作,此时backgroundWorker1.CancellationPending的值会从false改为true,进而取消。
然而结果是我自定义的BackgroundWorker_ReportProgress方法形同虚设…
后来查了很久,主要是因为BackgroundWorker进程没有关闭,所以会一直运行,我尝试过关闭线程的Abort方法,会直接报错并且退出整个程序。最终我还是选择了每个任务都加入if判断,若CancellationPending值为真则不执行任务代码,这样完成了真正意义的取消。
然后这引发了我的思考。
1.有取消是否也应该有暂停继续呢?我还没腾出时间来实现这些。
2.一开始我选择把判断取消的代码放在自定义方法里是为了减少代码复用,然而这样我的方法就变得没有意义,依旧有大量复用,那么我设计真实进度的意义在哪里呢?此时想起假进度条的事,又觉得前辈们做的可能没错。任务完成就行,进度可能并不重要。

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值