顯示具有 C#、Thread 標籤的文章。 顯示所有文章
顯示具有 C#、Thread 標籤的文章。 顯示所有文章

2012年2月5日 星期日

Image與BitmapImage如何用多執行緒來達成預先載入?

如果沒有實做過,會覺得應該不難,沒什麼問題,但是,真的在做的時候,會因為不夠瞭解,而遇到這個陷阱的。




首先,你當然必須在Windows上放上一個Image元件(要用程式建立也行啦),然後,在Window的Class層級宣告以下元件(因為很多函式會共用到,所以不能在函式內宣告。


        private BitmapImage BitMap;
        private FileStream FStream;
        private BackgroundWorker BgWk;


然後再需要載入圖片的時機,產生BackgroundWorker 物件來幫你做多執行緒處理(看是要在某個按鈕被按下,還是在某個Timer事件裡,都好)



            BgWk = new BackgroundWorker( );
            BgWk.DoWork += new DoWorkEventHandler( BgWk_DoWork );
            BgWk.RunWorkerCompleted += new RunWorkerCompletedEventHandler( BgWk_RunWorkerCompleted );
            BgWk.RunWorkerAsync( );





上面需要綁定兩個函式來做BackgroundWorker的事件,一個是DoWork(),是開新的Thread要做的事情,另外一個是RunWorkerCompleted(),則是在Thread忙完之後要做的事情,這裡,非常簡單,很直覺的可以想像,把BitmapImage載入圖片的那一段程式,放到DoWork(),載入完成之後在RunWorkerCompleted()這個函式裡,Image要載入這個圖,兩個函式如下



        private void BgWk_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
        {
            ImageControl.BeginInit( );
            ImageControl.Source = BitMap;
            ImageControl.EndInit( );
        }


        private void BgWk_DoWork( object sender, DoWorkEventArgs e )
        {
            // 開檔
            String FilePath = @"C:\temp\IMG001.JPG";
            FStream = new FileStream( FilePath, FileMode.Open );


            // 將資料讀入 BitmapImage 變成圖檔資源
            BitMap = new BitmapImage( );
            BitMap.BeginInit( );
            BitMap.CacheOption = BitmapCacheOption.OnLoad;
            BitMap.StreamSource = FStream;
            BitMap.EndInit( );
            BitMap.Freeze( );
            FStream.Close( );
        }







以上想法沒有問題,但是問題就在紅色粗斜體這一行,BitmapImage有個特性,就是他的載入圖片這個行為會牽涉到UI的變更,也就是說,當你沒有紅色這一行的時候,BitmapImage跟「會顯示畫面的UI」是同等的,也就是你無法跨執行緒使用他,這個BitmapImage已經屬於DoWork()這個執行緒了(為什麼?因為在函式裡有New產生物件,所以屬於次程序),當你回到RunWorkerCompleted()主執行緒的時候,無法再用他了,會引發「跨執行緒使用UI物件」的InvalidOperationException,那怎麼辦?我已經想要把UI跟Data分開了,如果BitmapImage是個UI物件,那就無法預先載入圖檔了呀,因為我又要在背景執行緒載入圖,不同執行緒卻又不能使用同一個物件,如果你用Dispatch來做,那也失去意義,因為Dispatch最後也是回歸到UI Thread來做事,一樣卡了UI,造成無反應狀態,原來,其中的關鍵就在於當你把圖檔載入之後,要呼叫Freeze()來把資源固定,就可以跟一般物件一樣,可以跨執行緒來使用。達成「讓沒有UI的繁瑣事情可以在背景執行,有UI的更新在前景進行啦」。

2011年9月8日 星期四

C# Background Worker介紹

Background worker:一個簡單、好用的Thread方案
它可以以很簡單的方法產生工作執行緒,並且保持主執行緒的可用性,並且也支援回報狀態與交換資料能力,更方便的是他在處理跨執行緒使用UI介面的部分,大多時候可以不用透過Delegate、Invoke、WaitCallBack......等等複雜的觀念與機制,因為他已經幫你實作掉了。




寫法:可以由工具箱當中直接拉Background worker的控制項到Form上面,這樣也可以直接雙擊IDE介面來產生事件Method。


或是也可以自己宣告來產生實體,自己設定Event物件與Event Method。








Method:
RunWorkerAsync(object)
呼叫此函式,會讓Background worker以多執行緒的方式來執行DoWork()內的程式




DoWork(object sender, DoWorkEventArgs e)
將你想執行的耗時的程式片段寫在其中,在呼叫Background worker的RunWorkeAsync()時,就會以多執行緒的方式執行此函式。注意,在這個函式中,不可以執行任何跟UI有關的動作,不允許跨執行緒存取UI。




RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
當DoWork()被執行完成之後,會自動呼叫這個函式,如果預期完成之後要做什麼動作,可以寫在這個函式裡。這個函式可以存取Main Thread的UI,不會有誇執行緒的問題。或者說,除非寫法上有多執行緒的特殊用法,否則一般來說這個函式會在Main Thread執行,也因此才不會有跨執行緒存取UI的問題產生。





ProgressChanged(object sender, ProgressChangedEventArgs e)
Background worker提供了一個ReportProgress()的函式,可以隨時對這個背景執行的工作定義完成進度,而通常,想當然爾,這個函式,會在DoWrok裡呼叫,依照做的進度,來呼叫函式傳給Background worker,而當你呼叫了ReportProgress()之後,就會立刻一併觸發ProgressChanged()。而這個函式可以存取Main Thread的UI,不會有誇執行緒的問題。或者說,除非寫法上有多執行緒的特殊用法,否則一般來說這個函式會在Main Thread執行,也因此才不會有跨執行緒存取UI的問題產生。 也就是說,一般我們會把需要變更UI的程式寫在這裡,而不會寫在DoWork(),當DoWrok()當中的,需要對UI有什麼控制時,就以回報進度的方式,在ProgressChanged()裡頭來做。