快捷搜索:

在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 以对其进行节制的历程。本文就先容了这样一个架构,您可以根据必要应用或进行调剂,以满意特定的利用必要。

您可能还会对下面的文章感兴趣: