WPF 多线程 UI:设计一个异步加载 UI 的容器

简介: 原文 WPF 多线程 UI:设计一个异步加载 UI 的容器 对于 WPF 程序,如果你有某一个 UI 控件非常复杂,很有可能会卡住主 UI,给用户软件很卡的感受。但如果此时能有一个加载动画,那么就不会感受到那么卡顿了。

原文 WPF 多线程 UI:设计一个异步加载 UI 的容器

对于 WPF 程序,如果你有某一个 UI 控件非常复杂,很有可能会卡住主 UI,给用户软件很卡的感受。但如果此时能有一个加载动画,那么就不会感受到那么卡顿了。UI 的卡住不同于 IO 操作或者密集的 CPU 计算,WPF 中的 UI 卡顿时,我们几乎没有可以让 UI 响应的方式,因为 WPF 一个窗口只有一个 UI 线程。

No!WPF 一个窗口可以不止一个 UI 线程,本文将设计一个异步加载 UI 的容器,可以在主线程完全卡死的情况下显示一个加载动画。


本文是对我另一篇博客 WPF 同一窗口内的多线程 UI(VisualTarget) 的一项应用。阅读本文,你将得到一个 UI 控件 AsyncBox,放入其中的控件即便卡住主线程,也依然会有一个加载动画缓解用户的焦虑情绪。

异步加载的效果预览

下图的黑屏部分是正在加载一个布局需要花 500ms 的按钮。我们可以看到,即便是主线程被占用了 500ms,依然能有一个加载动画缓解用户的等待焦虑。

异步加载效果预览
▲ 异步加载效果预览

使用我写的 WPF 异步加载控件 AsyncBox

控件的名字为 AsyncBox,意为异步加载显示 UI 的容器。如果要使用它,可以很简单地写出以下代码:

<ww:AsyncBox LoadingViewType="demo:LoadingView">
    <demo:LongTimeView /> </ww:AsyncBox> 

其中,LoadingView 是在指定用哪一个控件来做加载动画。由于这个控件会在后台线程创建并执行,为了避免意外的线程问题,这里传入类型,而不是实例。

LongTimeView 是一个用来模拟耗时 UI 的模拟控件。

如果要看整个窗口,则是下面这样:

<Window x:Class="Walterlv.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Walterlv.Demo" xmlns:ww="clr-namespace:Walterlv.Windows;assembly=Walterlv.Windows" xmlns:demo="clr-namespace:Walterlv.Demo" Title="walterlv.com" Height="450" Width="800" Background="Black"> <Grid> <ww:AsyncBox LoadingViewType="demo:LoadingView"> <demo:LongTimeView /> </ww:AsyncBox> </Grid> </Window> 

LongTimeView 则是这样:

<UserControl x:Class="Walterlv.Demo.LongTimeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Walterlv.Demo" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" FontSize="48" FontFamily="Monaco"> <Grid> <Button Content="walterlv.com" Click="DelayButton_Click" /> </Grid> </UserControl> 
using System.Threading;
using System.Windows; using System.Windows.Controls; namespace Walterlv.Demo { public partial class LongTimeView : UserControl { public LongTimeView() { InitializeComponent(); } protected override Size MeasureOverride(Size constraint) { Thread.Sleep(500); return base.MeasureOverride(constraint); } private void DelayButton_Click(object sender, RoutedEventArgs e) { Thread.Sleep(3000); } } } 

而 LoadingView 则很简单,只是一个无限旋转的动画而已。同时它还没有后台代码:

LoadingView 的动画效果
▲ LoadingView 的动画效果

<UserControl x:Class="Walterlv.Demo.LoadingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Walterlv.Demo" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <FrameworkElement.Resources> <Storyboard x:Key="Storyboard.Loading"> <DoubleAnimation Storyboard.TargetName="Target" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" From="0" To="1440" Duration="0:0:1.5" RepeatBehavior="Forever"> </DoubleAnimation> </Storyboard> </FrameworkElement.Resources> <Grid> <Ellipse x:Name="Target" Width="48" Height="48" Stroke="White" StrokeThickness="8" StrokeDashArray="10" StrokeDashCap="Round" RenderTransformOrigin="0.5 0.5"> <Ellipse.RenderTransform> <RotateTransform /> </Ellipse.RenderTransform> <Ellipse.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard Storyboard="{StaticResource Storyboard.Loading}" /> </EventTrigger> </Ellipse.Triggers> </Ellipse> </Grid> </UserControl> 

现在,我们来实现这个异步加载 UI 的容器

你需要为你的项目添加以下文件:

项目文件

其中,1、2、3、4、6 这几个文件可分别从以下链接找到并下载到你的项目中:

  1. Annotations.cs
  2. AwaiterInterfaces.cs
  3. DispatcherAsyncOperation.cs
  4. UIDispatcher.cs
  5. VisualTargetPresentationSource.cs

这些文件都是通用的异步类型。

第 5 个文件 AsyncBox 就是我们要实现的主要类型。

实现思路是建一个 PresentationSource(类似于窗口的根 HwndSource),这可以用来承载一个新的可视化树(Visual Tree)。这样,我们就能在一个窗口中显示两个可视化树了。

这两个可视化树通过 HostVisual 跨线程连接起来,于是我们能在一个窗口中得到两个不同线程的可视化树。

由于这两棵树不在同一个线程中,于是主线程即便卡死,也不影响后台用来播放加载动画的线程。

附 AsyncBox 的源码

如果你不能在下面看到 AsyncBox 的源码,那么你的网络应该是被屏蔽了,可以访问 AsyncBox.cs - A UI container for async loading. 查看。

using System;
using System.Collections;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
using Walterlv.Annotations;
using Walterlv.Demo;
using Walterlv.Demo.Utils.Threading;
using DispatcherDictionary = System.Collections.Concurrent.ConcurrentDictionary<System.Windows.Threading.Dispatcher, Walterlv.Demo.Utils.Threading.DispatcherAsyncOperation<System.Windows.Threading.Dispatcher>>;

namespace Walterlv.Windows
{
    [ContentProperty(nameof(Child))]
    public class AsyncBox : FrameworkElement
    {
        /// <summary>
        /// 保存外部 UI 线程和与其关联的异步 UI 线程。
        /// 例如主 UI 线程对应一个 AsyncBox 专用的 UI 线程;外面可能有另一个 UI 线程,那么对应另一个 AsyncBox 专用的 UI 线程。
        /// </summary>
        private static readonly DispatcherDictionary RelatedAsyncDispatchers = new DispatcherDictionary();

        [CanBeNull]
        private UIElement _child;

        [NotNull]
        private readonly HostVisual _hostVisual;

        [CanBeNull]
        private VisualTargetPresentationSource _targetSource;

        [CanBeNull]
        private UIElement _loadingView;

        [NotNull]
        private readonly ContentPresenter _contentPresenter;

        private bool _isChildReadyToLoad;

        [CanBeNull]
        private Type _loadingViewType;

        public AsyncBox()
        {
            _hostVisual = new HostVisual();
            _contentPresenter = new ContentPresenter();
            Loaded += OnLoaded;
        }

        [CanBeNull]
        public UIElement Child
        {
            get => _child;
            set
            {
                if (Equals(_child, value)) return;

                if (value != null)
                {
                    RemoveLogicalChild(value);
                }

                _child = value;

                if (_isChildReadyToLoad)
                {
                    ActivateChild();
                }
            }
        }

        [NotNull]
        public Type LoadingViewType
        {
            get
            {
                if (_loadingViewType == null)
                {
                    throw new InvalidOperationException(
                        $"在 {nameof(AsyncBox)} 显示之前,必须先为 {nameof(LoadingViewType)} 设置一个 {nameof(UIElement)} 作为 Loading 视图。");
                }

                return _loadingViewType;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(LoadingViewType));
                }

                if (_loadingViewType != null)
                {
                    throw new ArgumentException($"{nameof(LoadingViewType)} 只允许被设置一次。", nameof(value));
                }

                _loadingViewType = value;
            }
        }

        /// <summary>
        /// 返回一个可等待的用于显示异步 UI 的后台 UI 线程调度器。
        /// </summary>
        [NotNull]
        private DispatcherAsyncOperation<Dispatcher> GetAsyncDispatcherAsync() => RelatedAsyncDispatchers.GetOrAdd(
            Dispatcher, dispatcher => UIDispatcher.RunNewAsync("AsyncBox"));

        [NotNull]
        private UIElement CreateLoadingView()
        {
            var instance = Activator.CreateInstance(LoadingViewType);
            if (instance is UIElement element)
            {
                return element;
            }

            throw new InvalidOperationException($"{LoadingViewType} 必须是 {nameof(UIElement)} 类型");
        }

        private async void OnLoaded(object sender, RoutedEventArgs e)
        {
            if (DesignerProperties.GetIsInDesignMode(this)) return;

            var dispatcher = await GetAsyncDispatcherAsync();
            _loadingView = await dispatcher.InvokeAsync(() =>
            {
                var loadingView = CreateLoadingView();
                _targetSource = new VisualTargetPresentationSource(_hostVisual)
                {
                    RootVisual = loadingView
                };
                return loadingView;
            });
            AddVisualChild(_contentPresenter);
            AddVisualChild(_hostVisual);

            await LayoutAsync();
            await Dispatcher.Yield(DispatcherPriority.Background);

            _isChildReadyToLoad = true;
            ActivateChild();
        }

        private void ActivateChild()
        {
            var child = Child;
            if (child != null)
            {
                _contentPresenter.Content = child;
                AddLogicalChild(child);
                InvalidateMeasure();
            }
        }

        private async Task LayoutAsync()
        {
            var dispatcher = await GetAsyncDispatcherAsync();
            await dispatcher.InvokeAsync(() =>
            {
                if (_loadingView != null)
                {
                    _loadingView.Measure(RenderSize);
                    _loadingView.Arrange(new Rect(RenderSize));
                }
            });
        }

        protected override int VisualChildrenCount => _loadingView != null ? 2 : 0;

        protected override Visual GetVisualChild(int index)
        {
            switch (index)
            {
                case 0:
                    return _contentPresenter;
                case 1:
                    return _hostVisual;
                default:
                    return null;
            }
        }

        protected override IEnumerator LogicalChildren
        {
            get
            {
                if (_isChildReadyToLoad)
                {
                    yield return _contentPresenter;
                }
            }
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            if (_isChildReadyToLoad)
            {
                _contentPresenter.Measure(availableSize);
                return _contentPresenter.DesiredSize;
            }

            var size = base.MeasureOverride(availableSize);
            return size;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (_isChildReadyToLoad)
            {
                _contentPresenter.Arrange(new Rect(finalSize));
                var renderSize = _contentPresenter.RenderSize;
                LayoutAsync().ConfigureAwait(false);
                return renderSize;
            }

            var size = base.ArrangeOverride(finalSize);
            LayoutAsync().ConfigureAwait(false);
            return size;
        }
    }
}

 

 
view raw AsyncBox.cs hosted with by  GitHub

本文会经常更新,请阅读原文: https://walterlv.com/post/design-an-async-loading-view.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

目录
相关文章
|
8月前
|
C# 开发者 Windows
基于Material Design风格开源、易用、强大的WPF UI控件库
基于Material Design风格开源、易用、强大的WPF UI控件库
416 0
|
1月前
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
34 1
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
3月前
|
传感器 数据处理 定位技术
多线程;顺序容器;智能指针
【10月更文挑战第14天】多线程的创建创建线程比较简单,C++提供头文件thread,使用std的thread实例化一个线程对象创建。 std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。
|
3月前
|
传感器 数据处理 定位技术
多线程;顺序容器;智能指针
多线程的创建创建线程比较简单,C++提供头文件thread,使用std的thread实例化一个线程对象创建。 std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。 #include &lt;iostream&gt; #include &lt;thread&gt; #include &lt;stdlib.h&gt; //sleep using namespace std; void t1() //普通的函数,用来执行线程 { for (int i = 0; i &lt; 10; ++i)
多线程;顺序容器;智能指针
|
4月前
|
搜索推荐 前端开发 C#
推荐7款美观且功能强大的WPF UI库
推荐7款美观且功能强大的WPF UI库
200 2
|
4月前
|
传感器 数据处理 定位技术
多线程;顺序容器;智能指针
多线程的创建创建线程比较简单,C++提供头文件thread,使用std的thread实例化一个线程对象创建。 std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。 #include &lt;iostream&gt; #include &lt;thread&gt; #include &lt;stdlib.h&gt; //sleep using namespace std; void t1() //普通的函数,用来执行线程 { for (int i = 0; i &lt; 10; ++i)
|
5月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
5月前
|
C# 开发者 Windows
一款基于Fluent设计风格、现代化的WPF UI控件库
一款基于Fluent设计风格、现代化的WPF UI控件库
132 1
|
5月前
|
vr&ar C# 图形学
WPF与AR/VR的激情碰撞:解锁Windows Presentation Foundation应用新维度,探索增强现实与虚拟现实技术在现代UI设计中的无限可能与实战应用详解
【8月更文挑战第31天】增强现实(AR)与虚拟现实(VR)技术正迅速改变生活和工作方式,在游戏、教育及工业等领域展现出广泛应用前景。本文探讨如何在Windows Presentation Foundation(WPF)环境中实现AR/VR功能,通过具体示例代码展示整合过程。尽管WPF本身不直接支持AR/VR,但借助第三方库如Unity、Vuforia或OpenVR,可实现沉浸式体验。例如,通过Unity和Vuforia在WPF中创建AR应用,或利用OpenVR在WPF中集成VR功能,从而提升用户体验并拓展应用功能边界。
97 0

热门文章

最新文章