zoukankan      html  css  js  c++  java
  • WPF多线程UI更新——两种方法

    转载:https://www.cnblogs.com/Jarvin/p/3756061.html

    WPF多线程UI更新——两种方法

    前言

      在WPF中,在使用多线程在后台进行计算限制的异步操作的时候,如果在后台线程中对UI进行了修改,则会出现一个错误:(调用线程无法访问此对象,因为另一个线程拥有该对象。)这是很常见的一个错误,一不小心就会有这个现象。在WPF中,如果不是用多线程的话,例如单线程应用程序,就是说代码一路过去都在GUI线程运行,可以随意更新任何东西,包括UI对象。但是使用多线程来更新UI就可能会出现以上所说问题,怎么解决?本文章提供两个方法:Dispatcher(大部分人使用),TaskScheduler(任务调度器)。

    问题再现

      可能有的WPF新手不懂这是什么情况,先来个问题的再现,再使用本文章的两个方法进行解决。

      为了演示方便,我使用了最简单的布局,一个开始按钮,三个TextBlock。按一下开始按钮,开一个后台线程随机得到一个数字,并且更新第一个TextBlock。再开另外一个后台线程得到另外一个数字,更新第二个TextBlock。第三个TextBlock处理同理。

      XAML代码:

    复制代码
    <Window x:Class="UpdateUIDemo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="130" Width="363">
        <Canvas>
            <TextBlock Width="40" Canvas.Left="38" Canvas.Top="27" Height="29" x:Name="first" Background="Black" Foreground="White"></TextBlock>
            <TextBlock Width="40" Canvas.Left="128" Canvas.Top="27" Height="29" x:Name="second" Background="Black" Foreground="White"></TextBlock>
            <TextBlock Width="40" Canvas.Left="211" Canvas.Top="27" Height="29" x:Name="Three" Background="Black" Foreground="White"></TextBlock>
            <Button Height="21" Width="50" Canvas.Left="271" Canvas.Top="58" Content="开始" Click="Button_Click"></Button>
        </Canvas>
    </Window>
    复制代码

      后台代码:

    复制代码
    public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                Task.Factory.StartNew(Work);
            }
    
            private void Work()
            {
                Task task = new Task((tb) => Begin(this.first), this.first);
                Task task2 = new Task((tb) => Begin(this.second), this.first);
                Task task3 = new Task((tb) => Begin(this.Three), this.first);
                task.Start();
                task.Wait();
                task2.Start();
                task2.Wait();
                task3.Start();
            }
            private void Begin(TextBlock tb)
            {
                int i=100000000;
                while (i>0)
                {
                    i--;
                }
                Random random = new Random();
                String Num = random.Next(0, 100).ToString();
                tb.Text = Num;
            }
        }
    复制代码

        运行一下,在点击开始按钮的时候,得到了一个错误信息:

      果然不出所料,Begin函数是在后台线程执行的,tb这个TextBlock是前台UI线程的对象,所以无法在后台线程改变UI线程拥有的对象,很多有点经验的WPF程序员就会使用下面我要说的Dispatcher了!

    问题解决

      方法一:Dispatcher

        1.把UI更新的代码放到一个函数中:

    private void UpdateTb(TextBlock tb, string text)
            {
                tb.Text = text;
            }

        2.使用Dispatcher,大家看修改后的Begin函数(红色内容):

    复制代码
    private void Begin(TextBlock tb)
            {
                int i=100000000;
                while (i>0)
                {
                    i--;
                }
                Random random = new Random();
                String Num = random.Next(0, 100).ToString();
                Action<TextBlock, String> updateAction = new Action<TextBlock, string>(UpdateTb);
                tb.Dispatcher.BeginInvoke(updateAction,tb,Num);
            }
    复制代码

      再运行一次程序,可以看到能正常显示了,并且不会出现假死现象。

      方法二:任务调度器(TaskScheduler)

        有很多任务调度器,在CLR Var C#中就提出了线程池任务调度器,I/O任务调度器,任务限时调度器等,调度器的职责就是负责任务的调度,调节任务执行。同步上下文任务调度器就是该方法二所使用的调度器,其作用是将所有任务都调度给应用程序的GUI线程。

    复制代码
    public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
            private readonly TaskScheduler _syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                Task.Factory.StartNew(SchedulerWork);
            }
            private void SchedulerWork()
            {
                Task.Factory.StartNew(Begin, this.first).Wait();
                Task.Factory.StartNew(Begin, this.second).Wait();
                Task.Factory.StartNew(Begin, this.Three).Wait();
            }
    
            private void Begin(object obj)
            {
                TextBlock tb = obj as TextBlock;
                int i = 100000000;
                while (i>0)
                {
                    i--;
                }
                Random random = new Random();
                String Num = random.Next(0,100).ToString();
                Task.Factory.StartNew(() => UpdateTb(tb, Num),
                        new CancellationTokenSource().Token, TaskCreationOptions.None, _syncContextTaskScheduler).Wait();
            }
            private void UpdateTb(TextBlock tb, string text)
            {
                tb.Text = text;
            }
        }
    复制代码

       结果展示:

        

    总结

      任务调度器还有很多种,按照自己喜欢的方法来实现后台多线程更新UI。还有任务调度器也可以应用到Winform中。下面提供示例Demo下载。

                                       完整Demo下载

  • 相关阅读:
    2015.2.27 UltraEdit中显示XML结构
    2015.1.31 DataGridView自动滚动到某行
    2015.1.15 利用函数实现将一行记录拆分成多行记录 (多年想要的效果)
    2015.1.15 利用Oracle函数返回表结果 重大技术进步!
    2015.1.15 利用Oracle函数插入表结构 Bulk collect into 不用循环,简洁高效
    2015.1.8 Left join 左连接
    2015.1.10 解决DataGridView SelectionChanged事件自动触发问题
    delphi 遍历窗口
    delphi 访问 protected 属性 哈哈
    clientdataset 读取excel 如果excel 文件不存在的时候 相应的gird 会不显示数据, 鼠标掠过 gird 格子 才会显示数据。 这是一个bug 哈哈
  • 原文地址:https://www.cnblogs.com/Jeely/p/11712011.html
Copyright © 2011-2022 走看看