[WPF自定义控件库] 模仿UWP的ProgressRing
1. 为什么需要ProgressRing
虽然我认为这个控件库的控件需要模仿Aero2的外观,但总有例外,其中一个就是ProgressRing。ProgressRing是来自UWP的控件,部分代码参考了 这里。ProgressRing的使用方式运行效果如下:
<kino:ProgressRing IsActive="True"
Height="40"
Width="40"
Margin="8"
MinHeight="9"
MinWidth="9" />

在Windows 10中ProgressRing十分常见,而且十分好用。它还支持自适应尺寸,在紧凑的地方使用ProgressRing会给UI增色不少,而且不会显得格格不入:

那为什么不使用ProgressBar?其中一个原因是ProgressBar功能太多,而我很多时候只需要一个简单的显示正在等待的元素,另一个原因是条状的ProgressBar在紧凑的地方不好看,所以才需要结构相对简单的ProgressRing。
2. 基本结构
[TemplateVisualState(GroupName = VisualStates.GroupActive, Name = VisualStates.StateActive)]
[TemplateVisualState(GroupName = VisualStates.GroupActive, Name = VisualStates.StateInactive)]
public partial class ProgressRing : Control
{
// Using a DependencyProperty as the backing store for IsActive. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.Register("IsActive", typeof(bool), typeof(ProgressRing), new PropertyMetadata(false, new PropertyChangedCallback(IsActiveChanged)));
private bool hasAppliedTemplate = false;
public ProgressRing()
{
DefaultStyleKey = typeof(ProgressRing);
}
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set { SetValue(IsActiveProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
hasAppliedTemplate = true;
UpdateState(IsActive);
}
private static void IsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
var pr = (ProgressRing)d;
var isActive = (bool)args.NewValue;
pr.UpdateState(isActive);
}
private void UpdateState(bool isActive)
{
if (hasAppliedTemplate)
{
string state = isActive ? VisualStates.StateActive : VisualStates.StateInactive;
VisualStateManager.GoToState(this, state, true);
}
}
}
ProgressRing的基本代码如上所示,它只包含IsActive这个属性,并使用这个属性控制它在Active和Inactive两种状态之间切换。参考Silverlight Toolkit,我也把常用的各种VisualState的状态名称作为常量写到一个统一的VisualStates类里:
#region GroupActive
/// <summary>
/// Active state.
/// </summary>
public const string StateActive = "Active";
/// <summary>
/// Inactive state.
/// </summary>
public const string StateInactive = "Inactive";
/// <summary>
/// Active state group.
/// </summary>
public const string GroupActive = "ActiveStates";
#endregion GroupActive
3. 旋转
XAML部分几乎全部照抄UWP的ProgressRing,所以实际运行效果和UWP的ProgressRing很像,区别很小。
通常来说,ProgressRing的Active状态持续时间不会太长,而且ProgressRing的尺寸也不会太大,所以ProgressRing的Active状态可以说不计成本。Active状态下有5个Ellipse 不停旋转,或者说做绕着中心点做圆周运动,而为了不需要任何计算圆周中心点的代码,ProgressRing给每个Ellipse外面都套上一个Canvas,让这整个Canvas旋转。XAML大概这样:
<Storyboard RepeatBehavior="Forever" x:Key="Sb">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="E1R" BeginTime="0" Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-110" KeySpline="0.13,0.21,0.1,0.7" />
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="10" KeySpline="0.02,0.33,0.38,0.77" />
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="93" />
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="205" KeySpline="0.57,0.17,0.95,0.75" />
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="357" KeySpline="0,0.19,0.07,0.72" />
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="439" />
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="585" KeySpline="0,0,0.95,0.37" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Canvas RenderTransformOrigin=".5,.5" Height="100" Width="100">
<Canvas.RenderTransform>
<RotateTransform x:Name="E1R" />
</Canvas.RenderTransform>
<Ellipse x:Name="E1"
Width="20"
Height="20"
Fill="MediumPurple" />
</Canvas>

然后运行效果这样:

4. 自适应大小
为了让ProgressRing中各个Ellipse都可以自适应大小,ProgressRing提供了一个TemplateSettings属性,类型为TemplateSettingValues,它里面包含以下记个依赖属性:
public double MaxSideLength
{
get { return (double)GetValue(MaxSideLengthProperty); }
set { SetValue(MaxSideLengthProperty, value); }
}
public double EllipseDiameter
{
get { return (double)GetValue(EllipseDiameterProperty); }
set { SetValue(EllipseDiameterProperty, value); }
}
public Thickness EllipseOffset
{
get { return (Thickness)GetValue(EllipseOffsetProperty); }
set { SetValue(EllipseOffsetProperty, value); }
}
XAML中的元素大小及布局绑定到这些属性:
<Grid x:Name="Ring"
Background="{TemplateBinding Background}"
MaxWidth="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
MaxHeight="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
Visibility="Collapsed"
RenderTransformOrigin=".5,.5"
FlowDirection="LeftToRight">
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E1R" />
</Canvas.RenderTransform>
<Ellipse x:Name="E1"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}" />
</Canvas>
每当ProgressRing调用MeasureOverrride都重新计算这些值:
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
var width = 20d;
var height = 20d;
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this) == false)
{
width = double.IsNaN(Width) == false ? Width : availableSize.Width;
height = double.IsNaN(Height) == false ? Height : availableSize.Height;
}
TemplateSettings = new TemplateSettingValues(Math.Min(width, height));
return base.MeasureOverride(availableSize);
}
public TemplateSettingValues(double width)
{
if (width <= 40)
{
EllipseDiameter = (width / 10) + 1;
}
else
{
EllipseDiameter = width / 10;
}
MaxSideLength = width - EllipseDiameter;
EllipseOffset = new System.Windows.Thickness(0, EllipseDiameter * 2.5, 0, 0);
}
这样就实现了外观的自适应大小功能。需要注意的是,过去很多人喜欢将这种重新计算大小的操作放到LayoutUpdated事件中进行,但LayoutUpdated是整个布局的最后一步,这时候如果改变了控件的大小有可能重新触发Measure和Arrange及LayoutUpdated,这很可能引起“布局循环”的异常。正确的做法是将计算尺寸及改变尺寸的操作都放到最初的MeasureOverride中。
TemplateSettings在UWP中很长见到,它的其它用法可以参考这篇文章:了解模板化控件:UI指南
5. 参考
brian dunnington - ProgressRing for Windows Phone 8
FrameworkElement.MeasureOverride(Size) Method (System.Windows) Microsoft Docs.html
UIElement.InvalidateMeasure Method (System.Windows) Microsoft Docs
UIElement.IsMeasureValid Property (System.Windows) Microsoft Docs
UIElement.LayoutUpdated Event (System.Windows) Microsoft Docs
6. 源码
Kino.Toolkit.Wpf_ProgressRing at master
[WPF自定义控件库] 模仿UWP的ProgressRing的更多相关文章
- WPF 如何创建自己的WPF自定义控件库
在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...
- [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)
原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...
- [WPF自定义控件库]使用WindowChrome自定义RibbonWindow
原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...
- [WPF自定义控件库] 让Form在加载后自动获得焦点
原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...
- [WPF自定义控件库]好用的VisualTreeExtensions
1. 前言 A long time ago in a galaxy far, far away....微软在Silverlight Toolkit里提供了一个好用的VisualTreeExtensio ...
- [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...
- [WPF自定义控件库]自定义Expander
1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能).这 ...
- [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互
1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void ...
- [WPF自定义控件库]为Form和自定义Window添加FunctionBar
1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...
随机推荐
- Abstract Factory抽象工厂模式
抽象工厂模式是是用一个超级工厂去创建其他工厂,简单点说就是工厂的父类,属于创建型模式. 目标:提供一个创建一组对象的方法,而无需指定它们具体的类(同工厂方法). 使用场景:系统的产品有多于一个的产品族 ...
- Eclipse For Mac下中文乱码解决
在Mac os 版本的eclipse下引入java项目或是源代码,经常会碰到其中中文部分都是乱码.对于这一问题,经过小试,可以解决. 1.打开eclipse 偏好设置 2.General ——> ...
- .Net基础篇_学习笔记_第六天_for循环语法_正序输出和倒序输出
for TAB 和 forr TAB using System; using System.Collections.Generic; using System.Linq; using System. ...
- DevExpress的对话框XtraMessageBox的使用
场景 在Winform中一般弹出对话框使用的是MessageBox,而在 DevExpress中使用的是XtraMessageBox实现对话框. 效果 实现 首先新建确认按钮的调用方法: public ...
- 在命令行已经pip install flask-script,但是导包时出错
问题:(已经安装好了flask-script,但是导入不成功) 然后在代码中导入相应的包:(报红) 后来发现是在自己创建项目的时候勾选的是创建的是在虚拟环境下的项目,所以环境有问题 所以我应该在虚拟环 ...
- Java基本数据类型转换及运算符
上次我们说到完了Java中的基本数据类型,今天我们来说说Java中的基本数据类型转换和Java中的运算符 基本数据类型转换 java中可以从任意基本数据类型转型到外的基本数据类型 注意:(boolea ...
- APP自動化測試腳本2
package com.lemon.day01; import java.net.MalformedURLException; import java.net.URL; import java.uti ...
- 一次五分钟 angularJS (1)—— Binding
引用angularjs 需要使用AngularJS,需要引用AngularJS的文件 ng-app 要将angular用到页面绑定的时候,我们需要指明它的作用域. 在上图中,ng-app=" ...
- CentOS 7上编写自定义系统审计规则
1)简介 Linux审计系统创建审计跟踪,这是一种跟踪系统上各种信息的方法.它可以记录大量数据,如事件类型,日期和时间,用户ID,系统调用,进程,使用的文件,SELinux上下文和敏感度级别.它可以跟 ...
- frp 端口映射
简介 frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp 协议,为 http 和 https 应用协议提供了额外的能力,且尝试性支持了点对点穿透. 场景 利用处于内网或防火墙后 ...