13
\$\begingroup\$

After reading the same question on SO about how to bold a word in a label for the nth time today, I decided to write a formatted Label.

At first I toyed with the idea of writing a MarkDown clone (again), but I really wanted a rather lightweight Label. So I came up wih another easy to use markup.

The original answer was a quick hack, supporting either bold or italics for a formatted chunk.

This version can also combine them, supports one alternative color and multiple embedded links. It exposes several properties.

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
  • 1 is bold
  • 2 is italic
  • 4 is alternative color
  • 1 to 4 can be combined
  • after the link text the url must follow in another pair of braces
  • instead of braces other splitter strings can be defined
  • to create a line break enter a pair of empty braces

The Label sizes itself to accomodate the text.

Here is screenshot of how it looks at work:

enter image description here

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace FBtest01
{
    public class RichLabel : Label
    {
        public RichLabel()
        {
            MouseClick += RichLabel_MouseClick;
            this.Paint += RichLabel_Paint;
            Splitters = new string[] { "{", "}" };
            AutoSize = false;
        }

        // a few properties:
        public Color TextColor    { get; set; }
        public Color TextAltColor { get; set; }
        public float Leading      { get; set; }
        public string[] Splitters { get; set; }

        private Dictionary<Rectangle, string> links = new Dictionary<Rectangle, string>();
        private Rectangle lastLink = Rectangle.Empty;

        public void RichLabel_MouseClick(object sender, MouseEventArgs e)
        {
            foreach (Rectangle R in links.Keys)
                 if (R.Contains(e.Location)) MessageBox.Show(links[R], "Click!");
        }

        public void RichLabel_Paint(object sender, PaintEventArgs e)
        {
            SolidBrush textBrush = null, backBrush;
            Font textFont = null;
            if (TextColor == System.Drawing.Color.Empty) TextColor = SystemColors.ControlText;
            if (TextColor == System.Drawing.Color.Empty) TextAltColor = SystemColors.HighlightText;
            ForeColor = this.BackColor;
            backBrush = new SolidBrush(ForeColor);
            bool linking = false;
            links.Clear();
            // separate splitter pairs
            this.Text = this.Text.Replace(Splitters[1] + Splitters[0], 
                                          Splitters[1] + "\v" + Splitters[0]);
            var parts = this.Text.Split(Splitters, StringSplitOptions.None);
            // the box to draw in
            float x = 0f;
            float y = 0f;
            float h = 0f;
            float w = 0f;
            e.Graphics.FillRectangle(backBrush, this.ClientRectangle);
            foreach (string p in parts)
            {
                SizeF box = Size.Empty;
                // create a line break
                if (p == "")
                {
                    box = Size.Empty;  // nothing to display
                    x = 0f;            // CR
                    y += h + Leading;  // NL + extra vertical space
                }
                // test 1st character
                else
                {
                    int o = ( p[0]  - '0'  ) ;
                    // if 1-7: valid text format
                    if ( o > 0  && o < 8)
                    {
                        textBrush = new SolidBrush(  o < 4 ? TextColor: TextAltColor  );
                        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);
                        e.Graphics.DrawString(p.Substring(1), textFont, textBrush, new Point((int)x, (int)y));
                        box = e.Graphics.MeasureString(p.Substring(1), textFont);
                    }
                    else if (o==0) //  0 = link
                    {
                        linking = true;  // we need to wait for the url
                        textFont = new Font(this.Font.FontFamily, this.Font.Size, FontStyle.Underline);
                        Point l = new Point((int)x, (int)y);
                        e.Graphics.DrawString(p.Substring(1), textFont, Brushes.Blue, l);
                        box = e.Graphics.MeasureString(p.Substring(1), textFont);
                        lastLink = new Rectangle(l.X, l.Y, (int)box.Width, (int)box.Height);
                        links.Add(lastLink, p);
                    }
                    else // no formatting
                    {
                        // if we need it, use as url and continue
                        if (linking && p != "\v") { links[lastLink] = p; linking = false; continue; }
                        // regular text
                        textBrush = new SolidBrush(TextColor);
                        e.Graphics.DrawString(p, this.Font, textBrush, new Point( (int)x, (int)y) );
                        box = e.Graphics.MeasureString(p, this.Font);
                    }
                }    
                // keep count of x-position
                x += box.Width;
                // check if a dimension has grown
                w = Math.Max(w, x );
                h = Math.Max(h, box.Height);
            }
            // adjust label size
            this.Width  = (int)w;
            this.Height = (int)(y + h);
            // clean up
            textBrush.Dispose();
            backBrush.Dispose();
            if (textFont != null) textFont.Dispose();
        }

    }
}

Any comments? I'd especially like to hear if the hack of setting the ForeColor to the BackColor to make the base text rendering invisible can be replaced by preventing the base paint happening in the first place.

\$\endgroup\$
  • \$\begingroup\$ @Simon: May I ask if is is ok to change/improve the code according to the suggestions? I wouldn't want to mess up anybody's efforts but I want to make progress.. Is there a recommended standard to make edits? \$\endgroup\$ – TaW Jul 2 '14 at 20:06
  • \$\begingroup\$ @TaW It just so happens that there is: Please see our meta question Can I edit my own question to include revised code? for information. The most common approach is to post a follow-up question, which is what I would normally recommend. You might want to wait a bit before you do so though as there might be more reviewers coming. \$\endgroup\$ – Simon Forsberg Jul 2 '14 at 20:08
  • 1
    \$\begingroup\$ By the way, I have to make a feature-request. Using the numbers, like 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\$ – Simon Forsberg Jul 2 '14 at 20:13
  • \$\begingroup\$ Yes, I agree, coming form MD it fells dirty; it is was 'inspired' by the internal rtf format. But Markdown has many problems for my purpose. For one it is harder to implement but that is just the start: How would I treat underscore or asterisk characters? Escape them? Argh. I also wanted to have a second color. The case braces vs. bracktes and angulars has no clear winner either. All in all the guiding aim of MarkDown's design is to use a format that remains very well readable even without being rendered. Which is not important to me. I rather wanted to stay compatible with any text input.. \$\endgroup\$ – TaW Jul 2 '14 at 20:25
  • \$\begingroup\$ .. and you can always define the splitter as strings like [? ?] 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\$ – TaW Jul 2 '14 at 20:30
8
\$\begingroup\$

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: AutoRichLabel with formatted RTF content

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!

\$\endgroup\$
  • \$\begingroup\$ I'm a bit tied up atm but am looking forward to checking out your code in depth. I should mention that a) 'light-weight' was of importance at the time b) I was really not well-versed in c# then and c) some of it (like links), came really more as an afterthought.. Expect to accept.. \$\endgroup\$ – TaW Sep 16 '15 at 10:58
  • \$\begingroup\$ @TaW Absolutely no problem and I fully understand. After improving your control for my needs (padding, maximum size, auto size) I just felt obliged to tell you about my changes and leave the code for other people, too. But I think 'light-weight' refers the speed of getting it done typing. Sometimes it's better to take a few more minutes to build something more adaptable with a better structure. Specially when posting it on codereview. Whatever, I still have to thank you, it was easier than building everything from scratch. \$\endgroup\$ – modiX Sep 16 '15 at 11:18
  • \$\begingroup\$ full ack on all points :-) \$\endgroup\$ – TaW Sep 16 '15 at 16:50
  • \$\begingroup\$ @TaW Finally, the mess with your control did not stop in my application. I ended up building my own AutoRichLabel that is a UserControl that contains a readonly TransparentRichTextBox. It's syntax for formatted content is not hard to get, thus I expanded my answer with my own version for anybody to take. \$\endgroup\$ – modiX Oct 9 '15 at 9:35
4
\$\begingroup\$

Code Improvements

I personally feel the paint function is a little long, it would be nice to break it into some sub functions. One could initialize the properties, one to draw the parts that calls sub functions to draw each type of part separately, and final one to adjust the label size. It might make the code seem longer, but with good names it would be easier to read and refactor/extend later on.

Logic Suggestions

I think the code is fine overall, but I have some suggestions for the features/logic. For one, what if I want to display curly braces inside the label? I suppose they could set up different separators, but if the label is rendering unknown input (i.e. read from a file or from a user), you would have to pick something obscure or pre-filter it by removing it. Would be nice to have a way to escape them or ignore them. I could see a situation where someone has a gamer tag like {awesomesauce} and would want to make that a link to their profile.

I like how you added the ability to click links, but I would suggest making a public property for the links or a helper function to determine if a click is on a link, so if they want to hook into the mouse click event to determine what to do if a link is clicked. Or perhaps a link clicked event that returns the url of the link.

Overall I like the simplicity of it, good job.

\$\endgroup\$
  • \$\begingroup\$ Yes, the paint code is a little too long. Actually all those property intialization was/belong in the constructor, but it doesn't work that way. So, yes that's a candidate for a function call. As for ' picking something obscure': Yes that's right. But you are free to do so as they are not characters but strings, see my comment to @Simon! Escaping them makes no sense if you want to process unknown input, does it? \$\endgroup\$ – TaW Jul 2 '14 at 20:34
  • \$\begingroup\$ Glad you put your finger on the Links. No, the link list must stay in the text as it is a part of it. But the clicked url must be exposed as a property and set in the clicked event instead of the messagbox. I have done that and it works fine. I'll do a bit more thinking how to refactor the paint event.. Thanks for the input to both of you! \$\endgroup\$ – TaW Jul 2 '14 at 20:38
  • \$\begingroup\$ @TaW I think it might actually be common to have unknown input in labels. If I want to display a username, or have a bio in italics, either could have a single curly brace that will break the input. One option is to remove them before adding to the label, another option is to escape them so they can stay in the text. I guess it depends what the software developer/user prefers :). I do see value in strings instead of chars, so I feel that is easier to make unique separators, but I would still add a filter to be safe if I used this label. \$\endgroup\$ – Steven Kohus Jul 2 '14 at 20:58
  • \$\begingroup\$ I can't see how I could filter the text I receive? But I guess I could and probably should offer an escape character property. I could replace an escasped splitter with some special character and bring it back after the split. OK, tomorrow.. On second thought, maybe exposing the links list at least as read-only may not be such a bad idea after all.. \$\endgroup\$ – TaW Jul 2 '14 at 21:11
  • \$\begingroup\$ I didn't mean for you to filter, just to offer the option of an escape character. As for exposing the links, I think the link click event should be good enough. Exposing the links would just be a way to do the same thing or allow them to do custom things like change the cursor to a pointer when hovering over the link. \$\endgroup\$ – Steven Kohus Jul 2 '14 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.