Resizing a WPF control while maintaining a specific aspect ratio

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.

Advertisement

Tags: , , , ,

26 Responses to “Resizing a WPF control while maintaining a specific aspect ratio”

  1. vppuzakov Says:

    Thank you!

  2. makamp Says:

    thank you

  3. Mark Pitt Says:

    Thanks! Just what I was looking for

  4. Kef Says:

    Thank you, works perfectly!

  5. Amr Gamal Says:

    many thanks to you 🙂

  6. Alexander L Dagana Says:

    Many thanks. I’m working on a video app for a client and my aspect ratio was off. Not anymore. Thanks again.

  7. Yvan Rodrigues Says:

    Perfect! Thanks

  8. Shruti Says:

    Thank you !

  9. johnlammers Says:

    Thanks. That works perfectly. Saved me a ton of messing around.

  10. Soleil Says:

    Thanks muchly!

  11. Peter Says:

    Thanks!

  12. Donatello Says:

    Works very nicely. Thanks!

  13. Tim Says:

    great stuff! Thanks a lot

  14. Romain Coury Says:

    very nice, thank you

  15. Gareth Says:

    Thanks. Just what i’m looking for! I’ve just tried converting to VB but getting an error in the code registering the DependencyProperty in the callback part, Argument not specified for ValidateAspectRatio. Does this need altering massively to get it to work?

  16. akzent Says:

    Since you want to pass the function as a delegate instead of calling it, I think you need to use the AddressOf operator in VB.NET. So instead of “ValidateAspectRatio” you should write it as “AddressOf ValidateAspectRatio”

  17. Maciej Says:

    Hi, could you provide some more informations about how to use your code? I tried pasting it in many places also creating a new UC for it and nothing worked. I get this error: The tag ‘AspectRatioLayoutDecorator’ does not exist in XML namespace ‘http://schemas.microsoft.com/winfx/2006/xaml/presentation’. Thanks in advance!

  18. Christopher Watford Says:

    Thank you, this is great. Putting this into use. It would be outstanding if you could officially license this code (under something like Creative Commons, MIT, Apache 2.0); helps with business adoption.

    • akzent Says:

      You can do with the code what you want. If you need something more formal, I will hereby license it under the MIT license as well. Thanks for letting me know that it was useful for you. Good luck in your projects!

  19. Tomas Chabada Says:

    Good job, thank you!

  20. Jamie Says:

    Unfortunately, I get a “Error BC30284 function ‘MeasureOverride’ cannot be declared ‘Overrides’ because it does not override a function in a base class.” (the same for ArrangeOverride)….no idea why!?

  21. MikeNakis Says:

    Thanks, it works for me.

  22. Jonathan Says:

    A massive thank you for this one. I had been banging my head for hours, due to a canvas in a grid element not resizing when I have a full window height panel that can expand only horizontally.

  23. Aleksandra Says:

    Thank you!

  24. Ken Parker Says:

    Awesome solution! I tried about 10 different approaches that caused screen flickers or just didn’t work. We have soo much ugly code around this problem.
    This looks like the “right” way!

  25. Ivan Issaev Says:

    Great solution, it works! Thank you very much! It takes for me sereval days “fighting” incorrect resizing and aspect ratio for one control of third-party vendor. But now it works like a charm.

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 )

Connecting to %s


%d bloggers like this: