Silverlight and scaling

I have tried so many different ways to scale a Silverlight app while resizing the browser. When I almost got everything to work, the Grid control start to behave really strange, start to clip my app. Why should it be so hard to get scaling to work, why not just add a property to the Silverlight host, scaling=”enable”, and it will automatically scale our Silverlight app. After some time I got everything to work perfectly and thought maybe some other people would be interested in how to create a Silverlight app that can scale, so here is my solution. This post will ONLY focus on scaling the whole Silverlight host content with the ScalingTransform feature, to make the app fit into any kind of resolution. The code is tested on Silverlight 3.0.

To get scaling to work we need to have a Canvas as our root element, if not, we can’t get it to work perfectly, if we for example should have used the Grid layout control as our root element, it will start cutting your app like a cutting machine, the worst possible scaling enemy. Best way to get an app scale perfectly is to not have a fixed size on the User Control and let the host control have the size set to 100%. We need to have a default size of our app, for example 1024x768 to make sure we have a number to use in our calculation to get the correct width and height ratio. Make sure to only add one child element to the Canvas, and that should be the “root” element of your content. For example:

<Canvas>
        <Grid x:Name="contentRoot">
            <!-- Add your content here -->
        </Grid>
</Canvas>

The child element of the Canvas, don’t need to be a Grid layout control, we can instead for example use a StackPanel or other layout control which we want to be our new “LayoutRoot” element (referring to the default Silvelright template, where the User Control’s first element is named “LayoutRoot”). If we want to set the first child element of the Canvas to a specific size, we must also make sure it will be our default (idle) size which will be used in the scaling calculation in our code. We can use different events to perform our scaling calculation and set the scaling, for example a FrameworkElement’s SizeChanged event, such as the UserControl.SizeChanged event (as long as the User Control doesn’t have a fixed size), we can also use the Application.Host.Content.Resized event (will only work if we don’t have a fixed size on our Silverlight host, will execute the first time the app is started, but will not be trigged when we resize the browser window). In this post I will use the Application.Host.Content.Resized event. We can hook up to the Resized event in the constructor of our app.

public ScaleableContent()
{
      Application.Current.Host.Content.Resized += new EventHandler(Content_Resized);
}

void Content_Resized(object sender, EventArgs e)
{
}


For our default (idle) size for the scaling calculation we can either use a private field or get the information from our Canvas’s child element if we have given it a size. For a normal web page, the recommended size seems still to be 1024x768, so why not use that. I decided to use a private field of type Size.

private Size _contentActualSize = new Size(1024, 768);

When we resize our browser we need to get the ratio between our idle size and the browser window’s size. The formula for this is really simple, everyone should have learned it in school ;) We add the calculation code to the Resized event.

var hostContentActualHeight = Application.Current.Host.Content.ActualHeight;
var hostContentActualWidth = Application.Current.Host.Content.ActualWidth;

var heightRatio = hostContentActualHeight / _contentActualSize.Height;
var widthRatio = hostContentActualWidth / _contentActualSize.Width;


The Application.Current.Host.Content.ActualHeight and ActualWidth will give us the browser’s content size (or to be correct the size of the host content). We divide it with our default size to get the ratio between our default size and the browser's content size. We use this ratio to create a ScalingTransform and set it the RenderTransform property of Canvas’s child element.

var ratio = 1.0;

if (heightRatio < widthRatio && heightRatio < 1)
   ratio = heightRatio;
else if (widthRatio < 1)
   ratio = widthRatio;


contentRoot.RenderTransform = new ScaleTransform() { ScaleY = ratio, ScaleX = ratio };

The ScaleY and ScaleX property uses a decimal value, where 0.5 is 50% of the original size, and 1 is 100%, 2 is 200% etc. We only want to apply the smallest ratio, that is why we do a check if heightRatio is lesser than the widthRatio. If we don’t do this check, our resizing will not work correctly,  because we do the calculate of the X and Y scaling separately. In the code above there is also a check to only set the scaling if the width or height ratio is lesser than 1, it will only make sure our app will stay on 100% or less in scaling, to not expand the app. If you want it to expand and be larger than the default size, just remove the check form the code. Here is the final code:

private Size _contentActualSize = new Size(1024, 768);

public ScaleableContent()
{
      Application.Current.Host.Content.Resized += new EventHandler(Content_Resized);
}

void Content_Resized(object sender, EventArgs e)
{
    var hostContentActualHeight = Application.Current.Host.Content.ActualHeight;
    var hostContentActualWidth = Application.Current.Host.Content.ActualWidth;

    var heightRatio = hostContentActualHeight / _contentActualSize.Height;
    var widthRatio = hostContentActualWidth / _contentActualSize.Width;

    var ratio = 1.0;

    if (heightRatio < widthRatio && heightRatio < 1)
ratio = heightRatio;
else if (widthRatio < 1)
ratio = widthRatio;
contentRoot.RenderTransform = new ScaleTransform() { ScaleY = ratio, ScaleX = ratio }; }

This code is kind of boring to add to every app where we want to use scaling, isn’t it? So what I did was to create a ScalableContent control. The ScalableContent control inherits from the Canvas control and only want one child element, the new “LayoutRoot” element, the element to add the ScaleTransform to. I also need to take care about some alignment issues, for example if we set the Canvas’s HorizontalAlignment property to center, it will start render the content from the center to the right, not align the content into the middle of the screen. So solve this I decided to use the Left margin, I could have used the Canvas.Left attached property, but didn’t. Here is the code of the Scalable control:

public class ScalableContent : Canvas
{
     private FrameworkElement _scaleableElement;


     public ScalableContent()
     {
          this.Loaded += (o,e) =>
                 {
                     if (this.Children != null && this.Children.Count > 0)
                    {
                        if (this.Children.Count > 1)
                            throw new SystemException("ScalableContent can only have one child element");

                        _scaleableElement = this.Children[0] as FrameworkElement;

                        if (_scaleableElement != null)
                            Application.Current.Host.Content.Resized += new EventHandler(Content_Resized);
                    }
                };
     }


     public Size IdleSize { get; set; }


     void Content_Resized(object sender, EventArgs e)
     {
            var contentActualWidth = Application.Current.Host.Content.ActualWidth;
            var contnetActualHeight = Application.Current.Host.Content.ActualHeight;

            var heightRatio = contnetActualHeight / IdleSize.Height;
            var widthRatio = contentActualWidth / IdleSize.Width;

            var ratio = 1.0;

            if (heightRatio < widthRatio && heightRatio < 1)
                ratio = heightRatio;
            else if (widthRatio < 1)
                ratio = widthRatio;

            var margin = new Thickness(0);

            if (HorizontalAlignment == HorizontalAlignment.Center)
                margin.Left = -((IdleSize.Width * ratio) / 2);
            else if (HorizontalAlignment == HorizontalAlignment.Right)
                margin.Left = -(IdleSize.Width * ratio);

            if (VerticalAlignment == VerticalAlignment.Center)
                margin.Top = -((IdleSize.Height * ratio) / 2);
            else if (VerticalAlignment == VerticalAlignment.Bottom)
                margin.Top = -(IdleSize.Height * ratio);

            _scaleableElement.RenderTransform = new ScaleTransform() { ScaleX = ratio, ScaleY = ratio };;
            _scaleableElement.Margin = margin;
        }
}

Here is how to use it in a XAML file:

<uc:ScalableContent VerticalAlignment="Top" HorizontalAlignment="Center" IdleSize="1024,880">
    
    <Grid x:Name="LayoutRoot" VerticalAlignment="Center" HorizontalAlignment="Center">
    
         <!-- Content here -->

    </Grid>

</uc:ScalableContent>

The control has a IdleSize property, which can be used to set the default (idle) size. To make this control work perfectly, make sure to not give the UserControl a fixed size, nor the Host object or surrounding divs etc on the page where you host the Silverlight app.

I hope this blog post will give some value to some people, the scaling can be tricky to solve if we don’t know where to look etc.

3 Comments

Comments have been disabled for this content.