在 WinForms 應用程式中實作檔案下載功能,需處理非同步下載、進度回報、UI 執行緒安全、暫停/繼續等細節。本文提供一個以 WebClient 為基礎、搭配 BackgroundWorker(或現代 Task+Progress<T>)的完整下載器範例,並修正原始碼中串流處理邏輯錯誤與 UI 凍結問題。
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Windows.Forms;
namespace FileDownloaderDemo
{
public partial class MainForm : Form
{
private BackgroundWorker _bw;
private string _downloadUrl;
private string _savePath;
public MainForm()
{
InitializeComponent();
InitializeBackgroundWorker();
}
private void InitializeBackgroundWorker()
{
_bw = new BackgroundWorker
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
_bw.DoWork += Bw_DoWork;
_bw.ProgressChanged += Bw_ProgressChanged;
_bw.RunWorkerCompleted += Bw_RunWorkerCompleted;
}
// UI 事件:開始下載按鈕
private void btnDownload_Click(object sender, EventArgs e)
{
_downloadUrl = txtUrl.Text.Trim();
if (string.IsNullOrWhiteSpace(_downloadUrl))
{
MessageBox.Show("請輸入下載網址");
return;
}
using (var sfd = new SaveFileDialog
{
FileName = Path.GetFileName(new Uri(_downloadUrl).LocalPath),
Filter = "所有檔案|*.*"
})
{
if (sfd.ShowDialog() != DialogResult.OK) return;
_savePath = sfd.FileName;
}
// 重置 UI
progressBar.Value = 0;
lblStatus.Text = "準備中...";
btnDownload.Enabled = false;
btnCancel.Enabled = true;
_bw.RunWorkerAsync();
}
// 背景下載邏輯
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
using (var client = new WebClient())
{
// 註冊進度事件(WebClient 內建)
client.DownloadProgressChanged += (s, args) =>
{
if (_bw.CancellationPending)
{
args.Cancel = true;
e.Cancel = true;
return;
}
_bw.ReportProgress(args.ProgressPercentage, args.BytesReceived);
};
try
{
client.DownloadFile(new Uri(_downloadUrl), _savePath);
e.Result = "下載完成";
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
{
e.Cancel = true;
e.Result = "已取消";
}
catch (Exception ex)
{
e.Result = $"錯誤:{ex.Message}";
}
}
}
// 進度回報(自動切回 UI 執行緒)
private void Bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
double mb = (long)e.UserState / 1024.0 / 1024.0;
lblStatus.Text = $"已下載 {mb:F2} MB ({e.ProgressPercentage}%)";
}
// 完成處理
private void Bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
btnDownload.Enabled = true;
btnCancel.Enabled = false;
if (e.Cancelled)
lblStatus.Text = "下載已取消";
else if (e.Error != null)
lblStatus.Text = $"失敗:{e.Error.Message}";
else
lblStatus.Text = e.Result?.ToString() ?? "完成";
}
private void btnCancel_Click(object sender, EventArgs e)
{
_bw.CancelAsync();
lblStatus.Text = "正在取消...";
}
}
}
ProgressChanged、RunWorkerCompleted 自動切換至 UI 執行緒,無需 Invoke,適合 .NET Framework 專案。await、取消權杖(CancellationToken)。Control.Invoke 更新 UI,易發生跨執行緒例外與死結。client.DownloadProgressChanged += (s, args) => {
// args.ProgressPercentage:0-100
// args.BytesReceived:已接收位元組數
// args.TotalBytesToReceive:總大小(-1 表未知)
_bw.ReportProgress(args.ProgressPercentage, args.BytesReceived);
};
原始碼錯誤嘗試手動讀取 Stream 並寫入 FileStream,不只重複造輪子,還導致進度條邏輯錯誤(fbytes 未更新、progressBar1.value 累加錯誤)。
BackgroundWorker.CancellationPending 搭配 WebClient.CancelAsync()。DownloadProgressChanged 中檢查 CancellationPending,設定 args.Cancel = true 觸發 WebException(RequestCanceled)。e.Cancelled 顯示「已取消」。private async Task DownloadAsync(string url, string path, IProgress progress, CancellationToken ct)
{
using var client = new HttpClient();
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
long? total = response.Content.Headers.ContentLength;
using var stream = await response.Content.ReadAsStreamAsync(ct);
using var fileStream = File.Create(path);
byte[] buffer = new byte[8192];
long downloaded = 0;
int read;
while ((read = await stream.ReadAsync(buffer, ct)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, read), ct);
downloaded += read;
if (total.HasValue)
progress.Report((double)downloaded / total.Value * 100);
ct.ThrowIfCancellationRequested();
}
}
優點:完全非同步、無阻塞 UI、支援 HTTP/2、標準取消模式、可攜式至 Linux/macOS。
WebClient.DownloadFile 內部會緩衝,改用 HttpClient + ReadAsStreamAsync 串流寫入,記憶體固定在緩衝區大小(8KB)。Content-Length 標頭導致 TotalBytesToReceive = -1。改用 HttpClient 讀取 Content.Headers.ContentLength,或僅顯示已下載 MB 數不顯示百分比。ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 或 HttpClientHandler.SslProtocols。Range 標頭。下載前檢查本地檔案長度,請求 Range: bytes=已下載-,以 FileMode.Append 續寫。建議使用成熟庫(如 SharpDownloader)。WinForms 檔案下載器的關鍵在於:背景執行緒下載 + 安全跨執行緒進度回報 + 取消支援 + 串流處理大檔案。現代專案建議直接採用 HttpClient + Task + Progress<T> 模式,程式碼更簡潔、效能更好、跨平台相容。