一、前面的話
對於一些耗時型操作(如文件下載),讓主線程去處理不是明智的選擇,雖然這樣做會使得程序開發起來很簡單。因為WinForm程序設計的 准則之一就是Responsive,即讓用戶覺得程序一直在工作,而不是感覺它在罷工(呵呵,事實上,程序不會罷工,只是你沒給他表現得機會, 如果它有情感,會覺得委屈死)。.Net FrameWork支持在程序用應用線程編程,這可以很好的解決上述問題,不過有時候直接使用Thread和 Threadstart顯得有些繁瑣也沒必要,為此.Net Framework提供了BackgroundWorker組件來應付一些簡單的應用環境。
本文將分別對上述兩種情況的跨線程操作控件方法進行闡述。
二、BackgroundWorker下的跨線程操作控件
BackgroundWorker是個很好的伙計,因為它可以忙你搞定許多髒活累活。具體的講,它可以自動的幫你創建工作線程,可以在工作時把工作 的進展情況告訴給你,可以在工作完成時通知並幫你做一些收尾的工作,當你覺得他很煩的時候,你還可以隨時讓他停下來。
BackgroundWorker組件提供了三個事件:DoWork,ProgressChanged和RunWorkerCompleted。Dowork顧名思義是用來處理工作業務的 ,在這裡面加入你想讓工作線程在後台處理的代碼即可。但是在這個事件中不能加入跨線程操作的代碼。如下圖,當我試圖改變Label.Text的 值時,拋出了異常信息:
不過這裡有個例外,就是對於ToolStrip及其從該類繼承過來的容器控件,某些在該容器上的控件(如StatusLabel)可以在工作線程 中直接操作。至於為啥,我沒有去深究,不過根據圖中的提示信息,一個很合理的解釋就是這類控件和BackgroundWorker由同一個線程創建。
ProgressChanged和RunWorkerCompleted事件分別用來報告工作線程的工作情況和在工作線程結束後進行一些操作。這兩個事件都支 持跨線程操作控件。下面通過一個簡單的實例進行驗證。
用程序實現將一個目錄中的文件拷貝至另外一個目錄。
1.程序界面設計如下:
2.工作流程:(1)設置源目錄和目標目錄(2)拷貝文件,在ListView中顯示拷貝信息,更新狀態欄中的進度條和當前處理文件信息(3) 拷貝過程結束後,用MessageBox提示拷貝文件數量,同時清空源目錄和目標目錄信息。
3.代碼實現
1private void bwFileCopy_DoWork(object sender, DoWorkEventArgs e)
2 {
3 DirectoryInfo di = (DirectoryInfo)e.Argument;
4 int iCur = 1;
5 foreach (FileInfo fi in di.GetFiles())
6 {
7 //為證明ToolTrip對於跨線程的特殊性,在此處更新狀態欄的當前處理文件信息
8 //實際應用時最好放到ProgressChanged中,通過ReportProgress的參數UserState傳遞要處理的信息!
9 tsslInfo.Text = string.Format("當前正在拷貝文件:{0}", fi.Name);
10
11 fi.CopyTo(Path.Combine(targetDir,fi.Name),true);
12 bwFileCopy.ReportProgress(GetPercent(iCur, iFileCount),fi.Name);
13 iCur++;
14
15 }
16 e.Result = iCur;
17 }
18
19private void bwFileCopy_ProgressChanged(object sender, ProgressChangedEventArgs e)
20 {
21 //在此處更新狀態欄中的進度條
22 tssbProcess.Value = e.ProgressPercentage;
23
24 //在Listview中添加拷貝信息
25 string FileName = e.UserState.ToString();
26 lvOutput.Items.Add(new ListViewItem(new string[] {System.DateTime.Now.ToLongTimeString (),FileName})).EnsureVisible();
27
28 }
29private void bwFileCopy_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
30 {
31 //清空源目錄和目標目錄
32 tbSource.Text = string.Empty;
33 tbTargetDir.Text = string.Empty;
34 //提示拷貝文件數量
35 MessageBox.Show(string.Format("此過程共拷貝了{0}個文件",e.Result));
36 }
37
4.運行結果
三、Thread/ThreadStart下的跨線程操作控件
在一些情況下,Thread/ThreadStart也是有一定市場的,特別在工作線程很多的情況下,顯得尤為突出。事實上,在這種環境下要實現上述 的例子並不難,代碼也沒有增加多少,前提是你必須理解Control.Invoke方法。該方法在MSDN上的解釋是:在擁有此控件的基礎窗口句柄的線 程上執行委托。如果你注意到了第一張圖片顯示的異常信息,你會很快理解這個方法的重大意義。它可以讓工作線程的中委托在主線程中執行 !因此實現上述例子的思路就是,在工作線程中使用委托來執行操作控件的方法,然後用主窗口的Invoke方法調用!為了實現 BackgroundWorker的ProgressChanged和RunWorkerCompleted事件,定義了ReportProcessInfo委托和DoneAfterCompleted委托。主要代碼如下 :
1//實現BackgroundWorker的ProgressChanged事件
2public delegate void ReportProcessInfo(string Info, int iPercent);
3/**/////實現BackgroundWorker的RunWorkerCompleted事件
4public delegate void DoneAfterCompleted(string Info);
5
6//更新Listview和ProgressBar的方法
7 private void UpdateInfoToUser(string info,int percent)
8 {
9 if (InvokeRequired)
10 Invoke(new ReportProcessInfo(UpdateInfoToUser), info, percent);
11 else
12 {
13 lvOutput.Items.Add(new ListViewItem(new string[] { System.DateTime.Now.ToLongTimeString(), info })).EnsureVisible();
14 tssbProcess.Value = percent;
15 }
16
17 }
18 //清空源目錄和目標目錄信息,顯示拷貝文件數的方法
19 private void ShowUserFilesCountInfo(string info)
20 {
21 if (InvokeRequired)
22 Invoke(new DoneAfterCompleted(ShowUserFilesCountInfo), info);
23 else
24 {
25 tbSource.Text = string.Empty;
26 tbTargetDir.Text = string.Empty;
27MessageBox.Show(info);
28 }
29
30 }
31//線程函數
32private void CopyFiles(object SourceDir)
33 {
34 DirectoryInfo di = (DirectoryInfo)SourceDir;
35 int icur = 0;
36 foreach (FileInfo fi in di.GetFiles())
37 {
38 icur++;
39 tsslInfo.Text = string.Format("當前正在處理文件:{0}",fi.Name);
40 fi.CopyTo(Path.Combine(targetDir,fi.Name),true);
41 CopyOneFileIsOK(fi.Name,GetPercent(icur,iFileCount));
42 }
43 CopyFilesIsCompleted(string.Format("本次操作共拷貝了{0}個文件!",icur));
44 }
45
運行結果如下:
四、結尾
代碼中的InvokeRequired用於判斷該段代碼是否是在其他線程中委托調用的,如果為真,就需要在本線程中重新創建一個該委托的實例,並 用Invoke方法調用它,讓這段代碼在本線程中調用。
當代碼中需要對多個控件進行操作,最好使用Form的InvokeRequired來判斷,並使用Form的Invoke方法調用新建的委托實例。當只對某個控 件操作時,就可以只用該控件的InvokeRequired和Invoke。比如tbSource,就可用tbSource.InvokeRequired和tbSource.Invoke。