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的更多相关文章

  1. WPF 如何创建自己的WPF自定义控件库

    在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...

  2. [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)

    原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...

  3. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  4. [WPF自定义控件库] 让Form在加载后自动获得焦点

    原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...

  5. [WPF自定义控件库]好用的VisualTreeExtensions

    1. 前言 A long time ago in a galaxy far, far away....微软在Silverlight Toolkit里提供了一个好用的VisualTreeExtensio ...

  6. [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

    1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...

  7. [WPF自定义控件库]自定义Expander

    1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能).这 ...

  8. [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

    1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void ...

  9. [WPF自定义控件库]为Form和自定义Window添加FunctionBar

    1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...

随机推荐

  1. unittest核心要素

    1 TestCase 一个TestCase的实例就是一个测试用例.什么是测试用例呢?就是一个完整的测试流程, 包括测试环境的准备(setUp),执行测试代码(run),以及测试后环境的还原(tearD ...

  2. mybatis动态拼接条件的技巧 where 1=1 或者where标签

    /**     * 根据输入的学生信息进行条件检索     * 1. 当只输入用户名时, 使用用户名进行模糊检索:     * 2. 当只输入邮箱时, 使用性别进行完全匹配     * 3. 当用户名 ...

  3. charles 编辑菜单总结

    本文参考:charles 编辑菜单总结 charles中proxy菜单的介绍:我的是4.1.2版本,mac系统下的菜单大同小异: 如下图: 这里其实都是常用的功能: 大概可以分为5个大块,看下分割线就 ...

  4. vue2.0搭建vue手脚架(vue-cli)

    1.安装node.js 从node官网下载并安装node,安装步骤很简单,只要一路“next”就可以了.安装完成后,打开命令行工具输入命令node -v,如下图,如果出现对应版本号,就说明安装成功了. ...

  5. [Spark] 01 - What is Spark

    大数据 云计算概念 课程:Spark编程基础(Python版) 大数据4V特性 Volumn, Variety, Velocity, Value. 思维方式 通过数据发现问题,再解决问题. 速度更重要 ...

  6. 修改tomcat 使用的JVM的内存

    一,前言 在文章让tomcat使用指定JDK中,我让tomcat成功使用了我指定的JDK1.8,而不是环境变量中配置的JDK10.本篇文章我们就来探讨一下怎么设置tomcat使用的JVM的内存. 为什 ...

  7. GStreamer基础教程08 - 多线程

    摘要 GStreamer框架会自动处理多线程的逻辑,但在某些情况下,我们仍然需要根据实际的情况自己将部分Pipeline在单独的线程中执行,本文将介绍如何处理这种情况. GStreamer多线程 GS ...

  8. element-ui入门

    element-ui入门 element-ui是一个ui库,它不依赖于vue.但是却是当前和vue配合做项目开发的一个比较好的ui框架. Layout布局(el-row.el-col) element ...

  9. mybatis 启用延迟加载和按需加载配置

    启用延迟加载和按需加载 Mybatis配置文件中通过两个属性lazyLoadingEnabled和aggressiveLazyLoading来控制延迟加载和按需加载. lazyLoadingEnabl ...

  10. C语言I—2019秋作业02

    1.[新增内容] 这个作业属于那个课程 C语言程序设计 这个作业要求在哪里 <C语言I-2019秋作业02> 我在这个课程的目标是 这个作业在那个具体方面帮助我实现目标 <实现一些基 ...