VB.NET BeginInvoke 筆記(進階篇)
BeginInvoke 常用在 Windows Forms 的跨執行緒 UI 更新。背景執行緒不能直接修改 Label、ProgressBar、ListBox 等控制項,因此需要把畫面更新動作交回控制項所屬的 UI 執行緒。
BeginInvoke 的核心是「非同步排入」。它會把指定的 UI 更新工作排進 UI 執行緒佇列,呼叫端不等待該工作完成,就會繼續往下執行。這讓它很適合做進度回報、狀態通知與清單紀錄,但不適合把真正耗時的工作放進 UI 更新區塊。
先理解 BeginInvoke 在解決什麼
BeginInvoke:把一段委派程式非同步排入控制項所屬的 UI 執行緒執行。它的目的不是建立背景工作,而是讓背景工作能安全通知 UI 更新畫面。
BeginInvoke 的重點
- 切回 UI 執行緒:背景執行緒不直接操作控制項。
- 非同步排入:呼叫後不等待 UI 更新完成。
- 適合通知型更新:進度、狀態、清單紀錄、完成提示。
- 不適合放重型工作:委派內容仍然會在 UI 執行緒執行。
- 不能立刻依賴結果:下一行程式執行時,畫面不一定已經完成更新。
基本安全模式
If LabelStatus.InvokeRequired Then
LabelStatus.BeginInvoke(New MethodInvoker(Sub()
LabelStatus.Text = "更新文字"
End Sub))
Else
LabelStatus.Text = "更新文字"
End If
InvokeRequired 用來判斷目前是否需要切回 UI 執行緒。若已經在 UI 執行緒,就可直接更新;若在背景執行緒,就使用 BeginInvoke 排回 UI 執行緒。
| 項目 | BeginInvoke | 實務理解 |
|---|---|---|
| 執行方式 | 非同步排入。 | 排進 UI 佇列後,呼叫端繼續執行。 |
| 是否等待完成 | 不等待。 | 不能假設下一行時畫面已更新。 |
| 主要用途 | 跨執行緒 UI 更新。 | 背景流程完成後通知畫面。 |
| 委派內容 | 在 UI 執行緒執行。 | 只放畫面更新,不放耗時計算。 |
基本使用:背景完成後更新狀態
場景一:櫃台資料檢查完成通知
這個範例用背景執行緒模擬資料檢查。檢查完成後,不直接修改 LabelStatus,而是透過共用方法把狀態文字排回 UI 執行緒。
需要的主控項
ButtonStartCheck:啟動資料檢查。LabelStatus:顯示目前狀態。
範例程式碼
Imports System.Threading
Public Class Form1
Private Sub ButtonStartCheck_Click(sender As Object, e As EventArgs) Handles ButtonStartCheck.Click
LabelStatus.Text = "資料檢查中..."
Dim worker As New Thread(AddressOf RunCheckJob)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub RunCheckJob()
Thread.Sleep(900)
QueueStatusText("資料檢查完成:可進行下一步")
End Sub
Private Sub QueueStatusText(ByVal message As String)
If LabelStatus.IsDisposed OrElse Not LabelStatus.IsHandleCreated Then
Return
End If
If LabelStatus.InvokeRequired Then
Try
LabelStatus.BeginInvoke(New MethodInvoker(Sub()
If Not LabelStatus.IsDisposed Then
LabelStatus.Text = message
End If
End Sub))
Catch ex As InvalidOperationException
' 表單關閉中,忽略尚未完成的畫面更新。
End Try
Else
LabelStatus.Text = message
End If
End Sub
End Class
邏輯解析
RunCheckJob在背景執行緒執行。QueueStatusText集中處理跨執行緒 UI 更新。BeginInvoke只排入畫面更新,不等待 Label 實際更新完成。IsHandleCreated與IsDisposed可降低表單關閉時排入更新的風險。
進度更新:背景流程持續回報 UI
場景二:文件掃描進度回報
背景工作分段執行時,可以每完成一段就排入一次 UI 更新。這個範例會更新 ProgressBar 與 Label,但真正的掃描流程仍留在背景執行緒。
需要的主控項
ButtonScan:開始掃描。ProgressBarScan:顯示進度。LabelProgress:顯示進度文字。
範例程式碼
Imports System.Threading
Public Class Form1
Private Sub ButtonScan_Click(sender As Object, e As EventArgs) Handles ButtonScan.Click
ProgressBarScan.Value = 0
LabelProgress.Text = "掃描進度:0%"
Dim worker As New Thread(AddressOf RunScan)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub RunScan()
For pageNo As Integer = 1 To 5
Thread.Sleep(350)
QueueScanProgress(pageNo * 20)
Next
End Sub
Private Sub QueueScanProgress(ByVal percent As Integer)
If ProgressBarScan.IsDisposed OrElse Not ProgressBarScan.IsHandleCreated Then
Return
End If
If ProgressBarScan.InvokeRequired Then
ProgressBarScan.BeginInvoke(New MethodInvoker(Sub()
ProgressBarScan.Value = percent
LabelProgress.Text = "掃描進度:" & percent.ToString() & "%"
End Sub))
Else
ProgressBarScan.Value = percent
LabelProgress.Text = "掃描進度:" & percent.ToString() & "%"
End If
End Sub
End Class
邏輯解析
- 背景執行緒用迴圈模擬分段掃描。
QueueScanProgress每次只排入簡短 UI 更新。- 多個控制項可以放在同一個
BeginInvoke區塊中一起更新。
更新頻率提醒:進度更新不需要每毫秒排入一次。若更新過於頻繁,UI 執行緒會忙著處理大量畫面訊息,反而造成卡頓。
清單紀錄:把背景訊息追加到 ListBox
場景三:批次標籤產生紀錄
背景工作產生多筆紀錄時,可以透過 BeginInvoke 將訊息逐筆追加到 ListBox。背景執行緒只負責產生資料,畫面追加交給 UI 執行緒。
需要的主控項
ButtonBuildTags:開始產生標籤。ListBoxLog:顯示紀錄。LabelLogStatus:顯示狀態。
範例程式碼
Imports System.Threading
Public Class Form1
Private Sub ButtonBuildTags_Click(sender As Object, e As EventArgs) Handles ButtonBuildTags.Click
ListBoxLog.Items.Clear()
LabelLogStatus.Text = "標籤產生中..."
Dim worker As New Thread(AddressOf BuildTags)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub BuildTags()
For tagNo As Integer = 1 To 4
Thread.Sleep(250)
QueueLogText("標籤完成:TAG-" & tagNo.ToString("000"))
Next
QueueLogText("全部標籤已完成")
End Sub
Private Sub QueueLogText(ByVal message As String)
If ListBoxLog.IsDisposed OrElse Not ListBoxLog.IsHandleCreated Then
Return
End If
If ListBoxLog.InvokeRequired Then
ListBoxLog.BeginInvoke(New MethodInvoker(Sub()
ListBoxLog.Items.Add(message)
LabelLogStatus.Text = message
End Sub))
Else
ListBoxLog.Items.Add(message)
LabelLogStatus.Text = message
End If
End Sub
End Class
邏輯解析
- 背景執行緒逐步產生標籤紀錄。
QueueLogText將清單追加與狀態顯示集中處理。- 這種寫法適合背景流程需要多次回報畫面的情境。
BeginInvoke 與 Invoke 的差異
| 比較項目 | BeginInvoke | Invoke |
|---|---|---|
| 執行模式 | 非同步排入 UI 執行緒。 | 同步切回 UI 執行緒。 |
| 是否等待完成 | 不等待。 | 會等待委派執行完成。 |
| 適合用途 | 狀態通知、進度回報、清單紀錄。 | 後續流程必須等待 UI 更新完成。 |
| 常見風險 | 排入後立刻讀取 UI,可能讀到舊狀態。 | 不當互相等待時可能造成卡住。 |
場景四:排入狀態後繼續背景流程
這個範例刻意在背景流程中先排入畫面更新,再繼續做背景端紀錄。因為 BeginInvoke 不等待 UI 完成,所以背景流程可以先往下執行。
需要的主控項
ButtonQueueState:開始排入狀態。ListBoxTrace:顯示流程紀錄。
範例程式碼
Imports System.Threading
Public Class Form1
Private Sub ButtonQueueState_Click(sender As Object, e As EventArgs) Handles ButtonQueueState.Click
ListBoxTrace.Items.Clear()
Dim worker As New Thread(AddressOf RunQueueState)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub RunQueueState()
QueueTrace("背景流程:準備排入 UI 更新")
QueueTrace("UI 更新:狀態文字已排入")
QueueTrace("背景流程:排入後繼續往下執行")
End Sub
Private Sub QueueTrace(ByVal message As String)
If ListBoxTrace.InvokeRequired Then
ListBoxTrace.BeginInvoke(New MethodInvoker(Sub()
ListBoxTrace.Items.Add(message)
End Sub))
Else
ListBoxTrace.Items.Add(message)
End If
End Sub
End Class
邏輯解析
BeginInvoke的重點是排入,不是立即完成。- 背景流程不會等 UI 實際完成更新才繼續。
- 若下一步一定要依賴 UI 更新結果,就不適合直接假設
BeginInvoke已完成。
表單關閉與生命週期處理
場景六:背景監看停止後不再排入 UI
背景執行緒執行期間,表單可能已經關閉。若控制項控制代碼已不存在,仍繼續呼叫 BeginInvoke,可能發生例外。關閉表單時應停止背景流程,排入前也要檢查控制項狀態。
需要的主控項
ButtonStartWatch:開始背景監看。LabelWatch:顯示監看狀態。
範例程式碼
Imports System.Threading
Public Class Form1
Private keepWatching As Boolean = False
Private Sub ButtonStartWatch_Click(sender As Object, e As EventArgs) Handles ButtonStartWatch.Click
If keepWatching Then
Return
End If
keepWatching = True
Dim worker As New Thread(AddressOf WatchStatus)
worker.IsBackground = True
worker.Start()
End Sub
Private Sub WatchStatus()
Dim count As Integer = 0
While keepWatching
Thread.Sleep(500)
count += 1
QueueWatchText("監看次數:" & count.ToString())
End While
End Sub
Private Sub QueueWatchText(ByVal message As String)
If LabelWatch.IsDisposed OrElse Not LabelWatch.IsHandleCreated Then
Return
End If
Try
If LabelWatch.InvokeRequired Then
LabelWatch.BeginInvoke(New MethodInvoker(Sub()
If Not LabelWatch.IsDisposed Then
LabelWatch.Text = message
End If
End Sub))
Else
LabelWatch.Text = message
End If
Catch ex As InvalidOperationException
' 表單關閉中,忽略排入失敗。
End Try
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
keepWatching = False
End Sub
End Class
邏輯解析
keepWatching控制背景迴圈是否繼續。- 表單關閉時設定
keepWatching = False,讓背景流程停止。 Try...Catch用來保護表單關閉過程中的排入失敗。
實務判斷與常見誤區
常見問題整理
- 把 BeginInvoke 當成背景執行:
BeginInvoke的委派內容仍在 UI 執行緒執行。 - 背景執行緒直接改控制項:應先使用
InvokeRequired判斷,再切回 UI 執行緒。 - 排入後立刻讀取 UI 結果:
BeginInvoke不等待完成,可能讀到舊狀態。 - 大量高頻率排入:太多 UI 更新會讓訊息佇列擁塞。
- 表單關閉仍繼續排入:控制項可能已釋放,需停止背景流程並檢查生命週期。
- 把耗時計算放進 BeginInvoke:會讓 UI 執行緒變慢,甚至造成畫面卡住。
| 需求 | 建議做法 | 原因 |
|---|---|---|
| 背景工作通知 UI | BeginInvoke |
排入後背景流程可繼續。 |
| 需要等待 UI 更新完成 | Invoke |
同步等待委派完成。 |
| 大量進度更新 | 降低更新頻率。 | 避免 UI 訊息佇列過多。 |
| 多處更新同一控制項 | 建立共用方法。 | 集中跨執行緒判斷與生命週期保護。 |
| 表單可能關閉 | 停止背景流程並檢查控制項狀態。 | 避免釋放後仍排入 UI 更新。 |
重點整理
BeginInvoke用來把 UI 更新非同步排回控制項所屬的 UI 執行緒。BeginInvoke不會等待委派執行完成,因此不能立刻依賴 UI 更新結果。- 背景工作應在背景執行緒完成,
BeginInvoke區塊只放畫面更新。 InvokeRequired可判斷目前是否需要切回 UI 執行緒。BeginInvoke適合進度回報、狀態通知與清單紀錄。Invoke適合必須等待 UI 更新完成後才繼續的流程。- 多處重複 UI 更新時,建議封裝成共用方法。
- 表單關閉時應停止背景工作,並避免控制項釋放後仍排入更新。