ClipBorder: A WPF Border that clips

While creating the next control for my WPFSpark project, I had the requirement of a WPF Grid with rounded corners. Since the Grid does not have the CornerRadius property, the other option I had was to encapsulate the Grid within a Border and set the ClipToBounds property of the Border to true. Then I found out that Border (and all decorators) do not perform the clipping even though the ClipToBounds is set to true.

The MSDN Forum provided an excellent solution on how to derive from the Border class and implement the clipping on your own. The ClippingBorder class mentioned in this site has a  _clipRect field (of type RectangleGeometry) which is set as the Clip property of the ClippingBorder’s Child. But in this case the corner radius of the RectangleGeometry is same for all corners. What if you have a border with different corner radius for each corner and you want it to act as a clipping border?

I modified the ClippingBorder class to create the ClipBorder class which will take into account the above mentioned issue. Instead of a RectangleGeometry field, the ClipBorder class uses a PathGeometry to define the clipping Geometry. For this purpose I have used the GeometryHelper class (mentioned in my previous post) to obtain the RoundedRectangleGeometry. I have modified the GeometryHelper code further to take into account the BorderThickness of the ClipBorder.

Here is the code for ClipBorder:

namespace WPFSpark
{
    /// <summary>
    /// Border which allows Clipping to its border.
    /// Useful especially when you need to clip to round corners.
    /// </summary>
    public class ClipBorder : Border
    {
        protected override void OnRender(DrawingContext dc)
        {
            OnApplyChildClip();
            base.OnRender(dc);
        }

        public override UIElement Child
        {
            get
            {
                return base.Child;
            }
            set
            {
                if (this.Child != value)
                {
                    if (this.Child != null)
                    {
                        // Restore original clipping of the old child
                        this.Child.SetValue(UIElement.ClipProperty, oldClip);
                    }

                    if (value != null)
                    {
                        // Store the current clipping of the new child
                        oldClip = value.ReadLocalValue(UIElement.ClipProperty);
                    }
                    else
                    {
                        // If we dont set it to null we could leak a Geometry object
                        oldClip = null;
                    }

                    base.Child = value;
                }
            }
        }

        protected virtual void OnApplyChildClip()
        {
            UIElement child = this.Child;
            if (child != null)
            {
                // Get the geometry of a rounded rectangle border based on the BorderThickness and CornerRadius
                clipGeometry = GeometryHelper.GetRoundRectangle(new Rect(Child.RenderSize), this.BorderThickness, this.CornerRadius);
                child.Clip = clipGeometry;
            }
        }

        private Geometry clipGeometry = null;
        private object oldClip;
    }
}

Update: There was a small error in the calculation of the RoundedRectangle Geometry when the BorderThickness was 1 pixel. I have rectified it and updated the code. Thanks to Gene for pointing it out. 🙂

Here is the updated code for the modified GeometryHelper:

public static Geometry GetRoundRectangle(Rect baseRect, Thickness thickness, CornerRadius cornerRadius)
{
    // Normalizing the corner radius
    if (cornerRadius.TopLeft < Double.Epsilon)
        cornerRadius.TopLeft = 0.0;
    if (cornerRadius.TopRight < Double.Epsilon)
        cornerRadius.TopRight = 0.0;
    if (cornerRadius.BottomLeft < Double.Epsilon)
        cornerRadius.BottomLeft = 0.0;
    if (cornerRadius.BottomRight < Double.Epsilon)
        cornerRadius.BottomRight = 0.0;

    // Taking the border thickness into account
    double leftHalf = thickness.Left * 0.5;
    if (leftHalf < Double.Epsilon)
        leftHalf = 0.0;
    double topHalf = thickness.Top * 0.5;
    if (topHalf < Double.Epsilon)
        topHalf = 0.0;
    double rightHalf = thickness.Right * 0.5;
    if (rightHalf < Double.Epsilon)
        rightHalf = 0.0;
    double bottomHalf = thickness.Bottom * 0.5;
    if (bottomHalf < Double.Epsilon) 
        bottomHalf = 0.0;

    // Create the rectangles for the corners that needs to be curved in the base rectangle 
    // TopLeft Rectangle 
    Rect topLeftRect = new Rect(baseRect.Location.X, 
                                baseRect.Location.Y, 
                                Math.Max(0.0, cornerRadius.TopLeft - leftHalf), 
                                Math.Max(0.0, cornerRadius.TopLeft - rightHalf)); 
    // TopRight Rectangle 
    Rect topRightRect = new Rect(baseRect.Location.X + baseRect.Width - cornerRadius.TopRight + rightHalf, 
                                 baseRect.Location.Y, 
                                 Math.Max(0.0, cornerRadius.TopRight - rightHalf), 
                                 Math.Max(0.0, cornerRadius.TopRight - topHalf));   
    // BottomRight Rectangle
    Rect bottomRightRect = new Rect(baseRect.Location.X + baseRect.Width - cornerRadius.BottomRight + rightHalf, 
                                    baseRect.Location.Y + baseRect.Height - cornerRadius.BottomRight + bottomHalf, 
                                    Math.Max(0.0, cornerRadius.BottomRight - rightHalf), 
                                    Math.Max(0.0, cornerRadius.BottomRight - bottomHalf)); 
    // BottomLeft Rectangle 
    Rect bottomLeftRect = new Rect(baseRect.Location.X, 
                                   baseRect.Location.Y + baseRect.Height - cornerRadius.BottomLeft + bottomHalf, 
                                   Math.Max(0.0, cornerRadius.BottomLeft - leftHalf),  
                                   Math.Max(0.0, cornerRadius.BottomLeft - bottomHalf)); 

    // Adjust the width of the TopLeft and TopRight rectangles so that they are proportional to the width of the baseRect 
    if (topLeftRect.Right > topRightRect.Left)
    {
        double newWidth = (topLeftRect.Width / (topLeftRect.Width + topRightRect.Width)) * baseRect.Width;
        topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, newWidth, topLeftRect.Height);
        topRightRect = new Rect(baseRect.Left + newWidth, topRightRect.Location.Y, Math.Max(0.0, baseRect.Width - newWidth), topRightRect.Height);
    }

    // Adjust the height of the TopRight and BottomRight rectangles so that they are proportional to the height of the baseRect
    if (topRightRect.Bottom > bottomRightRect.Top)
    {
        double newHeight = (topRightRect.Height / (topRightRect.Height + bottomRightRect.Height)) * baseRect.Height;
        topRightRect = new Rect(topRightRect.Location.X, topRightRect.Location.Y, topRightRect.Width, newHeight);
        bottomRightRect = new Rect(bottomRightRect.Location.X, baseRect.Top + newHeight, bottomRightRect.Width, Math.Max(0.0, baseRect.Height - newHeight));
    }

    // Adjust the width of the BottomLeft and BottomRight rectangles so that they are proportional to the width of the baseRect
    if (bottomRightRect.Left < bottomLeftRect.Right)
    {
        double newWidth = (bottomLeftRect.Width / (bottomLeftRect.Width + bottomRightRect.Width)) * baseRect.Width;
        bottomLeftRect = new Rect(bottomLeftRect.Location.X, bottomLeftRect.Location.Y, newWidth, bottomLeftRect.Height);
        bottomRightRect = new Rect(baseRect.Left + newWidth, bottomRightRect.Location.Y, Math.Max(0.0, baseRect.Width - newWidth), bottomRightRect.Height);
    }

    // Adjust the height of the TopLeft and BottomLeft rectangles so that they are proportional to the height of the baseRect
    if (bottomLeftRect.Top < topLeftRect.Bottom)
    {
        double newHeight = (topLeftRect.Height / (topLeftRect.Height + bottomLeftRect.Height)) * baseRect.Height;
        topLeftRect = new Rect(topLeftRect.Location.X, topLeftRect.Location.Y, topLeftRect.Width, newHeight);
        bottomLeftRect = new Rect(bottomLeftRect.Location.X, baseRect.Top + newHeight, bottomLeftRect.Width, Math.Max(0.0, baseRect.Height - newHeight));
    }

    StreamGeometry roundedRectGeometry = new StreamGeometry();

    using (StreamGeometryContext context = roundedRectGeometry.Open())
    {
        // Begin from the Bottom of the TopLeft Arc and proceed clockwise
        context.BeginFigure(topLeftRect.BottomLeft, true, true);
        // TopLeft Arc
        context.ArcTo(topLeftRect.TopRight, topLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true);
        // Top Line
        context.LineTo(topRightRect.TopLeft, true, true);
        // TopRight Arc
        context.ArcTo(topRightRect.BottomRight, topRightRect.Size, 0, false, SweepDirection.Clockwise, true, true);
        // Right Line
        context.LineTo(bottomRightRect.TopRight, true, true);
        // BottomRight Arc
        context.ArcTo(bottomRightRect.BottomLeft, bottomRightRect.Size, 0, false, SweepDirection.Clockwise, true, true);
        // Bottom Line
        context.LineTo(bottomLeftRect.BottomRight, true, true);
        // BottomLeft Arc
        context.ArcTo(bottomLeftRect.TopLeft, bottomLeftRect.Size, 0, false, SweepDirection.Clockwise, true, true);
    }

    return roundedRectGeometry;
}
Advertisements

7 thoughts on “ClipBorder: A WPF Border that clips

  1. Very handy control. Thanks for sharing. I’m having an issue with a 1 pixel border. It seems the clipping corner radius isn’t set quite right and overlaps the border in the corners. You can see a zoomed in screenshot here: http://tinypic.com/view.php?pic=21lo6tx&s=5

    I’m trying to figure out how to alter the math in GeometryHelper in order to remove the overlap. Any help would be appreciated.

    1. Hi Gene,
      I found out the problem. It was because of the ‘tolerance’ variable which I had introduced in the calculation. I have updated the code. Please do check it out.
      Thanks for pointing out the bug.

      Cheers,
      Ratish

  2. Hi,
    a bit late to the party but:
    I have done some testing as part of my own Decorator project and found out that if you set Child.Clip in Parent’s OnRender, you force the Child to redo its whole Layout circle (especially OnRender).
    Since Child was already rendered at start of Parents OnRender, and then is re-rendered after Parents OnRender again, you seem to be needlessly rendering it twice.

    Just sharing the experience 😉

    1. Thanks for you comment.
      I have revamped the ClipBorder and moved the setting of Child.Clip to the ArrangeOverride method. I will be publishing it with WPFSpark 1.2 soon.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s