1. 程式人生 > >C# Winform 跨線程更新UI控件常用方法總結(轉)

C# Winform 跨線程更新UI控件常用方法總結(轉)

sum tex ase adc 而是 this obj 出現 turn

出處:http://www.tuicool.com/articles/FNzURb

概述

C#Winform編程中,跨線程直接更新UI控件的做法是不正確的,會時常出現“線程間操作無效: 從不是創建控件的線程訪問它”的異常。處理跨線程更新Winform UI控件常用的方法有4種:
1. 通過UI線程的SynchronizationContext的Post/Send方法更新;
2. 通過UI控件的Invoke/BegainInvoke方法更新;

3. 通過BackgroundWorker取代Thread執行異步操作;
4. 通過設置窗體屬性,取消線程安全檢查來避免"跨線程操作異常"(非線程安全,建議不使用)。
下文中對以上3種方法應用進行舉例說明,希望能對初識C# Winform的同學們有些幫助。

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

正文

1.

用法:

//共分三步
        //第一步:獲取UI線程同步上下文(在窗體構造函數或FormLoad事件中)
        /// <summary>
        /// UI線程的同步上下文
        /// </summary>
        SynchronizationContext m_SyncContext = null;
        public Form1()
        {
            InitializeComponent();
            //獲取UI線程同步上下文
            m_SyncContext = SynchronizationContext.Current;
            
//Control.CheckForIllegalCrossThreadCalls = false; } //第二步:定義線程的主體方法 /// <summary> /// 線程的主體方法 /// </summary> private void ThreadProcSafePost() { //...執行線程任務 //在線程中更新UI(通過UI線程同步上下文m_SyncContext) m_SyncContext.Post
(SetTextSafePost, "This text was set safely by SynchronizationContext-Post."); //...執行線程其他任務 } //第三步:定義更新UI控件的方法 /// <summary> /// 更新文本框內容的方法 /// </summary> /// <param name="text"></param> private void SetTextSafePost(object text) { this.textBox1.Text = text.ToString(); } //之後,啟動線程 /// <summary> /// 啟動線程按鈕事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> 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(通過UI線程同步上下文m_SyncContext)
m_SyncContext.Post ( SetTextSafePost , " This text was set safely by SynchronizationContext-Post. " );
可以這樣解讀這行代碼:向UI線程的同步上下文(m_SyncContext)中提交一個異步消息(UI線程,你收到消息後以異步的方式執行委托,調用方法 SetTextSafePost,參數是“this text was ....”).

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

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

// 共分三步

        // 第一步:定義委托類型
        // 將text更新的界面控件的委托類型
        delegate void SetTextCallback(string text);

        //第二步:定義線程的主體方法
        /// <summary>
        /// 線程的主體方法
        /// </summary>
        private void ThreadProcSafe()
        {
            //...執行線程任務

            //在線程中更新UI(通過控件的.Invoke方法)
            this.SetText("This text was set safely.");

            //...執行線程其他任務
        }
        //第三步:定義更新UI控件的方法
        /// <summary>
        /// 更新文本框內容的方法
        /// </summary>
        /// <param name="text"></param>
        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;
            }
        }
        //之後,啟動線程
        /// <summary>
        /// 啟動線程按鈕事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void setTextSafeBtn_Click(
            object sender,
            EventArgs e)
        {
            this.demoThread =
                new Thread(new ThreadStart(this.ThreadProcSafe));

            this.demoThread.Start();
        }

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

解決窗體關閉時出現“訪問已釋放句柄“的異常 部分代碼參考 博客園-事理同學的文章 。

3.

用法:

//共分三步

        //第一步:定義BackgroundWorker對象,並註冊事件(執行線程主體、執行UI更新事件)
        private BackgroundWorker backgroundWorker1 =null;
        public Form1()
        {
            InitializeComponent();

           
            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更新。

需要註意的是: // 設置報告進度更新
backgroundWorker1.WorkerReportsProgress = true ;
默認情況下BackgroundWorker是不報告進度的,需要顯示設置報告進度屬性。

4. 通過設置窗體屬性,取消線程安全檢查來避免"線程間操作無效異常"(非線程安全,建議不使用)

用法:將Control類的靜態屬性CheckForIllegalCrossThreadCalls為false。

public Form1()
        {
            InitializeComponent();
            //指定不再捕獲對錯誤線程的調用
            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; }


綜述:

文中介紹的4種方法,前三種是線程安全的 ,可在實際項目中因地制宜的使用。最後一種方法是非線程安全的,初學者可以實驗體會但不建議使用它。

下面列表對比一下這四種方法

方法 線程安全 支持異步 / 同步 其他
UI Sync Context 更新 Post/Send 盡量在窗體構造函數、 FormLoad 中獲取同步上下文
控件 Invoke control.Invoke/BegainInvoke 註意檢查控件句柄是否已釋放
BackgroundWorker 更新

ProgressChanged、RunWorkerCompleted

事件同步更新
報告進度

CheckForIllegalCrossThreadCalls

取消跨線程調用檢查
同步更新 簡單,不建議使用

C# Winform 跨線程更新UI控件常用方法總結(轉)