Monday, October 24, 2011

Computing the viewport size within a WPF Border control

Copyright © 2011, Steven E. Houchin. All rights reserved.

I have a WPF application that renders a photo bitmap inside a System.Windows.Controls.Image control that is contained within a System.Windows.Controls.Border. If the photo bitmap is too large for the app window (either in the horizontal or vertical direction), I set the Image's corresponding size attribute(s) to 'Auto' (i.e. Double.NaN) within the Image's SizeChanged event handler. If the bitmap is smaller than the app window, I set the Image's size to the actual fixed size of the bitmap, so it doesn't get scaled.

But, that left a problem when I resized the app window smaller: a fixed-size Image was truncated by the border when the window shrank smaller than the photo's bitmap size. And, because the Image's width/height weren't set to Auto, its SizeChanged event wasn't firing.

However, the Border's SizeChanged event was firing. So, I added that handler for the Border control, planning to set the Image's proper width/height after resize.  That left another problem in this new event handler: how to calculate whether or not the Image control's width/height should be Auto or fixed.  Put another way, what was the size of the viewport inside the Border control where the Image would be rendered?

The Border's SizeChanged event handler is passed a SizeChangedEventArgs parameter that provides the new width/height of the Border control. The size available to the Image control inside that will clearly be smaller, but by how much? Here is the algorithm in C# that I came up with (keep in mind that a negative BorderThickness means the border is drawn outside the control's drawing area):

// Calculate the Border control's new viewport
// size within the drawn border where the Image control
// is rendered (add in any padding and non-negative
// border thickness).
Size viewport = new Size();
viewport.Width = e.NewSize.Width -
    (border.Padding.Left + border.Padding.Right) -
    ((border.BorderThickness.Left > 0) ?
        border.BorderThickness.Left : 0) -
    ((border.BorderThickness.Right > 0) ?
        border.BorderThickness.Right : 0);
viewport.Height = e.NewSize.Height -
    (border.Padding.Top + border.Padding.Bottom) -
    ((border.BorderThickness.Top > 0) ?
        border.BorderThickness.Top : 0) -
    ((border.BorderThickness.Bottom > 0) ?
        border.BorderThickness.Bottom : 0);
This viewport variable now holds the size available for the Image control inside the border. Now, all I have to do is determine if the Image needs to be scaled down in the viewport or set to a fixed size. One thing I discovered is that the Image's Margin value must be taken into account, since it is also rendered inside the viewport along with the photo bitmap. The code to determine the right sizing, in this case for the width is:

// Calculate the image's real width to determine if
// scaling is needed
double imageWidth = _bitmapActualSize.Width +
    image.Margin.Left + image.Margin.Right;
if (viewport.Width > imageWidth)
{
    // Restrict the Image control's width to the
    // bitmap's actual width
    image.Width = _bitmapActualSize.Width;
}
else if (viewport.Width < imageWidth)
{
    if (Double.NaN != image.Width)
    {
        // Set the Image control's width to 'Auto' so
        // it will scale down to fit
        image.Width = Double.NaN;
    }
}
The same calculation is done for the Image height immediately after. This results in the proper change to the photo bitmap size just at the right time.