#1,151 – Custom Arc Shape, part III

In the previous example of a custom arc shape, we use the standard Stroke and StrokeThickness properties when rendering the shape.  Note, however, that when we increase the thickness of the stroke, part of the stroke lies outside of the boundaries of the shape.

1151-001

We can fix this problem by accounting for the stroke thickness in the drawing logic.  The updated code is shown below.

        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = Math.Max(0.0, RenderSize.Width - StrokeThickness);
                double maxHeight = Math.Max(0.0, RenderSize.Height - StrokeThickness);
                //Console.WriteLine(string.Format("* maxWidth={0}, maxHeight={1}", maxWidth, maxHeight));

                double xStart = maxWidth / 2.0 * Math.Cos(StartAngle * Math.PI / 180.0);
                double yStart = maxHeight / 2.0 * Math.Sin(StartAngle * Math.PI / 180.0);

                double xEnd = maxWidth / 2.0 * Math.Cos(EndAngle * Math.PI / 180.0);
                double yEnd = maxHeight / 2.0 * Math.Sin(EndAngle * Math.PI / 180.0);

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point((RenderSize.Width / 2.0) + xStart,
                                  (RenderSize.Height / 2.0) - yStart),
                        false,
                        false);
                    ctx.ArcTo(
                        new Point((RenderSize.Width / 2.0) + xEnd,
                                  (RenderSize.Height / 2.0) - yEnd),
                        new Size(maxWidth / 2.0, maxHeight / 2),
                        0.0,     // rotationAngle
                        (EndAngle - StartAngle) > 180,   // greater than 180 deg?
                        SweepDirection.Counterclockwise,
                        true,    // isStroked
                        true);
                }

                return geom;
            }
        }

1151-002

 

#1,150 – Custom Arc Shape, part II

Below is code that implements a custom Shape to draw an arc.  The earlier code has been improved to:

  • Support elliptical arcs (height != width)
  • Add dependency properties for start and end angles

Start and end angles are automatically coerced to keep them within the range [0, 360).

    public class Arc : Shape
    {
        // Angle that arc starts at
        public double StartAngle
        {
            get { return (double)GetValue(StartAngleProperty); }
            set { SetValue(StartAngleProperty, value); }
        }

        // DependencyProperty - StartAngle
        private static PropertyMetadata startAngleMetadata =
                new PropertyMetadata(
                    0.0,     // Default value
                    null,    // Property changed callback
                    new CoerceValueCallback(CoerceAngle));   // Coerce value callback

        public static readonly DependencyProperty StartAngleProperty =
            DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc), startAngleMetadata);

        // Angle that arc ends at
        public double EndAngle
        {
            get { return (double)GetValue(EndAngleProperty); }
            set { SetValue(EndAngleProperty, value); }
        }

        // DependencyProperty - EndAngle
        private static PropertyMetadata endAngleMetadata =
                new PropertyMetadata(
                    90.0,     // Default value
                    null,    // Property changed callback
                    new CoerceValueCallback(CoerceAngle));   // Coerce value callback

        public static readonly DependencyProperty EndAngleProperty =
            DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc), endAngleMetadata);

        private static object CoerceAngle(DependencyObject depObj, object baseVal)
        {
            double angle = (double)baseVal;
            angle = Math.Min(angle, 359.9);
            angle = Math.Max(angle, 0.0);
            return angle;
        }

        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = RenderSize.Width;
                double maxHeight = RenderSize.Height;

                double xStart = maxWidth / 2.0 * Math.Cos(StartAngle * Math.PI / 180.0);
                double yStart = maxHeight / 2.0 * Math.Sin(StartAngle * Math.PI / 180.0);

                double xEnd = maxWidth / 2.0 * Math.Cos(EndAngle * Math.PI / 180.0);
                double yEnd = maxHeight / 2.0 * Math.Sin(EndAngle * Math.PI / 180.0);

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point((maxWidth / 2.0) + xStart,
                                   (maxHeight / 2.0) - yStart),
                        false,
                        false);
                    ctx.ArcTo(
                        new Point((maxWidth / 2.0) + xEnd,
                                  (maxHeight / 2.0) - yEnd),
                        new Size(maxWidth / 2.0, maxHeight / 2),
                        0.0,     // rotationAngle
                        (EndAngle - StartAngle) > 180,   // greater than 180 deg?
                        SweepDirection.Counterclockwise,
                        true,    // isStroked
                        true);
                }

                return geom;
            }
        }
    }

We can use the Arc shape as shown below.

1150-001

#1,149 – Drawing an Arc in a Custom Shape

We can use a StreamGeometryContext to render some geometry in a custom Shape element that we can then use in XAML.  Below is an example that draws a simple arc from 0 degress to 90 degrees.  It uses a PolarPoint class to allow describing the arc start and finish as polar coordinates.  (A future post will allow a user to specify arc start and end).

    public class Arc : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = RenderSize.Width;
                double maxHeight = RenderSize.Height;
                double maxRadius = Math.Min(maxWidth, maxHeight) / 2.0;

                PolarPoint arcStart = new PolarPoint(maxRadius, 0.0);
                PolarPoint arcFinish = new PolarPoint(maxRadius, 90.0);

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point((maxWidth / 2.0) + arcStart.X,
                                   (maxHeight / 2.0) - arcStart.Y),
                        false,
                        false);
                    ctx.ArcTo(
                        new Point((maxWidth / 2.0) + arcFinish.X,
                                  (maxHeight / 2.0) - arcFinish.Y),
                        new Size(maxRadius, maxRadius),
                        0.0,     // rotationAngle
                        false,   // greater than 180 deg?
                        SweepDirection.Counterclockwise,
                        true,    // isStroked
                        true);
                }

                return geom;
            }
        }
    }

Using the arc:

        <loc:Arc Stroke="Black" StrokeThickness="1"
                 Height="100" Width="100" Margin="5"
                 HorizontalAlignment="Center"/>

1149-001

#1,145 – Using RenderSize in Custom Shape

When drawing a geometry in a custom Shape element, you could draw using hard-coded coordinates.  It’s more common, however, to use the RenderSize property of the UIElement to render the object so that the geometry scales based on the size of the control.

Below, we create a custom shape that draws a diagonal line from the upper left corner of the control to the lower right.

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = RenderSize.Width;
                double maxHeight = RenderSize.Height;

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point(0.0, 0.0),
                        false,
                        false);
                    ctx.LineTo(
                        new Point(maxWidth, maxHeight),
                        true,
                        false);
                }

                return geom;
            }
        }
    }

We can use the shape in XAML as follows:

    <StackPanel>
        <loc:MyShape Stroke="Black" StrokeThickness="1"
                     Height="50" Width="50"
                     HorizontalAlignment="Center"/>
    </StackPanel>

Now when we change the size of the underlying control, the geometry adjusts as well.

1145-001

1145-002

1145-003

#1,144 – Geometry in Custom Shape Doesn’t Automatically Scale

If you define a custom Shape by creating some Geometry, the resulting geometry will not automatically scale when shape’s size is changed.

Suppose that we have the following custom shape.

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point(0.0, 0.0),
                        false,
                        false);
                    ctx.LineTo(
                        new Point(50.0, 50.0),
                        true,
                        false);
                }

                return geom;
            }
        }
    }

Placing this control in a StackPanel, it’s size is just large enough to accommodate the geometry.

1144-001

If we explicitly make the shape larger, the underlying geometry stays the same size.

1144-002

#1,143 – Coordinate System for StreamGeometry

You can use a StreamGeometry object, along with the StreamGeometryContext returned by its Open method, to draw simple geometric shapes.

When using the various methods of a StreamGeometryContext instance, you work with X and Y values.  The coordinate system used has the upper left corner of the drawing region at (0,0), with X values increasing from left to right and Y values increasing from top to bottom.

1143-001

Below, we have a custom shape that draws a line segment from (0,0) to (50,50) and then another line segment to (75,25).

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                return GetMyShapeGeometry();
            }
        }

        private Geometry GetMyShapeGeometry()
        {
            StreamGeometry geom = new StreamGeometry();
            using (StreamGeometryContext ctx = geom.Open())
            {
                ctx.BeginFigure(
                    new Point(0.0, 0.0),
                    false,    // is NOT filled
                    false);   // is NOT closed
                ctx.LineTo(
                    new Point(50.0, 50.0),
                    true,     // is stroked (line visible)
                    false);   // is not smoothly joined w/other segments
                ctx.LineTo(
                    new Point(75.0, 25.0),
                    true,     // is stroked (line visible)
                    false);   // is not smoothly joined w/other segments
            }

            return geom;
        }
    }

We can then use this shape from XAML.

    <Canvas>
        <loc:MyShape Canvas.Top="0" Canvas.Left="0"
                     Stroke="Black" />
    </Canvas>

1143-002

#240 – Shape vs. DrawingVisual

We’ve seen two ways to render custom 2D geometries–by inheriting from DrawingVisual and hosting in an UIElement or by inheriting from Shape and instancing your object directly in XAML.

You might wonder which of these methods to use for drawing custom 2D objects.

Shape is at a higher level of abstraction than DrawingVisualShape provides the following functionality, beyond what you get with DrawingVisual:

  • Derives from FrameworkElement, so you can include your subclass directly into a logical tree as a child of a Panel
  • Takes care of things like the Pen used to render the geometry (Stroke and StrokeThickness) and the Brush used to fill the interior of the geometry

Below is an example of including several instances of a custom Shape and specifying different stroke/fill properties for each instance.

	<StackPanel Orientation="Horizontal">
		<local:MyWeirdShape Stroke="Black" StrokeThickness="2" Fill="Orange"/>
		<local:MyWeirdShape Stroke="Red" StrokeThickness="1" Fill="DimGray"/>
		<local:MyWeirdShape Stroke="Blue" StrokeThickness="10" Fill="White"/>
	</StackPanel>