0

I've got a grid layout that looks like this:

this.

This is its code:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="2.5*"/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition Height="2.5*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="2.5*"/>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition Width="2.5*"/>
    </Grid.ColumnDefinitions>

    <TextBox x:Name="username" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Text="Username"
                    GotFocus="RemoveLabel" LostFocus="RestoreLabel"
                    HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
    <TextBox x:Name="password" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="Password"
                GotFocus="RemoveLabel" LostFocus="RestoreLabel"
                HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
    <Button x:Name="signUp" Grid.Row="5" Grid.Column="1" Content="Sign up"/>
    <Button x:Name="signIn" Grid.Row="5" Grid.Column="3" Content="Sign in"/>
</Grid>

This behavior is desired:

This

The only thing that is missing is on screen resize for the text in each control to also resize to utilize the free space in that control.

I tried using Viewbox, but regardless of the value of the Stretch property, it changes the size of the control and I don't know how to force it to remain sized according to the grid.

I primarily looked for answers on this post: How to automatically scale font size for a group of controls?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
  • So you want each box's font size to adjust automatically to fill all available space? And I assume different boxes can have different font sizes - or do you want them all to scale uniformly? In either case there are several different ways to accomplish what you're looking to do. Finding "the best" would depend on these specifics. – Emperor Eto May 07 '23 at 13:53
  • @EmperorEto, yes, I want them to fill the available space, and I don't care if each control has a different font size. If there are many solutions, the simplest or the one with least code both seem like a good option. – Ivan Gyulev May 07 '23 at 16:53

2 Answers2

1

There isn't going to be a one-size-fits-all solution to this as far as I'm able to imagine, but here is a solution to your specific problem that also demonstrates two different ways of handling this.

public static class AutoScale
{
    public static readonly DependencyProperty AutoscaleFontProperty = DependencyProperty.RegisterAttached(
        "AutoscaleFont",
        typeof(bool),
        typeof(AutoScale),
        new PropertyMetadata((sender, e) =>
        {
            if (!(sender is Control c))
                throw new NotSupportedException($"AutoscaleFont is for Control-derived classes only");
            if (e.NewValue == e.OldValue || !(e.NewValue is bool value))
                return;
            if (value)
                c.SizeChanged += OnSizeChangedRescaleFont;
            else
                c.SizeChanged -= OnSizeChangedRescaleFont;
        }));

    private static void OnSizeChangedRescaleFont(object sender, SizeChangedEventArgs e)
    {
        if (!(sender is Control c))
            throw new NotSupportedException($"AutoscaleFont is for Control-derived classes only");

        if (c is TextBox)
        {
            c.FontSize = c.ActualHeight * 0.8;
            return;
        }

        Border border = null;
        EnumVisual(c, fe =>
        {
            if (c is Button && fe is Border b)
            {
                border = b;
                return true;
            }
            return false;
        });
        if (border == null)
            return;

        if (!(border.Child is FrameworkElement child))
            return;

        double scale = 1;
        if (child.ActualWidth / child.ActualHeight > border.ActualWidth / border.ActualHeight)
        {
            // fit to width
            scale = border.ActualWidth / child.ActualWidth;
        }
        else
        {
            // fit to height
            scale = border.ActualHeight / child.ActualHeight;
        }

        child.RenderTransformOrigin = new Point(0.5, 0.5);
        child.RenderTransform = new ScaleTransform
        {
            ScaleX = scale,
            ScaleY = scale
        };
    }

    public static bool GetAutoscaleFont (DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoscaleFontProperty);
    } 
    public static void SetAutoscaleFont(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoscaleFontProperty, value);
    }

    private static void EnumVisual(FrameworkElement myVisual, Func<FrameworkElement, bool> action)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
        {
            // Retrieve child visual at specified index value.
            FrameworkElement child = VisualTreeHelper.GetChild(myVisual, i) as FrameworkElement;
            if (child == null) 
                continue;

            // Do processing of the child visual object.
            if (action != null)
            {
                if (action(child)) 
                    break;
            }

            // Enumerate children of the child visual object.
            EnumVisual(child, action);
        }
    }
}

To consume, just say:

    <TextBox x:Name="username"
             Grid.Row="1"
             Grid.Column="1"
             Grid.ColumnSpan="3"
             Text="Username"
             local:AutoScale.AutoscaleFont="True"
             HorizontalContentAlignment="Center"
             VerticalContentAlignment="Center" />

etc.

The meat of this is in OnSizeChangedRescaleFont. The way to do this for any particular control is going to be control dependent. This is what I think is the best way to scale the font for both a default Button and a default TextBox.

You'll note these are completely different methods - for TextBox I'd simply set the FontSize property to be a multiple of the actual height because the TextBox could horizontally scroll, and you probably don't want the font size to shrink as people type anyway.

For Button where the content is static, once you locate the Border and its child you can use a RenderTransform to make it scale as the window resizes. This does a best-fit depending on the width of the content vs. the width of the button.

Of course this is far from perfect but hopefully it demonstrates the concepts and contains code you can use to build on. A completely robust solution would involve subclassing your controls, overriding ArrangeOverride, and re-templating them. That is, indeed, much more complex. This should satisfy your literal example though.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32
  • Thanks for the response, from it I learned that there was a SIzeChanged event and from there I figured it out. You can cast everything to a Control so you don't have to make multiple methods, but it is true that each use case and control require a somewhat different formula for deciding their font size. – Ivan Gyulev May 08 '23 at 16:16
  • @IvanGyulev great, would you kindly accept my answer if it helped? – Emperor Eto May 08 '23 at 17:13
-1

There is a SizeChanged routed event that as the name implies is triggered every time a control changes it's size. So you can add a routed event handler(method) that listens for everytime the control's size is changed and changes its font size or any property for that matter.

Here's an implementation for the specific case:

private void ResizeFont(object sender, SizeChangedEventArgs e)
{
    var control = sender as Control;
    if (control == null) return;
    control.FontSize = control.ActualHeight / 2;
}

In the xaml you just need to add the SizeChanged property to the controls:

<TextBox x:Name="username" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Text="Username"
        GotFocus="RemoveLabel" LostFocus="RestoreLabel"
        HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
        SizeChanged="ResizeFont"/>
<TextBox x:Name="password" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="Password"
        GotFocus="RemoveLabel" LostFocus="RestoreLabel"
        HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
        SizeChanged="ResizeFont"/>
<Button x:Name="signUp" Grid.Row="5" Grid.Column="1" Content="Sign up" SizeChanged="ResizeFont"/>
<Button x:Name="signIn" Grid.Row="5" Grid.Column="3" Content="Sign in" SizeChanged="ResizeFont"/>

This works nicely for buttons and text boxes with a wide aspect ratio that have a single line of text (text blocks adapt their height to the font size, so it doesn't work for them). If you need some different behavior this method is also easy to modify, for example when using a TextBox with multiple lines you can change the font size to be based on the ActualWidth instead of ActualHeight.

This is the result in the specific case. enter image description here

  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 09 '23 at 07:33
  • Posting your own answer to follow up with how you solved your problem is totally fine but it's really poor SO etiquite to accept it over the one that actually provided you what you needed to know. You also get no points for accepting your own answer. – Emperor Eto May 09 '23 at 21:52
  • @EmperorEto I don't care for the points and I am sorry that I can't reward your help, but I want people who have the problem to see the concise and to the point SizeChanged event answer, which is exactly what I was looking for. – Ivan Gyulev May 10 '23 at 16:41
  • Well your answer may be concise but (1) is not scalable (adding event handlers in code-behind rarely is) and (2) doesn't address what would happen if the button text content is too wide. And frankly if your only gap in knowledge was not knowing about `SizeChanged` you could have found that in seconds through a search rather than soliciting someone to spend a half hour coming up with a complete answer to your problem. Like I said, poor SO etiquite. – Emperor Eto May 10 '23 at 16:51