.NET一个线程更新另一个线程的UI(两种实现方法及若干简化)

简介: 原文:.NET一个线程更新另一个线程的UI(两种实现方法及若干简化)本片博文接上一篇:.NET多线程执行函数,给出实现一个线程更新另一个线程UI的两种方法。 Winform中的控件是绑定到特定的线程的(一般是主线程),这意味着从另一个线程更新主线程的控件不能直接调用该控件的成员。
原文: .NET一个线程更新另一个线程的UI(两种实现方法及若干简化)

本片博文接上一篇:.NET多线程执行函数给出实现一个线程更新另一个线程UI的两种方法。

Winform中的控件是绑定到特定的线程的(一般是主线程),这意味着从另一个线程更新主线程的控件不能直接调用该控件的成员。

控件绑定到特定的线程这个概念如下:

为了从另一个线程更新主线程的Windows Form控件,可用的方法有:

首先用一个简单的程序来示例,这个程序的功能是:在Winfrom窗体上,通过多线程用label显示时间。给出下面的两种实现方式

1.结合使用特定控件的如下成员                                            

InvokeRequired属性:返回一个bool值,指示调用者在不同的线程上调用控件时是否必须使用Invoke()方法。如果主调线程不是创建该控件的线程,或者还没有为控件创建窗口句柄,则返回true。

Invoke()方法:在拥有控件的底层窗口句柄的线程上执行委托。

BeginInvoke()方法:异步调用Invoke()方法。

EndInvoke()方法:获取BeginInvoke()方法启动的异步操作返回值。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace 一个线程更新另一个线程UI2
{
    /// <summary>
    /// DebugLZQ
    /// http://www.cnblogs.com/DebugLZQ
    /// </summary>
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void UpdateLabel(Control ctrl, string s)
        {
            ctrl.Text = s;
        }
        private delegate void UpdateLabelDelegate(Control ctrl, string s);

        private void PrintTime()
        {
            if (label1.InvokeRequired == true)
            {
                UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel);
                while(true)
                {
                    label1.Invoke(uld, new object[] { label1, DateTime.Now.ToString() });
                }
            }
            else
            {
                while (true)
                {
                    label1.Text = DateTime.Now.ToString();
                }
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //PrintTime();//错误的单线程调用

            Thread t = new Thread(new ThreadStart(PrintTime));
            t.Start();
        }
    }
}

比较和BackgroundWorker控件方式的异同点。

2.使用BackgroundWorker控件。                               

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace 一个线程更新另一个线程UI
{
    /// <summary>
    /// DebugLZQ
    /// http://www.cnblogs.com/DebugLZQ
    /// </summary>
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void UpdateLabel(Control ctrl, string s)
        {
            ctrl.Text = s;
        }

        private delegate void UpdateLabelDelegate(Control ctrl, string s);

        private void PrintTime()
        {
            if (label1.InvokeRequired == true)
            {
                UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel);
                while (true)
                {
                    label1.Invoke(uld, new object[] { label1, DateTime.Now.ToString() });
                }
            }
            else
            {
                while (true)
                {
                    label1.Text = DateTime.Now.ToString();
                }
            }
        }

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            PrintTime();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            backgroundWorker1.RunWorkerAsync();
        }
    }
}

程序的运行结果如下:

--------------------

Update:请参考后续博文:WPF: Cancel an Command using BackgroundWorker

--------------------

更新另一个线程的进度条示例(第一种方法实现)                             

DebugLZQ觉得第一种方法要更直观一点,或是更容易理解一点。下面再用第一种方法来做一个Demo:输入一个数,多线程计算其和值更新界面上的Label,并用进度条显示计算的进度。实际上就是,更新另一个线程的两个UI控件。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace 一个线程更新另一个线程的UI3
{
    /// <summary>
    /// DebugLZQ
    /// http://www.cnblogs.com/DebugLZQ
    /// </summary>
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }


        private static long result = 0;
        

        //更新Label
        private void UpdateLabel(Control ctrl, string s)
        {
            ctrl.Text = s;
        }

        private delegate void UpdateLabelDelegate(Control ctrl, string s);

        //更新ProgressBar
        private void UpdateProgressBar(ProgressBar ctrl, int n)
        {
            ctrl.Value = n;
        }

        private delegate void UpdateProgressBarDelegate(ProgressBar ctrl, int n);


        private void Sum(object o)
        {
            result = 0;
            
            long num = Convert.ToInt64(o);

            UpdateProgressBarDelegate upd = new UpdateProgressBarDelegate(UpdateProgressBar);

            for (long i = 1; i <= num; i++)
            {
                result += i;
                //更新ProcessBar1
                if (i % 10000 == 0)//这个数值要选的合适,太小程序会卡死
                {
                    if (progressBar1.InvokeRequired == true)
                    {
                        progressBar1.Invoke(upd, new object[] { progressBar1, Convert.ToInt32((100 * i) / num) });//若是(i/num)*100,为什么进度条会卡滞?
                    }
                    else
                    {
                        progressBar1.Value = Convert.ToInt32(i / num * 100);
                    }                    
                }
                
            }            

            //更新lblResult
            if (lblResult.InvokeRequired == true)
            {
                UpdateLabelDelegate uld = new UpdateLabelDelegate(UpdateLabel);
                lblResult.Invoke(uld, new object[] { lblResult, result.ToString() });
            }
            else
            {
                lblResult.Text = result.ToString();
            }            

        }
        
        private void btnStart_Click(object sender, EventArgs e)
        {
            Thread t = new Thread(new ParameterizedThreadStart(Sum));
            t.Start(txtNum.Text);
        }
   

    }
}

程序的运行结果如下: 

 用BackgroundWorker控件可以实现相同的功能,个人觉得这样更容易理解~

 第一种方法的若干简化            

和异步方法调用一样,我们可以使用delegate、匿名方法、Action/Function等系统提供委托、Lambda表达式等进行简化。

如,第一种方法,更新界面时间,我们可以简化如下:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class FormActionFunction : Form
    {
        public FormActionFunction()
        {
            InitializeComponent();
        }

        private void UpdateLabel()
        {
            label1.Text = DateTime.Now.ToString();
            label2.Text = DateTime.Now.ToString();
        }

        private void PrintTime()
        {
            while (true)
            {
                PrintTime(UpdateLabel);
            }
        }

        private void PrintTime(Action action)
        {
            if (InvokeRequired)
            {
                Invoke(action);
            }
            else
            {
                action();
            }
        }


        private void FormActionFunction_Load(object sender, EventArgs e)
        {
            Thread t = new Thread(new ThreadStart(PrintTime));
            t.IsBackground = true;
            t.Start();
        }
    }
}

也可以再简化:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class FormActionFunction : Form
    {
        public FormActionFunction()
        {
            InitializeComponent();
        }      

        private void PrintTime()
        {
            while (true)
            {
                PrintTime(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); });//Lambda简写               
            }
        }

        private void PrintTime(Action action)
        {
            if (InvokeRequired)
            {
                Invoke(action);
            }
            else
            {
                action();
            }
        }


        private void FormActionFunction_Load(object sender, EventArgs e)
        {
            Thread t = new Thread(new ThreadStart(PrintTime));
            t.IsBackground = true;
            t.Start();
        }
    }
}

 进一步简化:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class FormBestPractice : Form
    {
        public FormBestPractice()
        {
            InitializeComponent();
        }

        private void PrintTime()
        {
            while (true)
            {
                Invoke(new Action(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); }));
            }
        }


        private void FormBestPractice_Load(object sender, EventArgs e)
        {
            Thread t = new Thread(new ThreadStart(PrintTime));
            t.IsBackground = true;
            t.Start();
        }
    }
}

 再进一步简化:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class FormBestPractice2 : Form
    {
        public FormBestPractice2()
        {
            InitializeComponent();
        }     

        private void FormBestPractice2_Load(object sender, EventArgs e)
        {
            Thread t = new Thread(new ThreadStart(() => {
                while (true)
                {
                    Invoke(new Action(() => { label1.Text = DateTime.Now.ToString(); label2.Text = DateTime.Now.ToString(); }));
                }            
            }));
            t.IsBackground = true;
            t.Start();
        }
    
    }
}

可根据代码风格要求,去掉  new ThreadStart()、new Action(),程序也可以正常运行,但是这样DebugLZQ不推荐这样,因为多线程调用方法参数不够清晰,可参考DebugLZQ关于多线程执行函数的博文。

根据个人编码习惯,可选择合适的编码方法。以上代码由DebugLZQ编写,可正常运行,就不附上界面截图了。

-----------------

说明:以上所有更新方法均默认为同步方法。

若需要异步执行,则把Invoke换成BeginInvoke即可,其优点是不会阻塞当前线程。

============================================

若是WPF程序,则只要把在Winform中用于更新拥有控件的线程使用的Invoke方法换成

Dispatcher.Invoke

即可。也给出一个Demo:

 MainWindow.xaml, MainWindow.xaml.cs如下:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <Button Content="Start" Click="ButtonBase_OnClick" Height="40" Margin="40"/>
            <Grid>
                <ProgressBar x:Name="ProgressBar1" Height="60"/>
                <TextBlock x:Name="TextBlock1" FontSize="40" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            </Grid>
        </StackPanel>
    </Grid>
</Window>
View Code
using System;
using System.Threading;
using System.Windows;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            ProgressBar1.Minimum = 0;
            ProgressBar1.Maximum = 100;

            Thread t = new Thread(() =>
            {
                for (int i = 0; i <= 100 * 1000; i++)
                {
                    Dispatcher.Invoke(() =>

                    {
                        if (i % 1000 == 0)
                        {
                            ProgressBar1.Value = i / 1000;
                            TextBlock1.Text = i/1000 + "%";
                        }
                    });
                }
            });
            t.IsBackground = true;
            t.Start();
        }
    }
}
View Code

效果如下:

若不想阻塞当前线程(注意:是当前线程,非UI线程,Dispatcher.BeginInvoke可能会阻塞UI线程,因为其做的事情是:将执行扔给UI线程去执行,立即返回当前线程~其不管UI线程死活),则可使用异步Invoke:

Dispatcher.BeginInvoke

.NET Framework 4.5 提供了新的异步模式Async,因此开发平台若为.NET 4.5,也可以使用:

Dispatcher.InvokeAsync

详细请参考DebugLZQ后续博文:WPF: Updating the UI from a non-UI thread

 

或是直接对同步方法进行异步封装,请参考DebugLZQ后续相关博文:从C#5.0说起:再次总结C#异步调用方法发展史

 

小结

无论WPF还是WinForm,UI都是由单个线程负责更新~

Invoke解决的问题是:非UI线程无法更新UI线程的问题. 其实现方法是将方法扔给UI线程执行(To UI Thread),Invoke等待UI线程执行完成才返回;BeginInvoke不等待UI线程执行完立刻返回~

Invoke/BeginInvoke可能会阻塞UI线程.(Invoke/BeginInvoke的区别是:是否等待UI线程执行完才返回当前线程继续执行~)

不阻塞UI线程:其解决方法是将耗时的非更新UI的操作操作放到后台线程里去,与Invoke/BeginInvoke没有半毛钱关系~也就是:别给UI线程太大压力!

 

希望对你有帮助~

目录
相关文章
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
26天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
16 1
|
26天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
26天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
24 1
|
26天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1
|
26天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
25 1
|
30天前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
19 3
|
30天前
|
监控 Java
捕获线程执行异常的多种方法
【10月更文挑战第15天】捕获线程执行异常的方法多种多样,每种方法都有其特点和适用场景。在实际开发中,需要根据具体情况选择合适的方法或结合多种方法来实现全面有效的线程异常捕获。这有助于提高程序的健壮性和稳定性,减少因线程异常带来的潜在风险。
21 1