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的更新在前景進行啦」。