Page 1 of 1

Resolution Fun An approrach at "resolution independance"

#1 ragingben  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 170
  • View blog
  • Posts: 637
  • Joined: 07-October 08

Posted 29 April 2010 - 02:34 AM

Prologue...

This started out around a year and a half ago (autumn 2008) and I remember reading about WPF I and it's ability to be resolution independant. I didn't really know what this meant, but I assumed that it meant that a WPF application would look the same regardless what resolution it was displayed at. WRONG! Although this baffeled me, it was no biggy. I wont go in to what it means to be truly resolution independant (but if your interested then check this) out.

So back to the the present day, and I'm developing apps that are used on a 1024x768 max resolution touch screen, but I'm developing generally at 1280x1024. Which means although my apps look the similar at both resolutions, they do look differnt.

This, again, was no biggy because I can change the resoultion of the development machine so that I can check what it will look like at a lower resolution, but trust me, doing this 20+ times a day gets pretty tedious.

So I started playing with the FrameworkElement.LayoutTransform property and using ScaleTransforms to "zoom in" on my app at runtime (with the help of a Zoom dependency property and some nifty keyboard shortcuts), which was pretty cool, but it only worked on the window that I gave the Zoom property to, and it felt very wrong defining a Zoom property and the appropriate implentation for each window. What I really wanted was to be able to create a style for a window that I could then forget about, and the style would automatically scale all windows to a required resoultion. Should be easy considering WPF's rendering versatility? You bet it is!

How this is going to work
Basically what we are trying to achieve here is the ability to take an actual resolution, a desired resolution and then from these work out a scale factor for both width and height, which we can then use as the ScaleX and ScaleY arguments for a ScaleTransform to strech out displayed to content to appear as if it is at a specific resolution. PHEW what a sentance!

If you have read this far and know much about WPF and data binding you will realise that we are certainly going to need to use a IValueConverter to convert between the two resolutions and a scale factor (unless ofcourse we did it in regular C# rather than XAML, but where would be the fun in that?!).

Anyway, start a WPF application called ResolutionIndependantApplication and we will go from there...

ResolutionConverter
This is the big boy who does all our calculation, and is vital to the whole process! Add a class to your project called ResolutionConverter, and make it inherit from IValueConverter (you will need to add a using statement at the top to System.Windows.Data). Below is the completated converter, I will step through it line by line below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows;

namespace ResolutionIndependantApplication
{
    /// <summary>
    /// Convert between an actual resolution and a scale factor to obtain a required resolution. A E2DDimension must be specified as the parameter
    /// </summary>
    [ValueConversion(typeof(Double), typeof(Double))]
    public class ResolutionConverter : IValueConverter
    {
        #region IValueConverter Members

        object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // if null value in any way
            if ((value == null) || !(value is Double) || (parameter == null) || !(parameter is E2DDimension))
            {
                throw new Exception("Either the value or the parameter are not set or the values are not of the required types");
            }

            // we know our data is ok
            Double actualValue;

            // get dimension
            E2DDimension dimension = (E2DDimension)parameter;

            // select dimension
            switch (dimension)
            {
                case (E2DDimension.Height): { actualValue = SystemParameters.PrimaryScreenHeight; break; }
                case (E2DDimension.Width): { actualValue = SystemParameters.PrimaryScreenWidth; break; }
                default: { throw new NotImplementedException(); }
            }

            // get required value
            Double requiredValue = (Double)value;

            // now get the scale
            return actualValue / requiredValue;
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // if null value in any way
            if ((value == null) || !(value is Double) || (parameter == null) || !(parameter is E2DDimension))
            {
                throw new Exception("Either the value or the parameter are not set or the values are not of the required types");
            }

            // we know our data is ok
            Double actualValue;

            // get dimension
            E2DDimension dimension = (E2DDimension)parameter;

            // select dimension
            switch (dimension)
            {
                case (E2DDimension.Height): { actualValue = SystemParameters.PrimaryScreenHeight; break; }
                case (E2DDimension.Width): { actualValue = SystemParameters.PrimaryScreenWidth; break; }
                default: { throw new NotImplementedException(); }
            }

            // get required value
            Double requiredValue = (Double)value;

            // now get the scale
            return requiredValue / actualValue;
        }

        #endregion
    }

    /// <summary>
    /// Enumeration of 2D dimensions
    /// </summary>
    internal enum E2DDimension
    {
        /// <summary>
        /// The x dimesnion, represents width
        /// </summary>
        Width = 0,
        /// <summary>
        /// The y dimension, represnts height
        /// </summary>
        Height
    }
}



Before we get into the nitty gritty, notice the decleration of the enumeration E2DDimension. This lets us specify which dimension we want to return a scale factor for from within our converter and the code that will call it later on. Basically this converter requires you to pass it a desired value for resolution as its value, and a E2DDimension for its parameter.

This statement checks that the data provided as the value and the parameter is correct in both the fact they are not null and are of the correct types, and throws an exception if they are not...

// if null value in any way
if ((value == null) || !(value is Double) || (parameter == null) || !(parameter is E2DDimension))
{
    throw new Exception("Either the value or the parameter are not set or the values are not of the required types");
}


...then this statement checks the dimension we are looking for, hopefully either width or height, and gets the actual value for the primary monitor...

// select dimension
switch (dimension)
{
    case (E2DDimension.Height): { actualValue = SystemParameters.PrimaryScreenHeight; break; }
    case (E2DDimension.Width): { actualValue = SystemParameters.PrimaryScreenWidth; break; }
    default: { throw new NotImplementedException(); }
}


...then this gets the required resolution, and compares if to the actual value, returning a scale factor!

// get required value
Double requiredValue = (Double)value;

// now get the scale
return actualValue / requiredValue;



And now for the XAML
Believe it or not we are nearly done! We just need to create a XAML style to apply to windows that implements this converter on a ScaleTransform. Just open your App.xaml file and add a namespace reference for the converter and our E2DDimension enumeration to the standard namespace declerations, which should be part of this project.

xmlns:resolution="clr-namespace:ResolutionIndependantApplication"


...and also add one for the System namespace, which will become clear in a minute...

xmlns:sys="clr-namespace:System;assembly=mscorlib"


Next, we will create double values (which is why we referenced System) for the required width and height (I'm going for 1024x768 but try toying with them), and an instance of our converter, do this in the <Application.Resources/> tag...

<Application.Resources>
        
    <!--Define the required width and height as doubles-->
    <sys:Double x:Key="requiredWidth">1024</sys:Double>
    <sys:Double x:Key="requiredHeight">768</sys:Double>
        
    <!--Define the converter-->
    <resolution:ResolutionConverter x:Key="resolutionConverter"/>
        
</Application.Resources>


... and now we need to set about our style. What we want to do is create a style for a Window, that replaces the default style for it's ContentPresenter - the object the ScaleTransform is applied to - and therefore all its children will appear at the new scale, which we specify. We will need to set a ScaleTransform for the ContentPresenters LayoutTransfrom property, which has its ScaleX and ScaleY properties bout to the double values we just defined in XAML for requiredWidth and requiredHeight, and passes these, as well as a E2DDimension into the resolutionConverter to obtain the correct scale factor to apply to the ContentPresenter. Phew... sounds a bit scary, but trust me it's not too bad. Take a look...

<Style x:Key="resolutionIndependantWindowStyle" TargetType="Window">
    <Style.Resources>
        <Style TargetType="{x:Type ContentPresenter}">
            <Setter Property="LayoutTransform">
                <Setter.Value>
                    <ScaleTransform>
                        <ScaleTransform.ScaleX>
                            <Binding Source="{StaticResource requiredWidth}" Mode="OneWay" Converter="{StaticResource resolutionConverter}">
                                <Binding.ConverterParameter>
                                    <resolution:E2DDimension>Width</resolution:E2DDimension>
                                </Binding.ConverterParameter>
                            </Binding>
                        </ScaleTransform.ScaleX>
                        <ScaleTransform.ScaleY>
                            <Binding Source="{StaticResource requiredHeight}" Mode="OneWay" Converter="{StaticResource resolutionConverter}">
                                <Binding.ConverterParameter>
                                    <resolution:E2DDimension>Height</resolution:E2DDimension>
                                </Binding.ConverterParameter>
                            </Binding>
                        </ScaleTransform.ScaleY>
                    </ScaleTransform>
                </Setter.Value>
            </Setter>
        </Style>
    </Style.Resources>
</Style>


Notice that we have given it a x:Key so that we can reference it, and also spend some time looking at the bindings. We could have used a dependency property or a regular property as our binding sources, which would be especially useful if we were going to want to change the resolution at runtime, but I'm pretty happy to have them sat there in XAML, as it is nice, neat and easy. Your finished XAML for the App.xaml file should look like this...

<Application x:Class="ResolutionIndependantApplication.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:resolution="clr-namespace:ResolutionIndependantApplication"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    StartupUri="Window1.xaml">
    <Application.Resources>
        
        <!--Define the required width and height as doubles-->
        <sys:Double x:Key="requiredWidth">1024</sys:Double>
        <sys:Double x:Key="requiredHeight">768</sys:Double>
        
        <resolution:ResolutionConverter x:Key="resolutionConverter"/>
        
        <Style x:Key="resolutionIndependantWindowStyle" TargetType="Window">
            <Style.Resources>
                <Style TargetType="{x:Type ContentPresenter}">
                    <Setter Property="LayoutTransform">
                        <Setter.Value>
                            <ScaleTransform>
                                <ScaleTransform.ScaleX>
                                    <Binding Source="{StaticResource requiredWidth}" Mode="OneWay" Converter="{StaticResource resolutionConverter}">
                                        <Binding.ConverterParameter>
                                            <resolution:E2DDimension>Width</resolution:E2DDimension>
                                        </Binding.ConverterParameter>
                                    </Binding>
                                </ScaleTransform.ScaleX>
                                <ScaleTransform.ScaleY>
                                    <Binding Source="{StaticResource requiredHeight}" Mode="OneWay" Converter="{StaticResource resolutionConverter}">
                                        <Binding.ConverterParameter>
                                            <resolution:E2DDimension>Height</resolution:E2DDimension>
                                        </Binding.ConverterParameter>
                                    </Binding>
                                </ScaleTransform.ScaleY>
                            </ScaleTransform>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Style.Resources>
        </Style>
    </Application.Resources>
</Application>


The reason we have done this in App.xaml is bcause the style is now in scope for the whole appication, where as if we had done it in a <window.Resources/> tag for a window it would only ever be in scope for that window.

Now if you go to your main window, in my case, Window1, and add the follwing line to the window tag...

Style="{StaticResource resolutionIndependantWindowStyle}"


...the window is all tooled up and ready to be displayed at your required resolution (you wont see this until runtime). By the way your finished Window1.xaml code should look like this...

<Window x:Class="ResolutionIndependantApplication.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" Style="{StaticResource resolutionIndependantWindowStyle}">
    <Grid>
        
    </Grid>
</Window>


... and I added this as some test content to the grid...

<Label Content="Hello" HorizontalAlignment="Center" VerticalAlignment="Center"/>


Go on then, press F5 and give it a go!

Conclusion
While this works great for me, you may notice that the window is not displayed at the resolution you specify, only the content. But for most people in a situation like mine I don't think that should be too much of a problem. Hope this helps someone!

Ben

Attached File  ResolutionIndependantApplication.zip (61.77K)
Number of downloads: 355

This post has been edited by ragingben: 30 April 2010 - 02:13 AM


Is This A Good Question/Topic? 0
  • +

Page 1 of 1