如何跨越线程调用窗体控件?(2)

一、前言

VS中,如果UI背后的处理工作复杂,可以启用多线程进行处理,用户不喜欢反应慢的程序。在执行耗时较长的操作时,使用多线程是明智之举,它可以提高程序 UI 的响应速度,使得一切运行显得更为快速。那么如果在UI上反应最新的处理状态呢?这就是如果在子线程(即新开启的处理复杂任务的线程)中更新UI处理状态信息。

 

二、使用Invoke

引用:http://dev.tot.name/csharp/html/20090314/20090314234018.htm

 

在Visual Studio 2005/Visual Studio 2008中创建一个C#的Windows窗体应用程序项目,并将其项目命名为UIThreading。程序的功能是为用户输入文字到第1个"TextBox"控件中,并单击"次线程显示"按钮,创建一个次线程,将第1个"TextBox"控件"Text"属性值复制到第2个"TextBox"控件的"Text"属性值中。相应地,如果用户单击"主线程显示"按钮,程序将不会创建新的线程,而是在UI线程中完成属性值的赋值操作。在Visual Studio 2005/Visual Studio 2008的"Form1.cs[设计]"视图中创建基本的窗体布局和控件,控件的命名如图7.36所示。

 
图7.36 "跨线程调用窗体控件"布局及命名示意图

本例的窗体中,双击"Display"控件和"MainTd"控件可编写其"Click"事件的处理方法,编写Form1.cs如代码7.23所示。

代码7.23  跨越线程调用窗体控件:Form1 .cs

using System;
…………………………
//导入必要的命名空间
using System.Threading;
namespace UIThreading
{
public partial class Form1 : Form
{
public delegate void MyDel(string t);
public Form1()
{
//初始化窗体
InitializeComponent();
//获取并输出UI线程的ID值
int TdId = Thread.CurrentThread.GetHashCode();
MessageBox.Show("UI线程ID为:" + TdId);
}

private void Display_Click(object sender, EventArgs e)
{
//创建ThreadStart类型委托对象Ts,指向Method方法
ThreadStart Ts = new ThreadStart(Method);
//创建次线程对象Td,传递ThreadStart类型Ts
Thread Td = new Thread(Ts);
//尽快执行新线程
Td.Start();
}
        void GetTxt(string t)
{           
//如果output控件的InvokeRequired值为false
if (!this.output.InvokeRequired)
{
//获取并输出当前线程的ID值
int TdId = Thread.CurrentThread.GetHashCode();
MessageBox.Show("GetTxt方法正在执行,该方法所在线程ID为:" + TdId);
//将t参数值赋值给output控件的Text属性
this.output.Text = t;
}
else
{
//调用output控件的Invoke方法,传递MyDel委托类型对象和t参数值
this.output.Invoke(new MyDel(GetTxt), t);
}           
}
        void Method()
{
//获取并输出当前线程的ID值
int TdId = Thread.CurrentThread.GetHashCode();
MessageBox.Show("Method方法正在执行,该方法所在线程ID为:" + TdId);
//当前线程阻塞4秒
Thread.Sleep(4000);
//调用GetTxt方法,并传递input控件的Text属性值
string t = this.input.Text;           
GetTxt(t);
}
        private void MainTd_Click(object sender, EventArgs e)
{
//获取并输出当前主线程的ID值
int TdId = Thread.CurrentThread.GetHashCode();
MessageBox.Show("执行方法在主线程上,其线程ID为:" + TdId);
//当前线程阻塞4秒
Thread.Sleep(4000);
//调用GetTxt方法,并传递input控件的Text属性值
string t = this.input.Text;
Thread.Sleep(4000);
GetTxt(t);
}       
}
}

示例程序窗体初始化时,通过信息对话框输出UI线程的ID以作参考。当用户输入文本到"input"控件中,单击"次线程显示"按钮时,程序将跳出信息对话框,输出Method()方法所属线程ID(即新创建的次线程ID),运行结果如图7.37所示。

注意该ID有别于UI的线程ID,说明Method()方法工作于新线程上。在Method()方法中将当前线程阻塞4秒,然后调用GetTxt()方法,并传递t参数(即用户输入值)。GetTxt()方法被调用2次,第1次调用时,GetTxt()方法体首先判断"output"控件的InvokeRequired属性的返回值。如果InvokeRequired属性返回true,则代表当前线程不是UI线程,立即调用"output"控件的Invoke()方法,并传递一个委托对象(指向所属方法)和所接收参数。通过委托在UI线程上第2次调用GetTxt()方法,"output"控件的InvokeRequired属性将返回false,即可直接在主线程上操作该控件,将t参数赋值给"output"控件的Text属性。在第2次调用GetTxt()方法时,将通过信息对话框输出当前所属线程的ID,结果如图7.38所示。

示。

 
图7.37  Method方法的所属线程
 
图7.38  GetTxt方法所属线程

由上可知,第2次调用GetTxt()方法时,其所属线程为UI线程,ID和程序窗体创建时所输出的UI线程一致。当用户在信息对话框上单击"确定"按钮后,"output"控件的赋值操作才最终完成,结果如图7.39所示。

 
图7.39  窗体控件已被成功调用

说明:可以单击"主线程显示"按钮,其操作仅调用了一次GetTxt()方法,而且线程阻塞4秒时,窗体处于无法响应的状态。

解析

在示例程序中,初始化窗体时调用了如以下代码来越过跨线程调用控件的检查


System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
事实上这是不安全的,因为在窗体程序的设计规则中,不允许多个线程操作窗体控件。所以这个方法只是跳过错误检查,并没有解决实际问题。为了使其他线程调用UI线程的控件,可利用窗体控件的Invoke()方法,使次线程通过委托挂在UI线程上执行对控件的操作。这样,实际操作就是在UI线程中完成,不会存在线程安全问题,只是会短暂地阻塞UI线程的其他操作。而控件的InvokeRequired属性可以获取当前是否需要调用Invoke()方法,即当前所属线程是否是UI线程以外的线程,其编写方法如以下代码所示:
//方法定义部分
访问修饰符 返回类型 方法名称(参数列表)
{
if(控件.InvokeRequired)
{
调用控件的操作代码段;
}
else
{
控件.Invoke(new 自定义委托类型(方法名称), 参数数组);
}
}

以上代码定义了一个方法,在次线程的代码中调用这个方法即可间接调用UI线程中的窗体控件。不过需要注意的是,在程序中首先需要定义了一个委托类型,确保该方法和委托类型的签名一致。

 

posted @ 2010-04-10 15:34  pjh123  阅读(1011)  评论(0)    收藏  举报