Archive for October, 2012

Resizing a WPF control while maintaining a specific aspect ratio

October 15, 2012

When you are developing a Windows Presentation Foundation (WPF) application you have a lot of options to layout your controls automatically. Builtin panels like the Grid, the DockPanel or the StackPanel allows for flexible layouts. There is also the Viewbox decorator control which scales it child while (optionally) preserving its aspect ratio.

The disadvantage of the Viewbox is however that it scales its content instead of resizing it. So if you put a Button inside a Viewbox you will notice that its text and the control itself will appear “zoomed in” which makes it inapt for certain applications.

For this reason I wrote my own decorator which overwrites the Measure and Arrange methods to resize its child to a specific AspectRatio that you can give it as a dependency property. After understanding how Measure and Arrange work together it was actually quite easy to develop and does not require a lot of code.

Here is a visual comparison between the Viewbox decorator and my own AspectRatioLayoutDecorator:

Comparison of ViewBox and AspectRatioLayoutDecorator

As you can see the ViewBox scales the button and makes it look really ugly, while our custom control will resize it content nicely.

The usage in WPF is quite easily. Just wrap the content you want to resize in the AspectRatioLayoutDecorator:

<my:AspectRatioLayoutDecorator AspectRatio="1.25">
   <Button>Test</Button>
</my:AspectRatioLayoutDecorator>

Of course instead of setting a fixed AspectRatio you can animate and data bind it like any other dependency property.

Here is the source code of the control:

public class AspectRatioLayoutDecorator : Decorator
{
   public static readonly DependencyProperty AspectRatioProperty =
      DependencyProperty.Register(
         "AspectRatio",
         typeof (double),
         typeof (AspectRatioLayoutDecorator),
         new FrameworkPropertyMetadata
            (
               1d,
               FrameworkPropertyMetadataOptions.AffectsMeasure
            ),
         ValidateAspectRatio);

   private static bool ValidateAspectRatio(object value)
   {
      if (!(value is double))
      {
         return false;
      }

      var aspectRatio = (double) value;
      return aspectRatio > 0
               && !double.IsInfinity(aspectRatio)
               && !double.IsNaN(aspectRatio);
   }

   public double AspectRatio
   {
      get { return (double) GetValue(AspectRatioProperty); }
      set { SetValue(AspectRatioProperty, value); }
   }

   protected override Size MeasureOverride(Size constraint)
   {
      if (Child != null)
      {
         constraint = SizeToRatio(constraint, false);
         Child.Measure(constraint);

         if(double.IsInfinity(constraint.Width)
            || double.IsInfinity(constraint.Height))
         {
            return SizeToRatio(Child.DesiredSize, true);
         }

         return constraint;
      }

      // we don't have a child, so we don't need any space
      return new Size(0, 0);
   }

   public Size SizeToRatio(Size size, bool expand)
   {
      double ratio = AspectRatio;

      double height = size.Width/ratio;
      double width = size.Height*ratio;

      if (expand)
      {
         width = Math.Max(width, size.Width);
         height = Math.Max(height, size.Height);
      }
      else
      {
         width = Math.Min(width, size.Width);
         height = Math.Min(height, size.Height);
      }

      return new Size(width, height);
   }

   protected override Size ArrangeOverride(Size arrangeSize)
   {
      if (Child != null)
      {
         var newSize = SizeToRatio(arrangeSize, false);

         double widthDelta = arrangeSize.Width - newSize.Width;
         double heightDelta = arrangeSize.Height - newSize.Height;

         double top = 0;
         double left = 0;

         if (!double.IsNaN(widthDelta)
            && !double.IsInfinity(widthDelta))
         {
            left = widthDelta/2;
         }

         if (!double.IsNaN(heightDelta)
            && !double.IsInfinity(heightDelta))
         {
            top = heightDelta/2;
         }

         var finalRect = new Rect(new Point(left, top), newSize);
         Child.Arrange(finalRect);
      }

      return arrangeSize;
   }
}

The control not only works in a Grid, but also quite nicely in a StackPanel or any other form of layout.

I hope it’s useful to someone else. If you think it’s useful for you and you want to use it in your own project it would be nice if you leave a small “thank you” in the comments, so that I know it actually helped someone.