Align bottoms of text in controls
Asked Answered
A

7

21

The following snippet:

<Window x:Class="Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <StackPanel Orientation="Horizontal"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center">            
            <Label Content="Name:"/>
            <Label Content="Itzhak Perlman" FontSize="44"/>
        </StackPanel>
    </Grid>
</Window>

Renders the following:
alt text

Is there any way I can set in the Labels' styles so that their text bottoms should be aligned?
I have the same question with TextBlocks as well.

NOTE: since I've been struggling with this issue for a while, please post only certains answers that you know that work.
I already tried: VerticalAlignment, VerticalContentAlignment, Padding, Margin. Is there anything else I am not aware of?

I've read this post, but it doesn't talk about a scenario of different font size.

UPDATE: The problem is, that even Padding is set to 0 there is still an indeterminate space around the font, within the ContentPresenter area. this space varies on the font size. If I could control this space I would be in a better situation.

Thanks

Allotment answered 10/1, 2010 at 3:9 Comment(1)
ah haa! Your update clarifies the problem.Pains
F
6

There is no XAML only solution, you have to use code behind. Also, even with code-behind, there's no general solution for this, because what if your text is multi-line? Which baseline should be used in that case? Or what if there are multiple text elements in your template? Such as a header and a content, or more, which baseline then?

In short, your best bet is to align the text manually using top/bottom margins.

If you're willing to make the assumption that you have a single text element, you can figure out the pixel distance of the baseline from the top of the element by instantiating a FormattedText object with all the same properties of the existing text element. The FormattedText object has a double Baseline property which holds that value. Note that you still would have to manually enter a margin, because the element might not sit exactly against the top or bottom of its container.

See this MSDN forum post: Textbox Baseline

Here's a method I wrote that extracts that value. It uses reflection to get the relevant properties because they are not common to any single base class (they are defined separately on Control, TextBlock, Page, TextElement and maybe others).

public double CalculateBaseline(object textObject)
{
    double r = double.NaN;
    if (textObject == null) return r;

    Type t = textObject.GetType();
    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

    var fontSizeFI = t.GetProperty("FontSize", bindingFlags);
    if (fontSizeFI == null) return r;
    var fontFamilyFI = t.GetProperty("FontFamily", bindingFlags);
    var fontStyleFI = t.GetProperty("FontStyle", bindingFlags);
    var fontWeightFI = t.GetProperty("FontWeight", bindingFlags);
    var fontStretchFI = t.GetProperty("FontStretch", bindingFlags);

    var fontSize = (double)fontSizeFI.GetValue(textObject, null);
    var fontFamily = (FontFamily)fontFamilyFI.GetValue(textObject, null);
    var fontStyle = (FontStyle)fontStyleFI.GetValue(textObject, null);
    var fontWeight = (FontWeight)fontWeightFI.GetValue(textObject, null);
    var fontStretch = (FontStretch)fontStretchFI.GetValue(textObject, null);

    var typeFace = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);

    var formattedText = new FormattedText(
        "W", 
        CultureInfo.CurrentCulture, 
        FlowDirection.LeftToRight, 
        typeFace, 
        fontSize, 
        Brushes.Black);

    r = formattedText.Baseline;

    return r;
}

EDIT: Shimmy, in response to your comment, I don't believe you've actually tried this solution, because it works. Here's an example:

Example Baseline Alignment

Here's the XAML:

<StackPanel>
    <StackPanel.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Margin" Value="0,40,0,0"/>
        </Style>
    </StackPanel.Resources>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb1" Text="Lorem " FontSize="10"/>
        <TextBlock Name="tbref" Text="ipsum"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb2" Text="dolor "  FontSize="20"/>
        <TextBlock Text="sit"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb3" Text="amet "  FontSize="30"/>
        <TextBlock Text="consectetuer"/>
    </StackPanel>
</StackPanel>

And here's the code behind that achieves this

double baseRef = CalculateBaseline(tbref);
double base1 = CalculateBaseline(tb1) - baseRef;
double base2 = CalculateBaseline(tb2) - baseRef;
double base3 = CalculateBaseline(tb3) - baseRef;
tb1.Margin = new Thickness(0, 40 - base1, 0, 0);
tb2.Margin = new Thickness(0, 40 - base2, 0, 0);
tb3.Margin = new Thickness(0, 40 - base3, 0, 0);
Fullmouthed answered 10/1, 2010 at 13:43 Comment(8)
@אביעד, and what do I do with this value? what is actually the 'BaseLine'?Allotment
The baseline is the distance between the top of the text element and the line on which all letters 'sit'. If for example you set your Top margin to say 50 minus the baseline offset, you'll be guaranteed to align to a specific line no matter the font size.Fullmouthed
That's what I tried but doesn't work. I am still seeking for the solution. The problem is the space between the margin and 'top of the text' you mention. I updated my question. Thanks for all your effort אביעד. Any news?Allotment
Thank you so much Aviad. I'd like to make all my text controls (especially the TextBlocks to go thru this routine, is there a Xamly way to set it globally or I will need to inherit these controls? I thought let's ask the experts, you're tha man!Allotment
You have to use code behind, what's more you have to tweak the margin based on the specific control template of your text element. I.e. if it has borders, padding, etc. you have to modify the code behind to account for that.Fullmouthed
There is still kind of a problem, because if we could know the parent's actual height we could replace that '40' that is just a constant default with the dynamic heighst font size control. And the problem is cuz the ActualSize of the parent is rendered only after the children, nor can you iterate thru the TextBlock's siblings since when you do with the convert and the iterator is on the 1st child, the other siblings are not rendered yet and doesn't exist. Any good idea? I would really love to make this thing more dynamic, thanks for all.Allotment
The 40 is indeed just a constant offset for the baseline, you don't really need it, you can just substitute it for 0.Fullmouthed
Please vote: connect.microsoft.com/WPF/feedback/…Allotment
R
31

Another fairly simple solution:

1) Use TextBlock controls instead of Labels. The reason being that TextBlock is lighter weight than Label - see http://joshsmithonwpf.wordpress.com/2007/07/04/differences-between-label-and-textblock/

2) Use the LineHeight and LineStackingStrategy = BlockLineHeight for your TextBlock style. This will align the two at their baseline easily.

<StackPanel Orientation="Horizontal"
            VerticalAlignment="Center"
            HorizontalAlignment="Center">
    <StackPanel.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="LineHeight" Value="44pt"/>
            <Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
        </Style>
    </StackPanel.Resources>            
    <TextBlock Text="Name:"/>
    <TextBlock Text="Itzhak Perlman" FontSize="44"/>
</StackPanel>
Reductase answered 29/11, 2010 at 16:40 Comment(1)
@Shimmy have you tried this? It's simple and effective and should be the answer. Works nicely for me.Furness
S
7

I really like the creative solutions that are presented here but I do think that in the long run (pun intended) we should use this:

<TextBlock>
   <Run FontSize="20">What</Run>
   <Run FontSize="36">ever</Run>
   <Run FontSize="12" FontWeight="Bold">FontSize</Run>
</TextBlock>

The only thing that is missing from the Run element is databinding of the Text property but that might be added sooner or later.

A Run will not fix the alignment of labels and their textboxes but for many simple situation the Run will do quite nicely.

Stephie answered 20/1, 2010 at 14:59 Comment(1)
Keep in mind that in many cases Labels are preferred to make styles easier to apply to labels without applying to TextBlocks. Given original posters example of 'Name: YOUR NAME HERE' it would be more proper to use a label to hold the "Name:" text.Hypnotize
F
6

There is no XAML only solution, you have to use code behind. Also, even with code-behind, there's no general solution for this, because what if your text is multi-line? Which baseline should be used in that case? Or what if there are multiple text elements in your template? Such as a header and a content, or more, which baseline then?

In short, your best bet is to align the text manually using top/bottom margins.

If you're willing to make the assumption that you have a single text element, you can figure out the pixel distance of the baseline from the top of the element by instantiating a FormattedText object with all the same properties of the existing text element. The FormattedText object has a double Baseline property which holds that value. Note that you still would have to manually enter a margin, because the element might not sit exactly against the top or bottom of its container.

See this MSDN forum post: Textbox Baseline

Here's a method I wrote that extracts that value. It uses reflection to get the relevant properties because they are not common to any single base class (they are defined separately on Control, TextBlock, Page, TextElement and maybe others).

public double CalculateBaseline(object textObject)
{
    double r = double.NaN;
    if (textObject == null) return r;

    Type t = textObject.GetType();
    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

    var fontSizeFI = t.GetProperty("FontSize", bindingFlags);
    if (fontSizeFI == null) return r;
    var fontFamilyFI = t.GetProperty("FontFamily", bindingFlags);
    var fontStyleFI = t.GetProperty("FontStyle", bindingFlags);
    var fontWeightFI = t.GetProperty("FontWeight", bindingFlags);
    var fontStretchFI = t.GetProperty("FontStretch", bindingFlags);

    var fontSize = (double)fontSizeFI.GetValue(textObject, null);
    var fontFamily = (FontFamily)fontFamilyFI.GetValue(textObject, null);
    var fontStyle = (FontStyle)fontStyleFI.GetValue(textObject, null);
    var fontWeight = (FontWeight)fontWeightFI.GetValue(textObject, null);
    var fontStretch = (FontStretch)fontStretchFI.GetValue(textObject, null);

    var typeFace = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);

    var formattedText = new FormattedText(
        "W", 
        CultureInfo.CurrentCulture, 
        FlowDirection.LeftToRight, 
        typeFace, 
        fontSize, 
        Brushes.Black);

    r = formattedText.Baseline;

    return r;
}

EDIT: Shimmy, in response to your comment, I don't believe you've actually tried this solution, because it works. Here's an example:

Example Baseline Alignment

Here's the XAML:

<StackPanel>
    <StackPanel.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Margin" Value="0,40,0,0"/>
        </Style>
    </StackPanel.Resources>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb1" Text="Lorem " FontSize="10"/>
        <TextBlock Name="tbref" Text="ipsum"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb2" Text="dolor "  FontSize="20"/>
        <TextBlock Text="sit"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Name="tb3" Text="amet "  FontSize="30"/>
        <TextBlock Text="consectetuer"/>
    </StackPanel>
</StackPanel>

And here's the code behind that achieves this

double baseRef = CalculateBaseline(tbref);
double base1 = CalculateBaseline(tb1) - baseRef;
double base2 = CalculateBaseline(tb2) - baseRef;
double base3 = CalculateBaseline(tb3) - baseRef;
tb1.Margin = new Thickness(0, 40 - base1, 0, 0);
tb2.Margin = new Thickness(0, 40 - base2, 0, 0);
tb3.Margin = new Thickness(0, 40 - base3, 0, 0);
Fullmouthed answered 10/1, 2010 at 13:43 Comment(8)
@אביעד, and what do I do with this value? what is actually the 'BaseLine'?Allotment
The baseline is the distance between the top of the text element and the line on which all letters 'sit'. If for example you set your Top margin to say 50 minus the baseline offset, you'll be guaranteed to align to a specific line no matter the font size.Fullmouthed
That's what I tried but doesn't work. I am still seeking for the solution. The problem is the space between the margin and 'top of the text' you mention. I updated my question. Thanks for all your effort אביעד. Any news?Allotment
Thank you so much Aviad. I'd like to make all my text controls (especially the TextBlocks to go thru this routine, is there a Xamly way to set it globally or I will need to inherit these controls? I thought let's ask the experts, you're tha man!Allotment
You have to use code behind, what's more you have to tweak the margin based on the specific control template of your text element. I.e. if it has borders, padding, etc. you have to modify the code behind to account for that.Fullmouthed
There is still kind of a problem, because if we could know the parent's actual height we could replace that '40' that is just a constant default with the dynamic heighst font size control. And the problem is cuz the ActualSize of the parent is rendered only after the children, nor can you iterate thru the TextBlock's siblings since when you do with the convert and the iterator is on the 1st child, the other siblings are not rendered yet and doesn't exist. Any good idea? I would really love to make this thing more dynamic, thanks for all.Allotment
The 40 is indeed just a constant offset for the baseline, you don't really need it, you can just substitute it for 0.Fullmouthed
Please vote: connect.microsoft.com/WPF/feedback/…Allotment
D
3
<TextBlock>
<InlineUIContainer BaselineAlignment="Baseline"><TextBlock>Small</TextBlock></InlineUIContainer>
<InlineUIContainer BaselineAlignment="Baseline"><TextBlock FontSize="50">Big</TextBlock></InlineUIContainer>
</TextBlock>

This should works well. Experiment with Baseline/Bottom/Center/Top.

Demon answered 28/3, 2013 at 8:10 Comment(0)
U
1

XAML designer supports aligning TextBlock controls by baseline at design time:

enter image description here

This assigns fixed margins to your controls. As long as font sizes do not change at run time, the alignment will be preserved.

Unreserved answered 20/11, 2014 at 15:35 Comment(1)
Yes, this works. Maybe this feature wasn't available at the time when the question was asked (2010) but I confirm aligning can be done this way in WPF designer in Viusal Studio 2015 (and obviously also in some earlier versions as date of this answer indicates). Both adjusted objects must contain text so if you are aligning e.g. Label and TextBox (which is typically empty), add temporarily some text inside it.Partiality
A
0

I actually found a simple answer based on Aviad's.

I created a converter that contains Aviad's function that accepts the element itself and returns calculated Thickness.

Then I set up

<Style TargetType="TextBlock">
    <Setter Property="Margin" 
        Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource converters:TextAlignmentMarginConverter}}" />
</Style>

The disadvantage is that this is obviously occupies the original Margin property, since a TextBlock doesn't have a template so we can't set it via TemplateBinding.

Allotment answered 11/1, 2010 at 0:56 Comment(2)
That's a good way, but a converter is only going to take you part of the way, not the whole way, because for example, if the font size changes during the run of the program, the margin property won't get automatically reevaluated, you'll have to explicitly do that. Also, the converter needs to account for the other template elements such as padding, borders, etc, so you still need manual tweaking (converter parameter maybe).Fullmouthed
As you can see, I am passing the whole control to the converter, so no need for params. Anyway, I hope to improve it when I will have time. I want it should be based on the parent's height so it doesn't have to be a static value like '40'.Allotment
M
0

In the blog article XAML text improvements in Windows 8.1 is explained how you can align two TextBlocks of different font sizes by their baseline. The trick is TextLineBounds="TrimToBaseline" combined with VerticalAlignment="Bottom". That removes the size below their baseline, and then moves the TextBlocks down. You can then move it them back up to the desired height by setting a Margin on a container you put them in.

Sample:

<Grid Margin="some margin to lift the TextBlocks to desired height">
    <TextBlock Text="{x:Bind ViewModel.Name, Mode=OneWay}"
                Style="{StaticResource HeaderTextBlockStyle}"
                VerticalAlignment="Bottom"
                TextLineBounds="TrimToBaseline" />
    <TextBlock Text="{x:Bind ViewModel.Description.Yield, Mode=OneWay}"
                Style="{StaticResource SubheaderTextBlockStyle}"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Right"
                TextLineBounds="TrimToBaseline" />
</Grid>
Misinform answered 25/9, 2020 at 23:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.