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?
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.
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' });
}
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'});
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With