本片博文接上一篇:.NET多線程執行函數,給出實現一個線程更新另一個線程UI的兩種方法。
Winform中的控件是綁定到特定的線程的(一般是主線程),這意味着從另一個線程更新主線程的控件不能直接調用該控件的成員。
控件綁定到特定的線程這個概念如下:
為了從另一個線程更新主線程的Windows Form控件,可用的方法有:
首先用一個簡單的程序來示例,這個程序的功能是:在Winfrom窗體上,通過多線程用label顯示時間。給出下面的兩種實現方式
1.結合使用特定控件的如下成員
InvokeRequired屬性:返回一個bool值,指示調用者在不同的線程上調用控件時是否必須使用Invoke()方法。如果主調線程不是創建該控件的線程,或者還沒有為控件創建窗口句柄,則返回true。
Invoke()方法:在擁有控件的底層窗口句柄的線程上執行委托。
BeginInvoke()方法:異步調用Invoke()方法。
EndInvoke()方法:獲取BeginInvoke()方法啟動的異步操作返回值。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; namespace 一個線程更新另一個線程UI2 { /// <summary> /// DebugLZQ /// http://www.cnblogs.com/DebugLZQ /// </summary> public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void UpdateLabel(Control ctrl, string s) { ctrl.Text = s; } private delegate void UpdateLabelDelegate(Control ctrl, string s); private void PrintTime() { if (label1.InvokeRequired == true) { UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel); while(true) { label1.Invoke(uld, new object[] { label1, DateTime.Now.ToString() }); } } else { while (true) { label1.Text = DateTime.Now.ToString(); } } } private void Form1_Load(object sender, EventArgs e) { //PrintTime();//錯誤的單線程調用 Thread t = new Thread(new ThreadStart(PrintTime)); t.Start(); } } }
比較和BackgroundWorker控件方式的異同點。
2.使用BackgroundWorker控件。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace 一個線程更新另一個線程UI { /// <summary> /// DebugLZQ /// http://www.cnblogs.com/DebugLZQ /// </summary> public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void UpdateLabel(Control ctrl, string s) { ctrl.Text = s; } private delegate void UpdateLabelDelegate(Control ctrl, string s); private void PrintTime() { if (label1.InvokeRequired == true) { UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel); while (true) { label1.Invoke(uld, new object[] { label1, DateTime.Now.ToString() }); } } else { while (true) { label1.Text = DateTime.Now.ToString(); } } } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { PrintTime(); } private void Form1_Load(object sender, EventArgs e) { backgroundWorker1.RunWorkerAsync(); } } }
程序的運行結果如下:
--------------------
Update:請參考后續博文:WPF: Cancel an Command using BackgroundWorker
--------------------
更新另一個線程的進度條示例(第一種方法實現)
DebugLZQ覺得第一種方法要更直觀一點,或是更容易理解一點。下面再用第一種方法來做一個Demo:輸入一個數,多線程計算其和值更新界面上的Label,並用進度條顯示計算的進度。實際上就是,更新另一個線程的兩個UI控件。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; namespace 一個線程更新另一個線程的UI3 { /// <summary> /// DebugLZQ /// http://www.cnblogs.com/DebugLZQ /// </summary> public partial class Form1 : Form { public Form1() { InitializeComponent(); } private static long result = 0; //更新Label private void UpdateLabel(Control ctrl, string s) { ctrl.Text = s; } private delegate void UpdateLabelDelegate(Control ctrl, string s); //更新ProgressBar private void UpdateProgressBar(ProgressBar ctrl, int n) { ctrl.Value = n; } private delegate void UpdateProgressBarDelegate(ProgressBar ctrl, int n); private void Sum(object o) { result = 0; long num = Convert.ToInt64(o); UpdateProgressBarDelegate upd = new UpdateProgressBarDelegate(UpdateProgressBar); for (long i = 1; i <= num; i++) { result += i; //更新ProcessBar1 if (i % 10000 == 0)//這個數值要選的合適,太小程序會卡死 { if (progressBar1.InvokeRequired == true) { progressBar1.Invoke(upd, new object[] { progressBar1, Convert.ToInt32((100 * i) / num) });//若是(i/num)*100,為什么進度條會卡滯? } else { progressBar1.Value = Convert.ToInt32(i / num * 100); } } } //更新lblResult if (lblResult.InvokeRequired == true) { UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel); lblResult.Invoke(uld, new object[] { lblResult, result.ToString() }); } else { lblResult.Text = result.ToString(); } } private void btnStart_Click(object sender, EventArgs e) { Thread t = new Thread(new ParameterizedThreadStart(Sum)); t.Start(txtNum.Text); } } }
程序的運行結果如下:
用BackgroundWorker控件可以實現相同的功能,個人覺得這樣更容易理解~
第一種方法的若干簡化
和異步方法調用一樣,我們可以使用delegate、匿名方法、Action/Function等系統提供委托、Lambda表達式等進行簡化。
如,第一種方法,更新界面時間,我們可以簡化如下:
using System; using System.Windows.Forms; using System.Threading; namespace WindowsFormsApplication1 { public partial class FormActionFunction : Form { public FormActionFunction() { InitializeComponent(); } private void UpdateLabel() { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); } private void PrintTime() { while (true) { PrintTime(UpdateLabel); } } private void PrintTime(Action action) { if (InvokeRequired) { Invoke(action); } else { action(); } } private void FormActionFunction_Load(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(PrintTime)); t.IsBackground = true; t.Start(); } } }
也可以再簡化:
using System; using System.Windows.Forms; using System.Threading; namespace WindowsFormsApplication1 { public partial class FormActionFunction : Form { public FormActionFunction() { InitializeComponent(); } private void PrintTime() { while (true) { PrintTime(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); });//Lambda簡寫 } } private void PrintTime(Action action) { if (InvokeRequired) { Invoke(action); } else { action(); } } private void FormActionFunction_Load(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(PrintTime)); t.IsBackground = true; t.Start(); } } }
進一步簡化:
using System; using System.Windows.Forms; using System.Threading; namespace WindowsFormsApplication1 { public partial class FormBestPractice : Form { public FormBestPractice() { InitializeComponent(); } private void PrintTime() { while (true) { Invoke(new Action(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); })); } } private void FormBestPractice_Load(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(PrintTime)); t.IsBackground = true; t.Start(); } } }
再進一步簡化:
using System; using System.Windows.Forms; using System.Threading; namespace WindowsFormsApplication1 { public partial class FormBestPractice2 : Form { public FormBestPractice2() { InitializeComponent(); } private void FormBestPractice2_Load(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(() => { while (true) { Invoke(new Action(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); })); } })); t.IsBackground = true; t.Start(); } } }
可根據代碼風格要求,去掉 new ThreadStart()、new Action(),程序也可以正常運行,但是這樣DebugLZQ不推薦這樣,因為多線程調用方法參數不夠清晰,可參考DebugLZQ關於多線程執行函數的博文。
根據個人編碼習慣,可選擇合適的編碼方法。以上代碼由DebugLZQ編寫,可正常運行,就不附上界面截圖了。
-----------------
說明:以上所有更新方法均默認為同步方法。
若需要異步執行,則把Invoke換成BeginInvoke即可,其優點是不會阻塞當前線程。
============================================
若是WPF程序,則只要把在Winform中用於更新擁有控件的線程使用的Invoke方法換成
Dispatcher.Invoke
即可。也給出一個Demo:
MainWindow.xaml, MainWindow.xaml.cs如下:

<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel> <Button Content="Start" Click="ButtonBase_OnClick" Height="40" Margin="40"/> <Grid> <ProgressBar x:Name="ProgressBar1" Height="60"/> <TextBlock x:Name="TextBlock1" FontSize="40" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </StackPanel> </Grid> </Window>

using System; using System.Threading; using System.Windows; namespace WpfApplication1 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { ProgressBar1.Minimum = 0; ProgressBar1.Maximum = 100; Thread t = new Thread(() => { for (int i = 0; i <= 100 * 1000; i++) { Dispatcher.Invoke(() => { if (i % 1000 == 0) { ProgressBar1.Value = i / 1000; TextBlock1.Text = i/1000 + "%"; } }); } }); t.IsBackground = true; t.Start(); } } }
效果如下:
若不想阻塞當前線程(注意:是當前線程,非UI線程,Dispatcher.BeginInvoke可能會阻塞UI線程,因為其做的事情是:將執行扔給UI線程去執行,立即返回當前線程~其不管UI線程死活),則可使用異步Invoke:
Dispatcher.BeginInvoke
.NET Framework 4.5 提供了新的異步模式Async,因此開發平台若為.NET 4.5,也可以使用:
Dispatcher.InvokeAsync
詳細請參考DebugLZQ后續博文:WPF: Updating the UI from a non-UI thread
或是直接對同步方法進行異步封裝,請參考DebugLZQ后續相關博文:從C#5.0說起:再次總結C#異步調用方法發展史。
小結
無論WPF還是WinForm,UI都是由單個線程負責更新~
Invoke解決的問題是:非UI線程無法更新UI線程的問題. 其實現方法是將方法扔給UI線程執行(To UI Thread),Invoke等待UI線程執行完成才返回;BeginInvoke不等待UI線程執行完立刻返回~
Invoke/BeginInvoke可能會阻塞UI線程.(Invoke/BeginInvoke的區別是:是否等待UI線程執行完才返回當前線程繼續執行~)
不阻塞UI線程:其解決方法是將耗時的非更新UI的操作操作放到后台線程里去,與Invoke/BeginInvoke沒有半毛錢關系~也就是:別給UI線程太大壓力!
希望對你有幫助~