WinForm-跨线程更新UI控件常用方法

举报
AI浩 发表于 2021/12/23 00:05:33 2021/12/23
【摘要】 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常。处理跨线程更新Winform UI控件常用的方法有4种: 通过UI线程...

C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常。处理跨线程更新Winform UI控件常用的方法有4种:

  1. 通过UI线程的SynchronizationContext的Post/Send方法更新;
  2. 通过UI控件的Invoke/BegainInvoke方法更新;
  3. 通过BackgroundWorker取代Thread执行异步操作;
  4. 通过设置窗体属性,取消线程安全检查来避免"跨线程操作异常"(非线程安全,建议不使用)。
    下文中对以上3种方法应用进行举例说明,希望能对初识C# Winform的同学们有些帮助。
    成文表分享交流之意,惶恐水平有限,文中理解和表述有错误之处还请大家多被批评指正。
  5. 通过UI线程的SynchronizationContext的Post/Send方法更新
    用法: //共分三步
    //第一步:获取UI线程同步上下文(在窗体构造函数或FormLoad事件中)
    ///
    /// UI线程的同步上下文
    ///
    SynchronizationContext m_SyncContext = null;
    public Form1()
    {
    InitializeComponent();
    //获取UI线程同步上下文
    m_SyncContext = SynchronizationContext.Current;
    //Control.CheckForIllegalCrossThreadCalls = false;
    }
    //第二步:定义线程的主体方法
    ///
    /// 线程的主体方法
    ///
    private void ThreadProcSafePost()
    {
    //…执行线程任务
    //在线程中更新UI(通过UI线程同步上下文m_SyncContext)
    m_SyncContext.Post(SetTextSafePost, “This text was set safely by SynchronizationContext-Post.”);
    //…执行线程其他任务
    }
    //第三步:定义更新UI控件的方法
    ///
    /// 更新文本框内容的方法
    ///
    ///
    private void SetTextSafePost(object text)
    {
    this.textBox1.Text = text.ToString();
    }
    //之后,启动线程
    ///
    /// 启动线程按钮事件
    ///
    ///
    ///
    private void setSafePostBtn_Click(object sender, EventArgs e)
    {
    this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafePost));
    this.demoThread.Start();
    }
    说明:三处加粗部分是关键。该方法的主要原理是:在线程执行过程中,需要更新到UI控件上的数据不再直接更新,而是通过UI线程上下文的Post/Send方法,将数据以异步/同步消息的形式发送到UI线程的消息队列;UI线程收到该消息后,根据消息是异步消息还是同步消息来决定通过异步/同步的方式调用SetTextSafePost方法直接更新自己的控件了。
    在本质上,向UI线程发送的消息并是不简单数据,而是一条委托调用命令。
    //在线程中更新UI(通过UI线程同步上下文m_SyncContext)
    m_SyncContext.Post(SetTextSafePost, “This text was set safely by SynchronizationContext-Post.”);
    可以这样解读这行代码:向UI线程的同步上下文(m_SyncContext)中提交一个异步消息(UI线程,你收到消息后以异步的方式执行委托,调用方法SetTextSafePost,参数是“this text was …”).
    2.通过UI控件的Invoke/BegainInvoke方法更新
    用法:与方法1类似,可分为三个步骤。
    // 共分三步
    // 第一步:定义委托类型
    // 将text更新的界面控件的委托类型
    delegate void SetTextCallback(string text);
    //第二步:定义线程的主体方法
    ///
    /// 线程的主体方法
    ///
    private void ThreadProcSafe()
    {
    //…执行线程任务
    //在线程中更新UI(通过控件的.Invoke方法)
    this.SetText(“This text was set safely.”);
    //…执行线程其他任务
    }
    //第三步:定义更新UI控件的方法
    ///
    /// 更新文本框内容的方法
    ///
    ///
    private void SetText(string text)
    {
    // InvokeRequired required compares the thread ID of the
    // calling thread to the thread ID of the creating thread.
    // If these threads are different, it returns true.
    if (this.textBox1.InvokeRequired)//如果调用控件的线程和创建创建控件的线程不是同一个则为True
    {
    while (!this.textBox1.IsHandleCreated)
    {
    //解决窗体关闭时出现“访问已释放句柄“的异常
    if (this.textBox1.Disposing || this.textBox1.IsDisposed)
    return;
    }
    SetTextCallback d = new SetTextCallback(SetText);
    this.textBox1.Invoke(d, new object[] { text });
    }
    else
    {
    this.textBox1.Text = text;
    }
    }
    //之后,启动线程
    ///
    /// 启动线程按钮事件
    ///
    ///
    ///
    private void setTextSafeBtn_Click(
    object sender,
    EventArgs e)
    {
    this.demoThread =
    new Thread(new ThreadStart(this.ThreadProcSafe));
    this.demoThread.Start();
    }
    说明:这个方法是目前跨线程更新UI使用的主流方法,使用控件的Invoke/BegainInvoke方法,将委托转到UI线程上调用,实现线程安全的更新。原理与方法1类似,本质上还是把线程中要提交的消息,通过控件句柄调用委托交到UI线程中去处理。
    Control.InvokeRequired属性获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用Invoke方法,因为调用方位于创建控件所在的线程以外的线程中。如果控件的handle是在与调用线程不同的线程中创建的(说明您必须通过invoke方法对控件进行调用),则为true,否者为false。
    Windows窗体中的控件被绑定到特定的线程,不具备线程安全性。因此,如果从另一个线程调用控件的方法,那么必须使用控件的一个invoke方法来将调用封送到适当的线程。该属性可用于确定是否必须调用invoke方法,当不知道什么线程拥有控件时这很有用。
    3.通过BackgroundWorker取代Thread执行异步操作
    用法: //共分三步
    //第一步:定义BackgroundWorker对象,并注册事件(执行线程主体、执行UI更新事件)
    private BackgroundWorker backgroundWorker1 =null;
    public Form1()
    {
    InitializeComponent();
    backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
    //设置报告进度更新
    backgroundWorker1.WorkerReportsProgress = true;
    //注册线程主体方法
    backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
    //注册更新UI方法
    backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
    //backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
    }
    //第二步:定义执行线程主体事件
    //线程主体方法
    public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
    //…执行线程任务
    //在线程中更新UI(通过ReportProgress方法)
    backgroundWorker1.ReportProgress(50, “This text was set safely by BackgroundWorker.”);
    //…执行线程其他任务
    }
    //第三步:定义执行UI更新事件
    //UI更新方法
    public void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
    this.textBox1.Text = e.UserState.ToString();
    }
    //之后,启动线程
    //启动backgroundWorker
    private void setTextBackgroundWorkerBtn_Click(object sender, EventArgs e)
    {
    this.backgroundWorker1.RunWorkerAsync();
    }
    说明:C# Winform中执行异步任务时,BackgroundWorker是个不错的选择。它是EAP(Event based Asynchronous Pattern)思想的产物,DoWork用来执行异步任务,在任务执行过程中/执行完成后,我们可以通过ProgressChanged,ProgressCompleteded事件进行线程安全的UI更新。
    需要注意的是://设置报告进度更新
    backgroundWorker1.WorkerReportsProgress = true;
    默认情况下BackgroundWorker是不报告进度的,需要显示设置报告进度属性。
  6. 通过设置窗体属性,取消线程安全检查来避免"线程间操作无效异常"
    用法:将Control类的静态属性CheckForIllegalCrossThreadCalls为false。
    public Form1()
    {
    InitializeComponent();
    //指定不再捕获对错误线程的调用
    Control.CheckForIllegalCrossThreadCalls = false;
    }
    说明:通过设置CheckForIllegalCrossThreadCalls属性,可以指示是否捕获线程间非安全操作异常。该属性值默认为ture,即线程间非安全操作是要捕获异常的("线程间操作无效"异常)。通过设置该属性为false简单的屏蔽了该异常。Control.CheckForIllegalCrossThreadCalls的注释如下
    //
    // 摘要:
    // 获取或设置一个值,该值指示是否捕获对错误线程的调用,这些调用在调试应用程序时访问控件的 System.Windows.Forms.Control.Handle
    // 属性。
    //
    // 返回结果:
    // 如果捕获了对错误线程的调用,则为 true;否则为 false。
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(“ControlCheckForIllegalCrossThreadCalls”)]
    [Browsable(false)]
    public static bool CheckForIllegalCrossThreadCalls { get; set; }

跨线程调用Windows窗体控件的另一示例
在这里插入图片描述
如对线程的操作不正确,在跨线程调用Windows窗体控件时会有产生InvalidOperationException异常。
该异常提示[线程间操作无效: 从不是创建控件“listBox1”的线程访问它.]。
我相信很多人通过设置Control.CheckForIllegalCrossThreadCalls属性为false禁止捕获对错误线程的调用。
这种强制性的禁止捕获不是人性化的选项。
我们可以通过控件的Invoke方法来实现跨线程调用Windows窗体控件。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace VJSDN.Tech.ThreadAccess
{
//跨线程调用的方法委托原型。
public delegate void ShowMessageMethod(string msg);
//跨线程调用的方法委托原型。
public delegate void CreateControlMethod();
//注:A线程:主线程. 主线程是指当前窗体所在的线程.
// B线程:用户创建的线程。
// C线程:带参数的线程。
public partial class Form1 : Form
{
private Thread _threadB = null; //第2个线程.创建控件。
private Thread _threadC = null; //第3个线程.带参数的线程。
private Button _btnOnB = null; //由第2个线程创建的按钮控件。
private ListBox _listBoxOnB = null;//由第2个线程创建的ListBox控件。
private Panel _PanelOnB = null;//由第2个线程创建的Panel控件。
public Form1()
{
InitializeComponent();
//是否捕获对错误线程的调用,这些调用访问控件的 System.Windows.Forms.Control.Handle 属性。
//Control.CheckForIllegalCrossThreadCalls = false;
}
private void btnCreateThreadB_Click(object sender, EventArgs e)
{
//在主线程内创建线程B.
_threadB = new Thread(new ThreadStart(MethodThreadB)); //启动线程。
_threadB.Start();
}
private void createC_Click(object sender, EventArgs e)
{
//在主线程内创建带参数的线程C.
_threadC = new Thread(new ParameterizedThreadStart(MethodThreadC));
_threadC.Start(100); //往线程C传送参数100,计算100以内的数字加总。
}
//C线程正在运行…
private void MethodThreadC(object param)
{
int total = int.Parse(param.ToString());
this.Invoke(new ShowMessageMethod(this.ShowMessage), “线程C正在计数:1+2+n=?(n<=” + param.ToString() + “)”);
int result = 0; //计数器
for (int i = 1; i <= total; i++) result += i;
this.Invoke(new ShowMessageMethod(this.ShowMessage), “线程C计算结果:” + result.ToString());
}
//B线程正在运行…
private void MethodThreadB()
{
//跨线程操作:试图在B线程内给A线程的panel2控件插入子控件.
this.Invoke(new CreateControlMethod(this.CreatePanelOnThreadB)); //创建Panel
this.Invoke(new CreateControlMethod(this.CreateButtonOnThreadB)); //创建按钮
this.Invoke(new CreateControlMethod(this.CreateListBoxOnThreadB)); //创建ListBox
this.Invoke(new ShowMessageMethod(this.ShowMessage), “线程B操作线程A内的ListBox控件”);
this.Invoke(new ShowMessageMethod(this.ShowMessage), “如能显示消息,表示操作成功!”);
}
//注意:这个方法与CreateControlMethod委托原型相同。
private void CreateControlCross(Label lbl)
{
_PanelOnB.Controls.Add(lbl);
}
//注意:这个方法与ShowMessageMethod委托原型相同。
private void ShowMessage(string msg)
{
this.listBox1.Items.Add(msg);
}
private void CreatePanelOnThreadB()
{
_PanelOnB = new Panel();
_PanelOnB.BackColor = System.Drawing.Color.Silver;
_PanelOnB.Location = new System.Drawing.Point(264, 12);
_PanelOnB.Name = “panel2”;
_PanelOnB.Size = new System.Drawing.Size(244, 355);
_PanelOnB.TabIndex = 1;
this.Controls.Add(_PanelOnB); //创建Panel容器
Label _lblB = new Label();
_lblB.AutoSize = true;
_lblB.Font = new System.Drawing.Font(“宋体”, 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
_lblB.Location = new System.Drawing.Point(3, 7);
_lblB.Name = “label1”;
_lblB.Size = new System.Drawing.Size(84, 12);
_lblB.TabIndex = 0;
_lblB.Text = “线程B,用户创建的线程”;
_PanelOnB.Controls.Add(_lblB); //Panel容器内加入一个label
}
private void CreateButtonOnThreadB()
{
_btnOnB = new Button();
_btnOnB.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
_btnOnB.Location = new System.Drawing.Point(5, 63);
_btnOnB.Name = “btnSendToA”;
_btnOnB.Size = new System.Drawing.Size(167, 23);
_btnOnB.TabIndex = 4;
_btnOnB.Text = “给线程A创建的控件发消息”;
_btnOnB.UseVisualStyleBackColor = true;
_btnOnB.Click += new System.EventHandler(this.btnSendToA_Click);
//panel2是主线程上创建的控件。
_PanelOnB.Controls.Add(_btnOnB);
}
private void CreateListBoxOnThreadB()
{
_listBoxOnB = new ListBox();
_listBoxOnB.FormattingEnabled = true;
_listBoxOnB.ItemHeight = 12;
_listBoxOnB.Location = new System.Drawing.Point(5, 240);
_listBoxOnB.Name = “listBox2”;
_listBoxOnB.Size = new System.Drawing.Size(236, 112);
_listBoxOnB.TabIndex = 2;
//panel2是主线程上创建的控件。
_PanelOnB.Controls.Add(_listBoxOnB);
}
private void btnSendToA_Click(object sender, EventArgs e)
{
listBox1.Items.Add(“线程B发消息给A”);
}
private void btnSendToB_Click(object sender, EventArgs e)
{
if (_listBoxOnB != null)
_listBoxOnB.Items.Add(“线程A发消息给B”);
else
MessageBox.Show(“线程B还没创建呢!”);
}
}
}

文章来源: wanghao.blog.csdn.net,作者:AI浩,版权归原作者所有,如需转载,请联系作者。

原文链接:wanghao.blog.csdn.net/article/details/105266097

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。