首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# WinForms 實作檔案下載器:WebClient、進度條與多執行緒完整範例

     編輯:關於C語言

前言

在 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 = "正在取消...";
        }
    }
}

關鍵技術點解析

1. 為什麼選 BackgroundWorker / Task 而非 Thread?

  • BackgroundWorker:內建 ProgressChanged、RunWorkerCompleted 自動切換至 UI 執行緒,無需 Invoke,適合 .NET Framework 專案。
  • Task + Progress<T>:現代 .NET Core/5+ 首選,語法更簡潔,支援 await、取消權杖(CancellationToken)。
  • 避免直接用 Thread:需手動 Control.Invoke 更新 UI,易發生跨執行緒例外與死結。

2. WebClient 進度事件的正確用法

client.DownloadProgressChanged += (s, args) => {
    // args.ProgressPercentage:0-100
    // args.BytesReceived:已接收位元組數
    // args.TotalBytesToReceive:總大小(-1 表未知)
    _bw.ReportProgress(args.ProgressPercentage, args.BytesReceived);
};

原始碼錯誤嘗試手動讀取 Stream 並寫入 FileStream,不只重複造輪子,還導致進度條邏輯錯誤(fbytes 未更新、progressBar1.value 累加錯誤)。

3. 取消下載的正確實作

  • BackgroundWorker.CancellationPending 搭配 WebClient.CancelAsync()。
  • 在 DownloadProgressChanged 中檢查 CancellationPending,設定 args.Cancel = true 觸發 WebException(RequestCanceled)。
  • 完成事件中判斷 e.Cancelled 顯示「已取消」。

現代化改寫:Task + Progress<T> + HttpClient (.NET 6+)

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。

常見問題 FAQ

Q: 下載大檔案(>2GB)記憶體爆增?
A: WebClient.DownloadFile 內部會緩衝,改用 HttpClient + ReadAsStreamAsync 串流寫入,記憶體固定在緩衝區大小(8KB)。
Q: 進度條卡在 0% 或 100%?
A: 伺服器未回傳 Content-Length 標頭導致 TotalBytesToReceive = -1。改用 HttpClient 讀取 Content.Headers.ContentLength,或僅顯示已下載 MB 數不顯示百分比。
Q: HTTPS 下載失敗「基礎連線已關閉」?
A: 目標站可能需 TLS 1.2+。啟用 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 或 HttpClientHandler.SslProtocols。
Q: 如何支援斷點續傳?
A: 需伺服器支援 Range 標頭。下載前檢查本地檔案長度,請求 Range: bytes=已下載-,以 FileMode.Append 續寫。建議使用成熟庫(如 SharpDownloader)。

總結

WinForms 檔案下載器的關鍵在於:背景執行緒下載 + 安全跨執行緒進度回報 + 取消支援 + 串流處理大檔案。現代專案建議直接採用 HttpClient + Task + Progress<T> 模式,程式碼更簡潔、效能更好、跨平台相容。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved