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

任意形状的控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.25/5 (7投票s)

2006年7月6日

4分钟阅读

viewsIcon

44022

downloadIcon

1439

本文描述了任意形状控件的使用及工作原理。

1. 引言

在实现我们一个项目时,我们遇到了对任意(凸)形状控件的需求。该形状不仅应该影响外观,还应该确定控件的哪个区域应该响应鼠标活动。对这些区域应该有一些限制:它不应该有空腔,并且应该是凸的。这些限制的含义将在下文中详细描述。

为了方便使用控件,我们决定通过图像(32位位图)来指定其形状。控件的边界在图像中表示为一条封闭的线。在分析过程中,控件被提取为定义曲线的点序列。由曲线限制的区域是控件的最终客户端区域。

控件的功能实现在方便的 SkinControl 基类中。通过从它继承控件,您可以创建具有非标准形状的自定义控件。

2. 使用从 SkinControl 类继承的控件

下载项目由 KB_Soft Group 公司为其内部需求实现了三个控件。这些控件基于 SkinControl 通用基类,并如下所述:

  • KBSoft.Components.SkinButton – 按钮,其外观由四张图像定义(每种状态一张 - 按下、禁用、鼠标悬停和正常)。
  • KBSoft.Components.SkinTextbox – 文本框,其外观和边界由图像指定。
  • KBSoft.Components.Skintooltip – 支持放置在其上的前两个控件的弹出窗口。具有多种动画。

SkinControl 基类具有以下属性:

  • PatternBitmap – 设置图像的属性,其边界将作为信息源用于形成限制控件的区域。
  • TransparentColor – 定义由 PatternBitmap 属性指定的图像区域的颜色,该区域不包含在控件内。
  • UseCashing – 设置该属性时,表示控件的区域仅在首次创建时计算一次,然后存储在静态集合中,从而可以大大缩短重复控件初始化的时间。

SkinControl 类继承自 UserControl 类,并实现 ISupportInitialize 接口。EndInit 方法在未设置 UseCashing 标志的情况下计算控件区域。当使用 Visual Studio 2003 设计器创建控件时,该方法会自动添加到控件初始化代码中。下面是一个不使用形状设计器手动创建 SkinButton 按钮的示例。

首先,创建一个新的 Windows 应用程序,并在主窗口类(默认情况下为 Form1)中添加一个新变量

private KBSoft.Components.SkinButton skinButton = null;

然后,在构造函数中添加以下代码

//creating an object.
skinButton = new SkinButton();

//setting the color that defines the image’s 
//regions that should be excluded.
skinButton = new SkinButton();

//the control’s location.
skinButton.Location = new Point;

//getting an image from the resources. 
//Do not forget that it is formed in VS 
//7.1 as a name of the default 
//namespace + name of all the folders in 
//Solution Explorer that contain 
//the resource + the resource name.

Assembly currentAssembly = 
         Assembly GetAssembly( this.GetType() );
Bitmap bmp = (Bitmap)Bitmap.FromStream( 
    currentAssembly.GetManifestResourceStream(
                    "TestControls.Power.png" ) );

//Setting the image – the pattern for 
//calculating the control’s regions.
skinButton.PatternBitmap = bmp;

//Setting the image that is displayed 
//in the initial state of the button.
skinButton.NormalImage = bmp;

//The image for the pressed state.
skinButton.PressedImage = (Bitmap)Bitmap.FromStream( 
    currentAssembly.GetManifestResourceStream(
                   "TestControls.Power_p.png");
);

//The image for the disabled state.
skinButton.DisabledImage = (Bitmap)Bitmap.FromStream( 
    currentAssembly( GetManifestResourceStream(
                   "TestControls.Power_d.png" ) );
    
//The image for the hot state.
skinButton.HotImage = (Bitmap)Bitmap.FromStream( 
    currentAssembly.GetManifestResourceStream(
                  "TestControls.Power_f.png" ) );
    
//Calling the method performing the needed calculations.
skinButton.EndInit();

//Don’t forget to define which window owns our button.
skinButton.Parent = this;

应用程序工作的结果如下图所示

更简单的方法是将包含组件的 DLL 添加到工具箱,并从属性窗口设置所有控件的属性。

3. 位图扫描算法描述

为了达到截图所示的效果,采用了一种相当简单的方法,如下面的图片所示。

图中显示了初始图像。设白色为裁剪颜色。逐像素、逐行扫描图像。像素枚举停止时,找到颜色与设定的裁剪颜色不匹配的像素。保留该像素的坐标。首先,从左到右、从上到下进行扫描。结果得到图像的左边界;然后,从下到上扫描像素,从左到右,结果得到右边界。

下面是执行这些操作的函数代码

public void UpdateRegion()
{
    if( patternBitmap == null )
        return;

    ArrayList pts = new ArrayList();

    Color pixelColor;

    //Getting the rectangular region of the control
    Region region = new Region( this.ClientRectangle );

    //Scanning the patternBitmap bitmap from 
    //the zero string from left to right.
    //If we find a pixel that is not transparent 
    //and does not coincide with the 
    //transparent color, we add it 
    //to the list of the points of the future 
    //boundary that bounds the control.
    for( int i = 0; i< patternBitmap.Height; i++)
    {
        for( int j = 0; j< patternBitmap.Width; j++ )
        {        
            pixelColor = patternBitmap.GetPixel( j,i );

            if( pixelColor != transparentColor && pixelColor.A != 0 )
            {
                pts.Add( new Point(j,i) );
                break;
            }            
        }
    }

    //correcting the lower bound of the boundary 
    //for storing a pixel of the image.
    Point last = (Point)pts[pts.Count-1];
    Point dop = new Point( last.X, last.Y +1 );

    pts.Add(dop);
    bool addDopPoint = true;

    // Do the same from right to left beginning from the last (lower)
    // string of pixels for obtaining the right bound of the outline.
    for( int i = patternBitmap.Height -1; i>=0; i-- )
    {
        for( int j = patternBitmap.Width-1; j>=0; j-- )
        {
            pixelColor = patternBitmap.GetPixel( j,i );
            if( pixelColor != transparentColor && pixelColor.A != 0 )
            {
                if( addDopPoint == true )
                {
                    addDopPoint = false;
                    pts.Add( new Point(j+1,i+1) );
                }

                pts.Add( new Point(j,i) );
                break;
            }
        }
    }

    //closing the outline.
    pts.Add( new Point( ((Point)pts[0]).X, ((Point)pts[0]).Y ) )  ;
            
    //putting the resulting points to the array of points.
    Point[] pp = new Point[pts.Count];
    for( int i=0; i< pts.Count; i++ )
    pp[i] = (Point)pts[i];

    //creating the GraphicsPath object basing on the array of points.
    controlArea = new GraphicsPath();
    controlArea.AddLines( pp );

    //Search the intersection of the initial region of the control with 
    //the found closed outline and set the resulting region as the 
    //control's region.
    region.Intersect( controlArea );
    this.Region = region;

    this.Invalidate();
    this.Update();
}

像素读取是通过 Color 类的 GetPixel(int i, int j) 函数完成的。当找到所有边界像素后,使用 GraphicsPath 类根据它们构建封闭的轮廓。然后,使用 Region 类的 Intersect() 函数,通过区域的内部与控件的初始矩形区域的交集来构建控件的最终区域。需要注意的是,控件的形状应该意味着沿着像素线绘制的线只在两个地方与边界相交。这就是为什么,例如,使用此算法无法创建带有空腔的按钮。选择简化算法可以解释为它对于 KB_Soft Group 的功能来说足够了。此外,位图的线扫描算法是最快的。您可以修改 UpdateRegion() 函数来扩展上述控件的功能。

© . All rights reserved.