Charles Petzold

OnRender Clipping Logic

January 24, 2006
New York City

In my WPF book, my original Chapter 10 ("Custom Elements") has now spilled over into Chapter 11 ("Single-Child Elements") and Chapter 12 ("Custom Panels") as I continue to wrestle with the art and science of overriding MeasureOverride, ArrangeOverride, and OnRender.

In the past I've noticed that sometimes OnRender seems to clip and sometimes it does not, and yesterday I tried to finally nail down a better analysis. My conclusion is simple: Sometimes OnRender clips and sometimes it does not. Fortunately, I believe I have found the "tipping point" and have some code to demonstrate it.

Background: A class that derives from FrameworkElement but which has no visual children will probably override MeasureOverride and OnRender. MeasureOverride has an argument of type Size commonly called availableSize or (in a naming scheme I prefer) sizeAvailable. This is generally the size of the element's container, for example, the client area of a window or the cell of a Grid. If the window has been set for SizeToContent or the Grid cell set for Auto, then one or more dimensions of sizeAvailable could be infinite (that is, the value Double.PositiveInfinity). The sizeAvailable dimensions exclude the Margin set for the element and are always non-negative. (It gets more complex when Width, MinWidth, MaxWidth, Height, MinHeight, or MaxHeight have been set on the element. I'll ignore those cases in this discussion.)

The return value of MeasureOverride is also a Size object that I name sizeDesired. It is the duty of the element's override of MeasureOverride to indicate a proper "natural" size of the element or at least a minimum size. For example, a button that contains text would return a size based on the text string, the font, and the button border.

The element also overrides OnRender, during which the element is responsible for drawing itself. The element assumes an origin of (0, 0) and drawing dimensions given by the RenderSize property. RenderSize is often set directly from the return value from MeasureOverride, but there's an important exception: If the container has a non-infinite width (that is, the parameter to sizeAvailable.Width argument to MeasureOverride was non-infinite), and if the HorizontalAlignment of the element is set to Stretch, then RenderSize.Width will the maximum of sizeAvailable.Width and sizeDesired.Width (the return value from MeasureOverride), and similarly for the height. This is how many elements exhibit a default behavior of filling up their containers.

Let's go to the code: RectAndText.cs (rename after downloading) is a simple no-child FrameworkElement derivative that overrides MeasureOverride and OnRender to display a text string inside a rectangle. To illustrate clipping, it also displays 4 lines extending from the rectangle to points beyond the window.

The MeasureOverride method calculates a desired size of the element based on the rectangle border width and the size of the FormattedText object. If the Mode property equals 0 (the default case) that's the size that the element returns from MeasureOverride. As it's supposed to, the OnRender method uses the RenderSize property to size the rectangle, and then displays the FormattedText object inside the rectangle.

The TryRectAndText.cs (rename after downloading) puts this custom element in the fill section of a DockPanel and sets up a menu to change the Mode property. (Here's the TryRectAndText.csproj (rename after downloading) project file to round out the project.) Notice that the RectAndText element is given HorizontalAlignment and VerticalAlignment values of Center and a Margin (on all four sides) of 192 device-independent units (two inches). The Margin setting implies that the sizeAvailable parameter to MeasureOverride equals the size of the window client area minus the menu minus 4 inches horizontally and vertically.

When you first run the program, you should see the entire rectangle with the text inside and four lines going off in the four directions. There's no clipping going on. Now gradually make the window narrower. When you reach the point where there's only two inches on the left and right, suddenly clipping kicks in. The lines at the left and right disappear, part of the border on the left and right disappears, and the lines on the top and bottom become much shorter. The clipping area is obviously based on sizeAvailable but it only kicks in when sizeDesired becomes greater than sizeAvailable.

As you make the window even narrower, you'll see the left and right sides of the rectangle disappear. If you've experimented with WPF buttons, you'll know they behave a little differently if the buttons are sized too small for their content. The regular button truncates its content but still attempts to display its border (unless the container gets way too small).

To mimic that behavior, I decided to set the return value from MeasureOverride to values no larger than the parameter to MeasureOverride. That's Mode equal to 1, which you can set from the program's menu by selecting "Constrain MeasureOverride return value from parameter". (Keep in mind that the parameter to MeasureOverride reflects the container size less the Margin property of the element.) The corresponding code in MeasureOverride is:

and similarly for the height. And what happens is very bizarre. RenderSize (which is based on the return value from MeasureOverride) now changes as the window is made too narrow or too short, but there is no clipping at all going on, so the text that's supposed to be inside the rectangle now spills out. You probably don't want this happening in a real-life program, so there are a couple solutions. The first is to simply set a clipping area for the DrawingContext right in the OnRender method:

The other solution is illustrated by setting a Mode value of 2 in my program, available from the menu by selecting "Add 0.1 to constrained return value." This option adds 0.1 to the value calculated from Mode 1. The additional code is:

and similarly for the height. And now if you make the window too narrow or short, clipping kicks in. Now the rectangle behaves more like the normal WPF button. The rectangle is always displayed and, if necessary, the contents get truncated.

So, if (sizeDesired.Width <= sizeAvailable.Width && sizeDesired.Height <= sizeAvailable.Height) then there's no clipping. Otherwise, OnRender output is clipped to sizeAvailable.

Personally, I would prefer that this logic involve < rather than <= so that fudge factors don't have to be used to make the clipping kick in those cases when sizeDesired is constrained to sizeAvailable .