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

GradientPanel 和 AlphaLabel: 控件构建简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (11投票s)

2006 年 9 月 6 日

CPOL

14分钟阅读

viewsIcon

53703

downloadIcon

704

本文面向有兴趣构建自定义控件的初学者程序员。作为示例,我们实现了一个 GradientPanel 控件。

Sample Image - GradientPanel.png

引言

拥有流畅、美观、炫酷但又不失微妙的用户界面有多重要?应用程序的外观和感觉可以通过使用视觉上吸引人的设计来快速提升,例如简单地使用渐变背景填充或(半)透明控件。GradientPanel 控件将为您提供此类功能,但使用起来就像常规的 Panel 控件一样简单。

由于 .NET 1.1 中缺乏像样的透明 Label 控件,因此我在源代码中包含了我的 AlphaLabel 控件(它继承自 Label)。AlphaLabel 控件使用 Alpha 混合来创建半透明或完全透明的背景效果。请注意,.NET 2.0 用户有一个默认透明的 Label 控件。但是,它不支持 Alpha 混合 :-p。

本文面向对设计和编写自定义控件感兴趣的初学者程序员。我知道有很多初学者文章解释如何构建自定义控件。本文仅从我的经验和视角描述了这一过程,我希望这能为社区做出有价值的贡献。

作为“案例”,我决定使用 GradientPanel 控件作为如何做到这一点的示例。本文不会解释 AlphaLabel 控件,但我已对源代码进行了全面注释,应该不难理解。

AlphaLabel 支持以下功能:

  • 通过设置 Alpha 属性来控制透明度级别(0 = 100% 透明,255 = 100% 不透明)。
  • 常规 Label 行为。

GradientPanel 将支持以下功能:

  • 标准的 Panel 功能(包括关闭渐变填充)。
  • 允许包含其他控件(就像 Panel 一样,它将是一个容器控件)。
  • 最多支持两种颜色之间的线性渐变填充。
  • 任意角度的渐变填充。
  • 调整控件大小后自动调整渐变。

此外,我将提供一些关于如何使控件看起来专业的技巧和窍门。

背景

本文中使用的 C# 代码基于 Visual Studio 2003 for .NET 1.1。但是,在 .NET 2.0 环境中实现此目的也并不困难。GradientPanel 控件是一个关于如何实现自己的自定义控件的绝佳示例,因为它需要并展示了几乎所有自定义控件所需的一些基本功能。

  • 常用的绘图操作是如何以及在哪里使用的?

    这里将涉及一些 GDI+(图形设备接口)编程。如果您打算创建更具视觉吸引力的控件,那么熟悉 GDI+ 函数肯定会事半功倍。但是,本文仅使用一些基本的 GDI 函数。此外,本文将展示如何保留 GDI+ 资源。

  • 如何以及覆盖哪个事件?

    这并不总是很直观,尤其是在实现从另一个控件派生的控件时,就像本例一样。本文将讨论最常用的需要覆盖的事件。

  • 如何在设计时在应用程序中配置控件?

    CodeProject 上提供的许多控件在这部分往往被忽略。它们只实现了控件的核心功能,却忘记了还有一个叫做“设计时”的开发环境。控件旨在供其他程序员和设计者使用。因此,公开发布的控件应支持“设计时”开发。本文将介绍如何实现这一点。

使用代码

我将一步一步地解释控件的构建。首先,我想向任何潜在的控件构建者指出我对自定义控件的个人看法;尽量坚持以下经验法则:

  1. 使自定义控件尽可能轻量级。

    每个程序员都喜欢轻量级的控件,只需拖放即可使用!没有人想处理安装程序、丢失的源代码、缺失的引用或缺失的资源(例如字体或图形)。程序员(是的,他们是您的最终用户)也是人,如果一次不起作用,他们通常会放弃。

  2. 限制使用外部(非系统)库。

    我个人建议除非绝对必要,否则永远不要使用外部或非系统库。如果一个组件在使用前需要安装其他组件,那它有什么用处?

  3. 选择一个合适的父控件进行派生。

    除非您打算从头开始完全构建控件(这是一个高级主题),否则我建议选择一个合适的基控件来为您处理大多数控件的功能。这样,所需的编码量就有限了。在此示例中,我们将使用 Panel 控件作为基控件。

  4. 为控件设计有限的功能集。

    基本上,这意味着将潜在的复杂控件分解成更小的部分。例如,为面板控件设置文本属性以允许用户在控件中写入文本可能很诱人。相反,选择创建一个渐变面板,它只添加渐变,不多也不少,这使得用户(程序员)非常清楚控件的功能。

  5. 尽量保持属性数量有限。

    属性定义了控件的状态。随着向控件引入的每个新属性,控件的状态数量会呈指数级增长,因此控件的复杂性也会呈指数级增长。因此,请明智地选择您的属性,并尽可能坚持使用其他控件中常用的属性。

希望这些建议能作为您未来控件设计的指南。那么,让我们开始实现吧。

步骤 1:设置项目

  1. 在 Visual Studio (2003) 中创建一个新的 C# Windows 应用程序项目,名为“GradientPanelApp”。

    目前,我将假定控件将直接实现到应用程序中。这有两个优点:源代码保持小巧,控件更容易调试(!)。或者,您可以将控件实现到单独的库中。目前,这超出了本文的范围。

  2. 添加一个名为 'GradientPanel.cs' 的新类。
  3. 按“ctrl-shift-B”:编译项目(!)。

    在进行任何更改后随时编译是一个好习惯。这可以防止在早期阶段出现错误。

步骤 2:选择基类

  1. 打开 GradientPanel.cs
  2. 为了使用控件、窗体和 GDI+,我们需要在文件中添加一些命名空间。在文件顶部添加/修改以下命名空间:
    // Default namespace 
    using System; 
    // used for adding property attributes 
    using System.ComponentModel; 
    // used for drawing with GDI+ 
    using System.Drawing; 
    // Drawing2D supports drawing gradients 
    using System.Drawing.Drawing2D; 
    // this namespace contains the base control: Panel 
    using System.Windows.Forms; 
  3. 将类派生自 Panel。按如下方式更改类:
    class GradientPanel : Panel
  4. 编译项目。

步骤 3:测试控件

  1. 在早期阶段测试控件是一个好习惯。因此,我们将把控件添加到工具箱中。
  2. 转到“工具”菜单,然后选择“添加/删除工具箱项...”。
  3. 单击“浏览”按钮,然后浏览到 GradientPanelApp 项目的位置。然后,选择 bin\debug 子文件夹。如果编译成功,此文件夹中应该包含 GradientPanelApp.exe
  4. 诀窍是选择自己的可执行文件作为组件。按确定,GradientPanel 控件现在应该显示在组件列表中。按确定,该控件现在将在工具箱中可用(我的用户控件选项卡)。
  5. 在设计模式下打开 Form1.cs
  6. GradientPanel 控件从工具箱拖到 Form 上。该控件应呈现为常规 Panel 控件,只是现在它被称为 GradientPanel 控件。
  7. 编译项目。

注意:如果您在 VS 2005 中尝试此操作,编译后控件会自动添加到工具箱。

步骤 3:自定义控件

现在基本项目结构已设置并运行,现在是时候开始进行实际的自定义了。基本上,我们希望保留与常规 Panel 相同的功能,只是它应该渲染渐变背景。因为渐变填充需要两种颜色,所以我们将重用 ForeColorBackColor 属性(这样可以减少编程!)。

此外,我们希望指定绘制渐变的角度。因为我们希望此参数在设计时可配置,所以最好的解决方案是为此实现一个名为 GradientAnglepublic 属性。

  1. 打开 GradientPanel.cs 源文件。
  2. 为了创建新属性,请将以下代码添加到类中:
    // declare the private members that will store
    // the property values private float gradientAngle;
    public float GradientAngle
    { 
        get {
            return     gradientAngle; 
        } 
        set { 
            gradientAngle = value; 
            
            // make sure the control refreshes itself and its childcontrols 
             // when this property changes
            this.Invalidate(true); 
        }
    }
  3. 请注意,属性“setter”包含 this.Invalidate() 调用。此调用将使控件要求操作系统刷新自身。这意味着刷新不是在 Invalidate() 调用本身中完成的,而是在操作系统有时间处理它时在其他某个时间完成。这是通过 OnPaintOnPaintBackground 事件完成的。
  4. 因为我们想在控件的背景上绘制渐变填充,所以我们将覆盖 OnPaintBackground 事件。只需键入“override”一词,然后按空格键,就会弹出一个列表,显示所有可以覆盖的基本控件方法!确实非常方便。选择 OnPaintBackground 并按 Tab 键。自动生成该方法的正文(打字更少!)。

    现在,只需添加一些代码来填充渐变背景,您的代码应该类似于:

    protected override void OnPaintBackground(PaintEventArgs pevent)
    {
        // call the base class to do it's background painting first
        base.OnPaintBackground (pevent);
        
        // create the brush (GDI object) used to paint the gradient fill
        Brush brush = new LinearGradientBrush(this.ClientRectangle, 
                          ForeColor, BackColor, gradientAngle, true);
        
        // use the brush to paint on the graphics device (GDI object)
        pevent.Graphics.FillRectangle(brush, this.ClientRectangle);
         
        // dispose of the brush after we're
        done painting
        brush.Dispose();
    }
  5. 再次编译您的代码。
  6. 切换回 Form1 的设计模式,并注意到已经从左到右(0 度角)实现了从黑色(前景色)到控件颜色(背景色)的渐变填充。

就这样,一个具有渐变背景的 GradientPanel 诞生了。随时可以玩弄控件的属性。在设计时更改角度以及前景色和背景色。

基本功能现已实现,控件已准备好使用。但是,有时需要最后一步。在这种情况下,您可以自行决定是否需要它。

步骤 4:优化代码

通常,在构建控件时,高效使用系统资源非常重要。尤其是在一个窗体可能包含多个控件实例的情况下。如果您的控件在资源使用方面效率低下,窗体可能会变得非常缓慢且无响应。在最坏的情况下,它会消耗您的系统资源,如内存或 CPU 周期,并最终冻结您的应用程序。

即使在这个简单的控件中,我们也使用了系统资源。请注意 OnPainBackground 方法中 BrushGraphics 对象的使用。这些对象来自 System.Drawing 命名空间,是操作系统使用的 GDI+ 对象的 .NET 包装类。请注意,在此特定示例中,Brush 对象在每次需要重绘背景时都会被创建、使用和释放。此处必须释放 Brush,因为我们需要尽快释放系统资源。但这确实是在浪费 CPU 周期:为什么要在每次 Paint 事件时创建和释放 Brush 对象?大多数情况下,Brush 对象不会改变。只有在前景色、背景色或角度发生变化时,Brush 才会改变。

如果您计划优化代码,可以采取以下步骤:

  1. 将此控件使用的 Brush 对象声明为 private 成员。为确保它具有有效值,请将其初始化为控件颜色。
    private Brush brush = new SolidBrush(SystemColors.Control);
  2. 在类中创建一个新的 private 方法 CreateBrush(),在该方法中创建线性画笔。插入释放 Brush(如果存在)并创建新画笔的代码。
    private void CreateBrush()
    {
        //dispose of the brush if it exists
        if (brush != null)
            brush.Dispose();
    
    
        // create the brush
        brush = new LinearGradientBrush(this.ClientRectangle, 
                ForeColor, BackColor, gradientAngle, true);
        
    
        // make sure the control refreshes itself
        // and its child controls when the brush changes
        this.Invalidate(true);
    }
  3. 覆盖 ForeColorBackColor 属性。在 GradientAngleForeColorBackColor 属性的 setter 中,添加对 CreateBrush() 的调用。
    public float GradientAngle
    {
        get { return gradientAngle; }
        set { 
            gradientAngle = value; 
            CreateBrush();
        }
    }
    public override Color ForeColor
    {
        get    { return base.ForeColor; }
        set {
            base.ForeColor = value;
            CreateBrush();
        }
    }
    
    public override Color BackColor
    {
        get { return base.BackColor; }
        set    {
            base.BackColor = value;
            CreateBrush();
        }
    }        
  4. OnPaintBackground 方法中删除低效的代码。
    protected override void OnPaintBackground(PaintEventArgs pevent)
    {
        base.OnPaintBackground (pevent);
        pevent.Graphics.FillRectangle(brush, this.ClientRectangle);
    }
  5. 在创建控件时初始化 Brush。这可以通过覆盖 OnCreateControl 事件来完成。
    protected override void OnCreateControl()
    {
        // first initialize the base control and its properties
        base.OnCreateControl ();
        
        // now create the brush after all properties have been initialized
        CreateBrush();
    }    
  6. 编译您的控件,并在设计时在 Form 中进行尝试。

    您可能会注意到,当您在设计时调整控件大小时,或者当您更改窗体的背景色时,渐变不会正确显示。在我们优化代码之前,Brush 是在每次 OnPaintBackground 事件中创建的。当您调整控件大小时,此事件会触发多次。但由于我们不再在 OnPaintBackground 事件中创建 Brush,因此我们需要手动处理此问题。

  7. 为了确保在调整控件大小时,或者在父控件或窗体的背景色更改时创建 Brush,请覆盖 OnResize()OnBackColorChanged 方法,并调用 CreateBrush()
    protected override void OnResize(EventArgs e)
    {
        base.OnResize (e);
        CreateBrush();
    }
    
    protected override void OnBackColorChanged(EventArgs e)
    {
        base.OnBackColorChanged (e);
        CreateBrush();
    }            
  8. 再次编译控件,并测试它在设计时和运行时在 Form 中的工作是否正常!

这标志着自定义控件开发入门的结束。如果您仍然遇到问题,请查找本页上提供的源代码。

关注点

正如本入门介绍所示,即使是创建像这样的简单控件,也需要进行一些覆盖和额外的编码才能在设计时和运行时获得所需的功能。

在结束本文之前,我想给初学者程序员一些一般的提示、技巧和提醒:

  • 这听起来可能微不足道,但请尝试使用 ///<summary> 符号注释您新定义的公共属性和方法。这些注释将显示在程序员的 IDE 中。特别是,当控件变得更复杂且属性更多时,拥有这些精心记录的文档以避免混淆非常重要。
  • 使用 #region#endregion 将相似的方法或功能分组在一起。这将大大提高代码的可读性(当然,这是一个非常普遍的陈述,适用于所有源代码)。
  • 使用所谓的“Attributes”(特性)来为您的属性设置“属性”。我在这里没有讨论这个主题,因为它更高级。但是,在提供的源代码中,我为我自定义创建的属性设置了特性。这些特性由 .NET 环境在编译时使用,并以很少的努力为您的代码添加额外的行为。例如,通过为 GradientAngle 属性设置 [Category("Appearance")] 特性,意味着该属性将显示在 IDE 的分类属性视图的“外观”部分。
  • 测试,测试,再测试。经常编译。尝试测试执行的每个路径。例如,对于控件,一个典型的情况是在设计时将控件的新实例拖到窗体上。通常,控件未正确呈现,需要一些额外的初始化代码。
  • 您可能会发现您想在设计时调试控件的行为。不幸的是,这是不支持的。为了获得某种反馈,您可以将“trace”语句放入代码中,显示相关信息(例如,触发事件的名称)。您可以通过插入以下代码来简单实现:
    Console.WriteLine("important debug information");

    这些跟踪语句也将显示在 IDE 的输出窗口中,即使在设计时也是如此!

  • 如前所述,保持控件尽可能简单。将复杂的控件分解成更小的简单控件。尽量保持小巧的“占地面积”。如果您的控件使用了大量外部引用(非标准库),那么所有这些库都需要与您的控件一起部署!这有什么坏处?嗯,控件旨在可重用。每次您想重用该控件时,您都会“感染”各种外部库。因此,尽量将引用保持在最低限度。相反,如果可能,包含源代码而不是使用外部引用。
  • 记住您的目标受众。您的最终用户是谁?大多数情况下,您的最终用户将是程序员而不是应用程序用户。因此,控件应设计为由程序员使用和完善。通过属性使控件可配置,通过触发事件(本文未讨论)使控件可扩展,并允许覆盖和继承您的控件(此处也未讨论)。
  • 如果您决定创建更复杂的属性,例如控件集合,您的 IDE 不会自动处理这些属性的序列化。这意味着什么?这意味着您必须实现自己的代码来存储程序员在设计时设置的属性的设置!幸运的是,如果您坚持使用简单的属性类型,VS2003 会为您处理这些,并将其存储在“Windows Form Designer generated code”(Windows 窗体设计器生成的代码)区域中。

显然,还有许多其他技巧和窍门。学习的最佳方法就是亲身体验。我希望本文能让您更深入地了解并非微不足道的自定义控件构建世界,并希望您能将其作为下一个自定义控件项目的起点。

历史

GradientPanelAlphaLabel 控件的 1.0 版本写于 2006 年 9 月。本文发表于 2006 年 9 月。

© . All rights reserved.