7

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?

Khushi
  • 1,031
  • 4
  • 24
  • 48
  • 2
    [Here](http://www.codeproject.com/Articles/234651/Basic-HTML-Markup-in-WPF-TextBlock)'s a good article on applying HTML markup to a WPF TextBlock. May be helpful. – JMK Dec 14 '13 at 17:20
  • 1
    Based on your sample it looks like you want a TextBox just before the colon. This would require you to calculate the locations of the characters to make sure it is laid out correctly, which also can dynamically change with operating system configuration. This would be managed by WPF automatically if you separated the TextBlocks. What do you gain from using a single element? – nmclean Dec 20 '13 at 00:57
  • @nmclean I want to use just 1 textblock because I have to define two textblocks just because of colon before every textbox. – Khushi Dec 20 '13 at 17:17
  • @Khushi I think two textblocks is the best way to implement it, but to simplify your code maybe you could create a simple parser that generates elements from a string, e.g. `"value is {Value}:"` produces `` – nmclean Dec 20 '13 at 17:49

3 Answers3

8

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();
    }
}
Community
  • 1
  • 1
McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • 1
    Oh... this is nice if you're willing to author the control. But, the `InlineUIContainer` merely specifies two `TextBlock`s. In that case I'd personally prefer to just code two `TextBlock`s. (I suggest breaking this up into two answers if you can.) – Rob Perkins Dec 19 '13 at 23:21
  • I have tried both of your approaches. But when Width of a TextBlock is set to Auto, none of your approaches seem to work. If you get time then please update your code by solving those problems. – Vishal Oct 11 '15 at 21:56
3

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.

Rob Perkins
  • 3,088
  • 1
  • 30
  • 51
1

If you're using (or willing to use) a fixed-width font, you can use String.PadRight. For instance, if the maximum text length for the TextBox is 30 chars, call:

myTextBox.Text = myString.PadRight(29, ' ') + ":";

That will make the colon align right regardless of the left aligned string length. There is no way to both left and right align a TextBox. I'm not a WPF guy so my next suggestion would also involve translating to WPF equivelents from Windows Forms instructions. Regardless though, another thing you could do if you have to use a variable-width font would be to do this:

  • Create a class that derives from TextBox.
  • Override the OnPaint function, or WPF equivelent.
  • Create code to fill the background and borders how ever you want.
  • Use Graphics.DrawString (or equivelent) aligned left for the main string, and then aligned right for the colon. In both cases using the ClientRectangle of your base class in the DrawString function.

Aside from creating a derived class with a custom OnPaint function, you're gonna have to use some sort of trickery.

I wish you the best.

drankin2112
  • 4,715
  • 1
  • 15
  • 20