WinForms 單一 UI 執行緒模型要求:只有建立控制項的執行緒能存取它。耗時工作必須移至背景執行緒,完成後再安全回傳 UI。本文對比兩種主流模式的實作細節與避坑指南。
BackgroundWorker 封裝執行緒池,提供三大事件自動處理上下文切換:
DoWork:背景執行緒,**不可直接操作 UI**(例外:ToolStrip 系控制項)ProgressChanged:UI 執行緒,安全更新進度條、列表RunWorkerCompleted:UI 執行緒,完成後收尾
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
var srcDir = (DirectoryInfo)e.Argument;
int i = 1, total = srcDir.GetFiles().Length;
foreach (var file in srcDir.GetFiles())
{
// 僅 ToolStrip 系控制項可在背景執行緒直接存取
tsslCurrentFile.Text = $"正在複製:{file.Name}";
file.CopyTo(Path.Combine(targetDir, file.Name), true);
bw.ReportProgress((i * 100) / total, file.Name); // 觸發 ProgressChanged
i++;
}
e.Result = total;
}
private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pgb.Value = e.ProgressPercentage; // 更新進度條
lvwFiles.Items.Add(new ListViewItem(new[] { DateTime.Now.ToLongTimeString(), (string)e.UserState })).EnsureVisible();
}
private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
txtSource.Clear(); txtTarget.Clear();
MessageBox.Show($"完成複製 {(int)e.Result} 個檔案");
}

適用於大量平行工作、需精細控制執行緒生命週期的場景。核心模式:
// 定義委派對應 UI 動作
delegate void ReportProgressDelegate(string fileName, int percent);
delegate void CompletedDelegate(string message);
private void UpdateUI(string file, int percent)
{
if (InvokeRequired)
Invoke(new ReportProgressDelegate(UpdateUI), file, percent);
else
{
lvw.Items.Add(new ListViewItem(new[] { DateTime.Now.ToLongTimeString(), file })).EnsureVisible();
pgb.Value = percent;
}
}
private void OnCompleted(string msg)
{
if (InvokeRequired)
Invoke(new CompletedDelegate(OnCompleted), msg);
else
{
txtSource.Clear(); txtTarget.Clear();
MessageBox.Show(msg);
}
}
private void WorkerThread(object srcDirObj)
{
var dir = (DirectoryInfo)srcDirObj;
int i = 1, total = dir.GetFiles().Length;
foreach (var f in dir.GetFiles())
{
tsslCurrentFile.Text = $"處理中:{f.Name}"; // 僅 ToolStrip 安全
f.CopyTo(Path.Combine(targetDir, f.Name), true);
UpdateUI(f.Name, (i * 100) / total);
i++;
}
OnCompleted($"共複製 {i-1} 個檔案");
}
// 啟動
new Thread(WorkerThread) { IsBackground = true }.Start(sourceDir);
| 面向 | BackgroundWorker | Thread + Invoke |
|---|---|---|
| 程式碼複雜度 | 低(事件式) | 中(手動委派/Invoke) |
| 取消支援 | 內建 CancellationPending | 自行實作旗標 |
| 進度回報 | 內建 ReportProgress | 自行 Invoke |
| 例外處理 | RunWorkerCompletedEventArgs.Error | try-catch 傳遞至 UI |
| 適用場景 | 單一背景任務、簡單進度 | 多執行緒、高併發、長生命週期 |
| .NET Core/5+ 建議 | 改用 Task + Progress<T> | 改用 Task.Run + Dispatcher/Control.Invoke |
InvokeRequired 再決定是否 Invoke,避免重複封送效能損耗。Invoke(this.Invoke),而非單一控制項,減少跨執行緒切換次數。ToolStripStatusLabel 等可在背景執行緒直接存取,但不建議依賴,統一走 Invoke 更安全。DoWork 或 Thread 入口點包裝 try-catch 並透過事件/委派回傳 UI。Task.Run(() => ...) 搭配 Progress<T> 或 await + SynchronizationContext,程式碼更簡潔、可組合。
private async void btnStart_Click(object sender, EventArgs e)
{
var progress = new Progress<(string File, int Percent)>(p =>
{
pgb.Value = p.Percent;
lvw.Items.Add(new ListViewItem(new[] { DateTime.Now.ToLongTimeString(), p.File })).EnsureVisible();
});
await Task.Run(() => CopyFilesWithProgress(sourceDir, targetDir, progress));
MessageBox.Show("完成");
}
private void CopyFilesWithProgress(DirectoryInfo src, string dest, IProgress<(string, int)> progress)
{
var files = src.GetFiles();
for (int i = 0; i < files.Length; i++)
{
files[i].CopyTo(Path.Combine(dest, files[i].Name), true);
progress.Report((files[i].Name, (i + 1) * 100 / files.Length));
}
}