从线程更新 UI - 最简单的方法






4.09/5 (8投票s)
一段可重用的代码,实现了从线程更新 UI 元素的功能。
引言
如今,多线程应用程序被广泛使用。我们为各种不同的目的创建多个线程。一个线程可能作为 TCP/IP 监听器,另一个可能定期查询数据库,还有另一个可能尝试从 Internet 连接并下载一些东西。在典型的 Windows 环境中,我们不能让 UI 线程处理所有这些。因为 UI 线程的开销很大,它需要响应用户的鼠标和键盘事件,而不是花费时间执行上述任务。如果我们让 UI 线程处理这些任务,应用程序很有可能会“卡死”。为了避免这种情况,许多开发人员使用计时器控件或 `System.Timer` 对象在后台处理某些事情。但是,使用多个线程是处理这种情况的首选,或者更准确地说,是专业的方式。尽管听起来不错,但有一个问题。我们不能直接从另一个线程更新任何 UI 元素。
问题
让我们来看一个非常简单的场景。我想运行两个线程,它们将一个变量从 0 递增到 9。每次变量递增时,我想用当前值更新窗体中的富文本框(如上所示)。如果我们不使用另一个线程来完成这项工作,那很简单。
上图描绘了在另一个线程内部未能更新 UI 元素的错误逻辑。
解决方案
如引言中所述,有几种方法可以处理这个问题。最简单的方法是使用计时器控件或 `System.Timer` 对象,它们是特殊的线程,可以直接更新 UI 元素。但是,我们不能始终将此技术应用于所有场景。例如,我不能使用计时器来“监听” TCP/IP 端口,而必须实现一个线程来连续监听。那么,我该如何从这个线程更新 UI 呢?
这可以通过多种方式完成。在本论坛中有几篇文章解释了许多用于从线程更新 UI 的技术。但是,我找到了一种更简单的方法,即使用容器 `Form` 或 `Control` 的 `Invoke` 方法、`Delegate` 对象以及一个与 `Delegate` 类似的方法。
在对象浏览器中浏览定义时,`Invoke` 方法的定义引起了我的注意,并帮助我轻松地完成了任务。
Public Function Invoke(ByVal method As System.Delegate) As Object
成员:`System.Windows.Forms.Control`。
摘要:在拥有控件底层窗口句柄的线程上执行指定的委托。
参数:`method` - 一个委托,其中包含要在控件的线程上下文中调用的方法。
返回值:被调用委托的返回值,如果委托没有返回值,则为 `null`。
在 `CallBackThread` 类中定义的两个委托
''' <summary>
''' Delegate for the call back function
''' </summary>
''' <param name="status">status message</param>
''' <remarks>Declare method with signature
''' Sub [AnyMethodName](status as String)'</remarks>
Public Delegate Sub CallBackDelegate(ByVal status As String)
''' <summary>
''' The parameterless Thread function delegate
''' </summary>
''' <remarks></remarks>
Public Delegate Sub ThreadFunctionDelegate()
调用 UI 回调的代码部分
''' <summary>
''' Sends the status to the Form/Control that implements the Call back method.
''' </summary>
''' <param name="msg">the message to send</param>
''' <remarks></remarks>
Public Sub UpdateUI(ByVal msg As String)
If m_BaseControl IsNot Nothing AndAlso _
m_CallBackFunction IsNot Nothing Then
m_BaseControl.Invoke(m_CallBackFun/ction, New Object() {msg})
End If
End Sub
线程函数以及调用 UI 回调的方式
Private Sub ThreadMethod1()
'Thread 1 simply starts from 0 and counts up to 10
For i As Integer = 1 To 10
objThread1.UpdateUI("1st Thread Ticking " & i)
Threading.Thread.Sleep(500)
Next
End Sub
Using the Code
此项目是一个简单的示例,其中包含一个可重用类,该类可以激活线程并将状态作为字符串发送到 UI。通过仅更改线程函数和回调函数的实现,可以修改它以适应任何其他场景。同时,通过更改类的 `Delegate` 和 `UpdateUI` 方法,可以实现任何高级功能。
- 将简单的状态文本发送到 UI:无需更改 `CallBackThread` 类。更改线程函数(例如,`Private Sub ThreadMethod1()`)和回调函数(例如,`Private Sub CallBackMethod1(ByVal status As String)`)以满足您的需求。
- 将多个参数发送回 UI:更新以下内容。
- 更改 `CallBackThread` 类中的 `CallBackDelegate`(例如,`Public Delegate Sub CallBackDelegate(ByVal msg As String, progress as Integer)`)
- 更改 `CallBackThread` 类中的 `UpdateUI` 函数
- 更改窗体/控件中的回调方法实现,以匹配回调委托的更改(例如,`Private Sub CallBackMethod1(ByVal msg As String, progrss as Integer)`)
- 调用带参数的线程函数:这需要更改代码的以下部分。
- `CallBackThread` 类的构造函数(`Public Sub New(...)`)。
- `CallBackThread` 类中的函数调用器(`Private Sub ThreadFunction(...)`)。
- 窗体/控件中的线程函数实现(`Private Sub ThreadMethod1(...)`)。
'Eg.
Public Sub UpdateUI(ByVal msg As String, progress as Integer)
If m_BaseControl IsNot Nothing AndAlso _
m_CallBackFunction IsNot Nothing Then
m_BaseControl.Invoke(m_CallBackFunction, New Object() {msg, progress})
End If
End Sub
有关如何调用带参数的线程函数的更多信息,请参考 MSDN。