zoukankan      html  css  js  c++  java
  • 在 WinForm 中使用进度条展示长时间任务的执行进度

    这篇文章是很久之前写的,最近有朋友问到这个问题,我重新更新了一篇,更全面探讨了它,请移步这里:再次探讨 WinForms 多线程开发

    下面是以前的内容:

    今天有人问道如何在 WinForm 程序中,使用进度条显示长时间任务的执行进度。

    这个问题是一个开发中很常见的问题,正好也整理和总结一下。

    这个问题我们从两个部分来看,第一,长时间执行的任务如何暴露出其执行进度,第二,WinForm 窗体如何显示执行进度。

    第一部分. 对象如何提供其处理进度

    先看第一个问题,如果希望一个长时间执行的任务能够展示其执行进度,显然它必须提供当前执行的进度值。但是,一般来说,一个任务通常是一个方法,执行完也就完了,怎么能在一个方法的执行过程中,向外界提供其执行的进度呢?

    答案就是设计模式中的观察者模式。我们可以将任务的执行者看作观察者模式中的主题,而窗体就是观察者了。在方法的执行过程中,主题不断改变其状态,而观察者通过观察主题的状态来显示其执行进度。

    在 .NET 中,典型的观察者模式是通过事件来实现的。事件参数则用来提供主题的状态,System.EventArgs 为事件参数提供了基类,我们实现的事件参数应当从这个基类派生,提供自定义的额外属性。

    首先定义进度状态的事件参数类,其属性 Value 表示当前进度的百分比。

    // 定义事件的参数类
    public class ValueEventArgs 
        : EventArgs
    {
        public int Value { set; get;}
    }

    然后,定义事件所使用的委托。这个委托使用事件参数对象作为方法的参数。

    // 定义事件使用的委托
    public delegate void ValueChangedEventHandler( object sender, ValueEventArgs e);

    最后,方法不能单独存在,我们定义业务对象,包含需要长时间执行的方法。

    class LongTimeWork
    {
        // 定义一个事件来提示界面工作的进度
        public event ValueChangedEventHandler ValueChanged;
    
        // 触发事件的方法
        protected void OnValueChanged( ValueEventArgs e)
        {
            if( this.ValueChanged != null)
            {
                this.ValueChanged( this, e);
            }
        }
    
        public void LongTimeMethod()
        {
            for (int i = 0; i < 100; i++)
            {
                // 进行工作
                System.Threading.Thread.Sleep(1000);
    
                // 触发事件
                ValueEventArgs e = new ValueEventArgs() { Value = i+1};
                this.OnValueChanged(e);
            }
        }
    }

    注意,在这个类中,我们使用了典型的事件模式,OnValueChanged 在类中用来触发事件,将当前的进度状态提供给观察者。在 LongTimeMethod 方法中,通过调用这个方法将当前的进度提供给窗体。这个方法中通过使用 Sleep,共需花费 100 秒以上的时间才能执行完毕。

    第二部分 窗体与线程问题

    在项目中创建一个窗体,放置一个进度条和一个按钮。

    双击按钮,就可以开始界面编程了。

    在按钮的处理事件中,写下如下的代码,通过事件来获取主题的通知,在 ValueChanged 事件的处理方法中更新进度条。

    private void button1_Click(object sender, EventArgs e)
    {
        // 禁用按钮
        this.button1.Enabled = false;
    
        // 实例化业务对象
        LongTime.Business.LongTimeWork workder = new Business.LongTimeWork();
    
        workder.ValueChanged += new Business.ValueChangedEventHandler(workder_ValueChanged);
    
        workder.LongTimeMethod();
    }

    下面是 ValueChanged 事件的处理方法。

    // 进度发生变化之后的回调方法
    private void workder_ValueChanged(object sender, Business.ValueEventArgs e)
    {
        this.progressBar1.Value = e.Value;
    }

    点击按钮,看起来执行正常呀,在窗体上点一下鼠标,或者在标题栏拖动一下窗口,马上就会看到界面失去了反应。

    为什么会这样的?我们使用的就是典型的事件处理模式呀?

    问题出在界面的线程问题上,整个界面的操作运行在一个线程上,在 Win32 时代被称为消息循环,你可以将系统对窗体的处理看成一个无限的循环,不断地获取消息,处理消息。但是,不要忘了,在一个循环中,如果一个步骤卡在了那里,其它的步骤就不会有机会执行了。

    对于我们这个长时间执行的方法来说,在开始调用这句代码的时候

    workder.LongTimeMethod();

    就已经阻塞了这个窗体的循环,使得 Windows 没有机会来处理用户的操作,不能处理按钮,不能处理菜单,也不能拖动,通常我们成为冻结了。

    显然,我们不希望这样的结果。

    解决的办法就是将这个长时间执行的方法在另外一个线程上执行,而不要占用我们窗体界面处理的宝贵时间。

    在 .NET 实现异步的基本方式就是委托,我们可以将这个方法表示为一个委托,然后通过委托的 BeginXXX 来实现异步调用。这样按钮的点击事件处理就成为了下面的样子。

    private void button1_Click(object sender, EventArgs e)
    {
        // 禁用按钮
        this.button1.Enabled = false;
    
        // 实例化业务对象
        LongTime.Business.LongTimeWork workder = new Business.LongTimeWork();
    
        workder.ValueChanged += new Business.ValueChangedEventHandler(workder_ValueChanged);
    
        // 使用异步方式调用长时间的方法
        Action handler = new Action(workder.LongTimeMethod);
        handler.BeginInvoke(
            new AsyncCallback(this.AsyncCallback),
            handler
            );
    }

    这里使用了系统定义的 Action 委托。由于使用 BeginInvoke 必须配合 EndInvoke , 而 EndInvoke 需要借助于开始的委托,所以在第二个参数中,将委托对象传递出去。

    这里的 AsyncCallback 是异步处理完成之后的回调方法,如下所示

    // 结束异步操作
    private void AsyncCallback(IAsyncResult ar)
    {
        // 标准的处理步骤
        Action handler = ar.AsyncState as Action;
        handler.EndInvoke(ar);
    
        MessageBox.Show("工作完成!");
    
        this.button1.Enabled = true;
    }

    再次执行程序,看起来还不错。

    不过,别高兴的太早,没准你现在就已经看到了这个异常。如果还没有看到,就在调试模式下看一看。

    第三部分 回到 UI 线程

    现在,我们的方法正在一步一步的进行,但是需要注意的是它工作在一个线程上,而 UI 工作在自己的线程上,这两个线程可能是同一个线程,更可能不是同一个线程。

    在 Windows 中规定,对于窗体的处理,例如修改窗体控件的属性,必须在窗体的线程上才允许进行,不仅 Windows 界面,几乎所有的图形界面皆是如此,这关系到效率问题。

    当我们在另外一个线程上修改窗体控件的属性的时候,异常被抛了出来。

    难道还要回到 UI 线程上来执行我们长时间的方法吗?当然不是,Control 基类就提供了两个方法 Invoke 和 BeginInvoke ,允许我们以委托的形式将需要进行的处理排到 UI 的线程处理列表中,等待 UI 线程在适当的时候来执行。

    使用什么委托呢?是委托都可以,Windows Forms 中提供了一个专用的委托,可以考虑使用一下。

    public delegate void MethodInvoker()

    其实跟 Action 一样,不过看起来专业一点,我们就使用它了。

    不过,也有可能我们的线程与 UI 的线程正好是同一个线程,那我们就没有必要这么麻烦了,Control 还定义了一个属性 InvokeRequired 用来检查是否在同一个线程之上,不是则返回真,需要使用委托进行,否则返回假,可以直接处理控件。

    [BrowsableAttribute(false)]
    public bool InvokeRequired { get; }

    这样,我们的方法,就可以修改为下面的形式

    // 进度发生变化之后的回调方法
    private void workder_ValueChanged(object sender, Business.ValueEventArgs e)
    {
        System.Windows.Forms.MethodInvoker invoker = ()=>this.progressBar1.Value = e.Value;
    
        if (this.progressBar1.InvokeRequired)
        {
            this.progressBar1.Invoke(invoker);
        }
        else
        {
            invoker();
        }
    }

    同样,结束异步的回调函数中,需要将按钮的状态重新启用,也如法炮制。

    // 结束异步操作
    private void AsyncCallback(IAsyncResult ar)
    {
        // 标准的处理步骤
        Action handler = ar.AsyncState as Action;
        handler.EndInvoke(ar);
    
        MessageBox.Show("工作完成!");
    
        // 重新启用按钮
        System.Windows.Forms.MethodInvoker invoker = ()=>this.button1.Enabled = true;
    
        if (this.InvokeRequired)
        {
            this.Invoke(invoker);
        }
        else
        {
            invoker();
        }
    
    }

    完整的代码可以在这里下载:轻轻地点一下就可以下载了!

  • 相关阅读:
    mybatis 查询一对多子表只能查出一条数据
    Docker 查看容器里Log4写的日 志文 件里的日志
    MYSQL之union的使用
    【前端开发】常见好用的流程图框架
    【推荐】好网站推荐
    【前端工具】好用的数据库工具Navicat
    jQuery ajax
    thinkphp6.0封装数据库及缓存模型
    Unity 3D使用C#脚本实例
    Unity 3D简单使用C#脚本,脚本的执行顺序
  • 原文地址:https://www.cnblogs.com/haogj/p/2817047.html
Copyright © 2011-2022 走看看