Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to automatically scale font size for a group of controls?

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

like image 718
Alan Avatar asked Mar 26 '13 15:03

Alan


People also ask

What is text scaling?

Some users have difficulty when they read texts that are small. They can use their device's Font size Accessibility setting to make text larger on the screen. However, this setting only affects the appearance of text if its font size was specified in units of scalable pixels (sp).

What is dynamic type font?

The Dynamic Type feature allows users to choose the size of textual content displayed on the screen. It helps users who need larger text for better readability. It also accomodates those who can read smaller text, allowing more information to appear on the screen.

Which property allows you to control the size of the font?

The font-size-adjust property gives you better control of the font size when the first selected font is not available. When a font is not available, the browser uses the second specified font. This could result in a big change for the font size. To prevent this, use the font-size-adjust property.


2 Answers

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; } 
like image 109
Mike Fuchs Avatar answered Oct 17 '22 08:10

Mike Fuchs


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> 
like image 39
mathieu Avatar answered Oct 17 '22 09:10

mathieu