Click here to Skip to main content
Click here to Skip to main content

A Simple Label Control with Border Effect

, 24 Jun 2008
Rate this:
Please Sign up or sign in to vote.
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:

/// <span class="code-SummaryComment"><summary></span>
///   Represents a Bordered label.
/// <span class="code-SummaryComment"></summary></span>
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
    /// <span class="code-SummaryComment"><summary></span>
    ///   The border's thickness
    /// <span class="code-SummaryComment"></summary></span>
    [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);
        }
    }

    /// <span class="code-SummaryComment"><summary></span>
    ///   The border color of this component
    /// <span class="code-SummaryComment"></summary></span>
    [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:

// 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.

/// <span class="code-SummaryComment"><summary></span>
///   Releases all resources used by this control
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="disposing">True to release both managed and unmanaged resources.</span>
/// <span class="code-SummaryComment"></param></span>
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)

About the Author

César de Souza
Engineer Xerox Research Center Europe
Brazil Brazil
Computer and technology enthusiast, interested in artificial intelligence and image processing. Has a Master's degree on Computer Science specialized on Image and Signal Processing, with expertise on Machine Learning, Computer Vision, Pattern Recognition and Data Mining systems. Author of the Accord.NET Framework for developing scientific computing applications.
 
If you would like to hire good developers to build your dream application, please check out DaitanGroup, one of the top outsourcing companies in Brazil. This company, located in Brazil's Sillicon Valley but with US-based offices, has huge experience developing telecommunications software for large and small companies worldwide.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
SuggestionAnother improvement (at least for me) PinmemberRado Buransky21-Feb-13 1:05 
GeneralRe: Another improvement (at least for me) PinmemberCésar de Souza21-Feb-13 1:37 
QuestionThanks PinmemberMember 954868230-Oct-12 12:38 
GeneralMy vote of 5 Pinmemberpcs041425-Apr-12 3:33 
QuestionCan we get this to work with VB6 PinmemberJohn Harris229-Jun-10 8:43 
AnswerRe: Can we get this to work with VB6 PinmemberCésar de Souza29-Jun-10 9:54 
GeneralRe: Can we get this to work with VB6 PinmemberJohn Harris230-Jun-10 0:01 
GeneralRe: Can we get this to work with VB6 PinmemberCésar de Souza9-Jul-10 13:13 
GeneralRe: Can we get this to work with VB6 PinmemberJohn Harris212-Jul-10 2:49 
GeneralSuggested improvements PinmemberMartin Stettner11-Jun-08 6:17 
GeneralRe: Suggested improvements Pinmembercesarsouza15-Jun-08 16:10 
Hi Martin,
 
Thanks for giving feedback on my control. You are completely right about the GetBounds() usage. I've also noticed that and changed my applications code some time ago, but forgot to submit changes back to CodeProject. By the time I first wrote it, I was just beginning with the Drawning namespace, so I guess many things could have been done better.
 
Currently I'm using the e.Graphics.MeasureString() rather than GetBounds because (for a reason I couldn't determine) even GetBounds() couldn't return the correct size for my drawning. I remember I couldn't use that method from the beginning because when I first submitted the article I wasn't aware of the DpiY trick needed to draw the text correctly, but now it works seamlessly. I've also corrected some bugs with the TextAlign property, which wasn't performing correctly.
 
Now about the path, I don't know if I can generate the GraphicsPath only when text is changed because I have to know the e.Graphics.DpiY before I can determine the correct font size to use, and I'm still unsure if I can do that without having a handle to PaintEventArgs e.
 

 
Soon I'll update the article with the newer changes I've made. If you have any other ideas, please share! I'll repost the article once all changes are done.
 

Thank you!
 
----------------------
César Roberto de Souza
[cesarsouza at gmail.com]

NewsRe: Suggested improvements Pinmembercesarsouza25-Jun-08 5:55 
GeneralRe: Suggested improvements PinmemberErakis28-Feb-10 14:01 
GeneralVery Nice Update PinmemberMartin#10-Oct-07 20:54 
GeneralRe: Very Nice Update Pinmembercesarsouza14-Oct-07 13:59 
GeneralRe: Very Nice Update Pinmembercesarsouza20-Oct-07 5:20 
GeneralVery nice! Pinmemberlogan133710-Oct-07 10:43 
GeneralFont size has changed Pinmemberst1420-Sep-07 19:58 
GeneralRe: Font size has changed Pinmembercesarsouza21-Sep-07 5:52 
GeneralRe: Font size has changed Pinmemberst1423-Sep-07 14:16 
GeneralCare about GDI Resources PinmemberMartin#17-Sep-07 19:49 
GeneralRe: Care about GDI Resources Pinmembercesarsouza18-Sep-07 2:47 
GeneralRe: Care about GDI Resources PinmemberMartin#18-Sep-07 2:54 
GeneralI like it !!! Pinmemberjoaoalbertofn14-Sep-07 9:32 
GeneralRe: I like it !!! Pinmembercesarsouza14-Sep-07 15:31 
GeneralTake a look at these Pinmemberfwsouthern12-Sep-07 19:47 
GeneralRe: Take a look at these Pinmembercesarsouza12-Sep-07 20:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 24 Jun 2008
Article Copyright 2007 by César de Souza
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid