一、前面的话
对于一些耗时型操作(如文件下载),让主线程去处理不是明智的选择,虽然这样做会使得程序开发起来很简单。因为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.代码实现
Code
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委托。
主要代码如下:
Code
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。
下面是本文相关代码
Thread_ThreadStart方式
BackgroundWorker方式