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

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

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


通过UI线程的SynchronizationContext的Post/Send方法更新;

通过UI控件的Invoke/BegainInvoke方法更新;

通过BackgroundWorker取代Thread执行异步操作;

通过设置窗体属性,取消线程安全检查来避免"跨线程操作异常"(非线程安全,建议不使用)。

下文中对以上3种方法应用进行举例说明,希望能对初识C# Winform的同学们有些帮助。

成文表分享交流之意,惶恐水平有限,文中理解和表述有错误之处还请大家多被批评指正。

通过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是不报告进度的,需要显示设置报告进度属性。

通过设置窗体属性,取消线程安全检查来避免"线程间操作无效异常"

用法:将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窗体控件的另一示例


tt.png

如对线程的操作不正确,在跨线程调用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还没创建呢!”);

}

}

}



目录
相关文章
|
1月前
|
存储 Oracle Java
|
3月前
|
Java
创建线程的方法
Java中实现多线程有四种方式:1. 继承Thread类,简单但占用继承机会,耦合度高;2. 实现Runnable接口,推荐方式,任务与线程解耦,支持Lambda;3. 实现Callable接口配合FutureTask,可获取返回值和异常;4. 使用线程池(ExecutorService),企业推荐,管理线程生命周期,提升性能,支持多种线程池类型。
76 1
|
4月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
277 5
|
10月前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
9296 5
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
10月前
|
前端开发 Linux C#
一款开源、免费、美观的 Avalonia UI 原生控件库 - Semi Avalonia
一款开源、免费、美观的 Avalonia UI 原生控件库 - Semi Avalonia
852 10
|
10月前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
10月前
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
5月前
|
开发框架 前端开发 JavaScript
【HarmonyOS Next之旅】基于ArkTS开发(二) -> UI开发一
本文介绍了方舟开发框架(ArkUI)及其两种开发范式:基于ArkTS的声明式开发范式和类Web开发范式。ArkUI是用于构建HarmonyOS应用界面的UI框架,提供极简UI语法和基础设施。声明式开发范式使用ArkTS语言,以组件、动画和状态管理为核心,适合复杂团队协作;类Web开发范式采用HML、CSS、JavaScript三段式开发,适用于简单界面应用,贴近Web开发者习惯。文中还概述了两者的架构和基础能力,帮助开发者选择合适的范式进行高效开发。
184 15
|
5月前
|
编解码 前端开发 Java
【HarmonyOS Next之旅】基于ArkTS开发(二) -> UI开发三
本文介绍了基于声明式UI范式的图形绘制与动画效果实现方法,涵盖绘制图形、添加动画效果及常见组件说明三部分内容。在绘制图形部分,详细讲解了如何通过Circle组件为食物成分表添加圆形标签,以及使用Path组件结合SVG命令绘制自定义图形(如应用Logo)。动画效果部分则展示了如何利用animateTo实现闪屏动画,包括渐出、放大效果,并设置页面跳转;同时介绍了页面间共享元素转场动画的实现方式。最后,文章列举了声明式开发范式中的各类组件及其功能,帮助开发者快速上手构建复杂交互页面。
174 11
|
1月前
|
存储 开发者 容器
鸿蒙 HarmonyOS NEXT星河版APP应用开发-ArkTS面向对象及组件化UI开发使用实例
本文介绍了ArkTS语言中的Class类、泛型、接口、模块化、自定义组件及状态管理等核心概念,并结合代码示例讲解了对象属性、构造方法、继承、静态成员、访问修饰符等内容,同时涵盖了路由管理、生命周期和Stage模型等应用开发关键知识点。
185 1
鸿蒙 HarmonyOS NEXT星河版APP应用开发-ArkTS面向对象及组件化UI开发使用实例

热门文章

最新文章