DCSIMG
WPF Arrow and Custom Shapes - Essential XAML

WPF Arrow and Custom Shapes

WPF is the best UI framework ever. It provided us with a large arsenal of vector graphic types such as Line, Ellipse, Path and others. Sometimes we need shapes which are not provided in the WPF arsenal (such an Arrow), and with all the respect to the Path shape, which can be used to create any type of 2D shape, we do not want to recalculate each point every time. This is a good reason and opportunity to create a custom shape.

WPF provides two kinds of vector types: Shapes and Geometries.

Shape is any type that derives from the Shape base class. It provides Fill, Stroke and other properties for coloring, and is actually a FrameworkElement. Thus we can place shapes inside Panels, we can register shapes routed events and do anything related to FrameworkElement. MSDN.

Geometry is any type that derives from the Geometry base type. It provides properties for describing any type of 2D geometry. A geometry is actually a Freezable type, thus can be frozen. A frozen object provides better performance by not notifying changes, and can be safely accessed by other threads. Geometry is not Visual, hence should be painted by other types such as Path. MSDN.

Now that we know what the different between a Geometry and Shape are, we can create our shape based on one of these two types. Correct?

Well, surprisingly we can't base our custom shape on the Geometry type, since its one and only default ctor is marked as internal. Shame on you Microsoft smile_embaressed.

Don't worry! We still have an option to base our custom shape on the Shape base class.

Now, let's say that we want to create an Arrow shape. An arrow is actually kind of line, so let's derive our custom type from the WPF Line type which has X1, Y1, X2 and Y2 properties.

image

Ooopps... Line is sealed! (Shame on you twice smile_wink).

Never mind, lets derive directly from the Shape base class, and add X1, Y1, X2, Y2 and two additional properties for defining the arrow's head width and height.

 

Our code should end up with something like this:

    public sealed class Arrow : Shape
    {
        public static readonly DependencyProperty X1Property = ...;
        public static readonly DependencyProperty Y1Property = ...;
        public static readonly DependencyProperty HeadHeightProperty = ...;
        ...

        [TypeConverter(typeof(LengthConverter))]
        public double X1
        {
            get { return (double)base.GetValue(X1Property); }
            set { base.SetValue(X1Property, value); }
        }

        [TypeConverter(typeof(LengthConverter))]
        public double Y1
        {
            get { return (double)base.GetValue(Y1Property); }
            set { base.SetValue(Y1Property, value); }
        }

        [TypeConverter(typeof(LengthConverter))]
        public double HeadHeight
        {
            get { return (double)base.GetValue(HeadHeightProperty); }
            set { base.SetValue(HeadHeightProperty, value); }
        }
        ...

        protected override Geometry DefiningGeometry
        {
            get
            {
                // Create a StreamGeometry for describing the shape
                StreamGeometry geometry = new StreamGeometry();
                geometry.FillRule = FillRule.EvenOdd;

                using (StreamGeometryContext context = geometry.Open())
                {
                    InternalDrawArrowGeometry(context);
                }

                // Freeze the geometry for performance benefits
                geometry.Freeze();

                return geometry;
            }
        }
        
        /// <summary>
        /// Draws an Arrow
        /// </summary>
        private void InternalDrawArrowGeometry(StreamGeometryContext context)
        {
            double theta = Math.Atan2(Y1 - Y2, X1 - X2);
            double sint = Math.Sin(theta);
            double cost = Math.Cos(theta);

            Point pt1 = new Point(X1, this.Y1);
            Point pt2 = new Point(X2, this.Y2);

            Point pt3 = new Point(
                X2 + (HeadWidth * cost - HeadHeight * sint),
                Y2 + (HeadWidth * sint + HeadHeight * cost));

            Point pt4 = new Point(
                X2 + (HeadWidth * cost + HeadHeight * sint),
                Y2 - (HeadHeight * cost - HeadWidth * sint));

            context.BeginFigure(pt1, true, false);
            context.LineTo(pt2, true, true);
            context.LineTo(pt3, true, true);
            context.LineTo(pt2, true, true);
            context.LineTo(pt4, true, true);
        }
    }

 

As you can see, it is very easy to implement a custom shape, thanks to the great work in the Shape base class. All we have to do is derive our custom shape type from Shape, and override the DefiningGeometry property. This property should return a Geometry of any type.

My solution creates and return a new frozen geometry on each call. Alternatively, you can cache a non frozen geometry by holding it as a field in the custom shape class.

 

Download the code from here.

Published Wednesday, January 23, 2008 12:00 AM by Tomer Shamam

Comments

# re: WPF Arrow and Custom Shapes

Wednesday, January 23, 2008 4:30 PM by Justin-Josef Angel [MVP]

What's the upside of drawing with the StreamGeometryContext object vs. just adding Geometry objects to a drawing?

# re: WPF Arrow and Custom Shapes

Wednesday, January 23, 2008 4:43 PM by Tomer Shamam

StreamGeometryContext is lightweight geometry. It does not support data binding, animation, or modification.

You can cache a Geometry object as a data member instead.

# re: WPF Arrow and Custom Shapes

Monday, July 21, 2008 10:15 AM by Tomer Shamam

To Grzegorz Wisniewski,

You can find the link of the demo code in the bottom of this post.

# re: WPF Arrow and Custom Shapes

Saturday, January 17, 2009 12:45 PM by KJ

how do you create a customer control with a rectangle and texbox in center?

# re: WPF Arrow and Custom Shapes

Saturday, January 17, 2009 3:59 PM by Tomer Shamam

Hi KJ,

You don't really need a custom control for this.

Just use a panel, such as DockPanel or Grid, then place inside a Rectangle, and then TextBox.

You may also create this as a ControlTemplate for the TextBox or simply use UserControl.

# re: WPF Arrow and Custom Shapes

Friday, May 20, 2011 12:44 PM by goldengel

Thanks. This helped me a lot.

# re: WPF Arrow and Custom Shapes

Friday, May 20, 2011 9:44 PM by Tomer Shamam

goldengel, my pleasure ;)

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above:
Powered by Community Server (Commercial Edition), by Telligent Systems