Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Controls of an arbitrary shape

0.00/5 (No votes)
6 Jul 2006 1  
The article describes the use and the principle of operation of control of an arbitrary shape.

1. Introduction

When implementing one of our projects, we faced with a necessity of a control of arbitrary (convex) shape. The shape should not only influence the appearance, but also determine what region of the control should respond to mouse activity. There should be some restrictions concerning the regions: it should have no cavities and should be convex. The meaning of the restrictions will be described in details below.

For the control to be conveniently used, we decided to specify its shape with the help of an image (a 32-bit bitmap). The control�s boundary is presented in the picture as a closed line. The control is extracted from the image during the analysis, as a sequence of points defining the curve. The region restricted by the curve is a client�s resulting region of the control.

The control�s functionality is implemented in the convenient SkinControl base class. By inheriting a control from it, you may create your own controls that have a non-standard shape.

2. Using the controls inherited from the SkinControl class

The project available for download presents three controls implemented by the KB_Soft Group company for its internal needs. These controls are based on the SkinControl common base class, and are described below:

  • KBSoft.Components.SkinButton � the button, whose appearance is defined by four images (one for each state � pressed, disabled, hot, and normal).
  • KBSoft.Components.SkinTextbox � the text box, whose appearance and boundaries are specified by an image.
  • KBSoft.Components.Skintooltip � the pop-up window supporting the first two controls placed on it. Has a number of animations.

The SkinControl base class has the following properties:

  • PatternBitmap � the property setting the image, which boundaries will be a source of information for forming the region that restricts the control.
  • TransparentColor � the color defining the region of the image, which is specified by the PatternBitmap property, that is not included in the control.
  • UseCashing � being set, the property indicates that the control�s region is calculated only once during the first creation, and after that it is stored to the static collection that allows the time for the repeated control�s initialization to be considerably shortened.

The SkinControl class is inherited from the UserControl class, and implements the ISupportInitialize interface. The EndInit method calculates the control�s region if the UseCashing flag is not set. The method is automatically added to the code of the control�s initialization when it is created with the help of the Visual Studio 2003 designer. Given below is an example of manual creation of the SkinButton button without using the shape designer.

First, create a new Windows application, and add the main window class (Form1, by default) a new variable:

private KBSoft.Components.SkinButton skinButton = null;

Then, add to the constructor, the following code:

//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;

The result of the application work is presented in the picture below:

A simpler way is to add the DLL containing the components to the toolbox, and to set all the control�s properties from the property window.

3. The description of the bitmap scan algorithm

To reach the effect shown in the screenshots, a rather simple approach is used, which is shown in the picture below:

Shown in the picture is the initial image. Let the white color be set as a color for cutting off. A scan of the image is performed pixel by pixel, line by line. The pixels enumeration stops when the pixel with the color that does not coincide with the color set for cutting off is found. The pixel�s coordinates are kept. First, scanning from left to right of the pass from up to down is performed. As a result, the left boundary of the image is obtained; after that, it goes from down to up scanning the pixels from left to right, as a result the right boundary is obtained.

Given below is the code of the function performing these operations:

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();
}

Reading pixels is performed with the help of the GetPixel(int i, int j) function of the Color class. When all boundary pixels are found, the closed outline is built based on them, with the help of the GraphicsPath class. Then, the resulting control�s region is built, formed by the intersection of the interior of the region and the initial rectangular region of the control, with the help of the Intersect() function of the Region class. It is important to note that the control�s shape should imply that the line drawn along the line of pixels should intersect the boundary only in two places. That is why, for example, a button with cavities can�t be created with the help of this algorithm. The choice of the simplified algorithm may be explained by the functionality being enough for KB_Soft Group. Besides, the algorithm of line scan of the bitmap is the fastest. You may modify the UpdateRegion() function to extend the functionality of the controls described above.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here