可视化控件的Invoke和BeginInvoke方法
当我们在线程函数中写代码直接访问UI控件的属性和调用它们的方法时,结果无一例外,都会得到Visual Studio给出的同样的报错信息。
引发上述异常的原因在于TextBox控件是由主线程创建的,不能直接从另一个线程访问。
在Windows应用程序中,绘制窗体和控件是由“UI线程”负责的,因此Windows不允许其它线程直接访问可视化控件。其原因是Windows无法控制其它线程将如何使用这些控件,而对控件某些属性的设置和方法调用有可能直接影响到控件的外观。这样想想就明白了:如果UI线程正在绘制按钮的同时,另一个线程要修改按钮上的文字,第三个线程又尝试着修改此按钮的背景色,事情会不会弄得一团糟?这个按钮能“画”好吗?
因此,在.NET中有这样一个基本编程原则:
不能跨线程直接访问窗体和控件,对它们的访问必须转由UI线程来负责处理。
在.NET Framework中,所有可视化的控件(包括窗体)都是从System.Windows.Forms.Control类派生出来的,考虑到跨线程访问控件的需要,Control类提供了相应的方法完成跨线程更新界面工作。
1.Invoke同步方法
Control.Invoke方法定义如下:
public object Invoke(Delegate method);
Invoke方法的参数是一个委托,代表在创建控件的线程中要执行的方法。
创建两个线程MyJob1与MyJob2,MyJob1线程每200ms计数一次,MyJob2线程每500ms计数一次,并将计数的结果显示在主窗体上。同时主线程可以执行输入输出操作,没有阻塞现象发生。
public partial class Form1 : Form
{
private long m_nCount1, m_nCount2 = 0;
private Thread m_Job1; //定义线程1
private Thread m_Job2; //定义线程2
private void MyJob1() //线程函数1
{
for (int i = 0; i < 100000; i++)
{
m_nCount1 = m_nCount1 + 1;
m_txt_Job1.Invoke((Action)delegate() { this.m_txt_Job1.Text = m_nCount1.ToString(); }); //跨线程访问UI控件,使用匿名方法
Thread.Sleep(200);
}
}
private void MyJob2() //线程函数2
{
for (int i = 0; i < 100000; i++)
{
m_nCount2 = m_nCount2 + 1;
m_txt_Job2.Invoke((Action)delegate() { this.m_txt_Job2.Text = m_nCount2.ToString(); }); //跨线程访问UI控件,使用匿名方法
Thread.Sleep(500);
}
}
private void m_btn_Set_Click(object sender, EventArgs e)
{
m_lbl_Output.Text = m_txt_Input.Text; //主线程显示
}
private void m_btn_Start_Click(object sender, EventArgs e)
{
m_Job1 = new Thread(MyJob1);
m_Job2 = new Thread(MyJob2);
m_Job1.Priority = ThreadPriority.Highest; //设置线程1为最高优先级
m_Job2.Priority = ThreadPriority.Lowest; //设置线程2为最高低先级
m_Job1.IsBackground = true; //设置为背景线程(主线程结束时让CRL自动地强行结束所有还在运行的辅助线程)
m_Job1.Start(); //启动线程
m_Job2.Start();
m_btn_Start.Enabled = false;
m_btn_Stop.Enabled = true;
}
private void m_btn_Stop_Click(object sender, EventArgs e)
{
m_Job1.Abort(); //终止线程
m_Job2.Abort();
m_btn_Start.Enabled = true;
m_btn_Stop.Enabled = false;
}
private void m_btn_Reset_Click(object sender, EventArgs e)
{
m_nCount1 = 0;
m_nCount2 = 0;
m_txt_Job1.Text = "0";
m_txt_Job2.Text = "0";
}
}
2.BeginInvoke方法
跨线程异步访问UI控件的方法——BeginInvoke,使用此方法,工作线程可以将一个方法传送个UI线程执行之后,继续执行下一步的任务而无需等待。注意,UI线程主要职责是更新用户界面和接收用户响应,它是使用串行方式从消息队列中提取消息并处理的,如果UI线程处理某个消息或执行其它线程穿丝过来的方法时间较长,将会导致用户界面停止响应,会让用户误以为死机了。因此,每项需要让UI线程执行的任务都不应该耗费过长的时间。对于的确需要运行较长时间的任务,可以使用独立的工作线程来完成,而只将处理结果交给UI线程显示。
BeginInvoke的使用方法和Invoke一样,只需将调用Invoke的那句代码改为BeginInvoke即可。
//m_txt_Job2.Invoke((Action)delegate() { this.m_txt_Job2.Text = m_nCount2.ToString(); });
m_txt_Job2.BeginInvoke((Action)delegate() { this.m_txt_Job2.Text = m_nCount2.ToString(); }); //异步调用方法
//m_txt_Job1.Invoke((Action)delegate() { this.m_txt_Job1.Text = m_nCount1.ToString(); });
m_txt_Job1.BeginInvoke((Action)delegate() { this.m_txt_Job1.Text = m_nCount1.ToString(); }); //异步调用方法
3.通过回调函数向跨线程访问控件的方法传送参数
Control类的Invoke方法有另一个重载的形式:
public object Invoke(Delegate method, params object[] args);
第一个参数为委托变量所引用的回调函数,第二个参数即为需要方法传送的参数。
定义一个委托变量CallbackJob引用__ShowMessage函数:
private Action<object, long> CallbackJob; //Action<>泛型委托
public Form1()
{
InitializeComponent();
CallbackJob = new Action<object, long>(__ShowMessage); //挂接
}
private void __ShowMessage(object txtJob,long count)
{
(txtJob as TextBox).Text = count.ToString();
Thread.SpinWait(1);
}
调用Invoke方法传入控件与数值。
m_txt_Job1.Invoke(CallbackJob, m_txt_Job1, m_nCount1); //传入参数
m_txt_Job2.Invoke(CallbackJob, m_txt_Job2, m_nCount2); //传入参数