How to justify text in a label

13,255

Solution 1

This is an implementation of the solution proposed by TaW. It is just the base code for the implementation - no automatic re-adjustments, etc.

public void Justify(System.Windows.Forms.Label label)
{
    string text = label.Text;
    string[] lines = text.Split(new[]{"\r\n"}, StringSplitOptions.None).Select(l => l.Trim()).ToArray();

    List<string> result = new List<string>();

    foreach (string line in lines)
    {
        result.Add(StretchToWidth(line, label));
    }

    label.Text = string.Join("\r\n", result);
}

private string StretchToWidth(string text, Label label)
{
    if (text.Length < 2)
        return text;

    // A hair space is the smallest possible non-visible character we can insert
    const char hairspace = '\u200A';

    // If we measure just the width of the space we might get too much because of added paddings so we have to do it a bit differently
    double basewidth = TextRenderer.MeasureText(text, label.Font).Width;
    double doublewidth = TextRenderer.MeasureText(text + text, label.Font).Width;
    double doublewidthplusspace = TextRenderer.MeasureText(text + hairspace + text, label.Font).Width;
    double spacewidth = doublewidthplusspace - doublewidth;

    //The space we have to fill up with spaces is whatever is left
    double leftoverspace = label.Width - basewidth;

    //Calculate the amount of spaces we need to insert
    int approximateInserts = Math.Max(0, (int)Math.Floor(leftoverspace / spacewidth));

    //Insert spaces
    return InsertFillerChar(hairspace, text, approximateInserts);
}

private static string InsertFillerChar(char filler, string text, int inserts)
{
    string result = "";
    int inserted = 0;

    for (int i = 0; i < text.Length; i++)
    {
        //Add one character of the original text
        result += text[i];

        //Only add spaces between characters, not at the end
        if (i >= text.Length - 1) continue;

        //Determine how many characters should have been inserted so far
        int shouldbeinserted = (int)(inserts * (i+1) / (text.Length - 1.0));
        int insertnow = shouldbeinserted - inserted;
        for (int j = 0; j < insertnow; j++)
            result += filler;
        inserted += insertnow;
    }

    return result;
}

In action:
Demo

Solution 2

Unfortunately only the three most basic and simple types of alignment are supported: Right, Left and Center.

The fourth one, Justified or Block, is not supported in any .NET control afaik, not even in a RichtTextBox :-(

The only workaround would be to add either spaces or better a smaller whitespace character like thin space(U+2009) or hair space (U+200A) between the words i.e. after the regular spaces until the Label's Height changes. Then step one back and try to find the next insertion point, i.e. the next line and so on.. until the end of the text is reached.

A little tricky but not terribly hard.

Solution 3

Another implementation.
This one inserts "Hair Spaces" between words only.

EDIT:
Added a method that implements paragraph Block Align.
Both JustifyParagraph() and JustifyLine() call the worker method Justify().

label1.Text = JustifyParagraph(label1.Text, label1.Font, label1.ClientSize.Width);

public string JustifyParagraph(string text, Font font, int ControlWidth)
{
    string result = string.Empty;
    List<string> ParagraphsList = new List<string>();
    ParagraphsList.AddRange(text.Split(new[] { "\r\n" }, StringSplitOptions.None).ToList());

    foreach (string Paragraph in ParagraphsList) {
        string line = string.Empty;
        int ParagraphWidth = TextRenderer.MeasureText(Paragraph, font).Width;

        if (ParagraphWidth > ControlWidth) {
            //Get all paragraph words, add a normal space and calculate when their sum exceeds the constraints
            string[] Words = Paragraph.Split(' ');
            line = Words[0] + (char)32;
            for (int x = 1; x < Words.Length; x++) {
                string tmpLine = line + (Words[x] + (char)32);
                if (TextRenderer.MeasureText(tmpLine, font).Width > ControlWidth)
                {
                    //Max lenght reached. Justify the line and step back
                    result += Justify(line.TrimEnd(), font, ControlWidth) + "\r\n";
                    line = string.Empty;
                    --x;
                } else {
                    //Some capacity still left
                    line += (Words[x] + (char)32);
                }
            }
            //Adds the remainder if any
            if (line.Length > 0)
            result += line + "\r\n";
        }
        else {
            result += Paragraph + "\r\n";
        }
    }
    return result.TrimEnd(new[]{ '\r', '\n' });
}

enter image description here
enter image description here

JustifyLines() only deals with single lines of text: (shorter than the client area)

textBox1.Text = JustifyLines(textBox1.Text, textBox1.Font, textBox1.ClientSize.Width);

public string JustifyLines(string text, Font font, int ControlWidth)
{
    string result = string.Empty;
    List<string> Paragraphs = new List<string>();
    Paragraphs.AddRange(text.Split(new[] { "\r\n" }, StringSplitOptions.None).ToList());

    //Justify each paragraph and re-insert a linefeed
    foreach (string Paragraph in Paragraphs) {
        result += Justify(Paragraph, font, ControlWidth) + "\r\n";
    }
    return result.TrimEnd(new[] {'\r', '\n'});
}

enter image description here
enter image description here

The worker method:

private string Justify(string text, Font font, int width)
{
    char SpaceChar = (char)0x200A;
    List<string> WordsList = text.Split((char)32).ToList();
    if (WordsList.Capacity < 2)
        return text;

    int NumberOfWords = WordsList.Capacity - 1;
    int WordsWidth = TextRenderer.MeasureText(text.Replace(" ", ""), font).Width;
    int SpaceCharWidth = TextRenderer.MeasureText(WordsList[0] + SpaceChar, font).Width
                       - TextRenderer.MeasureText(WordsList[0], font).Width;

    //Calculate the average spacing between each word minus the last one 
    int AverageSpace = ((width - WordsWidth) / NumberOfWords) / SpaceCharWidth;
    float AdjustSpace = (width - (WordsWidth + (AverageSpace * NumberOfWords * SpaceCharWidth)));

    //Add spaces to all words
    return ((Func<string>)(() => {
        string Spaces = "";
        string AdjustedWords = "";

        for (int h = 0; h < AverageSpace; h++)
            Spaces += SpaceChar;

        foreach (string Word in WordsList) {
            AdjustedWords += Word + Spaces;
            //Adjust the spacing if there's a reminder
            if (AdjustSpace > 0) {
                AdjustedWords += SpaceChar;
                AdjustSpace -= SpaceCharWidth;
            }
        }
        return AdjustedWords.TrimEnd();
    }))();
}

About the RichTextBox.
@TaW says that it doesn't support Block Align, but this is not exactly true.
RichTextBox is notoriously based on the RichEdit class and that class support "Justification".
This is reported in the old Platform SDK (with examples).
RichTextBox has its AdvancedTypographicsOption explicitly truncated during handle creation.
(It's not about implementing PARAFORMAT vs. PARAFORMAT2 structs, that's irrelevant, it's deliberate).

So this is a "cure" for poor RichTextBox.
A class that derives from it and uses SendMessage to send a EM_SETTYPOGRAPHYOPTIONS message to the base class, specifying TO_ADVANCEDTYPOGRAPHY to re-enable Justification.

It also shadows SelectionAlignment, to add back the missing Justify option.

This works on a paragraph level or from a point onward.

public class JustifiedRichTextBox : RichTextBox
{
    [DllImport("user32", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In] [Out] ref PARAFORMAT2 pf);

    [DllImport("user32", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

    public enum TextAlignment
    {
        Left = 1,
        Right,
        Center,
        Justify
    }

    private const int EM_SETEVENTMASK = 1073;
    private const int EM_GETPARAFORMAT = 1085;
    private const int EM_SETPARAFORMAT = 1095;
    private const int EM_SETTYPOGRAPHYOPTIONS = 1226;
    private const int TO_ADVANCEDTYPOGRAPHY = 0x1;
    private const int WM_SETREDRAW = 11;
    private const int PFM_ALIGNMENT = 8;
    private const int SCF_SELECTION = 1;

    [StructLayout(LayoutKind.Sequential)]
    private struct PARAFORMAT2
    {
        //----------------------------------------
        public int cbSize;             // PARAFORMAT
        public uint dwMask;
        public short wNumbering;
        public short wReserved;
        public int dxStartIndent;
        public int dxRightIndent;
        public int dxOffset;
        public short wAlignment;
        public short cTabCount;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
        public int[] rgxTabs;
        //----------------------------------------
        public int dySpaceBefore;     // PARAFORMAT2
        public int dySpaceAfter;
        public int dyLineSpacing;
        public short sStyle;
        public byte bLineSpacingRule;
        public byte bOutlineLevel;
        public short wShadingWeight;
        public short wShadingStyle;
        public short wNumberingStart;
        public short wNumberingStyle;
        public short wNumberingTab;
        public short wBorderSpace;
        public short wBorderWidth;
        public short wBorders;
    }

    private int updating = 0;
    private int oldEventMask = 0;

    public new TextAlignment SelectionAlignment
    {
        // SelectionAlignment is not overridable
        get
        {
            PARAFORMAT2 pf = new PARAFORMAT2();
            pf.cbSize = Marshal.SizeOf(pf);
            SendMessage(this.Handle, EM_GETPARAFORMAT, SCF_SELECTION, ref pf);
            if ((pf.dwMask & PFM_ALIGNMENT) == 0) return TextAlignment.Left;
            return (TextAlignment)pf.wAlignment;
        }
        set
        {
            PARAFORMAT2 pf = new PARAFORMAT2();
            pf.cbSize = Marshal.SizeOf(pf);
            pf.dwMask = PFM_ALIGNMENT;
            pf.wAlignment = (short)value;
            SendMessage(this.Handle, EM_SETPARAFORMAT, SCF_SELECTION, ref pf);
        }
    }

    //Overrides OnHandleCreated to enable RTB advances options
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        // EM_SETTYPOGRAPHYOPTIONS allows to enable RTB (RichEdit) Advanced Typography
        SendMessage(this.Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY);
    }
}   //JustifiedRichTextBox

enter image description here
enter image description here

Share:
13,255

Related videos on Youtube

SteeveDroz
Author by

SteeveDroz

As it seems, I tend to keep an aura of uncertainty about me.

Updated on May 25, 2022

Comments

  • SteeveDroz
    SteeveDroz about 2 years

    I have a label that displays on more than a line and I would like to justify the text in it (align left and right). What is the best way to achieve that?

    • TaW
      TaW about 8 years
      Do you mean block alignment? Not supported.
    • Maciej Los
      Maciej Los about 8 years
      Divide the text on two labels.
    • SteeveDroz
      SteeveDroz about 8 years
      I meant block alignment. @TaW, change your comment into an answer so I can consider the question answered.
    • Pikoh
      Pikoh about 8 years
      Hmm...I don't understand "Block Align". If your label has multiple lines, a fixed size and you use TextAlign, what's different from what you want to archieve?
    • TaW
      TaW about 8 years
      Done. I might try to code the workaround tomorrow, but don't have time atm.. @Pikoh Block or Justify means that both borders are aligned like in a book.
    • Pikoh
      Pikoh about 8 years
      O.k., now i understand. I think that it could be implemented using MeasureString and adjusting the spaces between the words.
  • Jimi
    Jimi over 6 years
    +1 The measure of that spacing char for some reason was eluding me. I'll post a solution that involves words spacing.
  • Armando Marques da S Sobrinho
    Armando Marques da S Sobrinho about 6 years
    Hi @Manfred Radlwimmer! how about TextRenderer in Mac OS, have a similar?
  • Armando Marques da S Sobrinho
    Armando Marques da S Sobrinho about 6 years
    this is bad man, I want and need to justify the text in my label, maybe I can take the length of the line in another way to replace the Testrenderer.MeasureText(...) for cross-platform, @ManfredRadlwimmer...
  • Leandro Bardelli
    Leandro Bardelli almost 5 years
    this is amazing, but it's possible modify it in order to get "<br>" instead of rn? I'm working with HTML Mark up. I tried it but can't get it done. I'm working with RDLC and is hardly work to workaround this incredible issue. Thanks in advance for any help, or if you prefeer I can make a new question.
  • Jimi
    Jimi almost 5 years
    @MacGyver I'm not really sure what you're asking here. Do you need to substitute \r\n or \n with </br> or <br>? When should this happen? When the Text is exported as Html? While typing? Maybe making a question about it is a good idea, so you can better describe your requirements. It could be interesting.
  • Leandro Bardelli
    Leandro Bardelli almost 5 years
    thanks for your reply, ive really done, it was for RDLC html mark up :)