带边框效果的简单标签控件






4.61/5 (30投票s)
介绍一个能够为任何所需文本添加类似边框效果的

引言
有一次,在尝试用户界面设计时,我需要显示一些带有类似边框效果的文本,以提高文本在透明背景上的可读性。不幸的是,不仅 Framework 没有提供这样的功能,而且我在网上也找不到任何免费的实现。考虑到这种情况,我决定开始创建自己的带有文本边框功能的标签控件。
背景
起初,我曾认为可以通过将文本绘制两次到屏幕上来模拟边框,但使用不同的尺寸。这样,较大的文本(外部)将模拟边框,较小的文本(绘制在较大的内部)将作为前景。
后来,当我第一次将这篇文章提交到 The Code Project 时,我希望有人能提出一种更好的方法来实现这种效果。而事实也正是如此。
由于我对 GDI+ 和直接在屏幕上绘制图形的经验很少,我遵循了 Bob Powell 的优秀指南,该指南位于 此处,这是 CodeProject 会员 fwsouthern 的建议。基本思想保持不变,只是我们现在将使用 GraphicPaths
、Brushes
和其他效果,而不是仅仅重叠文本,这确实是 更好 的处理方式。
创建代码
我开始编写这个控件,首先创建了一个派生自标准 System.Windows.Forms.Control
类的新组件,但后来我决定直接继承自 System.Windows.Forms.Label
。然后我重写了 OnPaint
方法以添加我自己的绘制逻辑,并添加了几个额外的属性来设置控件的“边框部分”。
创建我们的控件属性
由于我希望获得最大的标签式体验,因此实现 Text
、TextAlign
和 AutoSize
等属性是很自然的。但更重要的是,我需要属性来控制文本的边框方面,我称之为 BorderColor
和 BorderSize
。此时,继承自 Windows.Forms.Label
似乎是个不错的选择,因为我可以免费获得一些想要的属性,而无需担心,例如 AutoSizing
控件。
然而,为了正确实现这一点,此控件需要一些我花了一些时间才学会的技巧。
那么,让我们来看看必须做什么,从控件的构造函数、属性和重写事件开始。
/// <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
简单,不是吗?
好吧,实际上不是。我最初试图尽可能地保持与原始标签控件的最大相似性。这包括编写每个属性,定义正确的设计器特性和标志,并确保在修改属性后调用 this.Invalidate()
方法,以便在设计模式下立即反映更改。
现在,我们可以继续讨论这个组件最有趣的部分了。
重写 OnPaint
正如我之前所说,重写 OnPaint
方法似乎是解决我问题的唯一合适方法。但是,由于我们直接继承自 System.Windows.Forms.Label
,我们必须手动添加大部分绘制逻辑。这包括绘制适当大小的文本,以及确定文本应在控件区域的哪个位置绘制。
但是,出现了一个问题。由于我们将绘制到一个 GraphicsPath
而不是直接绘制到 Graphics
对象本身,因此出现了许多尺寸问题。显然,在屏幕上绘制的相同字体和在 GraphicsPath
内部绘制的字体不一定能产生相同的结果。因此,我无法让 AutoSize
按我想要的方式工作,甚至在控件内正确对齐文本也似乎是一项复杂的任务。
最后,经过大量的阅读(以及一些运气),我找到了一些关于如何正确处理这些问题的线索。最终的重写方法如下所示。
// 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' -
// https://codeproject.org.cn/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);
}
现在,最后但也许最重要的方法是(我最初忘记了 - 感谢 martin 的提示)Dispose
方法。我说这可能是最重要的方法,因为大多数 GDI+ 资源(如笔和画笔)不会被垃圾回收器自动收集,需要手动处理。否则,控件可能会导致内存泄漏,迟早会导致崩溃,因为 GDI 对象将一直保留在内存中。
/// <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);
}
好了,就这样。
如果您对代码的工作原理仍有任何疑问,请随时下载并尝试源代码,/或在文章讨论板上发布消息。我已经尽我所能记录了我的代码。
Using the Code
要使用此代码,只需将此组件添加到您的项目中,打开窗体设计器,然后将 BorderLabel
拖放到您的窗体中。您可以通过“设计器属性工具箱”定义 Text
、TextAlign
、BorderSize
和 BorderColor
,就像使用任何控件一样。
关注点
在创建此控件时,我首先尝试通过重叠不同大小的字符串来产生类似边框的效果。然而,这导致了非常不匹配的字符串,根本无法组合在一起。我不得不逐个字母绘制字符串,以保持文本从开始到结束的同步,这导致了与我预期的效果略有不同的效果。
在尝试实现 AutoSize
属性时,我也遇到了许多问题,最终导致了比好处更多的头痛。我删除了那段糟糕的代码,并找到了一种更好的解决方案,即直接继承自 Windows.Forms.Label
,并尝试将控件显示的字体大小调整为字符串的实际大小。
现在,如果您有任何更好的建议、批评,或者只是想告诉我我的代码很糟糕,请回复您的想法,以便我能了解更多关于这个主题的信息并继续改进我的控件。但请善良一些,因为这是我第一次提交文章!
历史
- 14/09/2007
- 首次提交版本
- 14/09/2007
- 感谢 The Code Project 的反馈,代码得到了很大改进
- 03/10/2007
- 已调整以显示正确的字体大小
- 添加了完全工作的
AutoSize
属性
- 22/06/2008
- 简化了绘制例程
- 文本放置也得到了改进