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:
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;
#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
#region Public Properties
[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)
{
this.drawPen.Color = Color.Transparent;
}
else
{
this.drawPen.Color = this.BorderColor;
this.drawPen.Width = value;
}
this.OnTextChanged(EventArgs.Empty);
}
}
[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
#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:
#region Drawning
protected override void OnPaint(PaintEventArgs e)
{
if (this.Text.Length == 0)
return;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
this.drawSize = e.Graphics.MeasureString(this.Text, this.Font, new PointF(),
StringFormat.GenericTypographic);
if (this.AutoSize)
{
this.point.X = this.Padding.Left;
this.point.Y = this.Padding.Top;
}
else
{
if (this.TextAlign == ContentAlignment.TopLeft ||
this.TextAlign == ContentAlignment.MiddleLeft ||
this.TextAlign == ContentAlignment.BottomLeft)
this.point.X = this.Padding.Left;
else if (this.TextAlign == ContentAlignment.TopCenter ||
this.TextAlign == ContentAlignment.MiddleCenter ||
this.TextAlign == ContentAlignment.BottomCenter)
point.X = (this.Width - this.drawSize.Width) / 2;
else point.X = this.Width - (this.Padding.Right + this.drawSize.Width);
if (this.TextAlign == ContentAlignment.TopLeft ||
this.TextAlign == ContentAlignment.TopCenter ||
this.TextAlign == ContentAlignment.TopRight)
point.Y = this.Padding.Top;
else if (this.TextAlign == ContentAlignment.MiddleLeft ||
this.TextAlign == ContentAlignment.MiddleCenter ||
this.TextAlign == ContentAlignment.MiddleRight)
point.Y = (this.Height - this.drawSize.Height) / 2;
else point.Y = this.Height - (this.Padding.Bottom + this.drawSize.Height);
}
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);
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.
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
- 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