63

I have a few TextBlocks in WPF in a Grid that I would like to scale depending on their available width / height. When I searched for automatically scaling Font size the typical suggestion is to put the TextBlock into a ViewBox.

So I did this:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Viewbox MaxHeight="18" Grid.Column="0" Stretch="Uniform" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <TextBlock Text="{Binding Text1}" />
    </Viewbox>

    <Viewbox MaxHeight="18" Grid.Column="1" Stretch="Uniform" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <TextBlock Text="{Binding Text2}" />
    </Viewbox>

    <Viewbox MaxHeight="18" Grid.Column="2" Stretch="Uniform" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <TextBlock Text="{Binding Text3}" />
    </Viewbox>
</Grid>

And it scales the font for each TextBlock automatically. However, this looks funny because if one of the TextBlocks has longer text then it will be in a smaller font while it's neighboring grid elements will be in a larger font. I want the Font size to scale by group, perhaps it would be nice if I could specify a "SharedSizeGroup" for a set of controls to auto size their font.

e.g.

The first text blocks text might be "3/26/2013 10:45:30 AM", and the second TextBlocks text might say "FileName.ext". If these are across the width of a window, and the user begins resizing the window smaller and smaller. The date will start making its font smaller than the file name, depending on the length of the file name.

Ideally, once one of the text fields starts to resize the font point size, they would all match. Has anyone came up with a solution for this or can give me a shot at how you would make it work? If it requires custom code then hopefully we / I could repackage it into a custom Blend or Attached Behavior so that is re-usable for the future. I think it is a pretty general problem, but I wasn't able to find anything on it by searching.


Update I tried Mathieu's suggestion and it sort of works, but it has some side-effects:

<Window x:Class="WpfApplication6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="270" Width="522">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Rectangle Grid.Row="0" Fill="SkyBlue" />

        <Viewbox Grid.Row="1" MaxHeight="30"  Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
                    <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
                    <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="SomeLongText" Margin="5" />
                <TextBlock Grid.Column="1" Text="TextA" Margin="5" />
                <TextBlock Grid.Column="2" Text="TextB" Margin="5" />

            </Grid>
        </Viewbox>
    </Grid>
</Window>

Side-Effects

Honestly, missing hte proportional columns is probably fine with me. I wouldn't mind it AutoSizing the columns to make smart use of the space, but it has to span the entire width of the window.

Notice without maxsize, in this extended example the text is too large:

<Window x:Class="WpfApplication6.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="270" Width="522">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <Rectangle Grid.Row="0" Fill="SkyBlue" />

    <Viewbox Grid.Row="1"  Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="col"/>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="SomeLongText" Margin="5" />
            <TextBlock Grid.Column="1" Text="TextA" Margin="5" />
            <TextBlock Grid.Column="2" Text="TextB" Margin="5" />

        </Grid>
    </Viewbox>
</Grid>

Text too large without MaxSize

Here, I would want to limit how big the font can get, so it doesn't waste vertical window real estate. I'm expecting the output to be aligned left, center, and right with the Font being as big as possible up to the desired maximum size.


@adabyron

The solution you propose is not bad (And is the best yet) but it does have some limitations. For example, initially I wanted my columns to be proportional (2nd one should be centered). For example, my TextBlocks might be labeling the start, center, and stop of a graph where alignment matters.

<Window x:Class="WpfApplication6.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:b="clr-namespace:WpfApplication6.Behavior"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Rectangle Grid.Row="0" Fill="SkyBlue" />
        <Line X1="0.5" X2="0.5" Y1="0" Y2="1" Stretch="Fill" StrokeThickness="3" Stroke="Red" />

        <Grid Grid.Row="1">

            <i:Interaction.Behaviors>
                <b:MoveToViewboxBehavior />
            </i:Interaction.Behaviors>

            <Viewbox Stretch="Uniform" />
            <ContentPresenter >
                <ContentPresenter.Content>
                    <Grid x:Name="TextBlockContainer">
                        <Grid.Resources>
                            <Style TargetType="TextBlock" >
                                <Setter Property="FontSize" Value="16" />
                                <Setter Property="Margin" Value="5" />
                            </Style>
                        </Grid.Resources>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="*"  />
                        </Grid.ColumnDefinitions>

                        <TextBlock Grid.Column="0" Text="SomeLongText" VerticalAlignment="Center" HorizontalAlignment="Center" />
                        <TextBlock Grid.Column="2" Text="TextA" HorizontalAlignment="Center" VerticalAlignment="Center" />
                        <TextBlock Grid.Column="4" Text="TextB" HorizontalAlignment="Center" VerticalAlignment="Center" />
                    </Grid>
                </ContentPresenter.Content>
            </ContentPresenter>
        </Grid>
    </Grid>
</Window>

And here is the result. Notice it does not know that it is getting clipped early on, and then when it substitutes ViewBox it looks as if the Grid defaults to column size "Auto" and no longer aligns center.

Scaling with adabyron's suggestion

Alan
  • 7,875
  • 1
  • 28
  • 48
  • This question might help you: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/c052fa89-4788-4d85-b266-fdd5c637a0ff – Matt Burland Mar 26 '13 at 15:57
  • 1
    I'm not sure I still know what the question is. You implemented the solution below and get the last screenshot in your question, right? Can you post another screenshot where a condition arises that you try to avoid now? I just cannot imagine it? =) – Akku Apr 10 '13 at 19:11
  • @Akku The solution should look like the first screen shot, but without wasted margins left/right. The textblocks should be all the same font size, but aligned left side, center, and right side. Does this make sense? I want to create a "group" of controls, that if they must be resized smaller because of limited space, then they all choose the same size but smaller font. – Alan Apr 10 '13 at 19:16
  • So should the left word be more on the left and the right word be more on the right? Or should the text stretch to look a bit awkwardly stretched? :-) – Akku Apr 10 '13 at 19:23
  • @Akku Yeah, I definitely do not want the font stretched out of proportion. I just wanted 3 equally spaced columns with the first text aligned left, second text aligned middle, and third text aligned right. e.g. Say the FontSize is 18 but the grid becomes too short or too thin to display the text, the text would become a smaller font size so that it would fit. Each text box should be the same size font. It should **not** look like one font is 8pt and the other 16pt. The spacing should also be maintained so that it does not look like a autosized grid that is centered, but with large margins L&R. – Alan Apr 10 '13 at 19:29
  • Wow, trying hard but this is really not doable I think. Either you have the Viewbox that doesn't fill the container above (and really not modifying the font size, but just scaling the content), or with Stretch=Fill stretching the content, so you cannot have whitespace in there. So the viewbox cannot be the solution to this. Testing on ... – Akku Apr 10 '13 at 20:13
  • @Akku I've tried quite a bit with ViewBoxes... I'm starting to believe the solution will involve measuring it manually and adjusting the FontSize or applying some sort of shared scale transform. The ViewBox's internal workings may give insight into how to achieve the effect. I just figured I'd offer a bounty, hope some has a great solution, or is interested in crafting one for reputation. And also, I think it is of general interest to the community. – Alan Apr 10 '13 at 20:16
  • It is far easier :) Go for the viewbox! http://stackoverflow.com/questions/1464185/how-to-set-textblock-or-label-with-resizable-font-size-in-wpf – Saw Jul 28 '14 at 09:16
  • @MohamedSakherSawan Mohamed, you should re-read the question. The question was how to scale fonts as a group without certain side-effects that ViewBoxes cause. – Alan Jul 28 '14 at 14:10
  • @Alan So sorry :), I am not that good in WPF, then the only way is to write a custom converters as guys mentioned. – Saw Jul 28 '14 at 15:17

7 Answers7

40

I wanted to edit the answer I had already offered, but then decided it makes more sense to post a new one, because it really depends on the requirements which one I'd prefer. This here probably fits Alan's idea better, because

  • The middle textblock stays in the middle of the window
  • Fontsize adjustment due to height clipping is accomodated
  • Quite a bit more generic
  • No viewbox involved

enter image description here

enter image description here

The other one has the advantage that

  • Space for the textblocks is allocated more efficiently (no unnecessary margins)
  • Textblocks may have different fontsizes

I tested this solution also in a top container of type StackPanel/DockPanel, behaved decently.

Note that by playing around with the column/row widths/heights (auto/starsized), you can get different behaviors. So it would also be possible to have all three textblock columns starsized, but that means width clipping does occur earlier and there is more margin. Or if the row the grid resides in is auto sized, height clipping will never occur.

Xaml:

<Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:beh="clr-namespace:WpfApplication1.Behavior"
            Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.9*"/>
            <RowDefinition Height="0.1*" />
        </Grid.RowDefinitions>

        <Rectangle Fill="DarkOrange" />

        <Grid x:Name="TextBlockContainer" Grid.Row="1" >
            <i:Interaction.Behaviors>
                <beh:ScaleFontBehavior MaxFontSize="32" />
            </i:Interaction.Behaviors>
            <Grid.Resources>
                <Style TargetType="TextBlock" >
                    <Setter Property="Margin" Value="5" />
                    <Setter Property="VerticalAlignment" Value="Center" />
                </Style>
            </Grid.Resources>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"  />
                <ColumnDefinition Width="Auto"  />
                <ColumnDefinition Width="*"  />
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="SomeLongText" />
            <TextBlock Grid.Column="1" Text="TextA" HorizontalAlignment="Center"  />
            <TextBlock Grid.Column="2" Text="TextB" HorizontalAlignment="Right"  />
        </Grid>
    </Grid>
</Window>

ScaleFontBehavior:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
using WpfApplication1.Helpers;

namespace WpfApplication1.Behavior
{
    public class ScaleFontBehavior : Behavior<Grid>
    {
        // MaxFontSize
        public double MaxFontSize { get { return (double)GetValue(MaxFontSizeProperty); } set { SetValue(MaxFontSizeProperty, value); } }
        public static readonly DependencyProperty MaxFontSizeProperty = DependencyProperty.Register("MaxFontSize", typeof(double), typeof(ScaleFontBehavior), new PropertyMetadata(20d));

        protected override void OnAttached()
        {
            this.AssociatedObject.SizeChanged += (s, e) => { CalculateFontSize(); };
        }

        private void CalculateFontSize()
        {
            double fontSize = this.MaxFontSize;

            List<TextBlock> tbs = VisualHelper.FindVisualChildren<TextBlock>(this.AssociatedObject);

            // get grid height (if limited)
            double gridHeight = double.MaxValue;
            Grid parentGrid = VisualHelper.FindUpVisualTree<Grid>(this.AssociatedObject.Parent);
            if (parentGrid != null)
            {
                RowDefinition row = parentGrid.RowDefinitions[Grid.GetRow(this.AssociatedObject)];
                gridHeight = row.Height == GridLength.Auto ? double.MaxValue : this.AssociatedObject.ActualHeight;
            }

            foreach (var tb in tbs)
            {
                // get desired size with fontsize = MaxFontSize
                Size desiredSize = MeasureText(tb);
                double widthMargins = tb.Margin.Left + tb.Margin.Right;
                double heightMargins = tb.Margin.Top + tb.Margin.Bottom; 

                double desiredHeight = desiredSize.Height + heightMargins;
                double desiredWidth = desiredSize.Width + widthMargins;

                // adjust fontsize if text would be clipped vertically
                if (gridHeight < desiredHeight)
                {
                    double factor = (desiredHeight - heightMargins) / (this.AssociatedObject.ActualHeight - heightMargins);
                    fontSize = Math.Min(fontSize, MaxFontSize / factor);
                }

                // get column width (if limited)
                ColumnDefinition col = this.AssociatedObject.ColumnDefinitions[Grid.GetColumn(tb)];
                double colWidth = col.Width == GridLength.Auto ? double.MaxValue : col.ActualWidth;

                // adjust fontsize if text would be clipped horizontally
                if (colWidth < desiredWidth)
                {
                    double factor = (desiredWidth - widthMargins) / (col.ActualWidth - widthMargins);
                    fontSize = Math.Min(fontSize, MaxFontSize / factor);
                }
            }

            // apply fontsize (always equal fontsizes)
            foreach (var tb in tbs)
            {
                tb.FontSize = fontSize;
            }
        }

        // Measures text size of textblock
        private Size MeasureText(TextBlock tb)
        {
            var formattedText = new FormattedText(tb.Text, CultureInfo.CurrentUICulture,
                FlowDirection.LeftToRight,
                new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch),
                this.MaxFontSize, Brushes.Black); // always uses MaxFontSize for desiredSize

            return new Size(formattedText.Width, formattedText.Height);
        }
    }
}

VisualHelper:

public static List<T> FindVisualChildren<T>(DependencyObject obj) where T : DependencyObject
{
    List<T> children = new List<T>();
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        var o = VisualTreeHelper.GetChild(obj, i);
        if (o != null)
        {
            if (o is T)
                children.Add((T)o);

            children.AddRange(FindVisualChildren<T>(o)); // recursive
        }
    }
    return children;
}

public static T FindUpVisualTree<T>(DependencyObject initial) where T : DependencyObject
{
    DependencyObject current = initial;

    while (current != null && current.GetType() != typeof(T))
    {
        current = VisualTreeHelper.GetParent(current);
    }
    return current as T;
}
Community
  • 1
  • 1
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • 1
    Thanks for the effort, this seems to work reasonably well and the code makes sense. Looks good. – Alan Apr 15 '13 at 13:43
  • This solution doesn´t work in WinRT unfortunately. The interaction namespace seems to be missing. The Behaviors SDK can be referenced by a WinRT project but it does only contain an IBehavior. It would be great if anyone could help me with a solution like this for WinRT apps. – SeBo Mar 31 '15 at 14:41
  • @SeBo: You don't need to use the Expression Blend behavior namespace, this just adds convenient features like the strongly typed `AssociatedObject`. You can rewrite the code without too many changes to an attached behavior. I've got an example [here](http://stackoverflow.com/a/25511558/385995), but you should find plenty of help when googling *attached behavior*. – Mike Fuchs Apr 07 '15 at 15:50
  • If you have a problem with using `System.Windows.Interactivity`, see the most voted answer from [here](https://stackoverflow.com/questions/8360209/how-to-add-system-windows-interactivity-to-project). And to avoid the obsolete warning for `FormattedText`, use the `PixelsPerDip`. It should be `var formattedText = new(tb.Text,CultureInfo.CurrentUICulture,FlowDirection.LeftToRight,new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch),this.MaxFontSize,Brushes.Black,VisualTreeHelper.GetDpi(tb).PixelsPerDip);`. – Polar Aug 17 '22 at 02:56
26

Put your grid in the ViewBox, which will scale the whole Grid :

<Viewbox Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Column="0" Text="{Binding Text1}" Margin="5" />
        <TextBlock Grid.Column="1" Text="{Binding Text2}" Margin="5" />
        <TextBlock Grid.Column="2" Text="{Binding Text3}" Margin="5" />

    </Grid>
</Viewbox>
mathieu
  • 30,974
  • 4
  • 64
  • 90
  • I'm getting unusual behavior with this, I may try a sample application that simplifies it to double check. But it doesn't seem to be working... – Alan Mar 26 '13 at 16:11
  • Alright, as you can see I had a MaxHeight set because I didn't want the font to become arbitrarily large (And in the designer it takes up half the screen because the bindings are blank). However, if I set MaxHeight on the ViewBox, the Grid's width doesn't span the window's width and neither does the text columns. If I set MaxHeight on the TextBlocks it has no effect. Also, inside of a ViewBox the ColumnDefinition Width="*" Doesn't work, it looks like I've set all my columns widths to "Auto" because they are not equal. See my update. – Alan Mar 26 '13 at 16:33
  • 15
    FYI: Did you know you can define {Binding Text1, FallbackValue='SomeLongText'} to show stuff in the designer when the Bindings are blank? Helps me a lot when developing. – Akku Apr 10 '13 at 19:07
  • 1
    Please note this is not the answer. It was helpful, and I upvoted it, but I started a bounty AFTER this answer was posted because it is not sufficient. – Alan Apr 10 '13 at 19:17
  • This was simple enough for me to fix – Ebrahim Karam Apr 15 '21 at 00:54
1

I think I know the way to go and will leave the rest to you. In this example, I bound the FontSize to the ActualHeight of the TextBlock, using a converter (the converter is below):

<Window x:Class="MyNamespace.Test"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Converters="clr-namespace:UpdateYeti.Converters"
    Title="Test" Height="570" Width="522">
<Grid Height="370" Width="522">
    <Grid.Resources>
        <Converters:HeightToFontSizeConverter x:Key="conv" />
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <Rectangle Grid.Row="0" Fill="SkyBlue" />
        <Grid Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="60" Background="Beige">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" Text="SomeLongText" Margin="5" 
                   FontSize="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight, Converter={StaticResource conv}}" />
        <TextBlock Grid.Column="1" Text="TextA" Margin="5" HorizontalAlignment="Center" 
                   FontSize="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight, Converter={StaticResource conv}}" />
        <TextBlock Grid.Column="2" Text="TextB" Margin="5" FontSize="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight, Converter={StaticResource conv}}" />
        </Grid>
    </Grid>
</Window>


[ValueConversion(typeof(double), typeof(double))]
class HeightToFontSizeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // here you can use the parameter that you can give in here via setting , ConverterParameter='something'} or use any nice login with the VisualTreeHelper to make a better return value, or maybe even just hardcode some max values if you like
        var height = (double)value;
        return .65 * height;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Akku
  • 4,373
  • 4
  • 48
  • 67
  • What about width? Width is probably more of a concern than height, and it depends on the text content. – Alan Apr 10 '13 at 21:08
  • I would limit the font size in the Converter based on how long the sum of all three texts is, but I didn't implement that in my example because it will be a lof of fiddling with the text-length of three strings. Also you said you didn't want to have a stretched text of of proportion (thereby setting a font-width, which doesn't exist anyways). So the rest is just fine-tuning the converter, maybe with the help of the viewModel object containing the strings. – Akku Apr 11 '13 at 04:44
1

General remark: A possible alternative to the whole text scaling could be to just use TextTrimming on the TextBlocks.

I've struggled to find a solution to this one. Using a viewbox is really hard to mix with any layout adjustments. Worst of all, ActualWidth etc. do not change inside a viewbox. So I finally decided to use the viewbox only if absolutely necessary, which is when clipping would occur. I'm therefore moving the content between a ContentPresenter and a Viewbox, depending upon the available space.


enter image description here

enter image description here


This solution is not as generic as I would like, mainly the MoveToViewboxBehavior does assume it is attached to a grid with the following structure. If that cannot be accomodated, the behavior will most likely have to be adjusted. Creating a usercontrol and denoting the necessary parts (PART_...) might be a valid alternative.

Note that I have extended the grid's columns from three to five, because that makes the solution a lot easier. It means that the middle textblock will not be exactly in the middle, in the sense of absolute coordinates, instead it is centered between the textblocks to the left and right.

<Grid > <!-- MoveToViewboxBehavior attached to this grid -->
    <Viewbox />
    <ContentPresenter>
        <ContentPresenter.Content> 
            <Grid x:Name="TextBlockContainer">                       
                <TextBlocks ... />
            </Grid>
        </ContentPresenter.Content>
    </ContentPresenter>
</Grid>

Xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:beh="clr-namespace:WpfApplication1.Behavior"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Rectangle Grid.Row="0" Fill="SkyBlue" />

        <Grid Grid.Row="1">

            <i:Interaction.Behaviors>
                <beh:MoveToViewboxBehavior />
            </i:Interaction.Behaviors>

            <Viewbox Stretch="Uniform" />
            <ContentPresenter >
                <ContentPresenter.Content>
                    <Grid x:Name="TextBlockContainer">
                        <Grid.Resources>
                            <Style TargetType="TextBlock" >
                                <Setter Property="FontSize" Value="16" />
                                <Setter Property="Margin" Value="5" />
                            </Style>
                        </Grid.Resources>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"  />
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="Auto"  />
                            <ColumnDefinition Width="*"  />
                            <ColumnDefinition Width="Auto"  />
                        </Grid.ColumnDefinitions>

                        <TextBlock Grid.Column="0" Text="SomeLongText" />
                        <TextBlock Grid.Column="2" Text="TextA"  />
                        <TextBlock Grid.Column="4" Text="TextB"  />
                    </Grid>
                </ContentPresenter.Content>
            </ContentPresenter>
        </Grid>
    </Grid>
</Window>

MoveToViewBoxBehavior:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
using WpfApplication1.Helpers;

namespace WpfApplication1.Behavior
{
    public class MoveToViewboxBehavior : Behavior<Grid>
    {
        // IsClipped 
        public bool IsClipped { get { return (bool)GetValue(IsClippedProperty); } set { SetValue(IsClippedProperty, value); } }
        public static readonly DependencyProperty IsClippedProperty = DependencyProperty.Register("IsClipped", typeof(bool), typeof(MoveToViewboxBehavior), new PropertyMetadata(false, OnIsClippedChanged));

        private static void OnIsClippedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var beh = (MoveToViewboxBehavior)sender;
            Grid grid = beh.AssociatedObject;

            Viewbox vb = VisualHelper.FindVisualChild<Viewbox>(grid);
            ContentPresenter cp = VisualHelper.FindVisualChild<ContentPresenter>(grid);

            if ((bool)e.NewValue) 
            {
                // is clipped, so move content to Viewbox
                UIElement element = cp.Content as UIElement;
                cp.Content = null;
                vb.Child = element;
            }
            else
            {
                // can be shown without clipping, so move content to ContentPresenter
                cp.Content = vb.Child;
                vb.Child = null;
            }
        }

        protected override void OnAttached()
        {
            this.AssociatedObject.SizeChanged += (s, e) => { IsClipped = CalculateIsClipped(); };
        }

        // Determines if the width of all textblocks within TextBlockContainer (using MaxFontSize) are wider than the AssociatedObject grid
        private bool CalculateIsClipped()
        {
            double totalDesiredWidth = 0d;
            Grid grid = VisualHelper.FindVisualChildByName<Grid>(this.AssociatedObject, "TextBlockContainer");
            List<TextBlock> tbs = VisualHelper.FindVisualChildren<TextBlock>(grid);

            foreach (var tb in tbs)
            {
                if (tb.TextWrapping != TextWrapping.NoWrap)
                    return false;

                totalDesiredWidth += MeasureText(tb).Width + tb.Margin.Left + tb.Margin.Right + tb.Padding.Left + tb.Padding.Right;
            }

            return Math.Round(this.AssociatedObject.ActualWidth, 5) < Math.Round(totalDesiredWidth, 5);
        }

        // Measures text size of textblock
        private Size MeasureText(TextBlock tb)
        {
            var formattedText = new FormattedText(tb.Text, CultureInfo.CurrentUICulture,
                FlowDirection.LeftToRight,
                new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch),
                tb.FontSize, Brushes.Black);

            return new Size(formattedText.Width, formattedText.Height);
        }
    }
}

VisualHelper:

public static class VisualHelper
{
    public static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
    {
        T child = null;
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            var o = VisualTreeHelper.GetChild(obj, i);
            if (o != null)
            {
                child = o as T;
                if (child != null) break;
                else
                {
                    child = FindVisualChild<T>(o); // recursive
                    if (child != null) break;
                }
            }
        }
        return child;
    }

    public static List<T> FindVisualChildren<T>(DependencyObject obj) where T : DependencyObject
    {
        List<T> children = new List<T>();
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            var o = VisualTreeHelper.GetChild(obj, i);
            if (o != null)
            {
                if (o is T)
                    children.Add((T)o);

                children.AddRange(FindVisualChildren<T>(o)); // recursive
            }
        }
        return children;
    }

    public static T FindVisualChildByName<T>(DependencyObject parent, string name) where T : FrameworkElement
    {
        T child = default(T);
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var o = VisualTreeHelper.GetChild(parent, i);
            if (o != null)
            {
                child = o as T;
                if (child != null && child.Name == name)
                    break;
                else
                    child = FindVisualChildByName<T>(o, name);

                if (child != null) break;
            }
        }
        return child;
    }
}
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • The idea of conditionally using a ViewBox is clever, but it does have some limitations as you mentioned. First, you need to modify your Grid's columns. Second, it is impossible to get proportional columns once the ViewBoxes are inside. Third, it doesn't consider height in IsClipped, but that is probably an easy change except I don't believe the width will work out if you use a ViewBox in that condition. Basically, not as general as you would like as you mentioned. It is not a bad idea though, packaged as a behavior and thinks outside the box by switching to ViewBoxes to avoid 1/2 the problem. – Alan Apr 12 '13 at 14:43
  • Please expand on your first and second limitation. I seem to have some problems getting what your goal is, because my solution _does_ look like "the first screen shot, but without wasted margins left/right", font size is always equal, maximum fontsize can be easily set... Please refer to my screenshots and what should look different. I haven't included clipping from the top, because I did not see how the windows should shrink so small and still make sense, but I agree that is missing. – Mike Fuchs Apr 12 '13 at 20:01
  • I updated my post to illustrate what I was referring to in the first two points. I hope this helps explain. In my first code snippet in the original question I had intended to use proportional ( Width="*" ) columns. The 3rd point I was referring to, was what if I had a Grid with more than 1 row (say 2-3) in which Height cutoff could become relevant. Then I would need to alter IsClipped to account for height. If the ViewBox is inserted because of Height-clipping then the width will end up with margins again, won't it? – Alan Apr 12 '13 at 20:31
  • For example, in your first screen shot, set the first TextBlock to "SomeLongLongLongLongText" and the second Text will be off-center because they are Auto sized. When ViewBox takes over, it always auto-sizes Grid Columns. (presumably because it gives it infinite space so proportional logical doesn't really work) – Alan Apr 12 '13 at 20:56
  • 1
    Cheers for the clarification. I have posted [another answer](http://stackoverflow.com/a/15982149/385995) that I hope suits you better, but I'll leave this one on because for e.g. a statusbar, that's how I'd want it to behave. – Mike Fuchs Apr 12 '13 at 23:04
1

You can use a hidden ItemsControl in a ViewBox.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Viewbox VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="SomeLongText"/>
            <ItemsControl Visibility="Hidden">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <TextBlock Text="SomeLongText"/>
                <TextBlock Text="TextA"/>
                <TextBlock Text="TextB"/>
            </ItemsControl>
        </Grid>
    </Viewbox>
    <Viewbox Grid.Column="1" VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="TextA"/>
            <ItemsControl Visibility="Hidden">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <TextBlock Text="SomeLongText"/>
                <TextBlock Text="TextA"/>
                <TextBlock Text="TextB"/>
            </ItemsControl>
        </Grid>
    </Viewbox>
    <Viewbox Grid.Column="2" VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="TextB"/>
            <ItemsControl Visibility="Hidden">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <TextBlock Text="SomeLongText"/>
                <TextBlock Text="TextA"/>
                <TextBlock Text="TextB"/>
            </ItemsControl>
        </Grid>
    </Viewbox>
</Grid>

or

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Viewbox VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="{Binding Text1}"/>
            <ItemsControl Visibility="Hidden" ItemsSource="{Binding AllText}">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Viewbox>
    <Viewbox Grid.Column="1" VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="{Binding Text2}"/>
            <ItemsControl Visibility="Hidden" ItemsSource="{Binding AllText}">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Viewbox>
    <Viewbox Grid.Column="2" VerticalAlignment="Bottom">
        <Grid>
            <TextBlock Text="{Binding Text3}"/>
            <ItemsControl Visibility="Hidden" ItemsSource="{Binding AllText}">
                <ItemsPanelTemplate>
                    <Grid/>
                </ItemsPanelTemplate>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Viewbox>
</Grid>
Egemen Çiftci
  • 699
  • 5
  • 13
0

A solution could be something like that :

Choose a maxFontSize, then define the appropriate FontSize to be displayed considering the current Window by using a linear equation. Window's height or width would limit the final FontSize choice.

Let's take the case of a "single kind TextBlock" for the whole grid :

Window.Current.SizeChanged += (sender, args) =>
        {
            int minFontSize = a;
            int maxFontSize = b;
            int maxMinFontSizeDiff = maxFontSize - minFontSize;

            int gridMinHeight = c;
            int gridMaxHeight = d;
            int gridMaxMinHeightDiff = gridMaxHeight - gridMinHeight;

            int gridMinWidth = e;
            int gridMaxWidth = f;
            int gridMaxMinHeightDiff = gridMaxWidth - gridMaxWidth;

            //Linear equation considering "max/min FontSize" and "max/min GridHeight/GridWidth"
            double heightFontSizeDouble = (maxMinFontSizeDiff / gridMaxMinHeightDiff ) * Grid.ActualHeight + (maxFontSize - (gridMaxHeight * (maxMinFontSizeDiff  / gridMaxMinHeightDiff)))
            double widthFontSizeDouble = (maxMinFontSizeDiff / gridMaxMinWidthDiff ) * Grid.ActualWidth + (maxFontSize - (gridMaxWidth * (maxMinFontSizeDiff  / gridMaxMinWidthDiff)))

            int heightFontSize = (int)Math.Round(heightFontSizeDouble)
            int widthFontSize = (int)Math.Round(widthFontSizeDouble)

            foreach (var children in Grid.Children)
            {                    
                (children as TextBlock).FontSize = Math.Min(heightFontSize, widthFontSize);
            }
        }
Javert
  • 248
  • 2
  • 7
0

I use ScaleTransform. This scales the current font by the desired amount:

<ScaleTransform x:Key="FontDoubled" ScaleX="2" ScaleY="2" />
<ScaleTransform x:Key="FontHalved" ScaleX="0.5" ScaleY="0.5" />

The larger the ScaleX and ScaleY, the larger the font is scaled by. No need to change individual font sizes.

It can also be used with just ScaleX or ScaleY to change the font size in one direction only, or with different values.

To use it:

<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
                       FontWeight="Bold"
                       Content="This is normal the size" />

...gives:

enter image description here

And:

<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
                   FontWeight="Bold"
                   LayoutTransform="{StaticResource FontDoubled}"
                   Content="This is Double size" />

...gives:

enter image description here

Nathan Evans
  • 139
  • 2
  • 3