[C# Winform]BackGroundWorker实现进度条的那点事儿

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

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

小助手主页面

取消了最大化功能的小助手,如下。
其主要功能简单概括为对一批文件进行复制操作,一次配置过程大约耗时3~5分钟,为了能让使用者感受到程序的工作变化我决定加入进度条来显示。
[C# Winform]BackGroundWorker实现进度条的那点事儿
右键项目-》添加-》窗体-》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中的方法四。

进度条页面主要设计

[C# Winform]BackGroundWorker实现进度条的那点事儿

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

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;
            }
            else
            {
                Close();
            }
        }

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

运行效果
[C# Winform]BackGroundWorker实现进度条的那点事儿
[C# Winform]BackGroundWorker实现进度条的那点事儿

实际体验

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

先看一下原代码的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++;
        }

此时再调试效果如下
[C# Winform]BackGroundWorker实现进度条的那点事儿
总算成功了。
简单的进度条让我踩了很多坑但最终还是完成了它,决定写个博客记录一下学习过程,第一次写,如有误烦请告知。

上一篇:C#运用BackgroundWorker控件实现多线程


下一篇:.NET设计篇08-线程取消模型和跨线程访问UI