在WPF中使用多线程更新UI

 更新时间:2022年06月22日 15:28:39   作者:天方  
这篇文章介绍了在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。可以根据实际需要进行相关修改。这里有一个完整的示例,也可以参考一下。

到此这篇关于WPF使用多线程更新UI的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • UnityWebRequest前后端交互实现过程解析

    UnityWebRequest前后端交互实现过程解析

    这篇文章主要介绍了UnityWebRequest前后端交互实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • C# Winfrom实现Skyline画直线功能的示例代码

    C# Winfrom实现Skyline画直线功能的示例代码

    这篇文章主要介绍了C# Winfrom实现Skyline画直线功能的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • C#编程实现自定义热键的方法

    C#编程实现自定义热键的方法

    这篇文章主要介绍了C#编程实现自定义热键的方法,涉及C#键盘按键设置的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-08-08
  • C#实现调用迅雷下载的方法

    C#实现调用迅雷下载的方法

    这篇文章主要介绍了C#实现调用迅雷下载的方法,非常实用的一个技巧,对于进行C#程序设计有很好的借鉴价值,需要的朋友可以参考下
    2014-08-08
  • CefSharp过滤图片RequestHandler问题

    CefSharp过滤图片RequestHandler问题

    这篇文章主要介绍了CefSharp过滤图片RequestHandler问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • C#调用webservice接口的最新方法教程

    C#调用webservice接口的最新方法教程

    webservice 可以用于分布式应用程序之间的交互,和不同程序之间的交互。下面这篇文章主要给大家介绍了关于C#调用webservice接口的相关资料,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友下面随着小编来一起看看吧。
    2017-11-11
  • c#与js随机数生成方法

    c#与js随机数生成方法

    这篇文章主要介绍了c#与js随机数生成方法,实例分析了C#与js生成随机数的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • C#使用GZipStream实现文件的压缩与解压

    C#使用GZipStream实现文件的压缩与解压

    这篇文章主要为大家详细介绍了C#使用GZipStream实现文件的压缩与解压,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • C#使用NPOI对word进行读写

    C#使用NPOI对word进行读写

    这篇文章介绍了C#使用NPOI对word进行读写的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • C#简单遍历指定文件夹中所有文件的方法

    C#简单遍历指定文件夹中所有文件的方法

    这篇文章主要介绍了C#简单遍历指定文件夹中所有文件的方法,结合实例形式分析了文件夹与文件遍历的技巧,并附带说明了文件与文件夹操作的常用函数,需要的朋友可以参考下
    2016-06-06

最新评论