在 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();
    }

}

 

完整的代碼可以在這里下載:輕輕地點一下就可以下載了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM