newbie4ever

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

平常在做Winform的应用程序中,可能很少有人不用到多线程吧?一般说来,我们的工作线程多和界面的线程(主线程)不在一个线程中,道理也很简单,在一个线程中的话,较长时间等待的工作会阻塞界面线程,影响界面显示。下文中我提到的“工作线程”均指从界面线程引发的新的线程
那么,相信大家也经常会遇到在工作线程中需要更新界面元素的情况吧,比如,除错了我们要Msgbox一个出错信息,可能还要在Statusbar中显示一些东西(进度等等),那么,遇到这种情况你在工作线程中是怎么写的呢?是直接写:Msgbox("error")这样吗?以前我也一直这样做。
后来看了欧严亮的WebCast,我知道这样做是不准确的。当工作线程需要更新界面线程的元素时,应使用界面线程来更新。具体来说,类似下面这样:
比如我们在工作线程中调用方法ShowError(byval msg as String)来显示错误信息,那么这个ShowError方法不应该只有一句Msgbox(msg),而是应该这样:
Private Delegate Sub ShowErrDelegate(ByVal msg As String)
Protected Sub ShowErr(ByVal msg As String)
        If InvokeRequired Then
            BeginInvoke(New ShowErrDelegate(AddressOf ShowErr), New Object() {msg})
            Return
        End If
        MsgBox(msg, MsgBoxStyle.Exclamation, "Error")

        Me.Cursor = System.Windows.Forms.Cursors.Default
End Sub

这一段代码先判断调用是否是从工作线程中来的,即if Invokerequired的语句(当然,这里用的是me.invokerequired,你也可以用界面线程中的任一个控制,比如me.TextBox1.invokerequired这样),如果不是的话,那么,会通过界面线程的Delegate来调用此方法,当然,这个Delegate是指向自己的。看起来有点像递归,其实不是的。他的大概意思就是,如果不是从界面线程来调用我的,那么,我使用界面线程来调用我。

虽然我把代码中的相应部分改了,改成用界面线程来调用,但是我以前真的觉得多此一举,因为直接在工作线程中更新界面一样运行得很好啊,为什么要这么麻烦?这究竟只是一种规范,还是必不可少?

后来终于通过一个应用明白了其中的意义。

我的应用程序后来做多语言版了,我参考了一下msdn,决定使用资源文件的方式来实现多语言。dotnet中,系统可根据当前线程的CultrueInfo信息,决定使用哪个资源文件。于是乎,在我的界面线程中,我用了这样的语句来设定其CultureInfo信息:
If Language = GlobalVariableModule.Lang.English Then
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-US")
        Else
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("zh-CN")
End If
然后自然,有些语句得改了,比如在工作线程中,我这样调用ShowError方法:
ShowError(RscMgr.GetString("ERROR"))        'RscMgr为一ResourceManager

按理说,如果我的language设为中文,应显示中文的错误信息,设为英文应显示英文错误信息,其实不然,这个调用无论在什么情况下,都显示default的错误信息,即英文的。为什么呢?因为我们在工作线程中调用了ResourceManager来得到错误信息对应的字符串,但是在工作线程中,即cultureInfo其实是为英文的。所以,传给ShowError的msg,其实是英文的错误信息。
我马上想到了补救方法,两个:
1、修改ShowError的参数,改为rscId,即,我传一个resourceid过去,在本例中,为"ERROR",并且,在ShowError方法中,我们不再直接msgbox参数,而是MsgBox(RscMgr.GetString(rscId)),这样就使用了界面线程来得到错误信息,是会正常显示中文的错误信息的。当然,如果你试着把ShowError方法中的invokerequired语句部分去掉,还是不能正常显示中文错误信息,道理前面讲过,这样是用工作线程来调用了些方法。到了这里,你也明白了为什么一定要用界面线程来更新界面元素了吧,这只是其中一个例子。
2、在工作线程中设置CultrueInfo信息,即在工作线程中加入下面的语句:
If Language = GlobalVariableModule.Lang.English Then
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-US")
        Else
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("zh-CN")
End If
然后还是使用原来的ShowError方法。

我一开始想用第1种方法,不知道为什么,总觉得不应该在工作线程中加入太多可能和界面的工作。但其实第一种方法很不容易行得通,因为我的错误信息不会是简单的String,不是简单的通过RscMgr.GetString(rscId)就可以得到的,而是有相关的其它信息,比如,错误出现在什么位置等等,这时我的错误信息可能是以下面的方式组装起来的:String.Format(RscMgr.GetString(rscId),lineNo),其中,lineNo表示错误出现的行数,而资源文件中存放的错误信息是这样的格式:"Error in line {0}"。在这种情况下,因为lineNo这样的值来自于工作线程,所以错误信息只能再工作线程里组装完成,当然也可以把lineNo传递给ShowError方法,但这样会导致ShowError方法的重载版本太多,而且和特定应用偶合太紧。
所以不得已,还是得使用第二种方法。

当然,对于我最前面说的情况,在界面线程的方法中要判断对其的调用是否来自主线程,也可以不这样做。取而代之,我们可以在工作线程中引发事件,而界面线程的更新界面方法来响应这个事件,这样也可以达到“用界面线程来更新界面元素”的目的。

无论是哪种方法,在当前我遇到的情况下,如果你要在工作线程中组装错误信息,那么只能把工作线程的CultrueInfo也设置一下。事实上我不太喜欢这种的设计方式,不知道大家在遇到类似情况时有什么好的设计方法吗?一起交流一下吧。

posted on 2005-05-25 13:55  yinh  阅读(3241)  评论(0编辑  收藏  举报