Introduction
A few years back I wrote an article on creating a splash control derived from UserControl SplashScreen Control A reader asked me why his transparent images just showed a black background. This was because the control was not transparent itself, and the transparency in the image was revealing the unpainted background.
I said I would look at the requirement and at the time guessed it might involve BitBlt, a function in the Windows SDK to do block transfer of pixels. The technique in mind was to copy and paint the screen background before drawing the 'Transparent' image. It's a satisfactory solution until the 'host' image below changes or moves. Then it's exposed as a trick. I dropped this idea in favour of the solution presented here.
To achieve real transparency I override and consume OnPaintBackground
, and in the OnPaint
override I paint only the opaque pixels required.
Unsafe!
This solution will not appeal to the .NET purist because it goes 'off road' from .NET by using the Interop and Unsafe code, which is direct code access to memory via pointers. This is a way of life for C and C++ programmers. It also requires that you find a suitable image that will look good in all conditions. This is because the edges of your image will contrast with the background without dithering, shadowing or other techniques to smooth the result. But if you don't mind this, and know what you have in mind, this is how I've found you can do it, and I hope it works well for you.
What you'll need for your Implementation
You will need to provide an image resourse in your project that is masked by one color, representing the transparent or undrawn pixels. For an example see the demo project which uses a nicely composed sphere, in fact derived from the Armenian flag, which is for no other reason than I thought it looked nice and had nicely formed and smooth edges.
Understanding the Code
To find out about the basic principles behind this control please refer to the SplashScreen control I presented in SplashScreen Control
TransparentSplash is different because it draws the image pixel by pixel to be truly non-rectangular. Using GDI+ to do this is not viable, at this point in time at least, because it's too slow.
The image provided with the sample application is 256 x 256 pixels. To process this image requires 65536 Get operations and then the Set operations required to place the opaque pixels. We need to use native methods to do this very intense operation because they allow us to get a lock on some memory that contains the bitmap pixels and then use pointer methods to do the getting. This is considerably faster than managed GDI+ code can achieve.
Whilst investigating this operation I found a very nice class called FastBitmap at Visual C# Kicks which gives us all the Unsafe methods we need in a well tried and tested bit of code. Thank you to them, and I include and use the code in the sample project. Rememeber that the project containing the unsafe code needs its property page marked to allow unsafe code.
As you'll see in the following code extract from Onpaint
, FastBitmap
is used to lock the image in memory via its LockImage
method. We then loop through the pixels column by column, row by row testing each one against the mask color. If it matches the mask it is ignored, if it doesn't match it is painted by SetPixel a Windows SDK method provided by an interop import. Refer to the 'Interop definitions' region in the code for this and other imports used.
#region image painting
Color colorMatch = Color.FromArgb(TransparencyColor.R, TransparencyColor.G, TransparencyColor.B);
IntPtr hdc = g.GetHdc();
Point point = new Point();
Color pixelColor = new Color();
Bitmap bmp = (Bitmap)BackgroundImage;
BitmapProcessing.FastBitmap fastBitmap = new BitmapProcessing.FastBitmap(bmp);
fastBitmap.LockImage();
for (point.Y = 0; point.Y < BackgroundImage.Height; point.Y++)
{
for (point.X = 0; point.X < BackgroundImage.Width; point.X++)
{
if ((pixelColor = fastBitmap.GetPixel(point.X, point.Y)) != colorMatch)
{
SetPixel(hdc, point.X, point.Y, ColorToRGB(pixelColor));
}
}
}
fastBitmap.UnlockImage();
g.ReleaseHdc();
#endregion
That's the foreground painting done, but how did we paint the background? Here is more proof, if you need it, that less is more:
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
This summons up enough GDI Voodoo to achieve the desired result. I sympathise with anybody not finding this immediately intuitive. You just need to remember that this control is a child of the desktop window and that if it does not paint its own background, and doesn't ask the base class to paint the background - then the background will be the background. With the demo app you can bring up the the control and test moving the window below. You should see that the background is refreshed without delay.
Share your Tips
If you find this technique usefull and discover any other usefull tips or techniques in your implementation please consider sharing them here - it will be most appreciated.
History
- Version 1.0.0 Released: 5th July 2010