为 Marquee 效果制作旋转动画非常简单,只需为
RenderTransform.RotateTranform
属性设置动画即可。
你可以像这样实现它。这是
Storyboard
的声明,带有一些额外的旋转效果,以及由 IsMouseOver
在选定的 Path
上触发的示例。
<UserControl.Resources>
<Storyboard x:Key="SpinStoryboardSpinning" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Path.RenderTransform).(RotateTransform.Angle)">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="90"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="180"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="450">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Path Fill="Gold"
Data="M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70"
RenderTransformOrigin="1,1">
<Path.RenderTransform>
<RotateTransform Angle="90"/>
</Path.RenderTransform>
<Path.Style>
<Style TargetType="{x:Type Path}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource SpinStoryboardSpinning}"/>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
但是要创建完整的进度条,这是更复杂的任务。
首先,您需要控制创建的
Storyboards
,即在需要时停止和启动它们。另外,对于 ProgressBar
本身,我建议首先从 ProgressBar
控件派生并根据需要设置其样式。
好的开始是首先创建 standard,但基于 MSDN 中的此示例设计
ProgressBar
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/progressbar-styles -和-模板?view=netframeworkdesktop-4.8
圆形进度条示例。
自定义控件和转换器:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using static System.Math;
namespace Core2023.RoundProgressBar
{
public class RoundProgressBar : RangeBase
{
static RoundProgressBar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RoundProgressBar), new FrameworkPropertyMetadata(typeof(RoundProgressBar)));
}
}
[ValueConversion(typeof(ProgressBar), typeof(Geometry))]
public class ProgressBarToGeometryConverter : IMultiValueConverter
{
private static readonly double valPI = 2 * PI;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double min = (double)values[0];
double max = (double)values[1];
double value = (double)values[2];
double angle = (value - min) / (max - min);
angle *= valPI;
if (!double.IsNormal(angle))
return DependencyProperty.UnsetValue;
double cos = Cos(angle);
double sin = Sin(angle);
double x1 = 100 * cos + 100;
double y1 = 100 * sin + 100;
double x2 = 70 * cos + 100;
double y2 = 70 * sin + 100;
string data;
if (angle < PI)
{
data = @$"
M200,100
A100,100 0 0 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 0 0 170,100
z";
}
else
{
data = @$"
M200,100
A100,100 0 1 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 1 0 170,100
z";
}
return Geometry.Parse(data);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private ProgressBarToGeometryConverter() { }
public static ProgressBarToGeometryConverter Instance { get; } = new();
}
[MarkupExtensionReturnType(typeof(ProgressBarToGeometryConverter))]
public class ProgressBarToGeometryExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ProgressBarToGeometryConverter.Instance;
}
}
}
主题 Generic.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Core2023"
xmlns:rpb="clr-namespace:Core2023.RoundProgressBar">
<Style TargetType="{x:Type rpb:RoundProgressBar}">
<Setter Property="Background" Value="LightGray"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type rpb:RoundProgressBar}">
<ControlTemplate.Resources>
<Geometry x:Key="round">
M 0 100 a 100,100 0 1 1 200,0
a 100,100 0 1 1 -200,0
M 30 100 a 70,70 0 1 1 140,0
a 70,70 0 1 1 -140,0</Geometry>
</ControlTemplate.Resources>
<Grid>
<!-- Base Spinner -->
<Path Fill="{TemplateBinding Background}"
Data="{StaticResource round}"/>
<Path Stroke="{TemplateBinding BorderBrush}" StrokeThickness="3"
Data="{StaticResource round}" Panel.ZIndex="2"/>
<!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->
<Path Fill="Gold">
<Path.Data>
<MultiBinding Converter="{rpb:ProgressBarToGeometry}">
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Minimum"/>
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Maximum"/>
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Value"/>
</MultiBinding>
</Path.Data>
</Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
带有 Value 属性的使用和动画示例的窗口:
<Window x:Class="Core2023.RoundProgressBar.RoundProgressBarWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Core2023.RoundProgressBar"
mc:Ignorable="d"
Title="RoundProgressBarWindow" Height="450" Width="800">
<Window.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:5"
To="10"
Storyboard.TargetName="rpb"
Storyboard.TargetProperty="Value"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<local:RoundProgressBar x:Name="rpb"
Maximum="10" Minimum="-10" Value="-10"
BorderBrush="Aqua"/>
</Grid>
</Window>
Value 属性可以绑定到 ViewModel 或以任何其他方式设置。进度将正确显示。
P.S. 根据 Clements 的评论,我提供了使用 StreamGeometry 的转换器的改进版本。
[ValueConversion(typeof(ProgressBar), typeof(Geometry))]
public class ProgressBarToGeometryConverter : IMultiValueConverter
{
private static readonly double valPI = 2 * PI;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double min = (double)values[0];
double max = (double)values[1];
double value = (double)values[2];
if (value == max)
{
}
double angle = (value - min) / (max - min);
angle *= valPI;
if (!double.IsNormal(angle))
return DependencyProperty.UnsetValue;
double cos = Cos(angle);
double sin = Sin(angle);
double x1 = 100 * cos + 100;
double y1 = 100 * sin + 100;
double x2 = 70 * cos + 100;
double y2 = 70 * sin + 100;
StreamGeometry sg = new();
sg.FillRule = FillRule.EvenOdd;
using StreamGeometryContext sgc = sg.Open();
if (value >= max)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(0, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.ArcTo(new Point(200, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(170, 100), true, false);
sgc.ArcTo(new Point(30, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
if (angle < PI)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, true, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, true, SweepDirection.Counterclockwise, true, false);
}
return sg;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private ProgressBarToGeometryConverter() { }
public static ProgressBarToGeometryConverter Instance { get; } = new();
}