Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TextBlock with some text as left aligned and the remaining text as right aligned

Tags:

c#

wpf

xaml

I want to have a textblock which contains text like below:

My associated textbox is                :

Text is left-aligned and colon is right aligned.

I know how to get the above output using two textblocks. But I want to know that is the same behavior applicable to a single textblock?

like image 753
Khushi Avatar asked Dec 14 '13 17:12

Khushi


2 Answers

TextBlock is inherently opposed to the concept of aligned children, but there are of course some possible hacks work-arounds:

  1. Fill in spaces to give the appearance of alignment.
  2. Use InlineUIContainer to add actual UI elements (which you can align) inside the TextBlock.

I'll give an example of each by creating an ExtendedTextBlock control, with LeftAlignedText and RightAlignedText properties. Usage is like this:

<my:ExtendedTextBlock RightAlignedText=":" LeftAlignedText="My associated textbox is" />

1) Padding with spaces.

For this approach, I've borrowed from this answer to get the actual width of a text string. The idea is basically, subtract the total width of the text from the actual width of the control, and insert the appropriate number of spaces between them.

public class ExtendedTextBlock : TextBlock
{
    public string RightAlignedText
    {
        get { return (string)GetValue(RightAlignedTextProperty); }
        set { SetValue(RightAlignedTextProperty, value); }
    }
    public static readonly DependencyProperty RightAlignedTextProperty =
        DependencyProperty.Register("RightAlignedText", typeof(string), typeof(ExtendedTextBlock), new PropertyMetadata(SetText));

    public string LeftAlignedText
    {
        get { return (string)GetValue(LeftAlignedTextProperty); }
        set { SetValue(LeftAlignedTextProperty, value); }
    }
    public static readonly DependencyProperty LeftAlignedTextProperty =
        DependencyProperty.Register("LeftAlignedText", typeof(string), typeof(ExtendedTextBlock), new PropertyMetadata(SetText));

    public static void SetText(object sender, DependencyPropertyChangedEventArgs args)
    {
        SetText((ExtendedTextBlock)sender);
    }

    public static void SetText(ExtendedTextBlock owner)
    {
        if (owner.ActualWidth == 0)
            return;

        // helper function to get the width of a text string
        Func<string, double> getTextWidth = text =>
        {
            var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight,
                new Typeface(owner.FontFamily, owner.FontStyle, owner.FontWeight, owner.FontStretch),
                owner.FontSize,
                Brushes.Black);
            return formattedText.Width;
        };

        // calculate the space needed to fill in
        double spaceNeeded = owner.ActualWidth - getTextWidth(owner.LeftAlignedText ?? "") - getTextWidth(owner.RightAlignedText ?? "");

        // get the width of an empty space (have to cheat a bit since the width of an empty space returns zero)
        double spaceWidth = getTextWidth(" .") - getTextWidth(".");
        int spaces = (int)Math.Round(spaceNeeded / spaceWidth);

        owner.Text = owner.LeftAlignedText + new string(Enumerable.Repeat(' ', spaces).ToArray()) + owner.RightAlignedText;
    }

    public ExtendedTextBlock()
    {
        SizeChanged += (sender, args) => SetText(this);
    }
}

2) Using InlineUIContainer to add aligned text

The idea here is to add a panel, inside the TextBlock, which will be responsible for aligning each text string. This is the basic idea:

<TextBlock>
    <InlineUIContainer>
        <Grid Width="{Binding RelativeSource={RelativeSource AncestorType=TextBlock},Path=ActualWidth}">
            <TextBlock Text="Hello" />
            <TextBlock Text="World" TextAlignment="Right" />
        </Grid>
    </InlineUIContainer>
</TextBlock>

So, this version of the control re-creates the above but hides the implementation. It adds the InlineUIContainer control to the base TextBlock, and keeps a reference to the "left" and "right" TextBlocks, setting their text as needed.

public class ExtendedTextBlock2 : TextBlock
{
    private TextBlock _left, _right;

    public string RightAlignedText
    {
        get { return (string)GetValue(RightAlignedTextProperty); }
        set { SetValue(RightAlignedTextProperty, value); }
    }
    public static readonly DependencyProperty RightAlignedTextProperty =
        DependencyProperty.Register("RightAlignedText", typeof(string), typeof(ExtendedTextBlock2), new PropertyMetadata(SetText));

    public string LeftAlignedText
    {
        get { return (string)GetValue(LeftAlignedTextProperty); }
        set { SetValue(LeftAlignedTextProperty, value); }
    }
    public static readonly DependencyProperty LeftAlignedTextProperty =
        DependencyProperty.Register("LeftAlignedText", typeof(string), typeof(ExtendedTextBlock2), new PropertyMetadata(SetText));

            public static void SetText(object sender, DependencyPropertyChangedEventArgs args)
    {
        ((ExtendedTextBlock2)sender).SetText();
    }

    public void SetText()
    {
        if (_left == null || _right == null)
            return;
        _left.Text = LeftAlignedText;
        _right.Text = RightAlignedText;
    }

    public ExtendedTextBlock2()
    {
        Loaded += ExtendedTextBlock2_Loaded;
    }

    void ExtendedTextBlock2_Loaded(object sender, RoutedEventArgs e)
    {
        Inlines.Clear();
        var child = new InlineUIContainer();
        var container = new Grid();
        var widthBinding = new Binding { Source = this, Path = new PropertyPath(TextBlock.ActualWidthProperty) };
        container.SetBinding(Grid.WidthProperty, widthBinding);
        child.Child = container;
        container.Children.Add(_left = new TextBlock { HorizontalAlignment = System.Windows.HorizontalAlignment.Left });
        container.Children.Add(_right = new TextBlock { HorizontalAlignment = System.Windows.HorizontalAlignment.Right });
        Inlines.Add(child);

        SetText();
    }
}
like image 124
McGarnagle Avatar answered Sep 23 '22 18:09

McGarnagle


It is not. (I looked for quite a while.)

A TextBlock defines one block of text, and applies justification properties to it. Because TextBlock and other WPF elements are naturally auto-sizing, the approach of using two of them, each with different settings on their justification proprty, is the correct approach.

They can contain <Span> and <Run> elements, and @JMK pointed to a code tutorial

To do what you need, consider the FlowDocument element and its contents, which will permit you to describe justifications as hierarchical XAML markup. A FlowDocument can consume a very small amount of screen space.

Or, consider implementing a converter in which you discover the width of your TextBlock and the width of a string you intend to transform by adding spaces and that colon, and adjust the spacing within your string accordingly, using the FormattedText class.

like image 44
Rob Perkins Avatar answered Sep 25 '22 18:09

Rob Perkins