当前位置导航:炫浪网>>网络学院>>编程开发>>Visual Basic教程

在Visual Basic .NET 中实现后台进程二

  

委托的用途很广。在我们的示例中,委托非常重要,因为委托使我们可以让一个线程调用窗体上的方法,使其在该窗体的 UI 线程上运行。正如 IClient 所定义的那样,要在窗体上调用的三个方法都需要委托:

 

' 此委托签名与 IClient.Completed

' 中的签名相匹配,并用于安全地

' 调用 UI 线程上的方法

Private Delegate Sub CompletedDelegate(ByVal Cancelled As Boolean)

 

' 此委托签名与 IClient.Display

' 中的签名相匹配,并用于安全地

' 调用 UI 线程上的方法

Private Delegate Sub DisplayDelegate(ByVal Text As String)

 

' 此委托签名与 IClient.Failed

' 中的签名相匹配,并用于安全地

' 调用 UI 线程上的方法

Private Delegate Sub FailedDelegate(ByVal e As Exception)

 

IClient 还定义了 Start 方法,但是该方法可以从 UI 线程调用,因此不需要委托。

 

下面编写将从 UI 线程调用的代码。代码中包括 constructor 方法、Start 和 Cancel 方法以及 Percent 属性。我将这些内容放入 Region 中,便于大家清楚地了解它们是从 UI 线程调用的。

 

#Region " 从 UI 线程调用的代码 "

 

' 使用客户端初始化 Controller

Public Sub New(ByVal Client As IClient)

mClient = CType(Client, Form)

End Sub

 

' 此方法由 UI 调用,因此在

' UI 线程上运行。此处我们将

' 启动辅助线程

Public Sub Start(Optional ByVal Worker As IWorker = Nothing)

' 如果辅助线程已经启动,将产生错误

If mRunning Then

Throw New Exception("Background process already running")

End If

 

mRunning = True

 

' 存储对辅助对象的引用,并

' 初始化辅助对象,使其包含

' 对 Controller 的引用

mWorker = Worker

mWorker.Initialize(Me)

 

' 创建后台线程

' 以进行后台操作

Dim backThread As New Thread(AddressOf mWorker.Start)

 

' 开始后台工作

backThread.Start()

 

' 告诉客户端后台工作已开始

CType(mClient, IClient).Start(Me)

End Sub

 

' 此代码由 UI 调用,因此在 UI

' 线程上运行。它只设置了请求

' 取消的标志

Public Sub Cancel()

mRunning = False

End Sub

 

' 返回完成百分比值,并且

' 只被 UI 线程调用

Public ReadOnly Property Percent() As Integer

Get

Return mPercent

End Get

End Property

 

#End Region

 

此处唯一比较特殊的代码位于 Start 方法中,我们可以在该方法中创建辅助线程然后启动该线程:

 

Dim backThread As New Thread(AddressOf mWorker.Start)

 

backThread.Start()

 

要创建线程,需要在 Worker 对象的 IWorker 接口上传递 Start 方法的地址。然后,只需调用线程对象的 Start 方法即可开始操作。此时我们要特别注意,UI 不应直接与 Worker 交互,Worker 也不应直接与 UI 交互。

 

请注意,Cancel 方法只设置一个标志,表明我们不希望继续运行。辅助代码应定期查看此标志,以确定是否应该停止运行。

 

现在,我们可以实现 Worker 对象运行时将由辅助线程调用的代码。此代码比较有趣,因为它必须将 Display 和 Completed 从辅助线程中转至 UI 线程,同时还要在 UI 线程上完成此操作。

 

要完成此操作,我们可以使用 Form 对象的 Invoke 方法。此方法接受窗体应该调用的方法的委托指针,以及包含该方法的参数的 Object 类型数组。

 

Invoke 方法不直接调用窗体上的方法,而是请求窗体返回并使用窗体的 UI 线程调用该方法。此操作可通过向窗体发送 Windows 消息在后台完成。这说明窗体获得这些方法调用的方式与从操作系统中获得 click 或 keypress 事件的方式基本相同。

 

通常,这些细节不会影响大局。结果由 Invoke 方法触发一个进程,通过该进程窗体将终止其 UI 线程上运行的方法,这就是我们要实现的目标。

 

再次重申,此代码位于 Region 内,目的是为了明确它将在辅助线程上调用:

 

#Region " 从辅助线程调用的代码 "

 

' 从辅助线程调用,以更新显示

' 这将触发对包含状态文本的 UI 的

' 方法调用 - 该调用是在 UI 线程上

' 进行的

Private Sub Display(ByVal Text As String) _

Implements IController.Display

 

Dim disp As New DisplayDelegate( _

AddressOf CType(mClient, IClient).Display)

Dim ar() As Object = {Text}

 

' 调用 UI 线程上的客户端窗体

' 以更新显示

mClient.BeginInvoke(disp, ar)

 

End Sub

 

' 从辅助线程调用,以表明出现故障

' 这将触发对包含异常对象的 UI 的

' 方法调用 - 该调用是在 UI 线程上

' 进行的

Private Sub Failed(ByVal e As Exception) _

Implements IController.Failed

 

Dim disp As New FailedDelegate(_

AddressOf CType(mClient, IClient).Failed)

Dim ar() As Object = {e}

 

' 在 UI 线程上调用客户端窗体

' 以表明出现故障

mClient.Invoke(disp, ar)

 

End Sub

 

' 从辅助线程上调用,以指出完成的百分比

' 值将转到 Controller,由 UI 在需要时读取

Private Sub SetPercent(ByVal Percent As Integer) _

Implements IController.SetPercent

 

mPercent = Percent

 

End Sub

 

' 从辅助线程调用,以表明已完成

' 我们还传递参数,以表明是否真正完成,

' 以及是否取消在 UI 线程上进行的对 UI

' 的调用

Private Sub Completed(ByVal Cancelled As Boolean) _

Implements IController.Completed

 

mRunning = False

Dim comp As New CompletedDelegate( _

AddressOf CType(mClient, IClient).Completed)

Dim ar() As Object = {Cancelled}

 

' 调用 UI 线程上的客户端窗体

' 以表明已完成

mClient.Invoke(comp, ar)

 

End Sub

 

' 表明是否仍在运行或是否已请求取消

' 这将在辅助线程上进行调用,因此

' 辅助代码可以查看它是否应该正常

' 退出

Private ReadOnly Property Running() As Boolean _

Implements IController.Running

Get

Return mRunning

End Get

End Property

 

#End Region

 

Failed 和 Completed 方法利用窗体的 Invoke 方法。例如,Failed 方法可以执行以下操作:

 

Dim disp As New FailedDelegate(_

AddressOf CType(mClient, IClient).Failed)

Dim ar() As Object = {e}

 

' 调用 UI 线程上的客户端窗体

' 以表明出现故障

mClient.Invoke(disp, ar)

 

首先创建一个委托,从 IClient 接口指向客户端窗体的 Failed 方法。然后声明包含向方法传递参数值的 Object 类型数组。最后调用客户端窗体的 Invoke 方法,将委托指针和参数数组传递给窗体。

 

窗体将在 UI 线程(窗体在这里可以安全运行以更新显示)上使用这些参数调用此方法。

 

整个进程是同步进行的,即对窗体进行调用时辅助线程将停止。尽管可以在显示错误消息或完成消息时停止辅助线程,但我们并不希望显示每个小状态时都停止辅助线程。

 

为了避免显示状态时停止辅助线程,Display 方法将使用 BeginInvoke,而不使用 Invoke。BeginInvoke 使窗体上的方法调用异步进行,这样辅助线程可以一直保持运行状态,不需要等待窗体上的显示方法完成:

 

Dim disp As New DisplayDelegate( _

AddressOf CType(mClient, IClient).Display)

Dim ar() As Object = {Text}

 

' 调用 UI 线程上的客户端窗体

' 以更新显示

mClient.BeginInvoke(disp, ar)

 

以这种方式使用 BeginInvoke 可以防止辅助线程停止,使辅助线程具有尽可能高的性能。

 

ActivityBar 控件

最后,我们来创建显示动画点的 ActivityBar 控件。

 

在名为 ActivityBar 的项目中添加一个用户控件。

 

将该控件的宽度调整为约 110,高度调整为约 20。可以通过拖动边界进行调整,也可以通过在 Properties(属性)窗口中设置 Size 属性进行调整。

 

其余的操作将通过代码完成。要创建一系列在显示时不停闪烁的动画“灯”,可以使用带有 Timer 控件的一系列 PictureBox 控件。每次 Timer 控件关闭时,我们将使下一个 PictureBox 呈绿色显示,并将已经呈绿色显示的 PictureBox 更改为窗体的背景色。

 

将 Windows Forms(Windows 窗体)选项卡中的 Timer 控件放入窗体中,然后将其名称更改为 tmAnim。同时将 Interval 属性设置为 300,以获得较好的动画速度。

 

顺便说一句,Components(组件)选项卡中有一个不同的 Timer 控件。它是一个多线程计时器。也就是说,该计时器将在后台线程中引发 Elapsed 事件,而不是象 Windows 窗体计时器那样在 UI 线程上引发 Elapsed 事件。建立 UI 时这种方法通常会产生相反的效果,因为 Elapsed 事件中的代码显然不能直接与我们的 UI 进行交互。

 

现在,在控件中添加以下代码:

 

Private mBoxes As New ArrayList()

Private mCount As Integer

 

Private Sub ActivityBar_Load(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles MyBase.Load

 

Dim index As Integer

 

If mBoxes.Count = 0 Then

For index = 0 To 6

mBoxes.Add(CreateBox(index))

Next

End If

mCount = 0

 

End Sub

 

Private Function CreateBox(ByVal index As Integer) As PictureBox

Dim box As New PictureBox()

 

With box

SetPosition(box, index)

.BorderStyle = BorderStyle.Fixed3D

.Parent = Me

.Visible = True

End With

Return box

End Function

 

Private Sub GrayDisplay()

Dim index As Integer

 

For index = 0 To 6

CType(mBoxes(index), PictureBox).BackColor = Me.BackColor

Next

End Sub

 

Private Sub SetPosition(ByVal Box As PictureBox, ByVal Index As Integer)

Dim left As Integer = CInt(Me.Width / 2 - 7 * 14 / 2)

Dim top As Integer = CInt(Me.Height / 2 - 5)

 

With Box

.Height = 10

.Width = 10

.Top = top

.Left = left + Index * 14

End With

End Sub

 

Private Sub tmAnim_Tick(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles tmAnim.Tick

 

CType(mBoxes((mCount + 1) Mod 7), PictureBox).BackColor = _

Color.LightGreen

CType(mBoxes(mCount Mod 7), PictureBox).BackColor = Me.BackColor

 

mCount += 1

If mCount > 6 Then mCount = 0

End Sub

 

Public Sub Start()

CType(mBoxes(0), PictureBox).BackColor = Color.LightGreen

tmAnim.Enabled = True

End Sub

 

Public Sub [Stop]()

tmAnim.Enabled = False

GrayDisplay()

End Sub

 

Private Sub ActivityBar_Resize(ByVal sender As Object, _

ByVal e As System.EventArgs) Handles MyBase.Resize

 

Dim index As Integer

 

For index = 0 To mBoxes.Count - 1

SetPosition(CType(mBoxes(index), PictureBox), index)

Next

End Sub

 

窗体的 Load 事件创建 PictureBox 控件并将它们放入数组,这样便于我们在它们之间循环。Timer 控件的 Tick 事件循环显示,使各个控件依次呈绿色。

 

所有操作由 Start 方法开始,由 Stop 事件结束。由于 Stop 是一个保留字,因此把这个方法名放在方括号内:[Stop]。Stop 方法不仅可以停止计时器,还可以灰显所有框,告诉用户这些框中当前没有活动。

 

创建 Worker 类

 

本文前面已简单介绍了 Worker 类。因为我们已经定义了 IWorker 接口,所以可以增强该类,以利用我们创建的 Controller。

 

首先创建 Background.dll 文件。此步骤很重要,因为如果不完成此步骤,ActivityBar 控件将无法在我们建立测试窗体时显示在工具箱上。

 

在解决方案中添加名为 bgTest 的 Windows Forms Application(Windows 窗体应用程序)。在 Solution Explorer(解决方案资源浏览器)中用右键单击该项目并选择相应的菜单项,将该程序设置为启动项目。

 

然后使用 Add References(添加引用)对话框中的 Projects(项目)选项卡,添加对 Background 项目的引用。

 

现在,在名为 Worker 的项目中添加一个类。其中部分代码与前面所述的代码相同,但还包含一些不同的代码,用以实现 IWorker 接口(此处突出显示的部分):

 

Imports Background

 

Public Class Worker

Implements IWorker

 

Private mController As IController

Private mInner As Integer

Private mOuter As Integer

 

Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer)

mInner = InnerSize

mOuter = OuterSize

End Sub

 

' 由 Controller 调用,以便获取

' Controller 的引用

Private Sub Init(ByVal Controller As IController) _

Implements IWorker.Initialize

 

mController = Controller

 

End Sub

 

Private Sub Work() Implements IWorker.Start

Dim innerIndex As Integer

Dim outerIndex As Integer

 

Dim value As Double

 

Try

For outerIndex = 0 To mOuter

If mController.Running Then

mController.Display("Outer loop " & outerIndex & " starting")

mController.SetPercent(CInt(outerIndex / mOuter * 100))

 

Else

' 它们请求取消

mController.Completed(True)

Exit Sub

End If

 

For innerIndex = 0 To mInner

' 此处进行一些有意思的计算

value = Math.Sqrt(CDbl(innerIndex - outerIndex))

Next

Next

mController.SetPercent(100)

mController.Completed(False)

 

Catch e As Exception

mController.Failed(e)

 

End Try

End Sub

End Class

 

我们添加了能够实现 IWorker.Initialize 的 Init 方法。Controller 将调用此方法,因此以后我们可以引用 Controller 对象。

 

我们还将 Work 方法更改为 Private,只是为了实现 IWorker.Start 方法。此方法将在辅助线程上运行。

 

我们增强了 Work 方法,使其可以使用 Try..Catch 块。这样我们可以使用 Controller 上的 Failed 方法捕捉任何错误并将其返回给 UI。

 

假设代码正在运行,我们调用 Controller 对象的 Display 和 SetPercent 方法,使它们随着代码的运行更新其状态和完成的百分比。

 

我们还定期检查 Controller 对象的 Running 属性,查看是否存在取消请求。如果存在取消请求,则停止进程,并指示由于取消请求而停止操作。

 

创建显示的窗体

 

最后,我们可以创建窗体,将其用于启动或取消后台进程。该窗体还将显示活动和状态信息。

 

打开 Form1 的设计器并添加两个按钮(btnStart 和 btnRequestCancel)、两个标签(Label1 和 Label2)、一个 ProgressBar (ProgressBar1) 和一个 ActivityBar (ActivityBar1)。

 

该窗体需要实现 IClient,以便 Controller 对象与之交互:

 

Imports Background

 

Public Class Form1

Inherits System.Windows.Forms.Form

 

Implements IClient

 

该窗体还需要 Controller 对象和一个标志,用以跟踪后台操作是处于活动状态还是处于完成状态。

 

Private mController As New Controller(Me)

Private mActive As Boolean

 

然后,我们可以添加方法,以实现由 IClient 定义的接口。建议将这些方法放在 Region 中,以表示它们实现的是辅助接口:

 

#Region " IClient "

 

Private Sub TaskStarted(ByVal Controller As Controller) _

Implements IClient.Start

 

mActive = True

Label1.Text = "Starting"

Label2.Text = "0%"

ProgressBar1.Value = 0

ActivityBar1.Start()

 

End Sub

 

Private Sub TaskStatus(ByVal Text As String) _

Implements IClient.Display

 

Label1.Text = Text

Label2.Text = CStr(mController.Percent) & "%"

ProgressBar1.Value = mController.Percent

 

End Sub

 

Private Sub TaskFailed(ByVal e As Exception) _

Implements IClient.Failed

 

ActivityBar1.Stop()

Label1.Text = e.Message

MsgBox(e.ToString)

mActive = False

 

End Sub

 

Private Sub TaskCompleted(ByVal Cancelled As Boolean) _

Implements IClient.Completed

 

Label1.Text = "Completed"

Label2.Text = CStr(mController.Percent) & "%"

ProgressBar1.Value = mController.Percent

ActivityBar1.Stop()

mActive = False

 

End Sub

 

#End Region

 

请注意,这一段代码中的所有内容均与线程无关,其中的每一部分代码都可以在我们得知后台操作的状态时做出相应的响应。每次响应后,我们都会更新显示以表明进程的状态和完成百分比(以文字的形式或通过 ProgressBar 显示),并启动和停止 ActivityBar 控件。

 

mActive 标志非常重要。如果用户在辅助线程处于活动状态时关闭窗体,应用程序可能会挂起或变得不稳定。要防止出现这种情况,我们可以打断窗体的 Closing 事件并取消关闭尝试(如果后台进程处于活动状态)。

 

Private Sub Form1_Closing(ByVal sender As Object, _

ByVal e As System.ComponentModel.CancelEventArgs) _

Handles MyBase.Closing

 

e.Cancel = mActive

 

End Sub

 

我们还可以选择在这种情况下初始化取消操作,但是这取决于特定的应用程序要求。

 

其余的代码都是为了实现按钮的 Click 事件。

 

Private Sub btnStart_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnStart.Click

 

mController.Start(New Worker(2000000, 100))

 

End Sub

 

Private Sub btnStop_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnStop.Click

 

Label1.Text = "Cancelling ..."

mController.Cancel()

 

End Sub

 

Start(开始)按钮只调用 Controller 对象的 Start 方法,并将 Worker 对象的实例传递给它。

 

您可能需要调整用于初始化 Worker 对象的值,以便在您的计算机上获得所需的结果。这些特定的值提供了双处理器 P3/450 计算机上的一个良好示例。显然,这只是用于测试目的。真正的 Worker 对象将实现更有意义、运行时间更长的进程。

 

Cancel(取消)按钮将调用 Controller 对象的 Cancel 方法,同时还会更新显示,以表明已请求取消。请记住,这只是一个取消“请求”,在辅助线程真正停止运行之前可能需要等待一些时间。最好能够为用户提供即时反馈,至少应让用户知道系统已经注意到用户的单击按钮操作。

 

现在,我们可以运行应用程序了。单击 Start(开始)按钮时,Worker 就应该开始运行,而且显示的内容会在运行时更新。您可以将窗体移动到屏幕上的任意位置,也可以与其交互,因为 UI 线程本质上还处于空闲状态,可以随时与您交互。

 

同时,辅助线程在后台进行大量复杂的工作,并定期将状态更新信息发送给 UI 线程以进行显示。

 

小结

 

多线程是一个功能强大的工具,我们可以在每次需要执行长时间运行的任务时使用该工具。我们可以用它运行辅助代码,而无需绑定用户界面。但同时要注意,多线程操作非常复杂,要正确操作并不容易,而且调试起来也比较困难。

 

尽管不一定能够实现,但我们还是应该尽量为每个辅助线程提供一组它可以操作的独立数据。要达到这个目的,最简单的方法就是为每个线程创建一个对象,对象中包含该线程可以操作的数据以及完成工作所需的代码。

 

通过实现结构化的架构,使之充当辅助线程和 UI 线程之间的媒介,我们可以大大简化编写多线程代码和 UI 以对其进行控制的过程。本文就介绍了这样一个架构,您可以根据需要使用或进行调整,以满足特定的应用需要。

相关内容
赞助商链接