Home > front end >  C# WPF - justify single lines of text in TextBlocks
C# WPF - justify single lines of text in TextBlocks

Time:11-14

I have a StackPanel with several TextBlocks in it, like this:

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
    <TextBlock Text="First is quite lengthy" />
    <TextBlock Text="This one's shorter"/>
    <TextBlock Text="This one too"/>
    <TextBlock Text="This one is a bit longer again"/>
</StackPanel>

I want to justify all of them, so that both the left sides and the right sides of the TextBlocks are aligned vertically - how can I do that?

Here's what the above code looks like:

enter image description here


Rough estimation of what I want to achieve:

enter image description here

I have tried experimenting with TextAlignment (Justify does not work for me), FontStretch, HorizontalAlignment, DockPanel and Viewbox (which does not work since I want to keep the font size constant). Is there anything I can do to achieve this without hard-coding some spaces inside of the texts?

CodePudding user response:

I won't lie, it wasn't too long ago when I first picked up WPF, so I apologize if my answer doesn't exactly have the best solution. Anyway, I think the only option you quite have is making different words as different Text Blocks and playing around with the margin, and alignment settings. Otherwise, as far as I know, I might've misunderstood the question. Sorry if I have.

CodePudding user response:

I was surprised to find out WPF doesn't support justified single line TextBlocks by default. However, you can relatively easily achieve this behaviour yourself. I've cobbled together a quick solution that provides a UserControl with the single purpose of drawing a justified single line of text.

The solution is very simple, doesn't support text-wrapping, margins or any other special layout cases and only has the bare bones DependencyProperties. It should be a good starting point though.

public partial class JustifiedLine : UserControl
{
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text",
        typeof(string),
        typeof(JustifiedLine),
        new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty MinSpaceWidthProperty = DependencyProperty.Register(
        "MinSpaceWidth",
        typeof(double),
        typeof(JustifiedLine),
        new FrameworkPropertyMetadata(5.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public double MinSpaceWidth
    {
        get => (double)GetValue(MinSpaceWidthProperty);
        set => SetValue(MinSpaceWidthProperty, value);
    }

    public JustifiedLine()
    {
        InitializeComponent();
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        string[] words = Text.Split(' ');

        if (words.Length == 0)
            return;

        int numValidWords = 0;
        double totalWordsWidth = 0;

        /* add widths of all words */
        for (int i = 0; i < words.Length; i  )
        {
            if (String.IsNullOrWhiteSpace(words[i]))
                continue;

            FormattedText fText = MakeFormattedText(words[i]);
            totalWordsWidth  = fText.Width;

            numValidWords  ;
        }

        double fullWidth = GetFullWidth();

        double spaceWidth = (fullWidth - totalWordsWidth) / (numValidWords - 1);
        if (spaceWidth < MinSpaceWidth)
            spaceWidth = MinSpaceWidth;

        double currentWidth = 0;
        for (int i = 0; i < words.Length; i  )
        {
            if (String.IsNullOrWhiteSpace(words[i]))
                continue;

            FormattedText fText = MakeFormattedText(words[i]);

            drawingContext.DrawText(fText, new Point(currentWidth, 0));
            currentWidth  = fText.Width   spaceWidth;

            /* if Height isn't specified, automatically set from text height */
            if (double.IsNaN(Height))
                Height = fText.Height;
        }
    }

    /* return either width of this control, or the parent control if not set. If neither is set, return 0.0 */
    private double GetFullWidth()
    {
        if (!double.IsNaN(Width))
            return Width;

        FrameworkElement parent = Parent as FrameworkElement;
        if (parent != null && !double.IsNaN(parent.ActualWidth))
            return parent.ActualWidth;

        return 0.0;
    }

    /* might want to bind font etc. to dependency property, these here are just placeholders */
    private FormattedText MakeFormattedText(string text)
    {
        return new FormattedText(
                text,
                CultureInfo.GetCultureInfo("en-us"),
                FlowDirection.LeftToRight,
                new Typeface("Verdana"),
                16,
                Brushes.White,
                VisualTreeHelper.GetDpi(this).PixelsPerDip);
    }
}

if used like this:

<StackPanel Width="300">
            <usercontrols:JustifiedLine Text="First is quite lengthy"/>
            <usercontrols:JustifiedLine Text="This one's shorter"/>
            <usercontrols:JustifiedLine Text="This one too"/>
            <usercontrols:JustifiedLine Text="This one is a bit longer again"/>
</StackPanel>

xmlns:usercontrols="clr-namespace:MyProject.controls.usercontrols"

This is my result:

Resulting justified lines

  • Related