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

C# WinForms 多執行緒 UI 操作與 HTML 剪貼板完整指南

     編輯:關於C#

前言

WinForms 基於單執行緒 UI 模型 (STA),背景執行緒直接操作控制項會導致例外或未定義行為。本文詳細說明 ISynchronizeInvoke 模式、現代 Task/async-await 配合 SynchronizationContext 的最佳實務,並補充 HTML 格式剪貼板處理技術。

一、為什麼背景執行緒不能直接操作 UI?

Windows 視窗訊息迴圈綁定在建立視窗的執行緒 (UI 執行緒)。控制項的事件處理、屬性存取、繪圖都必須在該執行緒執行。跨執行緒存取會破壞訊息順序,導致死結、繪圖錯誤或存取違規。

二、傳統模式:ISynchronizeInvoke (Control.Invoke/BeginInvoke)

2.1 核心介面


public interface ISynchronizeInvoke
{
    object Invoke(Delegate method, object[] args);           // 同步封送
    IAsyncResult BeginInvoke(Delegate method, object[] args); // 非同步封送
    object EndInvoke(IAsyncResult result);
    bool InvokeRequired { get; } // 是否需要封送
}

Control 實作此介面,InvokeRequired 判斷呼叫執行緒是否為 UI 執行緒。

2.2 同步呼叫範例


// 背景執行緒更新 Label 文字
private void UpdateLabelSafe(string text)
{
    if (label1.InvokeRequired)
    {
        label1.Invoke(new Action(UpdateLabelSafe), text); // 阻塞等待
    }
    else
    {
        label1.Text = text; // UI 執行緒直接執行
    }
}

2.3 非同步呼叫 (不阻塞背景執行緒)


private void UpdateLabelAsync(string text)
{
    if (label1.InvokeRequired)
    {
        label1.BeginInvoke(new Action(UpdateLabelAsync), text);
    }
    else
    {
        label1.Text = text;
    }
}

三、現代模式:Task + SynchronizationContext (推薦)

3.1 核心概念

  • SynchronizationContext.Current 在 UI 執行緒捕獲為 WindowsFormsSynchronizationContext
  • TaskScheduler.FromCurrentSynchronizationContext() 取得 UI 執行緒排程器
  • await 自動捕獲上下文,續作在原執行緒執行

3.2 標準非同步 UI 更新模式


private async Task LoadDataAsync()
{
    // 1. UI 執行緒:顯示載入中
    btnLoad.Enabled = false;
    lblStatus.Text = "載入中...";

    try
    {
        // 2. 背景執行緒:耗時操作 (不阻塞 UI)
        var data = await Task.Run(() => FetchDataFromDatabase());

        // 3. 自動回到 UI 執行緒 (因為 await 捕獲了 SynchronizationContext)
        dataGridView1.DataSource = data;
        lblStatus.Text = $"完成,共 {data.Count} 筆";
    }
    catch (Exception ex)
    {
        MessageBox.Show($"錯誤:{ex.Message}");
    }
    finally
    {
        btnLoad.Enabled = true;
    }
}

private List FetchDataFromDatabase()
{
    Thread.Sleep(2000); // 模擬耗時
    return new List { new Order { Id = 1, Name = "訂單A" } };
}

3.3 進階:IProgress 回報進度


private async Task ProcessWithProgressAsync()
{
    var progress = new Progress(p => 
    {
        // 這裡自動在 UI 執行緒執行
        progressBar1.Value = p;
        lblProgress.Text = $"{p}%";
    });

    await Task.Run(() => 
    {
        for (int i = 0; i <= 100; i++)
        {
            Thread.Sleep(50);
            progress.Report(i); // 執行緒安全回報
        }
    });
    
    MessageBox.Show("處理完成");
}

3.4 避免死結:ConfigureAwait(false)


// 類別庫/非 UI 程式碼:不需要回到 UI 執行緒
public async Task GetDataAsync()
{
    var json = await httpClient.GetStringAsync(url).ConfigureAwait(false);
    return JsonSerializer.Deserialize(json); // 在執行緒池執行續作
}

四、HTML 格式剪貼板處理 (CF_HTML)

4.1 CF_HTML 格式規格

剪貼板資料結構:


Version:1.0
StartHTML:0000000105
EndHTML:0000000452
StartFragment:0000000140
EndFragment:0000000417
SourceURL:https://example.com



這是 粗體 文字

  • StartHTML/EndHTML:整個 HTML 區塊在資料流中的位移 (位元組數,10 位數字零填補)
  • StartFragment/EndFragment:使用者實際選取範圍 (供貼上時參考)
  • 註解標記: /

4.2 寫入 HTML 到剪貼板


public static void SetHtmlToClipboard(string htmlFragment, string sourceUrl = null)
{
    // 1. 建立完整 HTML
    string html = $@"




{htmlFragment}


";

    // 2. 計算位移 (UTF-8 位元組數)
    byte[] htmlBytes = Encoding.UTF8.GetBytes(html);
    byte[] headerBytes = Encoding.UTF8.GetBytes(
        "Version:1.0\r\n" +
        $"StartHTML:0000000000\r\n" + // 佔位
        $"EndHTML:0000000000\r\n" +
        $"StartFragment:0000000000\r\n" +
        $"EndFragment:0000000000\r\n" +
        (sourceUrl != null ? $"SourceURL:{sourceUrl}\r\n" : "") +
        "\r\n");

    // 3. 計算實際位移
    int startHtml = headerBytes.Length;
    int startFragment = startHtml + Encoding.UTF8.GetBytes(html.Substring(0, html.IndexOf("") + 20)).Length;
    int endFragment = startFragment + Encoding.UTF8.GetBytes(htmlFragment).Length;
    int endHtml = headerBytes.Length + htmlBytes.Length;

    // 4. 組裝最終資料
    string header = 
        "Version:1.0\r\n" +
        $"{startHtml:D10}\r\n" +
        $"{endHtml:D10}\r\n" +
        $"{startFragment:D10}\r\n" +
        $"{endFragment:D10}\r\n" +
        (sourceUrl != null ? $"SourceURL:{sourceUrl}\r\n" : "") +
        "\r\n";

    byte[] finalData = Encoding.UTF8.GetBytes(header + html);

    // 5. 寫入剪貼板
    var dataObj = new DataObject();
    dataObj.SetData(DataFormats.Html, new MemoryStream(finalData));
    dataObj.SetData(DataFormats.UnicodeText, StripHtml(htmlFragment)); // 純文字備援
    Clipboard.SetDataObject(dataObj, true);
}

private static string StripHtml(string html) => 
    System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "");

4.3 從剪貼板讀取 HTML


public static string GetHtmlFromClipboard()
{
    if (!Clipboard.ContainsData(DataFormats.Html)) return null;

    var stream = Clipboard.GetData(DataFormats.Html) as MemoryStream;
    if (stream == null) return null;

    using var reader = new StreamReader(stream, Encoding.UTF8);
    string fullData = reader.ReadToEnd();

    // 解析 Header 找到 Fragment 範圍
    var lines = fullData.Split('\n');
    int startFragment = -1, endFragment = -1;
    int bodyStart = -1;

    for (int i = 0; i < lines.Length; i++)
    {
        if (lines[i].StartsWith("StartFragment:"))
            startFragment = int.Parse(lines[i].Split(':')[1]);
        else if (lines[i].StartsWith("EndFragment:"))
            endFragment = int.Parse(lines[i].Split(':')[1]);
        else if (string.IsNullOrWhiteSpace(lines[i])) // Header 結束
        {
            bodyStart = i + 1;
            break;
        }
    }

    if (startFragment >= 0 && endFragment > startFragment)
    {
        // 計算 body 在全字串中的位移
        int headerLen = fullData.Substring(0, fullData.IndexOf(lines[bodyStart])).Length;
        int fragmentStart = startFragment - headerLen;
        int fragmentLen = endFragment - startFragment;
        return fullData.Substring(headerLen + fragmentStart, fragmentLen);
    }
    return null;
}

五、總結與最佳實務

場景推薦做法
簡單 UI 更新control.Invoke(Action) 或 BeginInvoke
非同步作業 + UI 回饋async/await + Task.Run (自動捕獲 UI Context)
長時間處理 + 進度回報IProgress + Progress
類別庫/可重用元件ConfigureAwait(false) 避免死結
複製富文本到 Word/Excel同時寫入 CF_HTML 與 UnicodeText

核心原則:所有 UI 控制項存取必須在 UI 執行緒。現代 .NET 使用 async/await 讓編譯器自動處理執行緒切換,比手動 Invoke 更安全、可讀性更高。

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