随着程序复杂度的提高,程序不可避免会出现多个线程,此时就很可能存在跨线程操作控件的问题。
跨线程操作UI控件主要有三类方式:
1、禁止系统的线程间操作检查。(此法不建议使用)
2、使用Invoke(同步)或者BeginInvoke(异步)。(使用委托实现,并用lambda表达式简化代码)
3、使用BackgroundWorker组件。(此法暂不介绍,详情可见文末的参考资料)
先看一个跨线程操作失败的例子:
新建一个Winform窗口程序项目,拖一个button1和label1控件到Form1窗体上。启动程序以后试图通过点击button1改变label1的值,完整代码如下:
1 using System; 2 using System.Threading; 3 using System.Windows.Forms; 4 5 namespace Windows跨线程调用控件 6 { 7 public partial class Form1 : Form 8 { 9 public Form1() 10 { 11 InitializeComponent(); 12 } 13 14 private void button1_Click(object sender, EventArgs e) 15 { 16 Thread thread1 = new Thread(new ParameterizedThreadStart(UpdateLabel)); 17 thread1.Start("label已更新"); 18 } 19 20 private void UpdateLabel(object str) 21 { 22 label1.Text = str.ToString(); 23 } 24 } 25 }
点击button1以后运行报错:
解决方案:
方法一:禁止系统的线程间操作检查。
代码就一句话:Control.CheckForIllegalCrossThreadCalls = false;通常写在Form1类的构造方法Form1()中。如下所示:
1 public Form1() 2 { 3 InitializeComponent(); 4 5 Control.CheckForIllegalCrossThreadCalls = false; 6 }
但是,这种方法是很不可靠的,有时候还是会报错。
方法二:使用Invoke(同步)或者BeginInvoke(异步)。最精简的代码如下:
1 using System; 2 using System.Threading; 3 using System.Windows.Forms; 4 5 namespace Windows跨线程调用控件 6 { 7 public partial class Form1 : Form 8 { 9 public Form1() 10 { 11 InitializeComponent(); 12 } 13 14 private void button1_Click(object sender, EventArgs e) 15 { 16 Thread thread1 = new Thread(UpdateLabel);//可以省略线程的委托类型ParameterizedThreadStart 17 thread1.Start("label已更新"); 18 } 19 20 private void UpdateLabel(object str) 21 { 22 if (label1.InvokeRequired)//不同线程为true,所以这里是true 23 { 24 BeginInvoke(new Action<string> (x => {label1.Text = x.ToString();}),str); 25 } 26 } 27 } 28 }
说明:Action是.NET预定义好的委托,可以简化委托的语法,如果不清楚它的用法,可以搜索“Action和Func的用法”。
将上面的两种方法总结在同一段程序里面如下所示:(注意看代码中的注释)
1 using System; 2 using System.Threading; 3 using System.Windows.Forms; 4 5 namespace Windows跨线程调用控件 6 { 7 public partial class Form1 : Form 8 { 9 public Form1() 10 { 11 InitializeComponent(); 12 13 /************* 方法一 ************/ 14 //Control.CheckForIllegalCrossThreadCalls = false; 15 16 } 17 18 private void button1_Click(object sender, EventArgs e) 19 { 20 Thread thread1 = new Thread(new ParameterizedThreadStart(UpdateLabel)); 21 thread1.Start("label已更新"); 22 } 23 24 25 //如果控件的 Handle(句柄) 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用),则为 true;否则为 false。 26 private void UpdateLabel(object str) 27 { 28 if (label1.InvokeRequired)//当是不同的线程在访问时为true,所以这里是true 29 { 30 /************* 方法二 ************/ 31 //Action<string> actionDelegate = (x) => { this.label1.Text = x.ToString(); }; 32 33 ////如果已经创建控件的句柄,则除了 InvokeRequired 属性以外,控件上还有四个可以从【任何线程】上安全调用的方法, 34 ////它们是:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。 35 ////在后台线程上创建控件的句柄之前调用 CreateGraphics 可能会导致非法的跨线程调用。 36 ////对于所有其他方法调用,则应使用调用 (invoke) 方法之一封送对控件的线程的调用。 37 //this.label1.BeginInvoke(actionDelegate, str); 38 39 40 /************* 方法二(变式) ************/ 41 //也可以直接用下面一句话来完成 42 //Control.BeginInvoke 方法有两个重载:BeginInvoke(Delegate) ,BeginInvoke(Delegate, Object[]),下式用的是第二个重载 43 this.BeginInvoke(new Action<string>((x) => { label1.Text = x.ToString(); }), str); 44 45 //如果启动的多线程不需要带可变的参数,那更简单: 46 //label1.BeginInvoke(new Action(() => { label1.Text = "aaa"; })); 47 } 48 } 49 } 50 }
参考资料:
http://www.cnblogs.com/TankXiao/p/3348292.html
http://www.cnblogs.com/txw1958/archive/2012/08/21/csharp-crossthread-widget.html