Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 483
Shahin Dohan Avatar asked Oct 30 '25 16:10

Shahin Dohan


1 Answers

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 :-)

like image 169
Shahin Dohan Avatar answered Nov 02 '25 09:11

Shahin Dohan