Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

A Simple Label Control with Border Effect

4.61/5 (32 votes)
24 Jun 2008CPOL5 min read 1   5K  
An article presenting a control capable of adding a border-like effect to any desired text
Screenshot - borderlabel.gif

Introduction

Once, while playing a bit with user interface design, I felt the need to display some text with something similar to a border effect to improve readability of the text on transparent backgrounds. Unfortunately, not only the Framework didn't provide such a functionality, I also couldn't find a free implementation anywhere on the Web. Considering the situation, I decided to start creating my own label control with text border capability.

Background

At first, I presumed it was possible to simulate a border by only drawing the text twice to the screen, but in different sizes. This way, the biggest text (external) would simulate the border, and the smaller (drawn inside the biggest) would act as the foreground.

Later, when I first submitted this article to The Code Project, I was hoping that someone would suggest a better way for performing this kind of effect. And that's just what happened.

Having little experience with GDI+ and drawing graphics directly to the screen, I've followed the excellent guide by Bob Powell located here, as a suggestion from the CodeProject member fwsouthern. The idea basically remained the same, except we are now going to use GraphicPaths, Brushes and other effects rather than just overlapping text, which is indeed a much better way of doing things.

Creating the Code

I started coding this control by creating a new component that derives from the standard System.Windows.Forms.Control class, but later decided to inherit directly from System.Windows.Forms.Label. Then I overrode the OnPaint method to add my own painting logic and added a few extra properties to setup the 'border part' of the control.

Creating our Control's Properties

Since I wanted to achieve the maximum label-like experience, it was natural to implement properties like Text, TextAlign and AutoSize. But more than this, I needed properties to control the border aspect of the text, which I called BorderColor and BorderSize. At this point, inheriting from Windows.Forms.Label seemed like a good idea, because I would have earned some of the properties I wanted for free, without having to worry about, for example, AutoSizing the control.

To properly achieve this, however, this control required some tricks which took me some time to learn.

Let's then see what had to be done, starting with the control constructor, properties and overridden events:

C#
/// <summary>
///   Represents a Bordered label.
/// </summary>
public partial class BorderLabel : Label
{
    private float borderSize;
    private Color borderColor;

    private PointF point;
    private SizeF drawSize;
    private Pen drawPen;
    private GraphicsPath drawPath;
    private SolidBrush forecolorBrush;

    // Constructor
    //-----------------------------------------------------

    #region Constructor
    public BorderLabel()
    {
        this.borderSize = 1f;
        this.borderColor = Color.White;
        this.drawPath = new GraphicsPath();
        this.drawPen = new Pen(new SolidBrush(this.borderColor), borderSize);
        this.forecolorBrush = new SolidBrush(this.ForeColor);
    
        this.Invalidate();
    }
    #endregion

    // Public Properties
    //-----------------------------------------------------

    #region Public Properties
    /// <summary>
    ///   The border's thickness
    /// </summary>
    [Browsable(true)]
    [Category("Appearance")]
    [Description("The border's thickness")]
    [DefaultValue(1f)]
    public float BorderSize
    {
        get { return this.borderSize; }
        set
        {
            this.borderSize = value;
            if (value == 0)
            {
                //If border size equals zero, disable the
                // border by setting it as transparent
                this.drawPen.Color = Color.Transparent;
            }
            else
            {
                this.drawPen.Color = this.BorderColor;
                this.drawPen.Width = value;
            }

            this.OnTextChanged(EventArgs.Empty);
        }
    }

    /// <summary>
    ///   The border color of this component
    /// </summary>
    [Browsable(true)]
    [Category("Appearance")]
    [DefaultValue(typeof(Color), "White")]
    [Description("The border color of this component")]
    public Color BorderColor
    {
        get { return this.borderColor; }
        set
        {
            this.borderColor = value;
            
            if (this.BorderSize != 0)
                this.drawPen.Color = value;
        
            this.Invalidate();
        }
    }
    #endregion

    // Event Handling
    //-----------------------------------------------------
    
    #region Event Handling
    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);
        this.Invalidate();
    }

    protected override void OnTextAlignChanged(EventArgs e)
    {
        base.OnTextAlignChanged(e);
        this.Invalidate();
    }

    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);
    }

    protected override void OnForeColorChanged(EventArgs e)
    {
        this.forecolorBrush.Color = base.ForeColor;
        base.OnForeColorChanged(e);
        this.Invalidate();
    }
    #endregion

Simple, isn't it?

OK, not really. I had originally tried to maintain the maximum similarity to the original label control as much as possible. This included writing every property with the proper designer's characteristics and flags defined, and ensuring that the this.Invalidate() method was called after a property is modified to reflect changes in design-mode immediately.

Now, we may proceed to the most interesting part of this component.

Overriding OnPaint

As I've said before, overriding the OnPaint method seemed to be the only suitable solution for my problem. However, because we are inheriting directly from System.Windows.Forms.Label, we have to add most of the painting logic manually. This includes drawing the properly sized text and determining where on the control's area our text should be drawn.

But then, there was a problem. Since we are going to draw into a GraphicsPath rather than to the Graphics object itself, a lot of sizing issues appeared. Apparently, drawing the same font on-screen and inside a GraphicsPath didn't necessarily result in drawing the same thing. Because of that, I just couldn't get AutoSize to work the way I wanted, and even properly aligning the text inside the control seemed to be a complicated task to implement.

Finally, after a lot of reading (and some luck), I've found a few hints on how to properly manage those problems. The final overridden method is shown below:

C#
// Drawning Events
//-----------------------------------------------------

#region Drawning
protected override void OnPaint(PaintEventArgs e)
{
    // First let's check if we indeed have text to draw.
    //  if we have no text, then we have nothing to do.
    if (this.Text.Length == 0)
        return;

    // Secondly, let's begin setting the smoothing mode to AntiAlias, to
    // reduce image sharpening and compositing quality to HighQuality,
    // to improve our drawnings and produce a better looking image.

    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.CompositingQuality = CompositingQuality.HighQuality;

    // Next, we measure how much space our drawning will use on the control.
    //  this is important so we can determine the correct position for our text.
    this.drawSize = e.Graphics.MeasureString(this.Text, this.Font, new PointF(), 
                StringFormat.GenericTypographic);
         
    // Now, we can determine how we should align our text in the control
    //  area, both horizontally and vertically. If the control is set to auto
    //  size itself, then it should be automatically drawn to the standard position.
            
    if (this.AutoSize)
    {
        this.point.X = this.Padding.Left;
        this.point.Y = this.Padding.Top;
    }
    else
    {
        // Text is Left-Aligned:
        if (this.TextAlign == ContentAlignment.TopLeft ||
            this.TextAlign == ContentAlignment.MiddleLeft ||
            this.TextAlign == ContentAlignment.BottomLeft)
            this.point.X = this.Padding.Left;
    
        // Text is Center-Aligned
        else if (this.TextAlign == ContentAlignment.TopCenter ||
            this.TextAlign == ContentAlignment.MiddleCenter ||
            this.TextAlign == ContentAlignment.BottomCenter)
            point.X = (this.Width - this.drawSize.Width) / 2;

        // Text is Right-Aligned
        else point.X = this.Width - (this.Padding.Right + this.drawSize.Width);
        
        // Text is Top-Aligned
        if (this.TextAlign == ContentAlignment.TopLeft ||
            this.TextAlign == ContentAlignment.TopCenter ||
            this.TextAlign == ContentAlignment.TopRight)
            point.Y = this.Padding.Top;

        // Text is Middle-Aligned
        else if (this.TextAlign == ContentAlignment.MiddleLeft ||
            this.TextAlign == ContentAlignment.MiddleCenter ||
            this.TextAlign == ContentAlignment.MiddleRight)
            point.Y = (this.Height - this.drawSize.Height) / 2;

        // Text is Bottom-Aligned
        else point.Y = this.Height - (this.Padding.Bottom + this.drawSize.Height);
    }

    // Now we can draw our text to a graphics path.
    //  
    //   PS: this is a tricky part: AddString() expects float emSize in pixel, 
    //   but Font.Size measures it as points.
    //   So, we need to convert between points and pixels, which in
    //   turn requires detailed knowledge of the DPI of the device we are drawing on. 
    //
    //   The solution was to get the last value returned by the 
    //   Graphics.DpiY property and
    //   divide by 72, since point is 1/72 of an inch, 
    //   no matter on what device we draw.
    //
    //   The source of this solution can be seen on CodeProject's article
    //   'OSD window with animation effect' - 
    //   http://www.codeproject.com/csharp/OSDwindow.asp
    
    float fontSize = e.Graphics.DpiY * this.Font.SizeInPoints / 72;
                
    this.drawPath.Reset();                           
    this.drawPath.AddString(this.Text, this.Font.FontFamily, 
                    (int)this.Font.Style, fontSize,
                        point, StringFormat.GenericTypographic);

    // And finally, using our pen, all we have to do now
    //  is draw our graphics path to the screen. Voila!
    e.Graphics.FillPath(this.forecolorBrush, this.drawPath);
    e.Graphics.DrawPath(this.drawPen, this.drawPath);
}

Now, finally, the last but maybe most important method I had to override (and which I initially forgot - thanks martin for the tip) was the Dispose method. I say this is probably the most important method because most GDI+ resources (like pens and brushes) are not automatically collected by the Garbage Collector and need to be disposed manually. Otherwise, the control could cause a memory leak and would sooner or later lead to a crash, because the GDI objects would always stay in memory.

C#
/// <summary>
///   Releases all resources used by this control
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources.
/// </param>
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (this.forecolorBrush != null)
            this.forecolorBrush.Dispose();

        if (this.drawPath != null)
            this.drawPath.Dispose();

        if (this.drawPen != null)
            this.drawPen.Dispose();
    }
    base.Dispose(disposing);
}

Well, that's it.

If you still have any doubts left about the workings of the code, please feel free to download and experiment with the source code and/or post a message on the article discussion board. I have tried to document my code as best as I could.

Using the Code

To use the code, just add this component to your project, open the form designer and drag and drop BorderLabel inside your form. You may define the Text, TextAlign, BorderSize and BorderColor through the Designer Properties Toolbox as you would do with any control.

Points of Interest

When creating this control, I first attempted to overlap strings in different sizes to produce a border-like effect. This, however, resulted in very discrepant strings that just wouldn't fit together. I had to draw the string letter-by-letter in order to maintain synchronism from start to the end of the text, which resulted in a slightly different effect from what I expected.

I also ran into a number of problems when trying to implement the AutoSize properties, which, in the end, resulted in more headaches than benefits. I've removed that ugly code and came to a much better solution, which was to inherit directly from Windows.Forms.Label and try to correct the displayed Font size of the control to the real size of the string.

Now, if you have any better suggestions, criticisms, or just want tell me that my code is horrible, please post back with your thoughts so I can learn more about this subject and continue improving my control. But please be kind as this is my first article submission!

History

  • 14/09/2007
    • First version submitted
  • 14/09/2007
    • Code greatly improved, thanks to input from The Code Project
  • 03/10/2007
    • Adjusted to display the proper font size
    • Added fully working AutoSize property
  • 22/06/2008
    • Simplified painting routine
    • Text placement has also improved

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)