How can I have a WPF TextBlock that has the same size for all font weights?

StackOverflow https://stackoverflow.com/questions/20556328

  •  01-09-2022
  •  | 
  •  

Question

I'll jump to the chase: Is there any way to tell the WPF TextBlock to measure itself such that its size doesn't change when its FontWeight changes?

I have a TextBlock that changes font weights dynamically based on a style. The TextBlock is inside a RadioButton so it is Bold when checked, Normal otherwise:

<Style x:Key="BoldWhenChecked" TargetType="RadioButton">
    <Style.Triggers>
        <Trigger Property="IsChecked" Value="True">
            <Setter Property="TextElement.FontWeight" Value="Bold" />
        </Trigger>
    </Style.Triggers>
</Style

and here are the radio buttons themselves:

<StackPanel Orientation="Horizontal">
    <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 1" />
    </RadioButton>
    <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 2" />
    </RadioButton>
    <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 3" />
    </RadioButton>
    etc...
</StackPanel>

Unfortunately, since I am not using a fixed-width font the width of the TextBlock changes when the font weight changes, and the entire panel of radio buttons shifts accordingly, which is visually jarring.

Était-ce utile?

La solution

I created a work-around for this by adding a hidden TextBlock in the content of the RadioButton with its FontStyle explicitly set to Bold:

<RadioButton Style="{StaticResource BoldWhenChecked}">
    <Grid>
        <TextBlock Text="Item 1" />
        <TextBlock Text="Item 1" FontStyle="Bold" Visibility="Hidden" />
    </Grid>
</RadioButton>

That way when the RadioButton is selected and the visible TextBlock is made bold, the width does not change because the hidden TextBlock has already sized the grid appropriately.

Autres conseils

Personally I'd try to organize my layouts better so that they weren't dependent on the size of the radio buttons. That said, if you absolutely insist on doing it this way then you'll need to set the width of each TextBlock according to its size when Bold, and in a way that will cause it to update if you need to change the text and/or font family etc. To do that you'll need to bind the Width property to a Converter which accepts all those other values.

Start with a style that sets the width of the textbox:

<Style TargetType="{x:Type TextBlock}">
        <Setter Property="Width">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource TextToWidthConverter}">
                    <Binding Path="Text" RelativeSource="{RelativeSource Self}" UpdateSourceTrigger="PropertyChanged"/>
                    <Binding Path="FontFamily" RelativeSource="{RelativeSource Self}" UpdateSourceTrigger="PropertyChanged"/>
                    <Binding Path="FontStyle" RelativeSource="{RelativeSource Self}" UpdateSourceTrigger="PropertyChanged"/>
                    <Binding Path="FontStretch" RelativeSource="{RelativeSource Self}" UpdateSourceTrigger="PropertyChanged"/>
                    <Binding Path="FontSize" RelativeSource="{RelativeSource Self}" UpdateSourceTrigger="PropertyChanged"/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
    </Style>

Now add the code for the value converter itself (note that I'm using code posted by Clarke Kent in another StackOverflow answer to measure the text):

public class TextToWidthConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var text = values[0] as String;
        var fontFamily = values[1] as FontFamily;
        var fontStyle = (FontStyle)values[2];
        var fontStretch = (FontStretch)values[3];
        var fontSize = (Double)values[4];
        var size = MeasureText(text, fontFamily, fontStyle, FontWeights.Bold, fontStretch, fontSize);
        return size.Width;
    }

    public object[] ConvertBack(object values, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Get the required height and width of the specified text. Uses Glyph's
    /// </summary>
    public static Size MeasureText(string text, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double fontSize)
    {
        Typeface typeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
        GlyphTypeface glyphTypeface;

        if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
        {
            return MeasureTextSize(text, fontFamily, fontStyle, fontWeight, fontStretch, fontSize);
        }

        double totalWidth = 0;
        double height = 0;

        for (int n = 0; n < text.Length; n++)
        {
            ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[n]];

            double width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;

            double glyphHeight = glyphTypeface.AdvanceHeights[glyphIndex] * fontSize;

            if (glyphHeight > height)
            {
                height = glyphHeight;
            }

            totalWidth += width;
        }

        return new Size(totalWidth, height);
    }

    /// <summary>
    /// Get the required height and width of the specified text. Uses FortammedText
    /// </summary>
    public static Size MeasureTextSize(string text, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double fontSize)
    {
        FormattedText ft = new FormattedText(text,
                                             CultureInfo.CurrentCulture,
                                             FlowDirection.LeftToRight,
                                             new Typeface(fontFamily, fontStyle, fontWeight, fontStretch),
                                             fontSize,
                                             Brushes.Black);
        return new Size(ft.Width, ft.Height);
    }
}

But again, in a real app I'd try to solve this properly by putting them in a grid or something.

From your comment here:

"...by putting them in a grid or something." How would that work? That would be exactly what I'm looking for but haven't figure out a way to do it.

I know this is an old question. And while you don't actually say so in the question, nor does the question include a good, Minimal, Complete, Verifiable example that fully illustrates your scenario, I do suspect that you have (or rather, had) some need in mind where the group of radio buttons don't change size as the font style changes, but do somehow resize dynamically otherwise. For that purpose, I think your work-around with the hidden TextBlock is not bad.

But I would say that if you can control the layout of the whole group of radio buttons externally, then using one of the grid elements to arrange the radio buttons themselves would be better, as hinted at by Mark in his answer.

The key is that the grid objects can distribute widths and heights evenly over their columns and rows, and so can layout the radio buttons independently of their nominal desired size.

For example:

<Window x:Class="TestSO20556328BoldRadioButtons.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

  <Window.Resources>
    <p:Style x:Key="BoldWhenChecked" TargetType="RadioButton">
      <p:Style.Triggers>
        <Trigger Property="IsChecked" Value="True">
          <Setter Property="TextElement.FontWeight" Value="Bold" />
        </Trigger>
      </p:Style.Triggers>
    </p:Style>
  </Window.Resources>

  <StackPanel>
    <UniformGrid Columns="3">
      <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 1" />
      </RadioButton>
      <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 2" />
      </RadioButton>
      <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 3" />
      </RadioButton>
    </UniformGrid>

    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <RadioButton Style="{StaticResource BoldWhenChecked}">
        <TextBlock Text="Item 1" />
      </RadioButton>
      <RadioButton Style="{StaticResource BoldWhenChecked}" Grid.Column="1">
        <TextBlock Text="Item 2" />
      </RadioButton>
      <RadioButton Style="{StaticResource BoldWhenChecked}" Grid.Column="2">
        <TextBlock Text="Item 3" />
      </RadioButton>
    </Grid>
  </StackPanel>
</Window>

Just to show a couple of different ways to approach the problem.

Note that the above can work even if you are otherwise trying to size the group of radio buttons dynamically somehow. But without more specifics in the question, I wouldn't be able to say what specific approach would accomplish that goal.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top