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

后端 未结 6 2036
予麋鹿
予麋鹿 2020-12-04 06:55

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 typi

相关标签:
6条回答
  • 2020-12-04 07:33

    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;
    }
    
    0 讨论(0)
  • 2020-12-04 07:44

    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-04 07:45

    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>
    
    0 讨论(0)
  • 2020-12-04 07:46

    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>
    
    0 讨论(0)
  • 2020-12-04 07:54

    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();
        }
    }
    
    0 讨论(0)
  • 2020-12-04 07:56

    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);
                }
            }
    
    0 讨论(0)
提交回复
热议问题