当前位置: 首页 > news >正文

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

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

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


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

本文内容

      • 异步加载的效果预览
      • 使用我写的 WPF 异步加载控件 AsyncBox
      • 现在,我们来实现这个异步加载 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. 查看。

相关文章:

  • .NET 命令行参数包含应用程序路径吗?
  • 分析现有 WPF / Windows Forms 程序能否顺利迁移到 .NET Core 3.0(使用 .NET Core 3.0 Desktop API Analyzer )
  • C# 空合并操作符(??)不可重载?其实有黑科技可以间接重载!
  • UWP 轻量级样式定义(Lightweight Styling)
  • 预编译框架,开发高性能应用 - 课程 - 微软技术暨生态大会 2018
  • 将 UWP 中 CommandBar 的展开方向改为向下展开
  • .NET 中创建支持集合初始化器的类型
  • .NET 中让 Task 支持带超时的异步等待
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference!
  • WPF 中的 NameScope
  • Windows 下的高 DPI 应用开发(UWP / WPF / Windows Forms / Win32)
  • 技术、产品、交流、思考 - 微软技术暨生态大会 2018
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • ES6指北【2】—— 箭头函数
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • flask接收请求并推入栈
  • iOS编译提示和导航提示
  • Material Design
  • Nacos系列:Nacos的Java SDK使用
  • PHP 7 修改了什么呢 -- 2
  • React as a UI Runtime(五、列表)
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • Vue2.0 实现互斥
  • vue2.0项目引入element-ui
  • zookeeper系列(七)实战分布式命名服务
  • 基于组件的设计工作流与界面抽象
  • 快速构建spring-cloud+sleuth+rabbit+ zipkin+es+kibana+grafana日志跟踪平台
  • 两列自适应布局方案整理
  • 普通函数和构造函数的区别
  • 前嗅ForeSpider中数据浏览界面介绍
  • 深入浅出Node.js
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • ​Kaggle X光肺炎检测比赛第二名方案解析 | CVPR 2020 Workshop
  • ​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?
  • ​力扣解法汇总946-验证栈序列
  • #pragam once 和 #ifndef 预编译头
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (4.10~4.16)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (阿里云万网)-域名注册购买实名流程
  • (二)hibernate配置管理
  • (附源码)spring boot基于Java的电影院售票与管理系统毕业设计 011449
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (免费领源码)python+django+mysql线上兼职平台系统83320-计算机毕业设计项目选题推荐
  • (三) diretfbrc详解
  • (算法)前K大的和
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • (转)iOS字体
  • (转)视频码率,帧率和分辨率的联系与区别
  • (转载)Linux 多线程条件变量同步