winform中更新UI控件的方案介绍
这是一个古老的话题。。。直入主题吧!
对winfrom的控件来说,多线程操作非常容易导致复杂且严重的bug,比如不同线程可能会因场景需要强制设置控件为不同的状态,进而引起并发、加锁、死锁、阻塞等问题。为了避免和解决上述可能出现的问题,微软要求必须是控件的创建线程才能操作控件资源,其它线程不允许直接操作控件。但是现代应用又不是单线程应用,无论如何肯定会存在其它线程需要更新控件的需求,于是微软两种方案来解决相关问题:InvokeRequired方案和BackgroundWorker方案。
演示程序效果图和源码


查看代码
using System.ComponentModel;
using System.Diagnostics;
using System.Timers;
using Tccc.DesktopApp.WinForms1.BLL;
namespace Tccc.DesktopApp.WinForms1
{
public partial class UIUpdateDemoForm : Form
{
/// <summary>
///
/// </summary>
public UIUpdateDemoForm()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
}
#region 演示InvokeRequired
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "invokeRequiredBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
new Thread(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "BeginWorking_Invoke 线程ID=" + Thread.CurrentThread.ManagedThreadId);
BLLWorker.BeginWorking_Invoke(this, "some input param");
}).Start();
}
/// <summary>
///
/// </summary>
public void UpdatingProgress(int progress)
{
if (this.InvokeRequired)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=true 线程ID=" + Thread.CurrentThread.ManagedThreadId);
this.Invoke(new Action(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "Sleep2秒 线程ID=" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);//模拟UI操作慢
UpdatingProgress(progress);
}));
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "after Invoke 线程ID=" + Thread.CurrentThread.ManagedThreadId);
}
else
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=false 线程ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += DateTime.Now.ToString("HH:mm:ss") + ":执行进度" + progress + "%" + Environment.NewLine;
}
}
#endregion
#region 演示BackgroundWorker
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void bgWorkerBtn_Click(object sender, EventArgs e)
{
new Thread(() =>
{
//Control.CheckForIllegalCrossThreadCalls = true;
//richTextBox1.Text = "可以了?";
}).Start();
Debug.WriteLine("bgWorkerBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (!backgroundWorker1.IsBusy)
{
richTextBox1.Text = String.Empty;
backgroundWorker1.RunWorkerAsync("hello world");//
}
}
private void bgWorkerCancelBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine("bgWorkerCancelBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (backgroundWorker1.IsBusy)
{
backgroundWorker1.CancelAsync();//
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Debug.WriteLine("backgroundWorker1_DoWork 线程ID=" + Thread.CurrentThread.ManagedThreadId);
BLLWorker.BeginWorking(sender, e);//控件遍历传递到业务处理程序中
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_ProgressChanged 线程ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行进度" + e.ProgressPercentage + "%";
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_RunWorkerCompleted 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (e.Cancelled)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":已取消";
}
else if (e.Error != null)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":发生错误:" + e.Error.Message;
}
else
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行完成";
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行结果=" + e.Result;
}
}
#endregion
}
public class BLLWorker
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking_Invoke(UIUpdateDemoForm form, string inputData)
{
int counter = 0;
int max = 5;
while (counter < max)
{
System.Threading.Thread.Sleep(200);
counter++;
form.UpdatingProgress(counter * 20);
}
}
/// <summary>
/// 模拟耗时操作(下载、批量操作等)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
Debug.WriteLine("inputArgument=" + e.Argument as string);
for (int i = 1; i <= 10; i++)
{
if (worker.CancellationPending == true)//检测是否被取消
{
e.Cancel = true;
break;
}
else
{
// Perform a time consuming operation and report progress.
System.Threading.Thread.Sleep(200);
worker.ReportProgress(i * 10);
}
}
e.Result = "result xxxx";
}
}
}
InvokeRequired方案
上述代码中,this.InvokeRequired属性就是用来判断当前线程和this控件的创建线程是否一致。
- 当其值=false时,代表当前执行线程就是控件的创建线程,可以直接操作控件。
- 当其值=true时,代表当前线程不是控件的创建线程,需要调用Invoke方法来实现操作控件。
问题来了,调用Invoke()怎么就能实现操作控件了呢?我们在演示程序中的UpdatingProgress()增加了详细的记录,调试输出如下:
16:47:44.907:invokeRequiredBtn_Click 线程ID=1
16:47:44.924:BeginWorking_Invoke 线程ID=11
16:47:45.133:InvokeRequired=true 线程ID=11
16:47:45.139:Sleep2秒 线程ID=1
16:47:47.144:InvokeRequired=false 线程ID=1
16:47:47.159:after Invoke 线程ID=11
16:47:47.363:InvokeRequired=true 线程ID=11
16:47:47.371:Sleep2秒 线程ID=1
16:47:49.392:InvokeRequired=false 线程ID=1
16:47:49.407:after Invoke 线程ID=11
16:47:49.622:InvokeRequired=true 线程ID=11
16:47:49.628:Sleep2秒 线程ID=1
16:47:51.638:InvokeRequired=false 线程ID=1
16:47:51.642:after Invoke 线程ID=11
16:47:51.857:InvokeRequired=true 线程ID=11
16:47:51.863:Sleep2秒 线程ID=1
16:47:53.880:InvokeRequired=false 线程ID=1
16:47:53.888:after Invoke 线程ID=11
16:47:54.099:InvokeRequired=true 线程ID=11
16:47:54.104:Sleep2秒 线程ID=1
16:47:56.118:InvokeRequired=false 线程ID=1
16:47:56.126:after Invoke 线程ID=11
结合程序与执行日志,可以得到以下结论:
- 首先,在Invoke()方法前是线程11在执行,Invoke()内的代码就变成线程1在执行了,说明此处发生了线程切换。这也是Invoke()的核心作用:切换到UI线程(1号)来执行Invoke()内部代码。
- after Invoke日志的线程ID=11,说明Invoke()执行结束后,还是由之前的线程继续执行后续代码。
- after Invoke操作的日志时间显示是1号线程睡眠2秒后执行的,说明Invoke()执行期间,其后的代码是被阻塞的。
- 最后,通过程序总耗时来看,由于操作控件都需要切换为UI线程来执行,因此UI线程执行的代码中一旦有耗时的操作(比如本例的Sleep),将直接阻塞后续其它的操作,同时伴随着客户端程序界面的响应卡顿现象。
BackgroundWorker方案
BackgroundWorker是一个隐形的控件,这是微软封装程度较高的方案,它使用事件驱动模型。
演示程序的日志输出为:
bgWorkerBtn_Click 线程ID=1
backgroundWorker1_DoWork 线程ID=4
inputArgument=hello world
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_RunWorkerCompleted 线程ID=1
通过日志同样可以看出:
- 其中DoWork事件的处理程序(本例的backgroundWorker1_DoWork)用来执行耗时的业务操作,该部分代码由后台线程执行,而非UI线程,也正因此,backgroundWorker1_DoWork代码中就无法操作控件资源。
- BackgroundWorker的其它几个事件处理程序,如backgroundWorker1_ProgressChanged、backgroundWorker1_RunWorkerCompleted,就都由UI线程来执行,因此也就可以直接操作控件资源。
Control.CheckForIllegalCrossThreadCalls是咋回事?
官方注释:
Gets or sets a value indicating whether to catch calls on the wrong thread
that access a control's System.Windows.Forms.Control.Handle property
when an application is being debugged.
When a thread other than the creating thread of a control tries to access one of that control's methods or properties, it often leads to unpredictable results. A common invalid thread activity is a call on the wrong thread that accesses the control's Handle property. Set CheckForIllegalCrossThreadCalls to true to find and diagnose this thread activity more easily while debugging.
通俗理解:
虽然微软不建议其它线程操作控件,但是如果就这么写了,程序也能执行,比如下面的情况:
private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + ":invokeRequiredBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
new Thread(() =>
{
richTextBox1.Text = DateTime.Now.ToString();
}).Start();
}
而Control.CheckForIllegalCrossThreadCalls这个属性就是用来设置,是否完全禁止跨线程的操作控件。当设置true,上述操作就完全不能执行了。
注意:在VS中F5调试时,此值默认=true。报错效果:

双击生成的exe执行时,此值默认=false,程序还可以执行。当Control.CheckForIllegalCrossThreadCalls设置为true时,双击exe执行程序会异常退出:

建议:如果是新开发的程序,建议设置为true,可以及早的发现隐患问题,避免程序复杂后需要付出高昂的分析成本。
总结
以上是winform开发的基础中的基础,本文在系统的查阅微软文档的基础上,通过演示程序推测和验证相关的逻辑关系。
同时联想到:由于控件的更新都需要UI线程来执行,因此当遇到程序客户端程序响应卡顿/卡死的情况,通过dump分析UI线程的堆栈,应该可以有所发现。
winform中更新UI控件的方案介绍的更多相关文章
- 富客户端 wpf, Winform 多线程更新UI控件
前言 在富客户端的app中,如果在主线程中运行一些长时间的任务,那么应用程序的UI就不能正常相应.因为主线程要负责消息循环,相应鼠标等事件还有展现UI. 因此我们可以开启一个线程来格外处理需要长时间的 ...
- winform中如何在多线程中更新UI控件--ListView实时显示执行信息
1.在winform中,所有对UI的操作,都得回到UI线程(主线程)上来,才不会报错 线程间操作无效: 从不是创建控件的线程访问它. 2.在winform中,允许通过Control.invoke对控件 ...
- WinForm/Silverlight多线程编程中如何更新UI控件的值
单线程的winfom程序中,设置一个控件的值是很easy的事情,直接 this.TextBox1.value = "Hello World!";就搞定了,但是如果在一个新线程中这么 ...
- (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例
http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...
- C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)
概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常.处理跨线程更新Winform UI控件常用的方法有4种:1. ...
- C# Winform 跨线程更新UI控件常用方法总结(转)
出处:http://www.tuicool.com/articles/FNzURb 概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件 ...
- C#子线程更新UI控件的方法总结
http://blog.csdn.net/jqncc/article/details/16342121 在winform C/S程序中经常会在子线程中更新控件的情况,桌面程序UI线程是主线程,当试图从 ...
- Winform中修改WebBrowser控件User-Agent的方法(已经测试成功)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ...
- WPF中嵌入WinForm中的webbrowser控件
原文:WPF中嵌入WinForm中的webbrowser控件 使用VS2008创建WPF应用程序,需使用webbrowser.从工具箱中添加WPF组件中的webbrowser发现其中有很多属性事件不能 ...
随机推荐
- django入门 02 初探app、view、url、templates、static
创建APP命令 python manage.py startapp myapp app组成介绍 如上图,在终端中展示树状结构-- windows为 tree /f macOS为 tree 注册APP ...
- 「JSOI2018」机器人
在本题当中为了方便,我们将坐标范围改至 \((0 \sim n - 1, 0 \sim m - 1)\),行走即可视作任意一维在模意义下 \(+1\). 同时,注意到一个位置只能经过一次,则可以令 \ ...
- 「CTSC 2011」幸福路径
[「CTSC 2011」幸福路径 蚂蚁是可以无限走下去的,但是题目对于精度是有限定的,只要满足精度就行了. \({(1-1e-6)}^{2^{25}}=2.6e-15\) 考虑使用倍增的思想. 定义\ ...
- 一次线上服务高 CPU 占用优化实践 (转)
线上有一个非常繁忙的服务的 JVM 进程 CPU 经常跑到 100% 以上,下面写了一下排查的过程.通过阅读这篇文章你会了解到下面这些知识. Java 程序 CPU 占用高的排查思路 可能造成线上服务 ...
- Android 四种方法写按钮点击事件
1.匿名内部类的方式 2. 创建一个类实现onclickListener,实现onclick方法,设置控件点击事件时传一个类的对象. 3. 让当前类实现onclickListener,设置控件点击事件 ...
- 版本控制SVN
为什么需要版本控制软件 代码的冻结 避免在重大的考核之前改动代码 每个稳定版本都在服务器保存进度,随时可以回退 需求频繁的变化不要改动稳定的代码,不要改别人写好的代码 为什么需求会变化?有时候产品自己 ...
- ARC快速入门
1.ARC机制判断 iOS5以后,创建项目默认的都是ARC ARC机制下有几个明显的标志: 不允许调用对象的 release方法 不允许调用 autorelease方法 再重写父类的dealloc方法 ...
- 关于MPMoviePlayerController 缓存播放的一些技术准备
如果是视频文件,比如Mp4,avi,rmvb等可根据下面的这边文章推荐的Demo(http://code4app.com/ios/5292c381cb7e8445678b5ac2),经过测试可以进行同 ...
- FLink迟到数据的处理之三
Flink迟到的数据更新窗口计算结果,窗口销毁后的迟到数据输出到测输出流 主程序: //TODO 使用迟到的数据更新窗口的计算结果 public static void main(String[] a ...
- LeetCode随缘刷题之赎金信
欢迎评论区讨论. package leetcode.day_12_04; /** * 为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思. * * 给你一个赎金信 (ransom ...