天天看點

WinForm-跨線程更新UI控件常用方法

C#Winform程式設計中,跨線程直接更新UI控件的做法是不正确的,會時常出現“線程間操作無效: 從不是建立控件的線程通路它”的異常。處理跨線程更新Winform UI控件常用的方法有4種:

通過UI線程的SynchronizationContext的Post/Send方法更新;

通過UI控件的Invoke/BegainInvoke方法更新;

通過BackgroundWorker取代Thread執行異步操作;

通過設定窗體屬性,取消線程安全檢查來避免"跨線程操作異常"(非線程安全,建議不使用)。

下文中對以上3種方法應用進行舉例說明,希望能對初識C# Winform的同學們有些幫助。

成文表分享交流之意,惶恐水準有限,文中了解和表述有錯誤之處還請大家多被批評指正。

通過UI線程的SynchronizationContext的Post/Send方法更新

用法: //共分三步

//第一步:擷取UI線程同步上下文(在窗體構造函數或FormLoad事件中)

///

/// UI線程的同步上下文

SynchronizationContext m_SyncContext = null;

public Form1()

{

InitializeComponent();

//擷取UI線程同步上下文

m_SyncContext = SynchronizationContext.Current;

//Control.CheckForIllegalCrossThreadCalls = false;

}

//第二步:定義線程的主體方法

/// 線程的主體方法

private void ThreadProcSafePost()

//…執行線程任務

//線上程中更新UI(通過UI線程同步上下文m_SyncContext)

m_SyncContext.Post(SetTextSafePost, “This text was set safely by SynchronizationContext-Post.”);

//…執行線程其他任務

//第三步:定義更新UI控件的方法

/// 更新文本框内容的方法

private void SetTextSafePost(object text)

this.textBox1.Text = text.ToString();

//之後,啟動線程

/// 啟動線程按鈕事件

private void setSafePostBtn_Click(object sender, EventArgs e)

this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafePost));

this.demoThread.Start();

說明:三處加粗部分是關鍵。該方法的主要原理是:線上程執行過程中,需要更新到UI控件上的資料不再直接更新,而是通過UI線程上下文的Post/Send方法,将資料以異步/同步消息的形式發送到UI線程的消息隊列;UI線程收到該消息後,根據消息是異步消息還是同步消息來決定通過異步/同步的方式調用SetTextSafePost方法直接更新自己的控件了。

在本質上,向UI線程發送的消息并是不簡單資料,而是一條委托調用指令。

可以這樣解讀這行代碼:向UI線程的同步上下文(m_SyncContext)中送出一個異步消息(UI線程,你收到消息後以異步的方式執行委托,調用方法SetTextSafePost,參數是“this text was …”).

2.通過UI控件的Invoke/BegainInvoke方法更新

用法:與方法1類似,可分為三個步驟。

// 共分三步

// 第一步:定義委托類型

// 将text更新的界面控件的委托類型

delegate void SetTextCallback(string text);

private void ThreadProcSafe()

//線上程中更新UI(通過控件的.Invoke方法)

this.SetText(“This text was set safely.”);

private void SetText(string text)

// InvokeRequired required compares the thread ID of the

// calling thread to the thread ID of the creating thread.

// If these threads are different, it returns true.

if (this.textBox1.InvokeRequired)//如果調用控件的線程和建立建立控件的線程不是同一個則為True

while (!this.textBox1.IsHandleCreated)

//解決窗體關閉時出現“通路已釋放句柄“的異常

if (this.textBox1.Disposing || this.textBox1.IsDisposed)

return;

SetTextCallback d = new SetTextCallback(SetText);

this.textBox1.Invoke(d, new object[] { text });

else

this.textBox1.Text = text;

private void setTextSafeBtn_Click(

object sender,

EventArgs e)

this.demoThread =

new Thread(new ThreadStart(this.ThreadProcSafe));

說明:這個方法是目前跨線程更新UI使用的主流方法,使用控件的Invoke/BegainInvoke方法,将委托轉到UI線程上調用,實作線程安全的更新。原理與方法1類似,本質上還是把線程中要送出的消息,通過控件句柄調用委托交到UI線程中去處理。

Control.InvokeRequired屬性擷取一個值,該值訓示調用方在對控件進行方法調用時是否必須調用Invoke方法,因為調用方位于建立控件所在的線程以外的線程中。如果控件的handle是在與調用線程不同的線程中建立的(說明您必須通過invoke方法對控件進行調用),則為true,否者為false。

Windows窗體中的控件被綁定到特定的線程,不具備線程安全性。是以,如果從另一個線程調用控件的方法,那麼必須使用控件的一個invoke方法來将調用封送到适當的線程。該屬性可用于确定是否必須調用invoke方法,當不知道什麼線程擁有控件時這很有用。

3.通過BackgroundWorker取代Thread執行異步操作

//第一步:定義BackgroundWorker對象,并注冊事件(執行線程主體、執行UI更新事件)

private BackgroundWorker backgroundWorker1 =null;

backgroundWorker1 = new System.ComponentModel.BackgroundWorker();

//設定報告進度更新

backgroundWorker1.WorkerReportsProgress = true;

//注冊線程主體方法

backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);

//注冊更新UI方法

backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);

//backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);

//第二步:定義執行線程主體事件

//線程主體方法

public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

//線上程中更新UI(通過ReportProgress方法)

backgroundWorker1.ReportProgress(50, “This text was set safely by BackgroundWorker.”);

//第三步:定義執行UI更新事件

//UI更新方法

public void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)

this.textBox1.Text = e.UserState.ToString();

//之後,啟動線程

//啟動backgroundWorker

private void setTextBackgroundWorkerBtn_Click(object sender, EventArgs e)

this.backgroundWorker1.RunWorkerAsync();

說明:C# Winform中執行異步任務時,BackgroundWorker是個不錯的選擇。它是EAP(Event based Asynchronous Pattern)思想的産物,DoWork用來執行異步任務,在任務執行過程中/執行完成後,我們可以通過ProgressChanged,ProgressCompleteded事件進行線程安全的UI更新。

需要注意的是://設定報告進度更新

預設情況下BackgroundWorker是不報告進度的,需要顯示設定報告進度屬性。

通過設定窗體屬性,取消線程安全檢查來避免"線程間操作無效異常"

用法:将Control類的靜态屬性CheckForIllegalCrossThreadCalls為false。

//指定不再捕獲對錯誤線程的調用

Control.CheckForIllegalCrossThreadCalls = false;

說明:通過設定CheckForIllegalCrossThreadCalls屬性,可以訓示是否捕獲線程間非安全操作異常。該屬性值預設為ture,即線程間非安全操作是要捕獲異常的("線程間操作無效"異常)。通過設定該屬性為false簡單的屏蔽了該異常。Control.CheckForIllegalCrossThreadCalls的注釋如下

//

// 摘要:

// 擷取或設定一個值,該值訓示是否捕獲對錯誤線程的調用,這些調用在調試應用程式時通路控件的 System.Windows.Forms.Control.Handle

// 屬性。

// 傳回結果:

// 如果捕獲了對錯誤線程的調用,則為 true;否則為 false。

[EditorBrowsable(EditorBrowsableState.Advanced)]

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

[SRDescription(“ControlCheckForIllegalCrossThreadCalls”)]

[Browsable(false)]

public static bool CheckForIllegalCrossThreadCalls { get; set; }

跨線程調用Windows窗體控件的另一示例

WinForm-跨線程更新UI控件常用方法

如對線程的操作不正确,在跨線程調用Windows窗體控件時會有産生InvalidOperationException異常。

該異常提示[線程間操作無效: 從不是建立控件“listBox1”的線程通路它.]。

我相信很多人通過設定Control.CheckForIllegalCrossThreadCalls屬性為false禁止捕獲對錯誤線程的調用。

這種強制性的禁止捕獲不是人性化的選項。

我們可以通過控件的Invoke方法來實作跨線程調用Windows窗體控件。

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using System.Threading;

namespace VJSDN.Tech.ThreadAccess

//跨線程調用的方法委托原型。

public delegate void ShowMessageMethod(string msg);

public delegate void CreateControlMethod();

//注:A線程:主線程. 主線程是指目前窗體所在的線程.

// B線程:使用者建立的線程。

// C線程:帶參數的線程。

public partial class Form1 : Form

private Thread _threadB = null; //第2個線程.建立控件。

private Thread _threadC = null; //第3個線程.帶參數的線程。

private Button _btnOnB = null; //由第2個線程建立的按鈕控件。

private ListBox _listBoxOnB = null;//由第2個線程建立的ListBox控件。

private Panel _PanelOnB = null;//由第2個線程建立的Panel控件。

//是否捕獲對錯誤線程的調用,這些調用通路控件的 System.Windows.Forms.Control.Handle 屬性。

private void btnCreateThreadB_Click(object sender, EventArgs e)

//在主線程内建立線程B.

_threadB = new Thread(new ThreadStart(MethodThreadB)); //啟動線程。

_threadB.Start();

private void createC_Click(object sender, EventArgs e)

//在主線程内建立帶參數的線程C.

_threadC = new Thread(new ParameterizedThreadStart(MethodThreadC));

_threadC.Start(100); //往線程C傳送參數100,計算100以内的數字加總。

//C線程正在運作…

private void MethodThreadC(object param)

int total = int.Parse(param.ToString());

this.Invoke(new ShowMessageMethod(this.ShowMessage), “線程C正在計數:1+2+n=?(n<=” + param.ToString() + “)”);

int result = 0; //計數器

for (int i = 1; i <= total; i++) result += i;

this.Invoke(new ShowMessageMethod(this.ShowMessage), “線程C計算結果:” + result.ToString());

//B線程正在運作…

private void MethodThreadB()

//跨線程操作:試圖在B線程内給A線程的panel2控件插入子控件.

this.Invoke(new CreateControlMethod(this.CreatePanelOnThreadB)); //建立Panel

this.Invoke(new CreateControlMethod(this.CreateButtonOnThreadB)); //建立按鈕

this.Invoke(new CreateControlMethod(this.CreateListBoxOnThreadB)); //建立ListBox

this.Invoke(new ShowMessageMethod(this.ShowMessage), “線程B操作線程A内的ListBox控件”);

this.Invoke(new ShowMessageMethod(this.ShowMessage), “如能顯示消息,表示操作成功!”);

//注意:這個方法與CreateControlMethod委托原型相同。

private void CreateControlCross(Label lbl)

_PanelOnB.Controls.Add(lbl);

//注意:這個方法與ShowMessageMethod委托原型相同。

private void ShowMessage(string msg)

this.listBox1.Items.Add(msg);

private void CreatePanelOnThreadB()

_PanelOnB = new Panel();

_PanelOnB.BackColor = System.Drawing.Color.Silver;

_PanelOnB.Location = new System.Drawing.Point(264, 12);

_PanelOnB.Name = “panel2”;

_PanelOnB.Size = new System.Drawing.Size(244, 355);

_PanelOnB.TabIndex = 1;

this.Controls.Add(_PanelOnB); //建立Panel容器

Label _lblB = new Label();

_lblB.AutoSize = true;

_lblB.Font = new System.Drawing.Font(“宋體”, 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));

_lblB.Location = new System.Drawing.Point(3, 7);

_lblB.Name = “label1”;

_lblB.Size = new System.Drawing.Size(84, 12);

_lblB.TabIndex = 0;

_lblB.Text = “線程B,使用者建立的線程”;

_PanelOnB.Controls.Add(_lblB); //Panel容器内加入一個label

private void CreateButtonOnThreadB()

_btnOnB = new Button();

_btnOnB.FlatStyle = System.Windows.Forms.FlatStyle.Flat;

_btnOnB.Location = new System.Drawing.Point(5, 63);

_btnOnB.Name = “btnSendToA”;

_btnOnB.Size = new System.Drawing.Size(167, 23);

_btnOnB.TabIndex = 4;

_btnOnB.Text = “給線程A建立的控件發消息”;

_btnOnB.UseVisualStyleBackColor = true;

_btnOnB.Click += new System.EventHandler(this.btnSendToA_Click);

//panel2是主線程上建立的控件。

_PanelOnB.Controls.Add(_btnOnB);

private void CreateListBoxOnThreadB()

_listBoxOnB = new ListBox();

_listBoxOnB.FormattingEnabled = true;

_listBoxOnB.ItemHeight = 12;

_listBoxOnB.Location = new System.Drawing.Point(5, 240);

_listBoxOnB.Name = “listBox2”;

_listBoxOnB.Size = new System.Drawing.Size(236, 112);

_listBoxOnB.TabIndex = 2;

_PanelOnB.Controls.Add(_listBoxOnB);

private void btnSendToA_Click(object sender, EventArgs e)

listBox1.Items.Add(“線程B發消息給A”);

private void btnSendToB_Click(object sender, EventArgs e)

if (_listBoxOnB != null)

_listBoxOnB.Items.Add(“線程A發消息給B”);

MessageBox.Show(“線程B還沒建立呢!”);

繼續閱讀