WPF 如何在控件上显示 Loading 等待动画
- 框架使用
.NET40
; Visual Studio 2022
;- 使用方式需引入命名空间后设置控件的附加属性
wd:Loading.IsShow="true"
,即可显示默认等待动画效果如下:
- 如需自定义
Loading
一定要 先设置wd:Loading.Child
在设置IsShow="true"
。 - 显示不同
Loading
内容需wd:Loading.Child ={x:Static wd:NormalLoading.Default}
进行复赋值显示NormalLoading
效果如下:
实现代码
也可以自定义 Loading
动画如下:
1、自定义控件 CustomLoading
。
public class CustomLoading : Control { public static CustomLoading Default = new CustomLoading(); static CustomLoading() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomLoading), new FrameworkPropertyMetadata(typeof(CustomLoading))); } }
2、编写 CustomLoading.xaml
代码如下。
<Style TargetType="{x:Type controls:CustomLoading}"> <Setter Property="Width" Value="40" /> <Setter Property="Height" Value="40" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type controls:CustomLoading}"> <!--此处编写自定义的动画逻辑--> </ControlTemplate> </Setter.Value> </Setter> </Style>
1)创建装饰 AdornerContainer
代码如下:
using System.Windows; using System.Windows.Documents; using System.Windows.Media; namespace WPFDevelopers.Utilities { public class AdornerContainer : Adorner { private UIElement _child; public AdornerContainer(UIElement adornedElement) : base(adornedElement) { } public UIElement Child { get => _child; set { if (value == null) { RemoveVisualChild(_child); _child = value; return; } AddVisualChild(value); _child = value; } } protected override int VisualChildrenCount { get { return _child != null ? 1 : 0; } } protected override Size ArrangeOverride(Size finalSize) { _child?.Arrange(new Rect(finalSize)); return finalSize; } protected override Visual GetVisualChild(int index) { if (index == 0 && _child != null) return _child; return base.GetVisualChild(index); } } }
2)创建蒙板控件 MaskControl
代码如下:
using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace WPFDevelopers.Controls { public class MaskControl : ContentControl { private readonly Visual visual; public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(MaskControl), new PropertyMetadata(new CornerRadius(0))); public MaskControl(Visual _visual) { visual = _visual; } public CornerRadius CornerRadius { get => (CornerRadius)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } } }
3)创建 Loading
继承 BaseControl
增加附加属性 IsShow
代码如下:
True
则动态添加装饰器AdornerContainer
并将MaskControl
添加到AdornerContainer.Child
中。False
则移除装饰器。
using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Markup; using System.Windows.Media; using WPFDevelopers.Helpers; using WPFDevelopers.Utilities; namespace WPFDevelopers.Controls { public class Loading : BaseControl { public static readonly DependencyProperty IsShowProperty = DependencyProperty.RegisterAttached("IsShow", typeof(bool), typeof(Loading), new PropertyMetadata(false, OnIsLoadingChanged)); private const short SIZE = 25; private const double MINSIZE = 40; private static FrameworkElement oldFrameworkElement; private static void OnIsLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.NewValue is bool isMask && d is FrameworkElement parent) { if (isMask) { if (!parent.IsLoaded) parent.Loaded += Parent_Loaded; else CreateMask(parent); } else { parent.Loaded -= Parent_Loaded; CreateMask(parent, true); } } } private static void Parent_Loaded(object sender, RoutedEventArgs e) { if (sender is UIElement element) CreateMask(element); } static void CreateMask(UIElement uIElement, bool isRemove = false) { var layer = AdornerLayer.GetAdornerLayer(uIElement); if (layer == null) return; if (isRemove && uIElement != null) { var adorners = layer.GetAdorners(uIElement); if (adorners != null) { foreach (var item in adorners) { if (item is AdornerContainer container) { var isAddChild = (bool)Loading.GetIsAddChild(uIElement); if (!isAddChild) Loading.SetChild(uIElement, null); container.Child = null; layer.Remove(container); } } } return; } var adornerContainer = new AdornerContainer(uIElement); var value = Loading.GetChild(uIElement); if (value == null) { var isLoading = GetIsShow(uIElement); if (isLoading) { var w = (double)uIElement.GetValue(ActualWidthProperty); var h = (double)uIElement.GetValue(ActualHeightProperty); var defaultLoading = new DefaultLoading(); if (w < MINSIZE || h < MINSIZE) { defaultLoading.Width = SIZE; defaultLoading.Height = SIZE; defaultLoading.StrokeArray = new DoubleCollection { 10, 100 }; } SetChild(uIElement, defaultLoading); value = Loading.GetChild(uIElement); } if (value != null) adornerContainer.Child = new MaskControl(uIElement) { Content = value, Background = ControlsHelper.Brush }; } else { var normalLoading = (FrameworkElement)value; var frameworkElement = (FrameworkElement)uIElement; Loading.SetIsAddChild(uIElement, true); if (oldFrameworkElement != null) value = oldFrameworkElement; else { string xaml = XamlWriter.Save(normalLoading); oldFrameworkElement = (FrameworkElement) XamlReader.Parse(xaml); } var _size = frameworkElement.ActualHeight < frameworkElement.ActualWidth ? frameworkElement.ActualHeight : frameworkElement.ActualWidth; if(_size < MINSIZE) { normalLoading.Width = SIZE; normalLoading.Height = SIZE; value = normalLoading; } adornerContainer.Child = new MaskControl(uIElement) { Content = value, Background = ControlsHelper.Brush }; } layer.Add(adornerContainer); } public static bool GetIsShow(DependencyObject obj) { return (bool)obj.GetValue(IsShowProperty); } public static void SetIsShow(DependencyObject obj, bool value) { obj.SetValue(IsShowProperty, value); } } }
4)创建 DefaultLoading.xaml
代码如下:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:WPFDevelopers.Controls"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Basic/ControlBasic.xaml"/> </ResourceDictionary.MergedDictionaries> <Style TargetType="{x:Type controls:DefaultLoading}"> <Setter Property="Width" Value="40" /> <Setter Property="Height" Value="40" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type controls:DefaultLoading}"> <Viewbox Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"> <controls:SmallPanel> <controls:SmallPanel.Resources> <Storyboard x:Key="StarStoryboard" RepeatBehavior="Forever"> <DoubleAnimation Storyboard.TargetName="PART_Ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="360" Duration="0:0:1.0" /> </Storyboard> </controls:SmallPanel.Resources> <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Stroke="{DynamicResource BaseSolidColorBrush}" StrokeDashArray="100,100" StrokeThickness="2" /> <Ellipse x:Name="PART_Ellipse" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Stretch="Uniform" RenderTransformOrigin=".5,.5" Stroke="{DynamicResource PrimaryPressedSolidColorBrush}" StrokeDashArray="{TemplateBinding StrokeArray}" StrokeThickness="2"> <Ellipse.RenderTransform> <RotateTransform Angle="0" /> </Ellipse.RenderTransform> <Ellipse.Triggers> <EventTrigger RoutedEvent="Loaded"> <BeginStoryboard Storyboard="{StaticResource StarStoryboard}" /> </EventTrigger> </Ellipse.Triggers> </Ellipse> </controls:SmallPanel> </Viewbox> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
5)创建 LoadingExample.xaml
实例代码如下:
<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.LoadingExample" 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:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid Margin="10"> <StackPanel Grid.Column="1"> <CheckBox Name="MyCheckBox" Content="启动 Loading 动画" VerticalAlignment="Center" HorizontalAlignment="Center"/> <UniformGrid Margin="10" Rows="2" Columns="3"> <Border Background="Red" wd:Loading.IsShow="{Binding ElementName=MyCheckBox,Path=IsChecked}"> <TextBlock Text="Mask 0" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Border> <Image Source="pack://application:,,,/WPFDevelopers.Samples;component/Images/Breathe/0.jpg" wd:Loading.IsShow="{Binding ElementName=MyCheckBox,Path=IsChecked}" wd:Loading.Child="{x:Static wd:NormalLoading.Default}"/> <Button Content="Mask 1" wd:Loading.IsShow="{Binding ElementName=MyCheckBox,Path=IsChecked}" Height="28" VerticalAlignment="Top" HorizontalAlignment="Center"/> <Button Content="Mask 2" wd:Loading.IsShow="{Binding ElementName=MyCheckBox,Path=IsChecked}" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,10"/> <Button Content="提交" wd:Loading.IsShow="{Binding ElementName=MyCheckBox,Path=IsChecked}" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,10" Style="{StaticResource PrimaryButton}"/> </UniformGrid> </StackPanel> </Grid> </UserControl>
效果图