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>
效果图