Friday, July 8, 2022

Implementing a WPF Zoom feature

Now that we have several complex WPF applications in production, one of our users is demanding a zoom feature because he is having trouble reading some of the text. The incident went to one of our less experienced developers who promptly added a ScaleTransform to the base window that all our applications use. Personally, I would have bought the user some reading glasses, but that's just me.

The result of the ScaleTransform is not brilliant. It is unpredictable and produces undesirable results on most of the screens. We are not going to implement it. I suggested that the user really wants us to increase the font size. We've been pretty good at using styles consistently, so I decided to see how hard it would be to alter our styles and bind font sizes. Our buttons sizes are fixed, so the text can be clipped as it increases in size. I added some intelligence via a converter to prevent clipping.

This is what I came up with. It demonstrates a ViewBox, the simple ScaleTransform technique and the FontSize technique. The ScaleTransform solution has the advantage of being much simpler. The FontSize solution works better. The ViewBox is also simple but does not work well for pages with a lot of controls because they tend to start large and can't get much bigger. It's written using VisualStudio 2022 and C#.

Start a new WPF C# project called ScaleTransform. Add a class called Converters. The MainWindow.xaml looks like this...

<Window x:Class="ScaleTransform.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ScaleTransform"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="350" Width="400">
    <Window.Resources>
        <local:ButtonWidthConverter x:Key="ButtonWidthConverter"/>
        <local:ButtonHeightConverter x:Key="ButtonHeightConverter"/>
        <local:FontSizeConverter x:Key="FontSizeConverter"/>
        <sys:Decimal x:Key="HeadingFactor">1.5</sys:Decimal>
        <Style TargetType="TextBlock" x:Key="Heading">
            <Setter Property="FontSize" Value="{Binding WindowFontSize, Converter={StaticResource FontSizeConverter}, ConverterParameter={StaticResource HeadingFactor}}"/>
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="{Binding WindowFontSize}"/>
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="{Binding WindowFontSize}"/>
            <Setter Property="Width" Value="{Binding WindowFontSize, Converter={StaticResource ButtonWidthConverter}}"/>
            <Setter Property="Height" Value="{Binding WindowFontSize, Converter={StaticResource ButtonHeightConverter}}"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Viewbox>
            <Grid>
                <Grid.RenderTransform>
                    <ScaleTransform ScaleX="{Binding ZoomLevel}" ScaleY="{Binding ZoomLevel}"/>
                </Grid.RenderTransform>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                    <RowDefinition Height="200"/>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Style="{StaticResource Heading}" Text="Meeting Notes"/>
                <TextBox Grid.Row="1" Text="The meeting was very boring and eventually everyone fell asleep" TextWrapping="Wrap"/>
                <Button Grid.Row="2" Content="Save Notes"/>
            </Grid>
        </Viewbox>
        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <TextBlock Text="ScaleTransform" Margin="10" FontSize="13"/>
            <Slider Width="100" Value="{Binding ZoomLevel}" Minimum="1" Maximum="5"/>
            <TextBlock Text="FontSize" Margin="10" FontSize="13"/>
            <Slider Width="100" Value="{Binding WindowFontSize}" Minimum="6" Maximum="20"/>
        </StackPanel>
    </Grid>
</Window>

 

The MainWindow.xaml.cs file contains a couple of properties and not much else.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
 
namespace ScaleTransform
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private double _ZoomLevel = 1;
        public double ZoomLevel
        {
            get { return _ZoomLevel; }
            set { SetProperty(ref _ZoomLevel, value); }
        }
 
        private double _WindowFontSize = 12;
        public double WindowFontSize
        {
            get { return _WindowFontSize; }
            set { SetProperty(ref _WindowFontSize, value); }
        }
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

The converters are similarly trivial.

using System;
using System.Globalization;
using System.Windows.Data;
 
namespace ScaleTransform
{
    class ButtonWidthConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Double FontSize = (Double)value;
            return Math.Max(FontSize * 6, 100);
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
 
    class ButtonHeightConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double FontSize = (Double)value;
            return Math.Max(FontSize * 2, 22);
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
 
    class FontSizeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double FontSize = (Double)value;
            double Factor  ;
            if (parameter == null || !Double.TryParse(parameter.ToString(), out Factor))  Factor = 1;
            return FontSize * Factor;
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
 
If you run this, you will see the default layout.


Moving the ScaleTransform slider results in bigger everything, but a lot of clipping.


Moving the FontSize slider instead produces a useful effect.


Resizing the window demonstrates the ViewBox feature.



No comments:

Post a Comment