VB.NET Invoke 筆記(進階篇)
在 Windows Forms 中,畫面控制項屬於建立它們的 UI 執行緒。背景執行緒若直接修改 Label、ProgressBar、ListBox 或 Button,容易發生跨執行緒存取錯誤。
Invoke、BeginInvoke 與 InvokeRequired 的重點,是把背景執行緒中的 UI 更新動作,安全送回控制項所屬的 UI 執行緒。這篇會從「為什麼需要切回 UI 執行緒」開始,再用 Windows Forms 範例整理同步更新、非同步排入、共用更新方法與表單關閉時的安全處理。
先理解 Invoke 在解決什麼
背景工作不能直接碰 UI 控制項
表單畫面由 UI 執行緒管理。背景執行緒可以負責耗時工作,但更新畫面時必須切回 UI 執行緒。Invoke 的作用,就是把一段程式交給 UI 執行緒執行。
- UI 執行緒:負責建立控制項、處理按鈕事件、重繪畫面。
- 背景執行緒:適合處理耗時工作,例如檔案整理、資料比對、網路等待。
- InvokeRequired:判斷目前是否需要切回控制項所屬的 UI 執行緒。
- Invoke / BeginInvoke:把 UI 更新工作交回 UI 執行緒。
Invoke 相關語法的分工:
- InvokeRequired:先判斷是否跨執行緒。
- Invoke:同步切回 UI 執行緒,會等待 UI 更新完成。
- BeginInvoke:非同步排入 UI 執行緒,不等待 UI 更新完成。
- MethodInvoker:常用來包住沒有參數、沒有回傳值的 UI 更新動作。
| 項目 | 用途 | 適合情境 |
|---|---|---|
| InvokeRequired | 判斷目前是否不在控制項所屬執行緒。 | 寫共用 UI 更新方法時常用。 |
| Invoke | 同步執行 UI 更新。 | 需要等畫面更新完成後才繼續。 |
| BeginInvoke | 把 UI 更新排入佇列。 | 只需要通知畫面,不需要等待結果。 |
基本模式:InvokeRequired + Invoke
場景一:櫃台報到狀態更新
這個範例用「報到資料檢查」來示範最基本的跨執行緒 UI 更新。按下按鈕後,背景執行緒模擬檢查流程;完成時使用 InvokeRequired 判斷是否需要切回 UI 執行緒。
需要的主控項
ButtonCheckIn:開始檢查報到資料。LabelStatus:顯示報到狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading
Public Class Form1
Private Sub ButtonCheckIn_Click(sender As Object, e As EventArgs) Handles ButtonCheckIn.Click
LabelStatus.Text = "報到資料檢查中..."
Dim worker As New Thread(AddressOf CheckGuestRecord)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub CheckGuestRecord()
Thread.Sleep(900)
SetStatusText("報到完成:座位 C-12")
End Sub
Private Sub SetStatusText(ByVal message As String)
If LabelStatus.InvokeRequired Then
LabelStatus.Invoke(New MethodInvoker(Sub()
LabelStatus.Text = message
End Sub))
Else
LabelStatus.Text = message
End If
End Sub
End Class
畫面輸出結果(LabelStatus.Text)
報到資料檢查中...
報到完成:座位 C-12邏輯解析
CheckGuestRecord在背景執行緒中執行。SetStatusText集中處理 UI 更新,不讓背景工作直接改控制項。InvokeRequired為True時,使用Invoke切回 UI 執行緒。MethodInvoker包住要在 UI 執行緒執行的畫面更新程式。
進度更新與多控制項更新
場景二:整理抽屜標籤進度
這個範例模擬背景工作分段整理標籤。背景執行緒每完成一段,就把進度交回 UI 執行緒更新 ProgressBar 與 Label。
需要的主控項
ButtonStartLabeling:開始整理標籤。ProgressBar1:顯示進度。LabelProgress:顯示進度文字。ButtonStartLabeling完成後會重新啟用。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading
Public Class Form1
Private Sub ButtonStartLabeling_Click(sender As Object, e As EventArgs) Handles ButtonStartLabeling.Click
ButtonStartLabeling.Enabled = False
ProgressBar1.Value = 0
LabelProgress.Text = "整理進度:0%"
Dim worker As New Thread(AddressOf RunDrawerLabeling)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub RunDrawerLabeling()
For drawerNo As Integer = 1 To 5
Thread.Sleep(350)
Dim percent As Integer = drawerNo * 20
UpdateLabelingProgress(percent)
Next
UpdateLabelingDone()
End Sub
Private Sub UpdateLabelingProgress(ByVal percent As Integer)
If ProgressBar1.InvokeRequired Then
ProgressBar1.Invoke(New MethodInvoker(Sub()
ProgressBar1.Value = percent
LabelProgress.Text = "整理進度:" & percent.ToString() & "%"
End Sub))
Else
ProgressBar1.Value = percent
LabelProgress.Text = "整理進度:" & percent.ToString() & "%"
End If
End Sub
Private Sub UpdateLabelingDone()
If ButtonStartLabeling.InvokeRequired Then
ButtonStartLabeling.Invoke(New MethodInvoker(Sub()
LabelProgress.Text = "標籤整理完成"
ButtonStartLabeling.Enabled = True
End Sub))
Else
LabelProgress.Text = "標籤整理完成"
ButtonStartLabeling.Enabled = True
End If
End Sub
End Class
畫面輸出結果
整理進度:20%
整理進度:40%
整理進度:60%
整理進度:80%
整理進度:100%
標籤整理完成邏輯解析
- 背景執行緒只負責模擬整理流程。
- 更新
ProgressBar1、LabelProgress與按鈕狀態都切回 UI 執行緒。 - 多個控制項可以放在同一個
Invoke區塊中一起更新。
BeginInvoke:排入 UI 更新,不等待完成
場景三:縮圖產生紀錄排入清單
若背景工作只需要把訊息丟回畫面,不需要等待畫面更新完成,就適合使用 BeginInvoke。這個範例會把縮圖產生紀錄排入 ListBox。
需要的主控項
ButtonMakeThumbs:開始產生縮圖。ListBoxLog:顯示縮圖紀錄。LabelThumbStatus:顯示目前狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading
Public Class Form1
Private Sub ButtonMakeThumbs_Click(sender As Object, e As EventArgs) Handles ButtonMakeThumbs.Click
ListBoxLog.Items.Clear()
LabelThumbStatus.Text = "縮圖工作已啟動"
Dim worker As New Thread(AddressOf BuildThumbnails)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub BuildThumbnails()
For imageNo As Integer = 1 To 4
Thread.Sleep(300)
QueueLogText("縮圖完成:圖片 " & imageNo.ToString())
Next
QueueStatusText("全部縮圖已完成")
End Sub
Private Sub QueueLogText(ByVal message As String)
If ListBoxLog.InvokeRequired Then
ListBoxLog.BeginInvoke(New MethodInvoker(Sub()
ListBoxLog.Items.Add(message)
End Sub))
Else
ListBoxLog.Items.Add(message)
End If
End Sub
Private Sub QueueStatusText(ByVal message As String)
If LabelThumbStatus.InvokeRequired Then
LabelThumbStatus.BeginInvoke(New MethodInvoker(Sub()
LabelThumbStatus.Text = message
End Sub))
Else
LabelThumbStatus.Text = message
End If
End Sub
End Class
畫面輸出結果(ListBoxLog)
縮圖完成:圖片 1
縮圖完成:圖片 2
縮圖完成:圖片 3
縮圖完成:圖片 4邏輯解析
BeginInvoke會把 UI 更新排入 UI 執行緒佇列。- 背景工作不需要等待
ListBoxLog實際完成更新。 - 若只是顯示紀錄或狀態通知,
BeginInvoke通常比Invoke更自然。
| 比較項目 | Invoke | BeginInvoke |
|---|---|---|
| 執行方式 | 同步。 | 非同步排入。 |
| 背景執行緒是否等待 | 會等待 UI 更新完成。 | 不等待,排入後繼續往下。 |
| 適合情境 | 需要明確完成順序。 | 紀錄、通知、進度顯示。 |
| 注意事項 | 避免互相等待造成卡住。 | 表單關閉時要注意控制項生命週期。 |
表單關閉與控制項生命週期
場景五:表單關閉時停止背景更新
背景執行緒執行期間,表單可能已經被關閉。此時若仍然呼叫 Invoke 或 BeginInvoke,可能發生控制項已釋放的錯誤。因此更新前要檢查表單與控制項狀態,關閉時也要停止背景工作。
需要的主控項
ButtonStartClock:開始背景時間更新。LabelClock:顯示時間文字。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading
Public Class Form1
Private clockThread As Thread
Private keepClockRunning As Boolean = False
Private Sub ButtonStartClock_Click(sender As Object, e As EventArgs) Handles ButtonStartClock.Click
If keepClockRunning Then
Return
End If
keepClockRunning = True
clockThread = New Thread(AddressOf RunClock)
clockThread.IsBackground = True
clockThread.Start()
End Sub
Private Sub RunClock()
While keepClockRunning
Thread.Sleep(500)
SafeSetClockText("更新時間:" & DateTime.Now.ToString("HH:mm:ss"))
End While
End Sub
Private Sub SafeSetClockText(ByVal message As String)
If Me.IsDisposed OrElse Not Me.IsHandleCreated Then
Return
End If
If LabelClock.InvokeRequired Then
Try
LabelClock.BeginInvoke(New MethodInvoker(Sub()
If Not LabelClock.IsDisposed Then
LabelClock.Text = message
End If
End Sub))
Catch ex As InvalidOperationException
' 表單關閉中,忽略尚未完成的畫面更新。
End Try
Else
LabelClock.Text = message
End If
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
keepClockRunning = False
End Sub
End Class
畫面輸出結果(LabelClock.Text)
更新時間:14:05:21
更新時間:14:05:22
更新時間:14:05:23邏輯解析
keepClockRunning用來控制背景迴圈是否繼續。IsDisposed與IsHandleCreated可降低表單關閉時仍更新 UI 的風險。BeginInvoke排入更新時,仍可能遇到表單關閉,因此範例用Try...Catch保護。
實務判斷與常見誤區
常見問題整理
- 背景執行緒直接改 UI:例如直接寫
Label1.Text = ...,容易發生跨執行緒例外。 - 每行 UI 更新都寫一次 Invoke:程式會變得難讀,可改成共用方法集中處理。
- 過度頻繁更新畫面:大量進度訊息每毫秒更新一次,可能拖慢 UI。
- 在互相等待的流程中使用 Invoke:可能造成卡住,通知型更新可考慮
BeginInvoke。 - 忽略表單關閉:背景工作尚未停止時,控制項可能已被釋放。
| 問題 | 建議做法 | 原因 |
|---|---|---|
| 不確定目前在哪個執行緒 | 先用 InvokeRequired 判斷。 |
避免在 UI 執行緒又重複切回。 |
| 需要等 UI 更新完成 | 使用 Invoke。 |
同步等待結果。 |
| 只是顯示紀錄或通知 | 使用 BeginInvoke。 |
排入 UI 更新後,背景工作可繼續。 |
| 同一控制項多處更新 | 建立共用方法。 | 集中跨執行緒判斷,降低重複程式。 |
| 表單可能關閉 | 檢查生命週期並停止背景工作。 | 避免控制項釋放後仍排入 UI 更新。 |
重點整理
- Windows Forms 控制項通常只能由建立它的 UI 執行緒安全更新。
- 背景執行緒要更新 UI 時,應使用
Invoke或BeginInvoke切回 UI 執行緒。 InvokeRequired用來判斷目前是否需要切回 UI 執行緒。Invoke是同步更新,會等待 UI 更新完成。BeginInvoke是非同步排入,不等待 UI 更新完成。- 多處重複 UI 更新時,建議建立共用方法集中處理。
- 進度更新不宜過度頻繁,避免 UI 被大量更新拖慢。
- 表單關閉時需停止背景工作,並注意控制項是否已釋放。