Using Unsafe Code in C# to converting color-keyed bitmaps to custom regions.
.
Environment: Microsoft .NET Framework 1.0, Microsoft Visual Studio .NET, Windows Forms
Introduction
There are two ways in the .NET Framework Windows Forms classes to make a nonrectangular window: Use the Form.TransparencyKey
(also exposed in the Win32 API through the (Get|Set)LayeredWindowAttributes
functions) property or use regions. The first one is by far easier to work with, but it is not supported on any of the Windows platforms earlier than Win2K. It is also only supported on Forms — if you want nonrectangular shapes on controls not derived from Form, you will have to resort to regions anyway.
What Is a Region?
A region is a combination of geometrically defined shapes, such as ellipses, rectangles, or polygons. It is mainly used by the GDI to clip drawing operations against windows. No painting operations will be allowed on a window outside its clipping region. Hit-testing (mouse clicks, and so forth) is also performed against the window’s region. For most windows, the clipping region is rectangular, but it does not have to be. In Windows Forms, the region for an arbitrary control can be set through the Region
property. Effectively setting the Region
property defines the shape of the window.
Creating complex window shapes by using regions is a tedious operation. You have to break the shape down into geometrical elements, and then add each separate element to the region. Creating anything but simple shapes this way is far from intuitive.
What Is a Transparency Key?
A transparency key is another approach to defining the transparent portions of a window. A transparency key is a color you assign to a form to represent the areas on it that should be transparent. If you assign Color.Yellow
as the TransparencyKey
to a form, and then draw a big yellow circle, you will end up with a big hole in the form through which you can see the windows below. Any mouse activity in that hole will be transferred to the underlying windows as well.
As you probably understand, this is a far more natural and intuitive way to create custom-shaped windows. You can just create a bitmap in any paint package; then fill the areas you want transparent with some color you’re not using elsewhere. Then you can just assign the bitmap to the form’s BackgroundImage
property, and the color you chose to the TransparencyKey
property. Voila! — your own custom-shaped window.
Unfortunately, as I mentioned above, this functionality is not supported on Windows 9X, Me, or NT 4.0. This of course severely limits the usability of this approach, since these platforms are by far the most prevalent out there.
Having Our Cake and Eating It, Too
So, how can we design our form using the second way and still have it be available on all .NET-supported platforms? We simply have to convert the bitmap containing the color key into a region. Unfortunately, because there are no classes in the framework that do this for us, we have to do it ourselves.
The Algorithm
The algorithm for doing this conversion is pretty well known. Basically, we scan the bitmap line by line and look for consecutive non-transparent stretches. Each stretch we find is added to the region as a rectangle with height 1. When we have scanned the whole bitmap, the resulting region will represent the bitmap minus the transparent areas. If we now assign the bitmap to the BackgroundImage
property of the form, and the region to the Region
property, we will get the same result as we would have by setting the TransparencyKey
property.
Unsafe Code
This algorithm obviously involves going through every individual pixel of the provided bitmap. Unfortunately, the GetPixel
method of the Bitmap
class is awfully slow. The other way to gain access to the bitmap’s pixels is through the LockBits
method. This method returns a BitmapData
object containing a pointer to the top left pixel of the bitmap.
Yes, I did say pointer. While you usually program using safe references instead of raw pointers, C# lets you use C-style pointers in special sections of code marked as unsafe
. The fact that these sections have to be explicitly labeled, and that you have to add the /unsafe
switch to the compiler to make them compile, goes a long way in showing that this is not something MS intends you to use on a regular basis. However, there are circumstances under which their use can be justified. I believe this is one of those circumstances.
The Code
The conversion is performed by a single static method, Convert
, which returns a Region
object. (Despite being a self-professed OO freak, I was unable to find any justification for making this an instance method.) This method takes three arguments, as follows:
public unsafe static Region Convert( Bitmap bitmap, Color transparencyKey, TransparencyMode mode )
The first two arguments should be fairly obvious, but the third one deserves some further explanation. TransparencyMode
is an enum defined like this:
public enum TransparencyMode { ColorKeyTransparent, ColorKeyOpaque }
Basically, if you use ColorKeyTransparent
, the code will exclude all the pixels that have the same color as the color key from the resulting region, making areas with that color transparent. With ColorKeyOpaque
, the opposite will happen; all areas NOT containing that color will be seen as transparent. This can be pretty useful in some circumstances.
We acquire the pointer to the bitmap surface with the following code:
BitmapData bitmapData = bitmap.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); uint* pixelPtr = (uint*)bitmapData.Scan0.ToPointer();
The pixel format we requested, Format32bppArgb
, will map nicely onto an unsigned int. We convert the color key into a uint
as well, so we can compare them efficiently:
uint key = (uint)((transparencyKey.A << 24) | (transparencyKey.R << 16) | (transparencyKey.G << 8) | (transparencyKey.B << 0));
We cannot add rectangles to a region directly, so we take an indirect approach and create a GraphicsPath
object instead. Now the main body of the loop looks like this:
for ( int y = 0; y < yMax; y++ ) { //store the pointer so we can offset the stride directly //from it later to get to the next line byte* basePos = (byte*)pixelPtr; for ( int x = 0; x < xMax; x++, pixelPtr++ ) { //is this transparent? if yes, just go on with the loop if ( modeFlag ^ ( *pixelPtr == key ) ) continue; //store where the scan starts int x0 = x; //not transparent - scan until we find the next //transparent byte while( x < xMax && !(modeFlag ^ (*pixelPtr == key))) { ++x; pixelPtr++; } //add the rectangle we have found to the path path.AddRectangle( new Rectangle( x0, y, x-x0, 1 ) ); } //jump to the next line pixelPtr = (uint*)(basePos + bitmapData.Stride); }
Basically it scans along each line until it finds a non-transparent pixel, using the continue statement to short circuit the inner loop. When one is found, it uses another loop to scan ahead to the next transparent pixel or the right end of the bitmap, whichever comes first. This scan is then added as a rectangle of height 1 to the GraphicsPath
object. As you can see, the BitmapData
object has a property Stride
, which gives the number of bytes per line.
This process is repeated for each line in the bitmap. When we have gone through them all, we use the GraphicsPath
object to create the region:
//now create the region from all the rectangles
Region region = new Region( path );
From here on, it’s just a matter of some cleanup before we can return our freshly created Region
object.
Sample
You can download the source for the whole BitmapRegion class, along with a sample project using it, through the link below.