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 rectangle
s 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
):
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
History
- 11/26/2007: Updated to include screenshots