在 WinForm 中使用进度条展示长时间任务的执行进度

今天有人问道如何在 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();
    }

}
posted on 2012-12-26 15:12  微澜  阅读(2144)  评论(1编辑  收藏  举报