2024年6月12日 星期三

14.VB.NET 筆記 進階篇 - Invoke

VB.NET Invoke 筆記 (進階篇)

VB.NET Invoke (呼叫) 筆記 (進階篇)

VB.NET(Visual Basic.NET),呼叫 (Invoke) 就像是指揮家指揮樂團一樣,它讓程式在正確的時機執行特定的方法。透過理解 Invoke 的使用方式,就能夠掌控程式的執行流程,構建出優雅、高效的程式。

認識 Invoke

Invoke: Invoke 是一種機制,用於在 Windows Form 應用程式中從非 UI 執行緒存取 UI 元素。它確保在正確的執行緒上執行指定的方法或委派,以避免跨執行緒操作引發的異常。

在 Windows Form 應用程式中,UI 元素(如按鈕、標籤等)屬於 UI 執行緒,而其他執行緒(如後台執行緒)不能直接存取 UI 元素。如果在非 UI 執行緒中嘗試存取 UI 元素,會引發 InvalidOperationException 異常InvalidOperationException 異常表示在操作無效的情況下嘗試執行該操作,例如在非 UI 執行緒中存取 UI 元素。

這時,就需要使用 Invoke 來確保在 UI 執行緒上執行相關的操作。Invoke 會將指定的方法或委派傳遞給 UI 執行緒,並等待執行完成後再返回。這樣,就可以安全地從非 UI 執行緒中存取和更新 UI 元素。

除了 Invoke 外,還有一個類似的方法叫做 BeginInvokeBeginInvoke 與 Invoke 的區別在於,BeginInvoke 是非同步的,它會將方法或委派傳遞給 UI 執行緒執行,但不會等待執行完成,而是立即返回。這在某些情況下可以提高程式的響應性能。

無論是 Invoke 還是 BeginInvoke,都需要透過控制項的 InvokeRequired 屬性來判斷是否需要進行跨執行緒調用。InvokeRequired 屬性會返回一個布林值,指示是否需要使用 Invoke 來存取該控制項。

Invoke 的屬性和方法

Invoke 和 BeginInvoke 方法都是 Control 類別的成員,它們提供了一些屬性和方法,用於控制跨執行緒的調用。以下是一些常用的屬性和方法:

屬性

屬性 說明
InvokeRequired 獲取一個值,指示是否需要使用 Invoke 來存取該控制項。

方法

方法 說明
Invoke 在 UI 執行緒上執行指定的委派,並等待執行完成。
BeginInvoke 在 UI 執行緒上執行指定的委派,但不等待執行完成,立即返回。
EndInvoke 等待 BeginInvoke 方法執行完成,並獲取返回值(如果有)。

InvokeRequired 屬性

獲取一個值,指示是否需要使用 Invoke 來存取該控制項。

語法:

控制項.InvokeRequired

範例:

Imports System ' 導入 System 命名空間
Imports System.Threading ' 導入 System.Threading 命名空間

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 創建一個後台執行緒
        Dim thread As New Thread(AddressOf UpdateLabel)
        thread.Start()
    End Sub
    
    Private Sub UpdateLabel()
        ' 檢查是否需要使用 Invoke
        If Label1.InvokeRequired Then
            ' 需要使用 Invoke,執行跨執行緒調用
            Label1.Invoke(Sub() Label1.Text = "Updated from background thread")
        Else
            ' 不需要使用 Invoke,直接更新標籤文字
            Label1.Text = "Updated from background thread"
        End If
    End Sub
End Class

在這個範例中,我們在 UpdateLabel 方法中檢查 Label1 控制項的 InvokeRequired 屬性。如果 InvokeRequiredTrue,表示需要使用 Invoke 進行跨執行緒調用;否則,可以直接更新標籤的文字。

Invoke 方法

在 UI 執行緒上執行指定的委派,並等待執行完成。

語法:

控制項.Invoke(委派, 參數)

參數:

  • 委派:要在 UI 執行緒上執行的方法或委派。
  • 參數:傳遞給委派的參數(可選)。

範例:

Imports System ' 導入 System 命名空間
Imports System.Threading ' 導入 System.Threading 命名空間

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 創建一個後台執行緒
        Dim thread As New Thread(AddressOf UpdateLabel)
        thread.Start("Hello from background thread")
    End Sub
    
    Private Sub UpdateLabel(message As String)
        ' 使用 Invoke 在 UI 執行緒上更新標籤文字
        If Label1.InvokeRequired Then
            Label1.Invoke(Sub() Label1.Text = message)
        Else
            Label1.Text = message
        End If
    End Sub
End Class

在這個範例中,我們創建了一個後台執行緒,並傳遞一個字串參數給 UpdateLabel 方法。在 UpdateLabel 方法中,我們使用 Invoke 方法在 UI 執行緒上更新標籤的文字,並將字串參數傳遞給委派。

BeginInvoke 方法

在 UI 執行緒上執行指定的委派,但不等待執行完成,立即返回。

語法:

控制項.BeginInvoke(委派, 參數)

參數:

  • 委派:要在 UI 執行緒上執行的方法或委派。
  • 參數:傳遞給委派的參數(可選)。

範例:

Imports System ' 導入 System 命名空間
Imports System.Threading ' 導入 System.Threading 命名空間

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 創建一個後台執行緒
        Dim thread As New Thread(AddressOf UpdateLabel)
        thread.Start("Hello from background thread")
    End Sub
    
    Private Sub UpdateLabel(message As String)
        ' 使用 BeginInvoke 在 UI 執行緒上更新標籤文字
        If Label1.InvokeRequired Then
            Label1.BeginInvoke(Sub() Label1.Text = message)
        Else
            Label1.Text = message
        End If
    End Sub
End Class

這個範例與上一個範例類似,但使用了 BeginInvoke 方法代替 Invoke 方法。BeginInvoke 方法會立即返回,不會等待委派的執行完成。

EndInvoke 方法

等待 BeginInvoke 方法執行完成,並獲取返回值(如果有)。

語法:

控制項.EndInvoke(異步結果)

參數:

  • 異步結果:BeginInvoke 方法返回的 IAsyncResult 物件。

範例:

Imports System ' 導入 System 命名空間
Imports System.Threading ' 導入 System.Threading 命名空間

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 創建一個後台執行緒
        Dim thread As New Thread(AddressOf UpdateLabel)
        thread.Start("Hello from background thread")
    End Sub
    
    Private Sub UpdateLabel(message As String)
        ' 使用 BeginInvoke 在 UI 執行緒上更新標籤文字
        If Label1.InvokeRequired Then
            Dim asyncResult As IAsyncResult = Label1.BeginInvoke(Sub() Label1.Text = message)
            ' 等待 BeginInvoke 執行完成
            Label1.EndInvoke(asyncResult)
        Else
            Label1.Text = message
        End If
    End Sub
End Class

在這個範例中,我們使用 BeginInvoke 方法在 UI 執行緒上更新標籤的文字,並將返回的 IAsyncResult 物件存儲在 asyncResult 變數中。然後,我們使用 EndInvoke 方法等待 BeginInvoke 的執行完成。

這些屬性和方法提供了靈活的方式來控制跨執行緒的調用,根據具體的需求選擇適合的方法。一般情況下,使用 Invoke 方法就可以滿足大多數場景的需求。而在需要非同步執行或提高響應性能的情況下,可以考慮使用 BeginInvoke 方法。

Invoke 應用範例

以下是一個完整的 VB.NET 範例,展示了如何使用 Invoke 在後台執行緒中更新 UI 元素,並在執行完成後顯示結果。

範例:後台計算器

這個範例模擬了一個長時間的計算過程,在後台執行緒中進行計算,並使用 Invoke 將計算結果更新到 UI 元素上。

使用的控制項:
  • Button1:用於開始計算。
  • ProgressBar1:用於顯示計算進度。
  • Label1:用於顯示計算結果。
Imports System ' 導入 System 命名空間
Imports System.Threading ' 導入 System.Threading 命名空間

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 禁用按鈕,防止重複點擊
        Button1.Enabled = False
        
        ' 創建一個後台執行緒
        Dim thread As New Thread(AddressOf PerformCalculation)
        thread.Start()
    End Sub
    
    Private Sub PerformCalculation()
        ' 模擬長時間的計算過程
        For i As Integer = 1 To 100
            ' 使用 Invoke 更新進度條
            If ProgressBar1.InvokeRequired Then
                ProgressBar1.Invoke(Sub() ProgressBar1.Value = i)
            Else
                ProgressBar1.Value = i
            End If
            
            ' 模擬計算耗時
            Thread.Sleep(100)
        Next
        
        ' 使用 Invoke 更新標籤和按鈕狀態
        If Label1.InvokeRequired Then
            Label1.Invoke(Sub()
                              Label1.Text = "Calculation completed"
                              Button1.Enabled = True
                          End Sub)
        Else
            Label1.Text = "Calculation completed"
            Button1.Enabled = True
        End If
    End Sub
End Class

在這個範例中,當點擊 Button1 時,我們禁用按鈕以防止重複點擊,然後創建一個後台執行緒來執行 PerformCalculation 方法。

PerformCalculation 方法中,我們使用一個 For 循環來模擬長時間的計算過程。在每次迭代中,我們檢查 ProgressBar1InvokeRequired 屬性,如果需要跨執行緒調用,就使用 Invoke 更新進度條的值;否則直接更新進度條的值。

在計算完成後,我們再次檢查 Label1InvokeRequired 屬性,如果需要跨執行緒調用,就使用 Invoke 更新標籤的文字和啟用按鈕;否則直接更新標籤的文字和啟用按鈕。

這個範例展示了如何使用 Invoke 在後台執行緒中安全地更新 UI 元素,以及如何在長時間的操作中提供視覺反饋給使用者。

透過合理地使用 Invoke,可以確保在多執行緒環境中正確地更新 UI 元素,提供流暢、響應迅速的使用者體驗,同時避免因跨執行緒操作而引發的異常。