tips on developing resolution independent application
Asked Answered
T

4

24

Is it a good practice to find the workarea measurement and set some properties in code so that it could be bound to Control's margin or height/Width properties in xaml?

I do this so that my window would resize according to the available workarea.

const int w = SystemParameters.WorkArea.Width;
const int h = SystemParameters.WorkArea.Height;

public Thickness OuterGridMargin { get; }

MainViewModel()
{
    OuterGridMargin = new Thickness(w/5,h/6,w/5,h/4);
}

xaml:

<Grid Margin="{Binding OuterGridMargin}" />

I do this for some outer containers so that the layout would not be messed in lower resolutions. Currently I work at 1600x900 res(96 dpi) in a 20". My application is gadget like and does not have the regular window.

I want to know if there are some alternative approaches.

A search of [wpf] resolution]1 gives a lot of questions addressing similar problem but still I'm stuck and not able to come to a conclusion how to achieve a good resolution-independent layout.

Typographer answered 7/7, 2010 at 9:7 Comment(0)
B
63

There are two ways to deal with resolution in WPF.

One option is to design to a minimum resolution and just make sure everything is docked appropriately so that the elements get larger as the Window resolution gets larger. This is how many people did things in WinForms and still works decently well for WPF. You probably already have some concept of how to deal with this by setting HorizontalAlignment, VerticalAlignment, and margins.

The newer, trendier thing to do in WPF that was nearly impossible to do in WinForms is have your application actually just zoom in so your controls get bigger as your Window does. To do this, you'll apply a ScaleTransform on some root element in your Window and let WPF take care of the rest. It's really cool.

To show what this is like, here's what a window would look like when you start the app, make it smaller, and make it bigger: https://i.sstatic.net/QeoVK.png

Here's the code-behind for the small sample app I made:

public partial class MainWindow : Window
{
    public MainWindow() => InitializeComponent();

    #region ScaleValue Depdency Property
    public static readonly DependencyProperty ScaleValueProperty = DependencyProperty.Register("ScaleValue", typeof(double), typeof(MainWindow), new UIPropertyMetadata(1.0, new PropertyChangedCallback(OnScaleValueChanged), new CoerceValueCallback(OnCoerceScaleValue)));

    private static object OnCoerceScaleValue(DependencyObject o, object value)
    {
        MainWindow mainWindow = o as MainWindow;
        if (mainWindow != null)
            return mainWindow.OnCoerceScaleValue((double)value);
        else return value;
    }

    private static void OnScaleValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        MainWindow mainWindow = o as MainWindow;
        if (mainWindow != null)
            mainWindow.OnScaleValueChanged((double)e.OldValue, (double)e.NewValue);
    }

    protected virtual double OnCoerceScaleValue(double value)
    {
        if (double.IsNaN(value))
            return 1.0f;

        value = Math.Max(0.1, value);
        return value;
    }

    protected virtual void OnScaleValueChanged(double oldValue, double newValue) { }

    public double ScaleValue
    {            
        get => (double)GetValue(ScaleValueProperty);
        set => SetValue(ScaleValueProperty, value);
    }
    #endregion

    private void MainGrid_SizeChanged(object sender, EventArgs e) => CalculateScale();

    private void CalculateScale()
    {
        double yScale = ActualHeight / 250f;
        double xScale = ActualWidth / 200f;
        double value  = Math.Min(xScale, yScale);

        ScaleValue = (double)OnCoerceScaleValue(myMainWindow, value);
    }
}

And the XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" 
    Name="myMainWindow"
    Width="200" Height="250">
<Grid Name="MainGrid" SizeChanged="MainGrid_SizeChanged">
    <Grid.LayoutTransform>
        <ScaleTransform x:Name="ApplicationScaleTransform"
                        CenterX="0"
                        CenterY="0"
                        ScaleX="{Binding ElementName=myMainWindow, Path=ScaleValue}"
                        ScaleY="{Binding ElementName=myMainWindow, Path=ScaleValue}" />
    </Grid.LayoutTransform>
    <Grid VerticalAlignment="Center" HorizontalAlignment="Center" Height="150">
        <TextBlock FontSize="20" Text="Hello World" Margin="5" VerticalAlignment="Top" HorizontalAlignment="Center"/>
        <Button Content="Button" VerticalAlignment="Bottom" HorizontalAlignment="Center"/>
    </Grid>
</Grid>
Bushwa answered 15/2, 2011 at 5:15 Comment(9)
+1 for simplicity, clarity and completeness, doesn't seem fair.Fanaticize
You can also put everything inside a Viewbox to achieve the same effect.Leatherworker
No, a viewbox does not have the same effect and comes with other limitations. Namely, how do you specify a "native" resolution starting point for your application. A viewbox just zooms to fill the bounds. This mechanism also allows you to set minimum and maximum scaling distances as you see fit. Maybe you only want to scale larger, or maybe you only want to scale smaller.Bushwa
The call to OnCoerceScaleValue(MainGrid, value); looks incorrect to me. OnCoerceScaleValue casts the first param to a MainWindow, so passing in the grid will simply return you the value without it ever being coerced. I think you want to call OnCoerceScaleValue(MainWindow, value); instead.Freetown
Yeah, I believe that was a typo on my part! Good find, my apologies!Bushwa
@Bushwa this should be OnCoerceScaleValue(myMainWindow,value); and not as Mark Miller suggested.Verrocchio
Note that a layout transform doesn't apply to detached trees such as tool tips and context menus.Entwistle
At first I forgot to change my values in calculateScale to match my window dimensions, but once I did that it worked like a charm for me. Thanks for the great solution. My particular problem was making me go crazy.Ezarra
I'm very late to the party on this, and this is a good answer for scaling individual windows. There is also a Windows font size setting (Windows 10) which applies globally across the system. WPF combines these two factors and the results can be... surprising. It seems absolutely valid that we should be asking ourselves how we should be applying scaling.Imply
S
19

Great answer by JacobJ, I tried it out and it worked perfectly.

For anyone who's interested I made an attached behavior which does the same thing. I also added the option to specify the width/height denominators from XAML. It can be used like this

<Grid Name="MainGrid"
      inf:ScaleToWindowSizeBehavior.Denominators="1000, 700"
      inf:ScaleToWindowSizeBehavior.ParentWindow="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
    <!--...-->
</Grid>

ScaleToWindowSizeBehavior

public static class ScaleToWindowSizeBehavior
{
    #region ParentWindow

    public static readonly DependencyProperty ParentWindowProperty =
        DependencyProperty.RegisterAttached("ParentWindow",
                                             typeof(Window),
                                             typeof(ScaleToWindowSizeBehavior),
                                             new FrameworkPropertyMetadata(null, OnParentWindowChanged));

    public static void SetParentWindow(FrameworkElement element, Window value)
    {
        element.SetValue(ParentWindowProperty, value);
    }

    public static Window GetParentWindow(FrameworkElement element)
    {
        return (Window)element.GetValue(ParentWindowProperty);
    }

    private static void OnParentWindowChanged(DependencyObject target,
                                              DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement mainElement = target as FrameworkElement;
        Window window = e.NewValue as Window;

        ScaleTransform scaleTransform = new ScaleTransform();
        scaleTransform.CenterX = 0;
        scaleTransform.CenterY= 0;
        Binding scaleValueBinding = new Binding
        {
            Source = window,
            Path = new PropertyPath(ScaleValueProperty)
        };
        BindingOperations.SetBinding(scaleTransform, ScaleTransform.ScaleXProperty, scaleValueBinding);
        BindingOperations.SetBinding(scaleTransform, ScaleTransform.ScaleYProperty, scaleValueBinding);
        mainElement.LayoutTransform = scaleTransform;
        mainElement.SizeChanged += mainElement_SizeChanged;
    }

    #endregion // ParentWindow

    #region ScaleValue

    public static readonly DependencyProperty ScaleValueProperty =
        DependencyProperty.RegisterAttached("ScaleValue",
                                            typeof(double),
                                            typeof(ScaleToWindowSizeBehavior),
                                            new UIPropertyMetadata(1.0, OnScaleValueChanged, OnCoerceScaleValue));

    public static double GetScaleValue(DependencyObject target)
    {
        return (double)target.GetValue(ScaleValueProperty);
    }
    public static void SetScaleValue(DependencyObject target, double value)
    {
        target.SetValue(ScaleValueProperty, value);
    }

    private static void OnScaleValueChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
    }

    private static object OnCoerceScaleValue(DependencyObject d, object baseValue)
    {
        if (baseValue is double)
        {
            double value = (double)baseValue;
            if (double.IsNaN(value))
            {
                return 1.0f;
            }
            value = Math.Max(0.1, value);
            return value;
        }
        return 1.0f;
    }

    private static void mainElement_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        FrameworkElement mainElement = sender as FrameworkElement;
        Window window = GetParentWindow(mainElement);
        CalculateScale(window);
    }

    private static void CalculateScale(Window window)
    {
        Size denominators = GetDenominators(window);
        double xScale = window.ActualWidth / denominators.Width;
        double yScale = window.ActualHeight / denominators.Height;
        double value = Math.Min(xScale, yScale);
        SetScaleValue(window, value);
    }

    #endregion // ScaleValue

    #region Denominators

    public static readonly DependencyProperty DenominatorsProperty =
        DependencyProperty.RegisterAttached("Denominators",
                                            typeof(Size),
                                            typeof(ScaleToWindowSizeBehavior),
                                            new UIPropertyMetadata(new Size(1000.0, 700.0)));

    public static Size GetDenominators(DependencyObject target)
    {
        return (Size)target.GetValue(DenominatorsProperty);
    }
    public static void SetDenominators(DependencyObject target, Size value)
    {
        target.SetValue(DenominatorsProperty, value);
    }

    #endregion // Denominators
}
Saury answered 25/10, 2013 at 1:38 Comment(3)
Your answer looks very interesting, but I'm still too bad in XAML and C# to let it work. I'm missing the usage of "inf:" in the XAML part of it...Champignon
@AndreaAntonangeli The 'inf:' is a namespace declared before using the AttachedProperty, ie xmlns:inf="clr-namespace:My.App.AttachedProps".Quevedo
This actually works perfectly and with added bonus of possibility of limiting it only to some parts of the view! Great work! +1 is not enough.Rheinlander
W
0

Small correction to the answer of Fredrik Hedblad:

because you have set the DependencyProperty "Denominators" in the Grid element:

<Grid Name="MainGrid"
      inf:ScaleToWindowSizeBehavior.Denominators="1000, 700"
    <!--...-->
</Grid>

you must call the GetDominator method using the grid. Instead of:

private static void CalculateScale(Window window)
{
    var denominators = GetDenominators(window);
}

you must use something like this:

private static void mainElement_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var mainElement = sender as FrameworkElement;
    var window = GetParentWindow(mainElement);

    CalculateScale(window, mainElement);
}

private static void CalculateScale(Window window, FrameworkElement mainElement)
{
    var denominators = GetDenominators(mainElement);
}
Willemstad answered 17/9, 2015 at 6:46 Comment(0)
M
-1

going with what mr JacobJ did I made my own spinoff. I made a Interface on wich we base change

public interface IResolutionDecorator
{
    double ActualWidth { get; }
    double ActualHeight { get; }
    double ResolutionHeight { get; set; }
    double ResolutionWidth { get; set; }
    object CurentContent { get; }
}

and for this Interface i have extension

    #region IResolutionDecorator
    private static double OnCoerceScaleValue(double value)
    {
        return double.IsNaN(value) ? 1d : Math.Max(0.1, value);
    }

    public static void UpdateScale(this IResolutionDecorator source)
    {
        if (source.CurentContent is Visual visual)
        {
            double yScale = source.ActualHeight / source.ResolutionHeight;
            double xScale = source.ActualWidth / source.ResolutionWidth;
            double value = Math.Min(xScale, yScale);
            double ScaleValue = (double)OnCoerceScaleValue(value);
            visual.SetValue(Grid.LayoutTransformProperty, new ScaleTransform(ScaleValue, ScaleValue, 0, 0));
        }
    }
    #endregion

now we only need add Parameters that we lack in main window and set this.UpdateScale() in event sizeChanged in main grid

public partial class MainWindow : Window,  IResolutionDecorator
{
    public MainWindow()
    {
        InitializeComponent();
        this.Height = ResolutionHeight;
        this.Width = ResolutionWidth;
    }
    #region IResolutionDecorator
    public object CurentContent { get{ return this.Content; } }
    public double ResolutionHeight { get; set; } = 400d;
    public double ResolutionWidth { get; set; } = 800d;
    #endregion

    private void MainGrid_SizeChanged(object sender, EventArgs e)
    {
        this.UpdateScale();
    }
}
Modern answered 27/6, 2020 at 7:52 Comment(1)
Please remove the extra tabs when you post, it doesn't take a lot of effortVigilant

© 2022 - 2024 — McMap. All rights reserved.