Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF PartiallyRoundedRectangle: Choose Which Corners You Want Rounded

0.00/5 (No votes)
26 Nov 2007 1  
This is my attempt at writing a new Shape class that allows the user to choose which corners of a rectangle should be rounded
Screenshot - Tab-Like.png

Introduction

I'm new to WPF, so this is my first attempt at a reusable solution to a problem I had: although WPF is ridiculously powerful and customizable, for some reason Microsoft didn't build-in any logic to allow a rounded rectangle to be only rounded on certain corners, like you might use for a tab at the top or side of a window for example.

Background

I'm still very new to WPF and there's so much to wrap my head around, I'm sure this probably isn't the best solution. As such, I welcome constructive comments for the benefit of myself and anyone else interested in this code.

Consider this article a learning experience.

Using the Code

Here is the code, in its entirety:

public class PartiallyRoundedRectangle : Shape {
    public static readonly DependencyProperty RadiusXProperty;
    public static readonly DependencyProperty RadiusYProperty;

    public static readonly DependencyProperty RoundTopLeftProperty;
    public static readonly DependencyProperty RoundTopRightProperty;
    public static readonly DependencyProperty RoundBottomLeftProperty;
    public static readonly DependencyProperty RoundBottomRightProperty;

    public int RadiusX {
        get { return (int)GetValue(RadiusXProperty); }
        set { SetValue(RadiusXProperty, value); }
    }

    public int RadiusY {
        get { return (int)GetValue(RadiusYProperty); }
        set { SetValue(RadiusYProperty, value); }
    }

    public bool RoundTopLeft {
        get { return (bool)GetValue(RoundTopLeftProperty); }
        set { SetValue(RoundTopLeftProperty, value); }
    }

    public bool RoundTopRight {
        get { return (bool)GetValue(RoundTopRightProperty); }
        set { SetValue(RoundTopRightProperty, value); }
    }

    public bool RoundBottomLeft {
        get { return (bool)GetValue(RoundBottomLeftProperty); }
        set { SetValue(RoundBottomLeftProperty, value); }
    }

    public bool RoundBottomRight {
        get { return (bool)GetValue(RoundBottomRightProperty); }
        set { SetValue(RoundBottomRightProperty, value); }
    }

    static PartiallyRoundedRectangle() {
        RadiusXProperty = DependencyProperty.Register
            ("RadiusX", typeof(int), typeof(PartiallyRoundedRectangle));
        RadiusYProperty = DependencyProperty.Register
            ("RadiusY", typeof(int), typeof(PartiallyRoundedRectangle));

        RoundTopLeftProperty = DependencyProperty.Register
            ("RoundTopLeft", typeof(bool), typeof(PartiallyRoundedRectangle));
        RoundTopRightProperty = DependencyProperty.Register
            ("RoundTopRight", typeof(bool), typeof(PartiallyRoundedRectangle));
        RoundBottomLeftProperty = DependencyProperty.Register
            ("RoundBottomLeft", typeof(bool), typeof(PartiallyRoundedRectangle));
        RoundBottomRightProperty = DependencyProperty.Register
            ("RoundBottomRight", typeof(bool), typeof(PartiallyRoundedRectangle));
    }

    public PartiallyRoundedRectangle() {
    }

    protected override Geometry DefiningGeometry {
        get {
            Geometry result = new RectangleGeometry
            (new Rect(0, 0, base.Width, base.Height), RadiusX, RadiusY);
            double halfWidth = base.Width / 2;
            double halfHeight = base.Height / 2;

            if (!RoundTopLeft)
                result = new CombinedGeometry
            (GeometryCombineMode.Union, result, new RectangleGeometry
            (new Rect(0, 0, halfWidth, halfHeight)));
            if (!RoundTopRight)
                result = new CombinedGeometry
            (GeometryCombineMode.Union, result, new RectangleGeometry
            (new Rect(halfWidth, 0, halfWidth, halfHeight)));
            if (!RoundBottomLeft)
                result = new CombinedGeometry
            (GeometryCombineMode.Union, result, new RectangleGeometry
            (new Rect(0, halfHeight, halfWidth, halfHeight)));
            if (!RoundBottomRight)
                result = new CombinedGeometry
            (GeometryCombineMode.Union, result, new RectangleGeometry
            (new Rect(halfWidth, halfHeight, halfWidth, halfHeight)));

            return result;
        }
    }
} 

How It Works

The design is very simple: we just derive a new class from Shape, implement a few DependencyProperties such as RadiusX and RadiusY which would normally be defined on Rectangle. Since Rectangle is marked sealed, we cannot inherit from it directly.

The most important properties are of course the RoundXXX where XXX is the corner. When these are false, a RectangleGeometry is unioned overtop the offending round corner so as to give it its point back. The rectangles cover the entire quarter of the shape, so it should work with any setting of RadiusX/RadiusY.

Here's what an instance of this class looks like in XAML:

<custom:PartiallyRoundedRectangle RoundTopLeft="True" 
    RoundBottomRight="True" RadiusX="20" RadiusY="20" 
    Fill="LightBlue" Height="112" Width="200" />

This sets the top-left and bottom-right corners to be rounded (the default is false):

Screenshot - Shape.png

Points of Interest

I am still not super clear on separating procedural code from declarative code, and the WPF libraries make this really confusing. For example, it would have been nice if Geometry simply provided a Union() method that accepted another Geometry. Instead, I've had to keep recursively creating new CombinedGeometry's and I fear these are going to be kept in memory whenever a shape is drawn. An alternative might be to call one of the Flatten__ methods before returning the Geometry.

This implementation has issues with stroke for some reason that I am unsure of. Although you can define a stroke, it ends up looking really bad and unbalanced compared to the native Rectangle. Furthermore, AttachedProperties like BitmapEffect don't seem to work. Like I said, I'm brand new to this so any insight would be appreciated.

The Stroke Problem

Screenshot - StrokeIssue.png

History

  • 11/26/2007: Updated to include screenshots

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here