多线程UI,是winform里面是一件非常简单的事情,然而在WPF里面,想要做到跨线程的UI渲染,可就没那么简单了。

我们知道,在Winform中,我们只需要在多线程里直接new一个新的窗口就可以实现多线程UI了。这对我们解决UI卡顿的问题有一定的帮助。

今天我们重点来学习一下,在WPF程序中,怎么去实现一个跨线程UI控件。

基本思路

在WPF中,负责线程调度的管理器是Dispatcher,UI线程必须要在主线程中执行,而且主线程必须是单线程实例。

UI线程实际上是运行在一个消息循环机制中,有DispatcherFrame以及消息处理,转发等机制。

所以我们要实现多线程UI,首先需要有一个专门的UI消息处理线程;其次需要将多线程上的UI渲染合并到现有主线程上去,以便可以加载到其他的主线程UI窗体上。

具体实现方法

  1. 定义UI线程,直接看代码

    /// <summary>
    /// 管理 DynamicRenderer 线程
    /// </summary>
    public class DispatcherUIThread : IDisposable
    {
        /// <summary>
        /// 启动新的线程
        /// </summary>
        public void Start()
        {
            _syncThreadStartResetEvent = new AutoResetEvent(false);
            var dynamicRendererThread = new Thread(InternalRunning)
            {
                Priority = ThreadPriority.Highest,
                IsBackground = true,
            };
            //设置线程为单线程
            dynamicRendererThread.SetApartmentState(ApartmentState.STA);
            dynamicRendererThread.Start();
            //等待Dispatcher已启动
            _syncThreadStartResetEvent.WaitOne();
            _syncThreadStartResetEvent.Dispose();
            _syncThreadStartResetEvent = null;
        }

        private AutoResetEvent _syncThreadStartResetEvent;

        /// <summary>
        /// 运行笔的线程
        /// </summary>
        public Dispatcher Dispatcher { get; private set; }

        [SecurityCritical]
        private void InternalRunning()
        {
            Dispatcher = Dispatcher.CurrentDispatcher;
            _syncThreadStartResetEvent.Set();
            //开启消息循环
            Dispatcher.Run();
        }

        /// <inheritdoc />
        public void Dispose()
        {
            Close();
            Dispatcher = null;
        }

        /// <summary>
        /// 关闭线程
        /// </summary>
        public void Close()
        {
            Dispatcher.InvokeShutdown();
        }
    }

对此段代码有不明白的,请在评论区留言。

  1. 跨线程UI合并

跨线程UI合并需要用到俩个关键类:HostVisualVisualTarget

HostVisual:用于承载多线程UI的子元素宿主,该Visual处于主线程上。 VisualTarget:用于衔接主线程和多线程UI的边界处理器。

代码如下:


    //异步UI容器
    public class AsyncUIContainer : UIElement
    {
        public AsyncUIContainer()
        {
            Initialize();
        }

        HostVisual hostVisual;
        ContainerVisual Container;
        Rect _hitTestRect = Rect.Empty;

        /// <summary>
        /// 
        /// </summary>
        void Initialize()
        {
            //定义主线程UI宿主
            hostVisual = new HostVisual();
            //开启UI线程
            var thread = new DispatcherUIThread();
            thread.Start();

            thread.Dispatcher.Invoke(() =>
            {
                //在新的UI线程中,初始化Visual容器
                Container = new ContainerVisual();
                //先此线程的UI和主线程UI衔接
                new VisualTarget(hostVisual)
                {
                    RootVisual = Container
                };
            });
            //将UI宿主,添加到视觉树
            AddVisualChild(hostVisual);
        }

        //因为是多线程上的UI,所以需要使用该UI所在线程才可调度
        public Dispatcher UIDispatcher
        {
            get
            {
                return Container.Dispatcher;
            }
        }

        public VisualCollection Children
        {
            get
            {
                return Container.Children;
            }
        }

        //重写命中测试,让多线程UI能够响应基础输入
        protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
        {
            if (_hitTestRect.Contains(hitTestParameters.HitPoint))
            {
                return new PointHitTestResult(this, hitTestParameters.HitPoint);
            }

            return base.HitTestCore(hitTestParameters);
        }

        //重写布局系统,让多线程UI元素能正常布局
        protected override Size MeasureCore(Size availableSize)
        {
            if (availableSize.Height > 0 && availableSize.Width > 0 && !(double.IsInfinity(availableSize.Height) || double.IsInfinity(availableSize.Width)))
            {
                return availableSize;
            }

            return new Size(200, 200);
        }

        //重写布局系统,让多线程UI元素能正常布局
        protected override void ArrangeCore(Rect finalRect)
        {
            base.ArrangeCore(finalRect);

            RenderSize = finalRect.Size;
            _hitTestRect = finalRect;
        }

        //指定起Visual的数量
        protected override int VisualChildrenCount => 1;
        //指定需要渲染的Visual对象
        protected override Visual GetVisualChild(int index)
        {
            return hostVisual;
        }
    }

基础实现方法就差不多了,大家可以根据自己需要进行魔改。

  1. 如何使用

多线程的UI控件我们已经写好了,我们只需要将AsyncUIContainer添加到我们的Xaml中。

对于其子元素,我们需要在后台代码中添加(想要在Xaml中添加也可以,需要自己实现Xaml扩展代码,让其支持直接在xaml中编辑)。

            asyncUIContainer.UIDispatcher.InvokeAsync(() =>
            {
                var Rectangle = new Rectangle()
                 {
                    Fill=Brushes.Red,
                    Height=400,
                    Width=400,
                 };

                //这里必须调用Arrange方法,否则界面不会渲染。
                //如果使用DrawingVisual,那么需要调用DrawingVisual的RenderOpen()方法,并主动调用渲染的绘制原语才可正常渲染。
                //下面是个DrawingVisual的例子
                //public void Draw()
                //{
                //    using var drawingContext = RenderOpen();
                //    drawingContext.DrawGeometry(_fillBrush, null, DrawingGeometry);
                //}

                rectangle.Arrange(new Rect(0, 0, 400, 400));
                asyncUIContainer.Children.Add(rectangle);
            });

通过以上就可以显示我们的多线程UI了。

在实际业务中,如果有遇到卡主线程UI的场景,便有用武之地了。


本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/WPF-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%86%99%E8%B7%A8%E7%BA%BF%E7%A8%8BUI%E6%8E%A7%E4%BB%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系