Windows 进度环






4.99/5 (27投票s)
Windows 进度环作为自定义控件。
- 下载源代码 - 35.1 KB
- 也可作为 NuGet 包 获取
引言
我读到一篇关于使用 Windows Forms 创建进度环的文章,我认为使用 WPF 会容易得多。
我想知道这需要多少努力,并给自己提出了这个挑战。
在这篇文章中,你可以阅读到结果。
进度环
进度环主要出现在 Windows 启动和安装过程中,但也用于许多应用程序和不同的变体中。
实现
这个进度环被实现为一个自定义的 WPF 控件。
基本上,该控件是一个包含 5 行和 5 列的 Grid
,以及一些围绕网格中心旋转的椭圆。
旋转是通过一个带有 QuarticEase
函数的动画 RotateTransform
完成的。
控件 Style
<Style x:Key="WindowsProgressRingStyle" TargetType="{x:Type local:WindowsProgressRing}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="ClipToBounds" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:WindowsProgressRing}">
<Grid x:Name="PART_body" Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
请注意,椭圆并没有存在于样式中,而是在相应的类 WindowsProgressRing
中添加。
前景色设置椭圆的 Fill
。
Background
设置背景,正如你可能猜到的那样。
/// <summary>
/// Class WindowsProgressRing.
/// </summary>
[TemplatePart(Name = "PART_body", Type = typeof(Grid))]
public class WindowsProgressRing : Control
{
/// <summary>
/// The part body
/// </summary>
private Grid partBody;
#region -- Properties --
public Grid Body { get { return partBody; } }
#region Speed
/// <summary>
/// The speed property
/// </summary>
public static readonly DependencyProperty SpeedProperty = DependencyProperty.Register(
"Speed ", typeof(Duration), typeof(WindowsProgressRing),
new FrameworkPropertyMetadata(new Duration(TimeSpan.FromSeconds(2.5)),
FrameworkPropertyMetadataOptions.AffectsRender, SpeedChanged, SpeedValueCallback));
/// <summary>
/// Gets or sets the speed.
/// </summary>
/// <value>The speed.</value>
public Duration Speed
{
get { return (Duration)GetValue(SpeedProperty); }
set { SetValue(SpeedProperty, value); }
}
/// <summary>
/// Speed changed.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="dependencyPropertyChangedEventArgs"
/// >The <see cref="DependencyPropertyChangedEventArgs" />
/// instance containing the event data.</param>
private static void SpeedChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var wpr = (WindowsProgressRing) dependencyObject;
if (wpr.Body == null) return;
var speed = (Duration)dependencyPropertyChangedEventArgs.NewValue;
wpr.SetStoryBoard(speed);
}
/// <summary>
/// Speed value callback.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="baseValue">The base value.</param>
/// <returns>System.Object.</returns>
private static object SpeedValueCallback(DependencyObject dependencyObject, object baseValue)
{
if (((Duration)baseValue).HasTimeSpan &&
((Duration)baseValue).TimeSpan > TimeSpan.FromSeconds(5))
return new Duration(TimeSpan.FromSeconds(5));
return baseValue;
}
#endregion // Speed
#region Items
/// <summary>
/// The items property
/// </summary>
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items", typeof(int),
typeof(WindowsProgressRing), new FrameworkPropertyMetadata(6,
FrameworkPropertyMetadataOptions.AffectsRender, ItemsChanged, ItemsValueCallback));
/// <summary>
/// Gets or sets the items.
/// </summary>
/// <value>The items.</value>
public int Items
{
get { return (int)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
/// <summary>
/// Items changed.
/// </summary>
/// <param name="d">The d.</param>
/// <param name="e">The <see
/// cref="DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
private static void ItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var wpr = (WindowsProgressRing)d;
if (wpr.Body == null) return;
wpr.Body.Children.Clear();
var items = (int)e.NewValue;
for (var i = 0; i < items; i++)
{
var ellipse = new Ellipse
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
ClipToBounds = false,
RenderTransformOrigin = new Point(0.5, 2.5)
};
wpr.Body.Children.Add(ellipse);
// Binding
var binding = new Binding(ForegroundProperty.Name)
{
RelativeSource = new RelativeSource
(RelativeSourceMode.FindAncestor, typeof (WindowsProgressRing), 1)
};
BindingOperations.SetBinding(ellipse, Shape.FillProperty, binding);
// Placement
Grid.SetColumn(ellipse, 2);
Grid.SetRow(ellipse, 0);
}
wpr.SetStoryBoard(wpr.Speed);
}
/// <summary>
/// Items callback.
/// </summary>
/// <param name="d">The d.</param>
/// <param name="basevalue">The base value.</param>
/// <returns>System.Object.</returns>
private static object ItemsValueCallback(DependencyObject d, object basevalue)
{
if ((int)basevalue > 20)
return 20;
if ((int)basevalue < 1)
return 1;
return basevalue;
}
#endregion
#endregion
/// <summary>
/// Sets the story board.
/// </summary>
/// <param name="speed">The speed.</param>
private void SetStoryBoard(Duration speed)
{
int delay = 0;
foreach (Ellipse ellipse in partBody.Children)
{
ellipse.RenderTransform = new RotateTransform(0);
var animation = new DoubleAnimation(0, -360, speed)
{
RepeatBehavior = RepeatBehavior.Forever,
EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseInOut },
BeginTime = TimeSpan.FromMilliseconds(delay += 100)
};
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
Storyboard.SetTarget(animation, ellipse);
Storyboard.SetTargetProperty(animation,
new PropertyPath("(Rectangle.RenderTransform).(RotateTransform.Angle)"));
storyboard.Begin();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowsProgressRing" /> class.
/// </summary>
public WindowsProgressRing()
{
var res = (ResourceDictionary)Application.LoadComponent(new Uri
("/WindowsProgressRing;component/Themes/WindowsProgressRingStyle.xaml", UriKind.Relative));
Style = (Style)res["WindowsProgressRingStyle"];
}
/// <summary>
/// When overridden in a derived class, is invoked whenever application code
/// or internal processes call <see cref="M:System.Windows.FrameworkElement.ApplyTemplate" />.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
partBody = Template.FindName("PART_body", this) as Grid;
ItemsChanged(this, new DependencyPropertyChangedEventArgs(ItemsProperty, 0, Items));
SpeedChanged(this, new DependencyPropertyChangedEventArgs
(SpeedProperty, Duration.Forever, Speed));
}
}
}
使用进度环
示例应用程序以不同的设置显示进度环,以便你了解控件的灵活性。
图片:看起来有点奇怪,但只是运行应用程序的快照。
这是 MainWindow.xaml
<Window x:Class="WindowsProgressRingSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:NMT.Wpf.Controls;assembly=WindowsProgressRing"
Title="WindowsProgressRing Sample" Height="284" Width="290" >
<Grid>
<controls:WindowsProgressRing Foreground="Black"
Speed="0:0:2.5" Margin="10,10,167,141" Items="6" />
<controls:WindowsProgressRing Foreground="White"
Background="DodgerBlue" Speed="0:0:2.5"
Margin="176,10,10,149" Items="6" />
<controls:WindowsProgressRing Foreground="Red"
Speed="0:0:2.5" Margin="10,117,222,86" Items="1" />
<controls:WindowsProgressRing Foreground="Blue"
Speed="0:0:2.5" Margin="65,117,167,86" Items="3" />
<controls:WindowsProgressRing Foreground="Green"
Speed="0:0:2.5" Margin="167,117,65,86" Items="10" />
<controls:WindowsProgressRing Foreground="Purple"
Speed="0:0:2.5" Margin="222,117,10,86" Items="20" />
<controls:WindowsProgressRing Foreground="DeepPink"
Speed="0:0:5" Margin="10,193,222,10" Items="20"/>
<controls:WindowsProgressRing Foreground="Orange"
Speed="0:0:7" Margin="65,193,167,10" Items="5"/>
<controls:WindowsProgressRing Foreground="DeepSkyBlue"
Speed="0:0:1.25" Margin="167,193,65,10" Items="5"/>
<controls:WindowsProgressRing Foreground="DarkSlateGray"
Speed="0:0:.8" Margin="222,193,10,10" Items="3"/>
</Grid>
</Window>
关注点
我将椭圆的 Fill
绑定到父级的 Foreground
画笔。
我最初是在 XAML 中使用 TemplatedParent
作为 RelativeSource
来完成的,就像这样
<Ellipse x:Name="PART_ellipse1" Grid.Column="2" Grid.Row="0"
Fill="{TemplateBinding Foreground}" ClipToBounds="False" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RenderTransformOrigin="0.5,2.5"/>
我真的尝试用代码实现相同的解决方案,但我只能使用 FindAncestor
才能使其工作。
// Binding
var binding = new Binding(ForegroundProperty.Name)
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof (WindowsProgressRing), 1)
};
BindingOperations.SetBinding(ellipse, Shape.FillProperty, binding);
结论
就个人而言,我发现 WPF 解决方案要简单得多,而且速度也快得多。
历史
- 初始版本