• [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

  • 相关阅读:
    可汗学院公开课:统计学
    libsvm 之 easy.py(流程化脚本)注释
    机器学习概览
    学习资源
    libsvm-3.21使用文档
    Machine Learning
    Machine Learning
    MySQL 5.7半同步复制after sync和after commit详解【转】
    网站架构设计【转】
    httpd功能配置之虚拟主机【转】
  • 原文地址:https://www.cnblogs.com/dino623/p/create_a_ProgressRing_for_wpf.html
Copyright © 2020-2023  润新知