65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.61/5 (30投票s)

2007 年 9 月 13 日

CPOL

5分钟阅读

viewsIcon

153057

downloadIcon

5025

介绍一个能够为任何所需文本添加类似边框效果的 控件的文章

Screenshot - borderlabel.gif

引言

有一次,在尝试用户界面设计时,我需要显示一些带有类似边框效果的文本,以提高文本在透明背景上的可读性。不幸的是,不仅 Framework 没有提供这样的功能,而且我在网上也找不到任何免费的实现。考虑到这种情况,我决定开始创建自己的带有文本边框功能的标签控件。

背景

起初,我曾认为可以通过将文本绘制两次到屏幕上来模拟边框,但使用不同的尺寸。这样,较大的文本(外部)将模拟边框,较小的文本(绘制在较大的内部)将作为前景。

后来,当我第一次将这篇文章提交到 The Code Project 时,我希望有人能提出一种更好的方法来实现这种效果。而事实也正是如此。

由于我对 GDI+ 和直接在屏幕上绘制图形的经验很少,我遵循了 Bob Powell 的优秀指南,该指南位于 此处,这是 CodeProject 会员 fwsouthern 的建议。基本思想保持不变,只是我们现在将使用 GraphicPathsBrushes 和其他效果,而不是仅仅重叠文本,这确实是 更好 的处理方式。

创建代码

我开始编写这个控件,首先创建了一个派生自标准 System.Windows.Forms.Control 类的新组件,但后来我决定直接继承自 System.Windows.Forms.Label。然后我重写了 OnPaint 方法以添加我自己的绘制逻辑,并添加了几个额外的属性来设置控件的“边框部分”。

创建我们的控件属性

由于我希望获得最大的标签式体验,因此实现 TextTextAlignAutoSize 等属性是很自然的。但更重要的是,我需要属性来控制文本的边框方面,我称之为 BorderColorBorderSize。此时,继承自 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 拖放到您的窗体中。您可以通过“设计器属性工具箱”定义 TextTextAlignBorderSizeBorderColor,就像使用任何控件一样。

关注点

在创建此控件时,我首先尝试通过重叠不同大小的字符串来产生类似边框的效果。然而,这导致了非常不匹配的字符串,根本无法组合在一起。我不得不逐个字母绘制字符串,以保持文本从开始到结束的同步,这导致了与我预期的效果略有不同的效果。

在尝试实现 AutoSize 属性时,我也遇到了许多问题,最终导致了比好处更多的头痛。我删除了那段糟糕的代码,并找到了一种更好的解决方案,即直接继承自 Windows.Forms.Label,并尝试将控件显示的字体大小调整为字符串的实际大小。

现在,如果您有任何更好的建议、批评,或者只是想告诉我我的代码很糟糕,请回复您的想法,以便我能了解更多关于这个主题的信息并继续改进我的控件。但请善良一些,因为这是我第一次提交文章!

历史

  • 14/09/2007
    • 首次提交版本
  • 14/09/2007
    • 感谢 The Code Project 的反馈,代码得到了很大改进
  • 03/10/2007
    • 已调整以显示正确的字体大小
    • 添加了完全工作的 AutoSize 属性
  • 22/06/2008
    • 简化了绘制例程
    • 文本放置也得到了改进
© . All rights reserved.