Okay, so after using your control I finally got what you did there, and I think it's not the right approach, because your control starts to show the background font, when you don't have a solid background color. You are tricking around hiding the base background text of the label instead of simply avoiding it.
To hide your background font the right correct way is to override the OnPaint
method without calling base.OnPaint()
. This way, you don't have to get rid of the font of the parent control.
Then you force your users to use AutoSize
, always. The very correct way is to implement it yourself. It would require to override the methods .NET use to auto size your control. But I could not handle this properly, as the auto sizing has to be happen before painting the content. In your control the desired size is only known after painting everything. The first call to GetPreferredSize()
decides the size and it's to early with this control. Too bad.
Additionally, I don't know why you work with floats. In WinForms all pixel positions are defined by full integers. The box calculation should be round up to full values, too.
Then, your control misses the link open functionality (obviously) and is not displayed with the system set link color (SystemColors.HotTrack
).
You defined {}
for a new line, You should change it back to \r\n
(Environment.NewLine
) and make sure it will work between splitters, too.
Your forground text is not respecting the padding alignment and the maximum / minimum size on your control.
Lastly, your parsing is not that good, specially how you parse links. I would rather split the name with url with an extra delimiter that is safe for using in urls. It's better than waiting for the link on the next loop cycle. I decided to use the pipe (|
), so a link would be like {0a link|www.google.com}
or only {0www.google.com}
, maybe. Try to capsule specific code in extra functions to get more readability.
EDIT: Here is my extended version of your control. In some circumstances (popups, etc.) overriding the Width
and the Height
did not work in the Paint
function at me. Thus, I decided to allow AutoSize
to set to true to let it calculate the size of the parent text, automatically. This is the reason I had to remove the Leading
property. In almost all cases, you should enable CustomAutoSize
when possible, to let it be calculated by the control itself, however. Also, I changed the default splitters to {{
and }}
, since only one bracket is used by the String.Format
method that should be able to be used when setting the Text
property of this control.
The following implementation respects everything I mentioned above:
/// <summary>
/// A Label that can contain formated text.
/// The formatting is done by enclosing a formatted chunk in curly braces. The first character is a format going from 0-7:
/// 0 is a link ("{{0a link|www.google.com}}" or "{{www.google.com}}"),
/// 1 is bold ("{{1bold text}}"),
/// 2 is italic ("{{2italic text}}"),
/// 4 is alternative color ("{{4alternate text}}"),
/// 1 to 4 can be combined like enum flag ("{{7alternate bold and italic text}}").
/// To insert a new line, use "\r\n".
///
/// http://codereview.stackexchange.com/a/104751/49345
/// </summary>
public class RichLabel : Label
{
/// <summary>
/// Alternate ForeColor that can be drawn when adding 4 to the formatting number.
/// </summary>
public Color ForeColorAlt { get; set; }
/// <summary>
/// Splitters that allow to implement custom formattings.
/// </summary>
public string[] Splitters { get; set; }
/// <summary>
/// Set this to true to force to calculate the width and height by the control itself.
/// This should always be true. If it does not work, rather use the legacy AutoSize.
/// Disable AutoSize and this property to disable auto size at all.
/// </summary>
public bool CustomAutoSize { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public RichLabel()
{
this.MouseClick += this.RichLabel_MouseClick;
this.Splitters = new string[] { "{{", "}}" };
this.ForeColor = SystemColors.ControlText;
this.ForeColorAlt = SystemColors.HighlightText;
this.AutoSize = this.AutoSize && !this.CustomAutoSize;
}
/// <summary>
/// Internal rectangle collection with rectangles that lay over hyperlinks.
/// </summary>
private Dictionary<Rectangle, string> links = new Dictionary<Rectangle, string>();
/// <summary>
/// Paints the formatted text into the control area and will be called on initialization and when the text has been changed.
/// </summary>
protected override void OnPaint(PaintEventArgs e)
{
SolidBrush textBrush = null;
Font textFont = null;
this.links.Clear(); // clear old links
// separate splitter pairs
this.Text = this.Text.Replace(Splitters[1] + Splitters[0], Splitters[1] + "\v" + Splitters[0]);
// the box to draw in
int x = this.Padding.Left;
int y = this.Padding.Top;
int w = this.MinimumSize.Width;
int h = this.MinimumSize.Height;
string[] parts = this.Text.Split(Splitters, StringSplitOptions.None);
foreach (string part in parts)
{
// test 1st character
if (part != "")
{
int o = (part[0] - '0');
// if 1-7: valid text format
if (o > 0 && o < 8)
{
textBrush = new SolidBrush(o < 4 ? ForeColor : ForeColorAlt);
FontStyle FS = ((o & 1) == 1) ? FontStyle.Bold : FontStyle.Regular;
FS |= ((o & 2) == 2) ? FontStyle.Italic : FontStyle.Regular;
textFont = new Font(this.Font.FontFamily, this.Font.Size, FS);
this.drawText(e, part.Substring(1).Trim(), textFont, textBrush, ref x, ref y, ref w, ref h);
}
else if (o == 0) // 0 = link
{
string urlText, urlLink;
if (part.Substring(1).Contains("|"))
{
string[] url = part.Substring(1).Split('|');
urlText = url[0];
urlLink = url[1];
}
else
{
urlText = urlLink = part.Substring(1);
}
List<Rectangle> linkRectangles;
textBrush = new SolidBrush(SystemColors.HotTrack);
textFont = new Font(this.Font.FontFamily, this.Font.Size, FontStyle.Underline);
this.drawText(e, urlText.Trim(), textFont, textBrush, ref x, ref y, ref w, ref h, out linkRectangles);
foreach (Rectangle rect in linkRectangles)
{
this.links.Add(rect, urlLink);
}
}
else // no formatting
{
textBrush = new SolidBrush(this.ForeColor);
this.drawText(e, part, this.Font, textBrush, ref x, ref y, ref w, ref h);
}
}
}
// adjust label size
if (this.CustomAutoSize)
{
this.Width = w + this.Padding.Right;
this.Height = y + h + this.Padding.Bottom;
}
// clean up
if (textBrush != null) textBrush.Dispose();
if (textFont != null) textFont.Dispose();
}
/// <summary>
/// Draws the text on the given position and respects the maximum width.
/// </summary>
/// <param name="e">PaintEventArgs from the OnPaint method. </param>
/// <param name="text">Text that should be drawn. </param>
/// <param name="textFont">Text font that should be used. </param>
/// <param name="colorBrush">Text brush that should be used. </param>
/// <param name="x">X position where to draw. This will be updated. </param>
/// <param name="y">Y position where to draw. This will be updated. </param>
/// <param name="w">The total width of the control. This will be updated. </param>
/// <param name="h">The total height of the control. This will be updated. </param>
private void drawText(PaintEventArgs e, string text, Font textFont, Brush colorBrush, ref int x, ref int y, ref int w, ref int h)
{
List<Rectangle> dummy;
this.drawText(e, text, textFont, colorBrush, ref x, ref y, ref w, ref h, out dummy);
}
/// <summary>
/// Draws the text on the given position and respects the maximum width.
/// </summary>
/// <param name="e">PaintEventArgs from the OnPaint method. </param>
/// <param name="text">Text that should be drawn. </param>
/// <param name="textFont">Text font that should be used. </param>
/// <param name="colorBrush">Text brush that should be used. </param>
/// <param name="x">X position where to draw. This will be updated. </param>
/// <param name="y">Y position where to draw. This will be updated. </param>
/// <param name="w">The total width of the control. This will be updated. </param>
/// <param name="h">The total height of the control. This will be updated. </param>
/// <param name="textRectangles">The returned rectangle collection of all text fragments. </param>
private void drawText(PaintEventArgs e, string text, Font textFont, Brush colorBrush, ref int x, ref int y, ref int w, ref int h, out List<Rectangle> textRectangles)
{
textRectangles = new List<Rectangle>();
string[][] linesWords = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Select(z => z.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
for (int j = 0; j < linesWords.Length; j++)
{
if (j > 0)
{
x = this.Padding.Left; // create a line break
y += h;
}
for (int k = 0; k < linesWords[j].Length; k++)
{
string word = linesWords[j][k];
SizeF box = e.Graphics.MeasureString(word, textFont);
if (this.MaximumSize.Width > 0 && x + (int)box.Width > this.MaximumSize.Width - this.Padding.Right) // no more space for the word
{
x = this.Padding.Left; // create a line break
y += h;
}
e.Graphics.DrawString(word, textFont, colorBrush, new Point(x, y));
Rectangle rect = new Rectangle(x, y, (int)box.Width, (int)box.Height);
x += (int)(box.Width - (textFont.Bold ? word.Length * 0.2 : 0)); // bold letters are too large on the box calculating
w = Math.Max(w, x);
h = Math.Max(h, (int)box.Height - 1);
textRectangles.Add(rect);
}
}
}
/// <summary>
/// Detects if a link was clicked.
/// Will be called when a click has been performed.
/// </summary>
/// <param name="sender">Source control. </param>
public void RichLabel_MouseClick(object sender, MouseEventArgs e)
{
foreach (Rectangle r in links.Keys)
{
if (r.Contains(e.Location))
{
Process.Start(links[r]);
}
}
}
}
HUGE EDIT:
I want to provide an alternate solution that is using a RichTextBox
as label instead.
As stated before I was using your RichLabel
in a third party PopUp
control that disallowed me to set the Width
and Height
in the Paint
method. I was hoping to solve the problem by letting the base control measure the size in the GetPreferredSize()
method with the base label text, because I cannot do it in my own GetPreferredSize()
implementation as I don't know the size on that time.
But this approach was in-acceptable, because the base text has no formatting, thus the required space would be always different. So measuring everything on base is wrong, too.
I ended up going in a far different direction by inheriting a RichTextBox
and allow it to be transparent:
TransparentRichTextBox.cs:
public class TransparentRichTextBox : RichTextBox
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern IntPtr LoadLibrary(string lpFileName);
protected override CreateParams CreateParams
{
get
{
CreateParams prams = base.CreateParams;
if (TransparentRichTextBox.LoadLibrary("msftedit.dll") != IntPtr.Zero)
{
prams.ExStyle |= 0x020; // transparent
prams.ClassName = "RICHEDIT50W";
}
return prams;
}
}
}
Now, I created a UserControl
as wrapper. Unfortunately, I had to limit it to AutoSize
on my own way, because the AutoSize
of the RichTextBox
became broken.
AutoRichLabel.designer.cs:
partial class AutoRichLabel
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.rtb = new HST.SCADA.OPCServer.Config.Controls.TransparentRichTextBox();
this.SuspendLayout();
//
// rtb
//
this.rtb.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.rtb.Dock = System.Windows.Forms.DockStyle.Fill;
this.rtb.Location = new System.Drawing.Point(0, 0);
this.rtb.Margin = new System.Windows.Forms.Padding(0);
this.rtb.Name = "rtb";
this.rtb.ReadOnly = true;
this.rtb.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.None;
this.rtb.Size = new System.Drawing.Size(46, 30);
this.rtb.TabIndex = 0;
this.rtb.Text = "";
this.rtb.WordWrap = false;
this.rtb.ContentsResized += new System.Windows.Forms.ContentsResizedEventHandler(this.rtb_ContentsResized);
//
// AutoRichLabel
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.BackColor = System.Drawing.Color.Transparent;
this.Controls.Add(this.rtb);
this.Name = "AutoRichLabel";
this.Size = new System.Drawing.Size(46, 30);
this.ResumeLayout(false);
}
#endregion
private TransparentRichTextBox rtb;
}
AutoRichLabel.cs:
/// <summary>
/// <para>An auto sized label with the ability to display text with formattings by using the Rich Text Format.</para>
/// <para></para>
/// <para>Short RTF syntax examples: </para>
/// <para></para>
/// <para>Paragraph: </para>
/// <para>{\pard This is a paragraph!\par}</para>
/// <para></para>
/// <para>Bold / Italic / Underline: </para>
/// <para>\b bold text\b0</para>
/// <para>\i italic text\i0</para>
/// <para>\ul underline text\ul0</para>
/// <para></para>
/// <para>Alternate color using color table: </para>
/// <para>{\colortbl ;\red0\green77\blue187;}{\pard The word \cf1 fish\cf0 is blue.\par</para>
/// <para></para>
/// <para>Additional information: </para>
/// <para>Always wrap every text in a paragraph. </para>
/// <para>Different tags can be stacked (i.e. \pard\b\i Bold and Italic\i0\b0\par)</para>
/// <para>The space behind a tag is ignored. So if you need a space behind it, insert two spaces (i.e. \pard The word \bBOLD\0 is bold.\par)</para>
/// <para>Full specification: http://www.biblioscape.com/rtf15_spec.htm </para>
/// </summary>
public partial class AutoRichLabel : UserControl
{
/// <summary>
/// The rich text content.
/// <para></para>
/// <para>Short RTF syntax examples: </para>
/// <para></para>
/// <para>Paragraph: </para>
/// <para>{\pard This is a paragraph!\par}</para>
/// <para></para>
/// <para>Bold / Italic / Underline: </para>
/// <para>\b bold text\b0</para>
/// <para>\i italic text\i0</para>
/// <para>\ul underline text\ul0</para>
/// <para></para>
/// <para>Alternate color using color table: </para>
/// <para>{\colortbl ;\red0\green77\blue187;}{\pard The word \cf1 fish\cf0 is blue.\par</para>
/// <para></para>
/// <para>Additional information: </para>
/// <para>Always wrap every text in a paragraph. </para>
/// <para>Different tags can be stacked (i.e. \pard\b\i Bold and Italic\i0\b0\par)</para>
/// <para>The space behind a tag is ignored. So if you need a space behind it, insert two spaces (i.e. \pard The word \bBOLD\0 is bold.\par)</para>
/// <para>Full specification: http://www.biblioscape.com/rtf15_spec.htm </para>
/// </summary>
[Browsable(true)]
public string RtfContent
{
get
{
return this.rtb.Rtf;
}
set
{
this.rtb.WordWrap = false; // to prevent any display bugs, word wrap must be off while changing the rich text content.
this.rtb.Rtf = value.StartsWith(@"{\rtf1") ? value : @"{\rtf1" + value + "}"; // Setting the rich text content will trigger the ContentsResized event.
this.Fit(); // Override width and height.
this.rtb.WordWrap = this.WordWrap; // Set the word wrap back.
}
}
/// <summary>
/// Dynamic width of the control.
/// </summary>
[Browsable(false)]
public new int Width
{
get
{
return base.Width;
}
}
/// <summary>
/// Dynamic height of the control.
/// </summary>
[Browsable(false)]
public new int Height
{
get
{
return base.Height;
}
}
/// <summary>
/// The measured width based on the content.
/// </summary>
public int DesiredWidth { get; private set; }
/// <summary>
/// The measured height based on the content.
/// </summary>
public int DesiredHeight { get; private set; }
/// <summary>
/// Determines the text will be word wrapped. This is true, when the maximum size has been set.
/// </summary>
public bool WordWrap { get; private set; }
/// <summary>
/// Constructor.
/// </summary>
public AutoRichLabel()
{
InitializeComponent();
}
/// <summary>
/// Overrides the width and height with the measured width and height
/// </summary>
public void Fit()
{
base.Width = this.DesiredWidth;
base.Height = this.DesiredHeight;
}
/// <summary>
/// Will be called when the rich text content of the control changes.
/// </summary>
private void rtb_ContentsResized(object sender, ContentsResizedEventArgs e)
{
this.AutoSize = false; // Disable auto size, else it will break everything
this.WordWrap = this.MaximumSize.Width > 0; // Enable word wrap when the maximum width has been set.
this.DesiredWidth = this.rtb.WordWrap ? this.MaximumSize.Width : e.NewRectangle.Width; // Measure width.
this.DesiredHeight = this.MaximumSize.Height > 0 && this.MaximumSize.Height < e.NewRectangle.Height ? this.MaximumSize.Height : e.NewRectangle.Height; // Measure height.
this.Fit(); // Override width and height.
}
}
The syntax of the rich text format is quite simple:
Paragraph:
{\pard This is a paragraph!\par}
Bold / Italic / Underline text:
\b bold text\b0
\i italic text\i0
\ul underline text\ul0
Alternate color using color table:
{\colortbl ;\red0\green77\blue187;}
{\pard The word \cf1 fish\cf0 is blue.\par
But please note: Always wrap every text in a paragraph. Also, different tags can be stacked (i.e. \pard\b\i Bold and Italic\i0\b0\par
) and the space character behind a tag is ignored. So if you need a space behind it, insert two spaces (i.e. \pard The word \bBOLD\0 is bold.\par
). To escape \
or {
or }
, please use a leading \
.
For more information there is a full specification of the rich text format online.
Using this quite simple syntax you can produce something like this:
The rich text content that was attached to the RtfContent
property of my AutoRichLabel
is:
{\colortbl ;\red0\green77\blue187;}
{\pard\b BOLD\b0 \i ITALIC\i0 \ul UNDERLINE\ul0 \\\{\}\par}
{\pard\cf1\b BOLD\b0 \i ITALIC\i0 \ul UNDERLINE\ul0\cf0 \\\{\}\par}
If you want to enable word wrap, please set the maximum width to a desired size. However, this will fix the width to the maximum width, even when the text is shorter.
Now this way I can use it in my PopUp
control. If the initial RichLabel
did not help you, try this neat AutoRichLabel
with a real readonly RichTextBox
in it.
Have fun!
0
for link and so on feels dirty. Consider using markdown instead, which is what Stack Exchange uses:Such as *italic*, **bold**. [Link to Google](http://google.com)
\$\endgroup\$[? ?]
or whatever you expect to work for your text input. The input can easily be construed by adding strings with variables and the format character might even be created by a integer index. So, yes, numbers feels ugly but the control is not so much meant for daily editing but for running inside applicatons. \$\endgroup\$