Fully justify paragraphs of text in C#

Fully justify paragraphs

The StringFormat class makes it fairly easy to left justify, right justify, or center a line of text, but strangely it doesn’t provide a method to fully justify a line of text (so it extends all the way to both the left and right margins. This example shows how you can fully justify paragraphs of text.

The following DrawParagraphs method draws the paragraphs with the proper alignment.

// Draw justified text on the Graphics
// object in the indicated Rectangle.
private RectangleF DrawParagraphs(Graphics gr, RectangleF rect,
    Font font, Brush brush, string text,
    TextJustification justification, float line_spacing,
    float indent, float paragraph_spacing)
{
    // Split the text into paragraphs.
    string[] paragraphs = text.Split('\n');

    // Draw each paragraph.
    foreach (string paragraph in paragraphs)
    {
        // Draw the paragraph keeping track of remaining space.
        rect = DrawParagraph(gr, rect, font, brush,
            paragraph, justification, line_spacing,
            indent, paragraph_spacing);

        // See if there's any room left.
        if (rect.Height < font.Size) break;
    }

    return rect;
}

This code splits the text into paragraphs and calls the following DrawParagraph to draw each one. After it draws a paragraph, it compares the height of the remaining rectangle to the font’s size. If there’s no room to draw any more text, the method returns.

// Draw a paragraph by lines inside the Rectangle.
// Return a RectangleF representing any unused
// space in the original RectangleF.
private RectangleF DrawParagraph(Graphics gr, RectangleF rect,
    Font font, Brush brush, string text,
    TextJustification justification, float line_spacing,
    float indent, float extra_paragraph_spacing)
{
    // Get the coordinates for the first line.
    float y = rect.Top;

    // Break the text into words.
    string[] words = text.Split(' ');
    int start_word = 0;

    // Repeat until we run out of text or room.
    for (; ; )
    {
        // See how many words will fit.
        // Start with just the next word.
        string line = words[start_word];

        // Add more words until the line won't fit.
        int end_word = start_word + 1;
        while (end_word < words.Length)
        {
            // See if the next word fits.
            string test_line = line + " " + words[end_word];
            SizeF line_size = gr.MeasureString(test_line, font);
            if (line_size.Width > rect.Width)
            {
                // The line is too wide. Don't use the last word.
                end_word--;
                break;
            }
            else
            {
                // The word fits. Save the test line.
                line = test_line;
            }

            // Try the next word.
            end_word++;
        }

        // See if this is the last line in the paragraph.
        if ((end_word == words.Length) &&
            (justification == TextJustification.Full))
        {
            // This is the last line. Don't justify it.
            DrawLine(gr, line, font, brush,
                rect.Left + indent,
                y,
                rect.Width - indent,
                TextJustification.Left);
        }
        else
        {
            // This is not the last line. Justify it.
            DrawLine(gr, line, font, brush,
                rect.Left + indent,
                y,
                rect.Width - indent,
                justification);
        }

        // Move down to draw the next line.
        y += font.Height * line_spacing;

        // Make sure there's room for another line.
        if (font.Size > rect.Height) break;

        // Start the next line at the next word.
        start_word = end_word + 1;
        if (start_word >= words.Length) break;

        // Don't indent subsequent lines in this paragraph.
        indent = 0;
    }

    // Add a gap after the paragraph.
    y += font.Height * extra_paragraph_spacing;

    // Return a RectangleF representing any unused
    // space in the original RectangleF.
    float height = rect.Bottom - y;
    if (height < 0) height = 0;
    return new RectangleF(rect.X, y, rect.Width, height);
}

This method breaks the paragraph into words and enters a loop to process the words. The loop continues until the method runs out of words or runs out of space to draw them.

Each time through the loop, the code builds a string, adding more words until the string no longer fits in the available width. It then removes the final word so the string fits.

If the string contains all of the remaining words, and if the program is fully justifying the text, then the method calls DrawLine to left justify this line. (When you fully justify text, you don’t justify the final line. Imagine how weird it would be if the last line contained only two words. You would have the first word on the far left, a huge space, and the second word on the far right.)

If this is not the end of the paragraph or the program isn’t fully justifying the text, then the method calls DrawLine to draw the line with the desired justification.

The following code shows the DrawLine method.

// Draw a line of text.
private void DrawLine(Graphics gr, string line, Font font,
    Brush brush, float x, float y, float width,
    TextJustification justification)
{
    // Make a rectangle to hold the text.
    RectangleF rect = new RectangleF(x, y, width, font.Height);

    // See if we should use full justification.
    if (justification == TextJustification.Full)
    {
        // Justify the text.
        DrawJustifiedLine(gr, rect, font, brush, line);
    }
    else
    {
        // Make a StringFormat to align the text.
        using (StringFormat sf = new StringFormat())
        {
            // Use the appropriate alignment.
            switch (justification)
            {
                case TextJustification.Left:
                    sf.Alignment = StringAlignment.Near;
                    break;
                case TextJustification.Right:
                    sf.Alignment = StringAlignment.Far;
                    break;
                case TextJustification.Center:
                    sf.Alignment = StringAlignment.Center;
                    break;
            }

            gr.DrawString(line, font, brush, rect, sf);
        }
    }
}

This method first checks the justification parameter. If justification is Full, the method calls the DrawJustifiedLine method to draw the text fully justified. See the post Fully justify a line of text in C# for information about how that method works.

If justification is not Full, the method makes a StringFormat object with the proper alignment and uses it to draw the text.

Note that full justification works best when the text contains words that are relatively small compared to the width of the rectangle. For example, if you run the program and shrink the form so it’s about half its original size, you’ll see that the code must add lots of extra space to each line and the result is fairly unattractive.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in algorithms, fonts, graphics, printing, strings and tagged , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

4 Responses to Fully justify paragraphs of text in C#

  1. Can I implement this solution in a RDLC report? and how?
    Tnks!

    • RodStephens says:

      Sorry but I don’t know. If you can use ASP code or some other code, then you might be able to do it. But I don’t know if the report-generating software will be able to properly measure the user’s font and window sizes.

      All in all if the reporting controls can’t do it for you, I suspect it would be hard if it’s possible.

  2. NH says:

    You are a GENIUS.. this code really really really helped my project. So I’ve just sent you a donation. Thanks, just thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *