zoukankan      html  css  js  c++  java
  • 拒绝卡顿——在WPF中使用多线程更新UI

    有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例:

        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.Dispatcher.Invoke(new Action(()=> { }));
                this.Loaded += MainWindow_Loaded;
            }

            private void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                this.Content = new UserControl1();
            }
        }

        class UserControl1 : UserControl
        {
            TextBlock textBlock;

            public UserControl1()
            {
                textBlock = new TextBlock();
                this.Content = textBlock;

                this.Dispatcher.BeginInvoke(new Action(updateTime), null);
            }

            private async void updateTime()
            {
                while (true)
                {
                    Thread.Sleep(900);            //
    模拟耗时操作

                    textBlock.Text = DateTime.Now.ToString();
                    await Task.Delay(100);
                }
            }
        }

    当我们运行这个程序的时候,就会发现:由于主线程大部分的时间片被占用,无法及时处理系统事件(如鼠标,键盘等输入),导致程序变得非常卡顿,连拖动窗口都变得不流畅;

    如何解决这个问题呢,初学者可能想到的第一个方法就是新启一个线程,在线程中执行更新:

        public UserControl1()
        {
            textBlock = new TextBlock();
            this.Content = textBlock;

            
    ThreadPool.QueueUserWorkItem(_ => updateTime());
        }

    但很快就会发现此路不通,因为WPF不允许跨线程访问程序,此时我们会得到一个:"The calling thread cannot access this object because a different thread owns it."的InvalidOperationException异常

        

    那么该如何解决这一问题呢?通常的做法是把耗时的函数放在线程池执行,然后切回主线程更新UI显示。前面的updateTime函数改写如下:

        private async void updateTime()
        {
            while (true)
            {
                
    await Task.Run(() => Thread.Sleep(900));
                textBlock.Text = DateTime.Now.ToString();
                await Task.Delay(100);
            }
        }

    这种方式能满足我们的大部分需求。但是,有的操作是比较耗时间的。例如,在多窗口实时监控的时候,我们就需要同时多十来个屏幕每秒钟各进行几十次的刷新,更新图像这个操作必须在UI线程上进行,并且它有非常耗时间,此时又会回到最开始的卡顿的情况。

    看起来这个问题无法解决,实际上,WPF只是不允许跨线程访问程序,并非不允许多线程更新界面。我们大可以对每个视频监控窗口单独其一个独立的线程,在那个线程中进行更新操作,此时就不会影响到主线程。MSDN上有篇文章介绍了详细的操作:Multithreaded UI: HostVisual。用这种方式将原来的程序改写如下:

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            HostVisual hostVisual = new HostVisual();

            UIElement content = new VisualHost(hostVisual);
            this.Content = content;

            Thread thread = new Thread(new ThreadStart(() =>
            {
                VisualTarget visualTarget = new VisualTarget(hostVisual);
                var control = new UserControl1();
                control.Arrange(new Rect(new Point(), content.RenderSize));
                visualTarget.RootVisual = control;

                System.Windows.Threading.Dispatcher.Run();

            }));

            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
        }

        public class VisualHost : FrameworkElement
        {
            Visual child;

            public VisualHost(Visual child)
            {
                if (child == null)
                    throw new ArgumentException("child");

                this.child = child;
                AddVisualChild(child);
            }

            protected override Visual GetVisualChild(int index)
            {
                return (index == 0) ? child : null;
            }

            protected override int VisualChildrenCount
            {
                get { return 1; }
            }
        }

    这个里面用来了两个新的类:HostVisual、VisualTarget。以及自己写的一个VisualHost。MSDN上相关的解释,也不算难理解,这里就不多介绍了。最后,再来重构一下代码,把在新线程中创建控件的方式改写如下:

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            createChildInNewThread<UserControl1>(this);
        }

        void createChildInNewThread<T>(ContentControl container)
            where T : UIElement , new()
        {
            HostVisual hostVisual = new HostVisual();

            UIElement content = new VisualHost(hostVisual);
            container.Content = content;

            Thread thread = new Thread(new ThreadStart(() =>
            {
                VisualTarget visualTarget = new VisualTarget(hostVisual);

                var control = new T();
                control.Arrange(new Rect(new Point(), content.RenderSize));

                visualTarget.RootVisual = control;
                System.Windows.Threading.Dispatcher.Run();

            }));

            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
        }

    当然,我这个函数多了一些不必要的的限制:容器必须是ContentControl,子元素必须是UIElement。可以根据实际需要进行相关修改。这里有一个完整的示例,也可以参考一下。

  • 相关阅读:
    Python3练习题 026:求100以内的素数
    【Python3练习题 025】 一个数,判断它是不是回文数。即12321是回文数,个位与万位相同,十位与千位相同
    Python3练习题 022:用递归函数反转字符串
    Python3练习题 021:递归方法求阶乘
    【Python3练习题 020】 求1+2!+3!+...+20!的和
    【Python3练习题 019】 有一分数序列:2/1,3/2,5/3,8/5,13/8,21/13...求出这个数列的前20项之和。
    Python3练习题 018:打印星号菱形
    Python3练习题 006 冒泡排序
    【Python3练习题 017】 两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比。请编程序找出三队赛手的名单。
    【Python3练习题 016】 猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少。
  • 原文地址:https://www.cnblogs.com/TianFang/p/3969430.html
Copyright © 2011-2022 走看看