Creating custom Shapes for UWP Apps

In WPF, creating custom shapes was as easy as extending Shape and overriding DefiningGeometry. In UWP, it isn’t that easy.

In UWP, Shape does not have a DefiningGeometry property. But instead, the Path class is not sealed. This means it is possible to inherit from Path. So this is what we’ll do.

Path has a property Data, its documentation says:

Gets or sets a Geometry that specifies the shape to be drawn.

Its documentation further specifies what Data can contain:

One of the simple geometries EllipseGeometry, LineGeometry, or RectangleGeometry. A single GeometryGroup, which supports other geometries as child elements. A PathGeometry, which supports child object elements that establish a path geometry object model of figures and segments. See the “XAML Values” section of PathGeometry.


So, essentially the only thing we have to do is setting the Data property to specify how we want our custom shape to be rendered.

Developing a Candlestick Shape

To illustrate what I’ve explained above, and to show you some gotchas, I’ll walk you through the process of developing a Candlestick shape.

At first, we have the empty class extending Path.

public class CandlestickShape : Path { }

Then we have to add some dependency properties that are needed for the candlestick:

public double StartValue
{
    get { return Convert.ToDouble(GetValue(StartValueProperty)); }
    set { SetValue(StartValueProperty, value); }
}
public static readonly DependencyProperty StartValueProperty =
    DependencyProperty.Register("StartValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

public double EndValue
{
    get { return Convert.ToDouble(GetValue(EndValueProperty)); }
    set { SetValue(EndValueProperty, value); }
}
public static readonly DependencyProperty EndValueProperty =
    DependencyProperty.Register("EndValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

public double MinValue
{
    get { return Convert.ToDouble(GetValue(MinValueProperty)); }
    set { SetValue(MinValueProperty, value); }
}
public static readonly DependencyProperty MinValueProperty =
    DependencyProperty.Register("MinValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

public double MaxValue
{
    get { return Convert.ToDouble(GetValue(MaxValueProperty)); }
    set { SetValue(MaxValueProperty, value); }
}
public static readonly DependencyProperty MaxValueProperty =
    DependencyProperty.Register("MaxValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

public double PixelPerPoint
{
    get { return Convert.ToDouble(GetValue(PointsPerPixelProperty)); }
    set { SetValue(PointsPerPixelProperty, value); }
}
public static readonly DependencyProperty PointsPerPixelProperty =
    DependencyProperty.Register("PixelPerPoint", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

Most of them should be straightforward. PixelPerPoint is needed because we don’t want the Candlesticks to span the whole height. Since the scale of the chart which uses the candlestick shape is not known by it, the height essentially has to be set by PixelPerPoint.

Calculating the Layout

This step is also quite easy. There are 3 basic steps:

  1. Calculating the dimensions of the top line
  2. Calculating the dimensions of the bottom line
  3. Calculating the dimensions of the body

This method does one additional thing: It sets the Stroke and Fill properties. This will override any value the user of this class set for these properties. I added these because I’m assuming that Stroke should always be black and Fill should be green if the candle is positive and red if it is negative.

If you like, you might as well create extra dependency properties for it.

private void SetRenderData()
{
    var maxBorderValue = Math.Max(this.StartValue, this.EndValue);
    var minBorderValue = Math.Min(this.StartValue, this.EndValue);
    double topLineLength = (this.MaxValue - maxBorderValue) * this.PixelPerPoint;
    double bottomLineLength = (minBorderValue - this.MinValue) * this.PixelPerPoint;
    double bodyLength = (this.EndValue - this.StartValue) * this.PixelPerPoint;

    var fillColor = new SolidColorBrush(Colors.Green);
    if (bodyLength < 0)
        fillColor = new SolidColorBrush(Colors.Red);

    bodyLength = Math.Abs(bodyLength);

    var bodyGeometry = new RectangleGeometry
    {
        Rect = new Rect(new Point(0, topLineLength), new Point(this.Width, topLineLength + bodyLength)),
    };

    var topLineGeometry = new LineGeometry
    {
        StartPoint = new Point(this.Width / 2, 0),
        EndPoint = new Point(this.Width / 2, topLineLength)
    };

    var bottomLineGeometry = new LineGeometry
    {
        StartPoint = new Point(this.Width / 2, topLineLength + bodyLength),
        EndPoint = new Point(this.Width / 2, topLineLength + bodyLength + bottomLineLength)
    };

    this.Data = new GeometryGroup
    {
        Children = new GeometryCollection
        {
            bodyGeometry,
            topLineGeometry,
            bottomLineGeometry
        }
    };
    this.Fill = fillColor;
    this.Stroke = new SolidColorBrush(Colors.Black);
}

To tell the Gui components which will use Candlestick the space it will occupy, ArrangeOverride and MeasureOverride have to be overwritten:

protected override Size ArrangeOverride(Size finalSize)
{
    double height = (MaxValue - MinValue) * PixelPerPoint;
    return new Size(this.Width, UnwrapDouble(height));
}

protected override Size MeasureOverride(Size availableSize)
{
    double height = (MaxValue - MinValue) * PixelPerPoint;
    return new Size(this.Width, height);
}

Updating the Layout on Change

Here is the tricky part. It seems to be that ArrangeOverride would be the appropriate place for it. There is just one problem: Setting Data will trigger another layout run, , which again executes ArrangeOverride so it will lead to an endless thread of layout runs.
UWP detects that and throws an exception.

Layout cycle detected. Layout could not complete

The only way I found to reliably update the shape is by listening for changes on all properties that affect it. To accomplish this, property changed callbacks are registered in the constructor of CandlestickShape.

public CandlestickShape()
{
    this.RegisterPropertyChangedCallback(CandlestickShape.WidthProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    this.RegisterPropertyChangedCallback(CandlestickShape.StartValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    this.RegisterPropertyChangedCallback(CandlestickShape.EndValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    this.RegisterPropertyChangedCallback(CandlestickShape.MinValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    this.RegisterPropertyChangedCallback(CandlestickShape.MaxValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    this.RegisterPropertyChangedCallback(CandlestickShape.PointsPerPixelProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
}

private void RenderAffectingPropertyChanged(DependencyObject o, DependencyProperty e)
{
    (o as CandlestickShape)?.SetRenderData();
}

Usage

This is an example of how the shape can be used. Make sure to set the Width property, otherwise you’ll get weird exceptions. You could also adapt the CandelstickShape to use a default width, which is probably a better choice (defensive programming and all).

<local:CandlestickShape StartValue="100" 
                        EndValue="200" 
                        MinValue="50" 
                        MaxValue="250" 
                        PixelPerPoint="1" 
                        Width="10"></local:CandlestickShape>

Result

This is the resulting candlestick:

Candlestick

Here the whole class for reference:

public class CandlestickShape : Path
{
    public double StartValue
    {
        get { return Convert.ToDouble(GetValue(StartValueProperty)); }
        set { SetValue(StartValueProperty, value); }
    }
    public static readonly DependencyProperty StartValueProperty =
        DependencyProperty.Register("StartValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

    public double EndValue
    {
        get { return Convert.ToDouble(GetValue(EndValueProperty)); }
        set { SetValue(EndValueProperty, value); }
    }
    public static readonly DependencyProperty EndValueProperty =
        DependencyProperty.Register("EndValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

    public double MinValue
    {
        get { return Convert.ToDouble(GetValue(MinValueProperty)); }
        set { SetValue(MinValueProperty, value); }
    }
    public static readonly DependencyProperty MinValueProperty =
        DependencyProperty.Register("MinValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

    public double MaxValue
    {
        get { return Convert.ToDouble(GetValue(MaxValueProperty)); }
        set { SetValue(MaxValueProperty, value); }
    }
    public static readonly DependencyProperty MaxValueProperty =
        DependencyProperty.Register("MaxValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

    /// <summary>
    /// Defines how many Pixel should be drawn for one Point
    /// </summary>
    public double PixelPerPoint
    {
        get { return Convert.ToDouble(GetValue(PointsPerPixelProperty)); }
        set { SetValue(PointsPerPixelProperty, value); }
    }
    public static readonly DependencyProperty PointsPerPixelProperty =
        DependencyProperty.Register("PixelPerPoint", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));

    public CandlestickShape()
    {
        this.RegisterPropertyChangedCallback(CandlestickShape.WidthProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
        this.RegisterPropertyChangedCallback(CandlestickShape.StartValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
        this.RegisterPropertyChangedCallback(CandlestickShape.EndValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
        this.RegisterPropertyChangedCallback(CandlestickShape.MinValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
        this.RegisterPropertyChangedCallback(CandlestickShape.MaxValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
        this.RegisterPropertyChangedCallback(CandlestickShape.PointsPerPixelProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
    }

    private void RenderAffectingPropertyChanged(DependencyObject o, DependencyProperty e)
    {
        (o as CandlestickShape)?.SetRenderData();
    }

    private void SetRenderData()
    {
        var maxBorderValue = Math.Max(this.StartValue, this.EndValue);
        var minBorderValue = Math.Min(this.StartValue, this.EndValue);
        double topLineLength = (this.MaxValue - maxBorderValue) * this.PixelPerPoint;
        double bottomLineLength = (minBorderValue - this.MinValue) * this.PixelPerPoint;
        double bodyLength = (this.EndValue - this.StartValue) * this.PixelPerPoint;

        var fillColor = new SolidColorBrush(Colors.Green);
        if (bodyLength < 0)
            fillColor = new SolidColorBrush(Colors.Red);

        bodyLength = Math.Abs(bodyLength);

        var bodyGeometry = new RectangleGeometry
        {
            Rect = new Rect(new Point(0, topLineLength), new Point(this.Width, topLineLength + bodyLength)),
        };

        var topLineGeometry = new LineGeometry
        {
            StartPoint = new Point(this.Width / 2, 0),
            EndPoint = new Point(this.Width / 2, topLineLength)
        };

        var bottomLineGeometry = new LineGeometry
        {
            StartPoint = new Point(this.Width / 2, topLineLength + bodyLength),
            EndPoint = new Point(this.Width / 2, topLineLength + bodyLength + bottomLineLength)
        };

        this.Data = new GeometryGroup
        {
            Children = new GeometryCollection
            {
                bodyGeometry,
                topLineGeometry,
                bottomLineGeometry
            }
        };
        this.Fill = fillColor;
        this.Stroke = new SolidColorBrush(Colors.Black);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double height = (MaxValue - MinValue) * PixelPerPoint;
        return new Size(this.Width, height);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        double height = (MaxValue - MinValue) * PixelPerPoint;
        return new Size(this.Width, height);
    }
}