Shahin Dohan
Shahin Dohan

Reputation: 6922

WPF create dashed ellipse of individual blocks

Is there any easy way to create a dashed ellipse made up of individual horizontal dashes, where the dash sizes are consistent, and their amount can be specified?

Something like this:

enter image description here

I want to be able to control each dash individually, like changing its color or binding it to an action in my viewmodel.

The only way I can think of to achieve this, is to create a custom control that contains a Path element for each dash, together making up an ellipse shape, having to calculate the Path data based on the amount of dashes and size of the ellipse.

Upvotes: 4

Views: 892

Answers (1)

Shahin Dohan
Shahin Dohan

Reputation: 6922

I came back to this problem now, and managed to solve it in a very flexible and generic way. The requirments have changed a bit since then, no need for binding, but it can be added easily.

Note that this is a circle, which is what I wanted. The question should really say circle rather than ellipse, even though a circle is an ellipse, but I digress...

Here's the UserControl I came up with:

StatusRing.xaml.cs

public partial class StatusRing
{
    #region Dependency Property registrations

    public static readonly DependencyProperty DashesProperty = DependencyProperty.Register("Dashes",
        typeof(int), typeof(StatusRing), new PropertyMetadata(32, DashesChanged));

    public static readonly DependencyProperty DiameterProperty = DependencyProperty.Register("Diameter",
        typeof(double), typeof(StatusRing), new PropertyMetadata(150.00, DiameterChanged));

    public static readonly DependencyProperty DashHeightProperty = DependencyProperty.Register("DashHeight",
        typeof(double), typeof(StatusRing), new PropertyMetadata(20.00, DashHeightChanged));

    public static readonly DependencyProperty DashWidthProperty = DependencyProperty.Register("DashWidth",
        typeof(double), typeof(StatusRing), new PropertyMetadata(5.00, DashWidthChanged));

    public static readonly DependencyProperty DashFillProperty = DependencyProperty.Register("DashFill",
        typeof(SolidColorBrush), typeof(StatusRing), new PropertyMetadata(Brushes.DimGray, DashFillChanged));

    public static readonly DependencyProperty DashAccentFillProperty = DependencyProperty.Register("DashAccentFill",
        typeof(SolidColorBrush), typeof(StatusRing), new PropertyMetadata(Brushes.White, DashAnimationFillChanged));

    public static readonly DependencyProperty TailSizeProperty = DependencyProperty.Register("TailSize",
        typeof(int), typeof(StatusRing), new PropertyMetadata(10, TailSizeChanged));

    public static readonly DependencyProperty AnimationSpeedProperty = DependencyProperty.Register("AnimationSpeed",
        typeof(double), typeof(StatusRing), new PropertyMetadata(50.00, AnimationSpeedChanged));

    public static readonly DependencyProperty IsPlayingProperty = DependencyProperty.Register("IsPlaying",
        typeof(bool), typeof(StatusRing), new PropertyMetadata(false, IsPlayingChanged));

    #endregion Dependency Property registrations

    private readonly Storyboard glowAnimationStoryBoard = new Storyboard();

    public StatusRing()
    {
        Loaded += OnLoaded;
        InitializeComponent();
    }

    #region Dependency Properties

    public int Dashes
    {
        get => (int)GetValue(DashesProperty);
        set => SetValue(DashesProperty, value);
    }

    public double Diameter
    {
        get => (double)GetValue(DiameterProperty);
        set => SetValue(DiameterProperty, value);
    }

    public double Radius => Diameter / 2;

    public double DashHeight
    {
        get => (double)GetValue(DashHeightProperty);
        set => SetValue(DashHeightProperty, value);
    }

    public double DashWidth
    {
        get => (double)GetValue(DashWidthProperty);
        set => SetValue(DashWidthProperty, value);
    }

    public Brush DashFill
    {
        get => (SolidColorBrush)GetValue(DashFillProperty);
        set => SetValue(DashFillProperty, value);
    }

    public Brush DashAccentFill
    {
        get => (SolidColorBrush)GetValue(DashAccentFillProperty);
        set => SetValue(DashAccentFillProperty, value);
    }

    public int TailSize
    {
        get => (int)GetValue(TailSizeProperty);
        set => SetValue(TailSizeProperty, value);
    }

    public double AnimationSpeed
    {
        get => (double)GetValue(AnimationSpeedProperty);
        set => SetValue(AnimationSpeedProperty, value);
    }

    public bool IsPlaying
    {
        get => (bool)GetValue(IsPlayingProperty);
        set => SetValue(IsPlayingProperty, value);
    }

    #endregion Dependency Properties

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        var thisControl = sender as StatusRing;
        Recreate(thisControl);
    }

    #region Dependency Property callbacks

    private static void DashesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void DiameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void DashHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void DashWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void DashFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void DashAnimationFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void TailSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        Recreate(thisControl);
    }

    private static void AnimationSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        if (thisControl.IsLoaded)
        {
            thisControl.glowAnimationStoryBoard.Stop();
            thisControl.glowAnimationStoryBoard.Children.Clear();

            ApplyAnimations(thisControl);

            thisControl.glowAnimationStoryBoard.Begin();
        }
    }

    private static void IsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var thisControl = d as StatusRing;
        if (thisControl.IsLoaded)
        {
            var isPlaying = (bool)e.NewValue;
            if (isPlaying)
            {
                thisControl.glowAnimationStoryBoard.Begin();
            }
            else
            {
                thisControl.glowAnimationStoryBoard.Stop();
            }
        }
    }

    #endregion Dependency Property callbacks

    private static void Recreate(StatusRing thisControl)
    {
        if (thisControl.IsLoaded)
        {
            thisControl.glowAnimationStoryBoard.Stop();
            thisControl.glowAnimationStoryBoard.Children.Clear();
            thisControl.RootCanvas.Children.Clear();

            Validate(thisControl);
            BuildRing(thisControl);

            ApplyAnimations(thisControl);

            if (thisControl.IsPlaying)
            {
                thisControl.glowAnimationStoryBoard.Begin();
            }
            else
            {
                thisControl.glowAnimationStoryBoard.Stop();
            }
        }
    }

    private static void Validate(StatusRing thisControl)
    {
        if (thisControl.TailSize > thisControl.Dashes)
        {
            throw new Exception("TailSize cannot be larger than amount of dashes");
        }
    }

    private static void BuildRing(StatusRing thisControl)
    {
        var angleStep = (double)360 / thisControl.Dashes;

        for (double i = 0; i < 360; i = i + angleStep)
        {
            var rect = new Rectangle
            {
                Fill = thisControl.DashFill,
                Height = thisControl.DashHeight,
                Width = thisControl.DashWidth
            };

            //Rotate dash to follow circles circumference 
            var centerY = thisControl.Radius;
            var centerX = thisControl.DashWidth / 2;
            var rotateTransform = new RotateTransform(i, centerX, centerY);
            rect.RenderTransform = rotateTransform;

            var offset = thisControl.Radius - thisControl.DashWidth / 2;
            rect.SetValue(Canvas.LeftProperty, offset);

            thisControl.RootCanvas.Children.Add(rect);
        }

        thisControl.RootCanvas.Width = thisControl.Diameter;
        thisControl.RootCanvas.Height = thisControl.Diameter;
    }

    private static void ApplyAnimations(StatusRing thisControl)
    {
        var baseColor = ((SolidColorBrush)thisControl.DashFill).Color;
        var animatedColor = ((SolidColorBrush)thisControl.DashAccentFill).Color;

        var dashes = thisControl.RootCanvas.Children.OfType<Rectangle>().ToList();

        double animationPeriod = thisControl.AnimationSpeed;
        double glowDuration = animationPeriod * thisControl.TailSize;

        for (int i = 0; i < dashes.Count; i++)
        {
            var beginTime = TimeSpan.FromMilliseconds(animationPeriod * i);

            var colorAnimation = new ColorAnimationUsingKeyFrames
            {
                BeginTime = beginTime,
                RepeatBehavior = RepeatBehavior.Forever
            };

            var toFillColor = new LinearColorKeyFrame(animatedColor, TimeSpan.Zero);
            colorAnimation.KeyFrames.Add(toFillColor);

            var dimToBase = new LinearColorKeyFrame(baseColor, TimeSpan.FromMilliseconds(glowDuration));
            colorAnimation.KeyFrames.Add(dimToBase);

            var restingTime = animationPeriod * dashes.Count;
            var delay = new LinearColorKeyFrame(baseColor, TimeSpan.FromMilliseconds(restingTime));
            colorAnimation.KeyFrames.Add(delay);

            Storyboard.SetTarget(colorAnimation, dashes[i]);
            Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("(Fill).(SolidColorBrush.Color)"));

            thisControl.glowAnimationStoryBoard.Children.Add(colorAnimation);
        }
    }
}

StatusRing.xaml:

<UserControl x:Class="WpfPlayground.StatusRing"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
<Canvas x:Name="RootCanvas" />

Usage:

<local:StatusRing Diameter="250" 
                  Dashes="32"
                  TailSize="16"
                  IsPlaying="True" />

Result:

StatusRing in action

The number of dashes, length and speed of animation, etc... are all configurable. The naming of the dependency properties could be better though...

Enjoy :-)

Upvotes: 2

Related Questions