HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Lightweight rich link Label

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
labellightweightrichlink

Problem

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:

```
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 links = new Dictionary();
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 s

Solution

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:

```
///
/// 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
///
public class RichLabel : Label
{
///
/// Alternate ForeColor that can be drawn when adding 4 to the formatting number.
///
public Color ForeColorAlt { get; set; }

///
/// Splitters that allow to implement custom formattings.
///
public string[] Splitters { get; set; }

///
/// 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.
///
public bool CustomAutoSize { get; set; }

///
/// Constructor.
///
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;
}

///
/// Internal rectangle collection with rectangles that lay over hyperlinks.
///
private Dictionary links = new Dictionary();

///
/// Paints the formatted text into the control area and will be called on initialization and when the text has been changed.
///
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.MinimumS

Code Snippets

/// <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 : 
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;
        }
    }
}
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;
}
/// <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;
        }
{\pard This is a paragraph!\par}

Context

StackExchange Code Review Q#55916, answer score: 8

Revisions (0)

No revisions yet.