专栏首页dino.c的专栏[WPF自定义控件库] 模仿UWP的ProgressRing

[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中。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • SAP各模块常见配置问题汇总

    20637 Inserting Fields in Validations and Substitutions

    用户5495712
  • JSP基础--九大内置对象

    Object findAttribute(String name):依次在page、request、session、application范围查找名称为name...

    eadela
  • React ref 的前世今生

    众所周知,React 通过声明式的渲染机制把复杂的 DOM 操作抽象成为简单的 state 与 props 操作,一时圈粉无数,一夜间将前端工程师从面条式的 D...

    Nealyang
  • 多线程真的会使用CPU所有的内核吗?

    学习多线程的时候,我们都知道如果多个线程分配到CPU多个内核是可以并发的执行。但真的是这样的吗?

    用户5224393
  • JVM故障分析及性能优化实战(IV)——jstack生成的Thread Dump日志线程状态

    前面文章中只分析了Thread Dump日志文件的结构,今天针对日志文件中 Java EE middleware, third party & custom a...

    IT技术小咖
  • 什么样的代码是好代码?

    关于什么是好代码,软件行业烂大街的名词一大堆,什么高内聚、低耦合、可复用、可扩展、健壮性等等。也有所谓设计6原则—SOLID:

    梁规晓
  • 推荐收藏 | 100个数据分析常用指标和术语

    有个朋友是金融行业产品经理,最近在对已有的站内用户做分层与标签分类,需要对用户进行聚类分析。一般从事数据分析行业的朋友对这类词并不陌生,但是像市场运营人员就会把...

    石晓文
  • 浅谈 DNS

    概念:万维网(WWW是环球信息网的缩写,亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),作为域名和IP地址相互映射的一个...

    小麦苗DBA宝典
  • 【OCP最新题库解析(052)--题51】 You want to use the ALTER SYSTEM statement

    该系列专题为2018年4月OCP-052考题变革后的最新题库。题库为小麦苗解答,若解答有不对之处,可留言,也可联系小麦苗进行修改。

    小麦苗DBA宝典
  • IOS-swift5.1快速入门之旅

    传统表明,新语言中的第一个程序应在屏幕上打印“Hello,world!”字样。在Swift中,这可以在一行中完成:

    eadela

扫码关注云+社区

领取腾讯云代金券