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窗體控件的另一示例

如對線程的操作不正确,在跨線程調用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還沒建立呢!”);