|
|
|
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;
}
}
Protected Overrides
ReadOnly Property
CreateParams() As CreateParams
Get
Dim cp As
CreateParams = MyBase.CreateParams
cp.ExStyle = cp.ExStyle Or 32
'WS_EX_TRANSPARENT
Return cp
End Get
End Property
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();
}
Public Overrides
Sub Refresh()
If Not Parent
Is Nothing
Then
Dim rc As
New Rectangle(Me.Location,
Me.Size)
Parent.Invalidate(rc, True)
End If
Invalidate()
End Sub
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
}
Protected Overloads
Overrides Sub
OnPaintBackground(ByVal e
As PaintEventArgs)
'do nothing
End Sub
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);
}
Protected Overloads
Overrides Sub
OnPaint(ByVal e As
PaintEventArgs)
If _image Is
Nothing Then
Return
End If
If _dirty Then
If Not _internalImage
Is Nothing
Then
_internalImage.Dispose()
_internalImage = Nothing
End If
'Create an image that fits the control
_internalImage
= New Bitmap(Me.Width,
Me.Height, PixelFormat.Format32bppArgb)
'Draw the original to the internal image
Dim g As
Graphics = Graphics.FromImage(_internalImage)
g.InterpolationMode = InterpolationMode.HighQualityBicubic
g.DrawImage(Me._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...
Dim gbm As
New Bitmap(Me.Width,
Me.Height, PixelFormat.Format32bppArgb)
g
= Graphics.FromImage(gbm)
Select Case
Me._style
Case GradientStyles.Rectangular
Dim lgb
As New
LinearGradientBrush(Me.ClientRectangle, _
Color.Black, _
Color.White, _
Me._angle, _
False)
If
Not Me.Blend Is
Nothing Then
lgb.Blend = Me.Blend
End
If
g.FillRectangle(lgb, Me.ClientRectangle)
g.Dispose()
lgb.Dispose()
Case GradientStyles.Elliptical
Dim pth
As New
GraphicsPath
pth.AddEllipse(Me.ClientRectangle)
Dim pgb
As New
PathGradientBrush(pth)
pgb.CenterColor = Color.White
pgb.SurroundColors = New
Color() {Color.Black}
If
Not Me.Blend Is
Nothing Then
pgb.Blend = Me.Blend
End
If
g.FillEllipse(pgb, Me.ClientRectangle)
g.Dispose()
pgb.Dispose()
end
select
'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.
Dim bmd1 As
BitmapData = CType(_internalImage,
Bitmap).LockBits(Me.ClientRectangle,
ImageLockMode.WriteOnly, _internalImage.PixelFormat)
Dim bmd2 As
BitmapData = gbm.LockBits(Me.ClientRectangle,
ImageLockMode.ReadOnly, gbm.PixelFormat)
'For maximum speed we use the Marshal class to access
the byte arrays directly
Dim x As
Integer
Dim y As
Integer
For y = 0 To _internalImage.Height
- 1
For x = 0
To _internalImage.Width - 1
Marshal.WriteByte(bmd1.Scan0, (bmd1.Stride * y) + (4 * x) +
3, Marshal.ReadByte(bmd2.Scan0, (bmd2.Stride * y) + (4 * x)))
Next
Next
CType(_internalImage, Bitmap).UnlockBits(bmd1)
gbm.UnlockBits(bmd2)
gbm.Dispose()
End If
e.Graphics.DrawImageUnscaled(_internalImage, 0, 0)
MyBase.OnPaint(e)
End Sub
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