In Depth Banner
Skip Navigation LinksWelcome > In Depth articles > Per-pixel alpha

Select your preferred language

Per-Pixel alpha-blending using GDI+

If you've used GDI+ even a little bit, you will be aware of its alpha-blending capabilities and may have used them to create semi-transparent  shapes and overlay images. Most applications of alpha-blending treat the object being blended as a whole and simply set a blanket alpha-blend level that affects all pixels equally. A couple of examples of this can be seen in the following application listing. First an image is drawn and objects of increasing opacity drawn on top of the image, then some solid objects are drawn and chunks of image with varying transparency drawn on top of them. The result is shown in figure 1.

Figure 1: Varying degrees of transparency for fills and images.

Click here for Listing 1.

While interesting, these effects have been created using no subtlety whatsoever in the transparency and these techniques will not deliver a soft edged drop-shadow or Photoshop style effects such as an image gradient. To do these kinds of operations the individual alpha component of each pixel must be adjusted.

The easiest way to take advantage of per-pixel alpha features is to let brushes such as LinearGradientBrush or PathGradientBrush to do the job for you. Creating a soft shadow under a rectangle can be performed by combining linear and path gradients or by using blitting effects.

Creating rectangular soft shadows.

A very reliable method is to take advantage of the capabilities of PathGradientBrush and blitting. Figure 2 shows a simple yet effective technique using them.

Figure 2. A path gradient used to construct a dropshadow.

In the above example, step 1 shows a PathGradientBrush used to create a small bitmap used as a pattern. Step 2 shows how the pattern bitmap is divided into four quadrants. This is a concept rather than a program stage. Step 3 shows how these four quadrants are used to create the corners of the drop-shadow. Finally, step 4 shows how a thin slice from the original pattern is used to create the edge of the shadow by stretching the slice over the edge of the rectangle so that it buts up to the quadrants. This is continued so that the corresponding slice of the pattern is used on all four sides to create the final image. The colours used to create the PathGradientBrush are chosen to make a gradient that changes in alpha, not colour which remains black, and so when blitted onto a background it has the effect of darkening the background by varying degrees.

Figure 3 shows an application that uses this technique to place a variable opacity drop-shadow under an image. The whole dropshadow is drawn to enable you to place the shadow anywhere under the image or at any distance giving the effect of raising the image from the page.

Figure 3: a drop-shadow with per-pixel alpha.

Click here for the code that generated this image.

Special image effects using per-pixel alpha blending.

The best example of per-pixel alpha blending is the ability of GDI+ to create image vignettes which show a range of transparencies. A few examples of this is shown in figure 4.

Figure 4: Some Image Effects using per-pixel alpha

The image in figure 4 shows a background with four alpha blended images overlaid on it. The first is a simple fade from transparent to opaque, the next a cameo style with a faded edge, next is a ring and finally a barn-door fade with the center opaque and the edges transparent. All of these effects are created in essentially the same way, by modifying the individual alpha component of each pixel before blitting the image to the page.

All of the effects in this example have been created by novel use of the LinearGradientBrush or PathGradientBrush capabilities. The various gradient brushes were used to create a pattern of gray which was transferred to the alpha layer of the image by directly accessing the byte array of the 32 bit per pixel image.

The GradientPictureBox control included with this article provides the means to create effects like those shown above.

The GradientPictureBox control.

The salient points of this control are that GDI+ is used to create a pattern of light and dark on a bitmap, the size of which is matched to the original image. This pattern is built using the LinearGradientBrush for rectangular effects and the PathGradientBrush for elliptical effects. Blends may also be used to make more complex effects in both rectangular and elliptical modes.

The control properties.

Properties are simple and include the following:

  • Image. The original image to which the effect will be applied.

  • Style. A setting of Rectangular or Elliptical

  • Blend. An optional Blend object used to modify the linearity of the gradient.

  • Angle. Rectangular linear gradients may be rotated to any angle.

  • MaintainAspectRatio When sized, the control will retain the aspect ratio of the original image

Other novel aspects of the control

Two things stand out in the implementation of the GradientPictureBox control. Primarily, because potentially every pixel of the image must be modified, we need to have access to the bitmap byte array to speed up the processing as much as possible. Modifying the alpha component using GetPixel and SetPixel is possible but these methods are very long-winded and not at-all performant. Therefore, we'll use the LockBits method to get access to the bytes in the image. Secondly, this control is a true transparent control that can be placed on top of drawn items or other controls and it won't obscure them. Note that you can't click through a transparent control so don't put it on top of anything that the user needs to change.

Code Breakdown

Making a truly transparent control requires the window style WS_EX_TRANSPARENT to be set. This is accomplished in the CreateParams property as shown in the following listing.

    protected override CreateParams CreateParams

    {

      get

      {

        CreateParams cp=base.CreateParams;

        cp.ExStyle|=0x20; //WS_EX_TRANSPARENT

        return cp;

      }

    }

A transparent control must have all that's behind it refreshed before it does any of it's own paint operations. The control's Refresh method is overridden to inform the parent that the area behind the control is invalid.

    public override void Refresh()

    {

      if(Parent!=null)

      {

        Rectangle rc=new Rectangle(this.Location,this.Size);

        Parent.Invalidate(rc,true);

      }

      Invalidate();

    }

A transparent control should not paint it's background so the OnPaintBackground method is stubbed out.

    protected override void OnPaintBackground(PaintEventArgs pevent)

    {

      //Don't paint the background

    }

Painting the image

Rather than do all the hard work every time the control needs painting, the control maintains an up-to-date copy of the modified image which is the right size for the surface of the control. It only updates this image when one of the properties change and modify the appearance of the control. A boolean variable, _dirty, is set whenever a property is changed and then the temporary image is created and stored. If the temporary image is still valid the it is drawn to the screen without further ado.

If the image is flagged as dirty the steps to create the image are as follows;

  • A temporary image the correct size for the control is created

  • The original image is blitted to the temporary image.

  • A second image the same size is created

  • A gray-scale gradient is used to fill this image using LinearGradientBrush or PathGradientBrush depending on whether the style is rectangular or elliptical.

  • The alpha component of each pixel in the temporary image is updated to the same value as the corresponding gray-level in the gradient image

In all cases, the clean image is drawn to the surface of the control.

The code in the following listing shows this process.

    protected override void OnPaint(PaintEventArgs e)

    {

      if(_image==null)

        return;

 

      if(_dirty)

      {

        if(this._internalImage!=null)

          _internalImage.Dispose();

        _internalImage=null;

        //Create an image that fits the control

        _internalImage=new Bitmap(this.Width,this.Height,PixelFormat.Format32bppArgb);

        //Draw the original to the internal image

        Graphics g=Graphics.FromImage(_internalImage);

        g.InterpolationMode=InterpolationMode.HighQualityBicubic;

        g.DrawImage(this._image, new Rectangle(0,0,_internalImage.Width,_internalImage.Height), 0, 0, _image.Width, _image.Height, GraphicsUnit.Pixel);

        g.Dispose();

        //Now create the gradient that will become the alpha pattern...

        Bitmap gbm=new Bitmap(this.Width,this.Height,PixelFormat.Format32bppArgb);

        g=Graphics.FromImage(gbm);

        switch(this._style)

        {

          case GradientStyles.Rectangular:

            LinearGradientBrush lgb=new LinearGradientBrush(this.ClientRectangle,

              Color.Black,

              Color.White,

              this._angle,

              false);

            if(this.Blend!=null)

              lgb.Blend=this.Blend;

            g.FillRectangle(lgb,this.ClientRectangle);

            g.Dispose();

            lgb.Dispose();

            break;

          case GradientStyles.Elliptical:

            GraphicsPath pth=new GraphicsPath();

            pth.AddEllipse(this.ClientRectangle);

            PathGradientBrush pgb=new PathGradientBrush(pth);

            pgb.CenterColor=Color.White;

            pgb.SurroundColors=new Color[]{Color.Black};

            if(this.Blend!=null)

              pgb.Blend=this.Blend;

            g.FillEllipse(pgb,this.ClientRectangle);

            g.Dispose();

            pgb.Dispose();

            break;

        }

        //The Gradient Bitmap gbm now contains a monochrome image with the correct values

        //We transfer these values to the internal image using the LockBits method.

 

        BitmapData bmd1=((Bitmap)_internalImage).LockBits(this.ClientRectangle, ImageLockMode.WriteOnly, _internalImage.PixelFormat);

        BitmapData bmd2=gbm.LockBits(this.ClientRectangle, ImageLockMode.ReadOnly, gbm.PixelFormat);

 

        //For maximum speed we use unsafe data pointers to access the byte arrays

        unsafe

        {

          for(int y=0; y<gbm.Height; y++)

          {

            byte* destRowPtr=(byte*)bmd1.Scan0.ToPointer()+(bmd1.Stride*y);

            byte* srcRowPtr=(byte*)bmd2.Scan0.ToPointer()+(bmd2.Stride*y);

            for(int x=0; x<gbm.Width; x++)

            {

              //One of the componet colours is used to update the alpha of the destination

              destRowPtr[(x*4)+3]=srcRowPtr[x*4];

            }

          }

        }// /unsafe

 

        //tidy up

        ((Bitmap)_internalImage).UnlockBits(bmd1);

        gbm.UnlockBits(bmd2);

        gbm.Dispose();

      }

 

      e.Graphics.DrawImageUnscaled(_internalImage,0,0);

 

      base.OnPaint (e);

    }

 

The control has been provided with comments for the properties and generally tidied for design time use.

 

Summary.

 

The potential of GDI+ for creating truly stunning graphics is there if you know where to get at the hidden secrets. Per-pixel alpha techniques make the difference between a flat and uninteresting UI and something that has professional polish. The techniques presented in this article will enable you to create great looking yet subtle images for your applications.

 

The code for the GradientPictureBox control is available as a ZIP archive in below locations.

Return to the main index

Copyright © Bob Powell 2003-2009. All rights reserved