Let's face it...we're all accustomed to them by now; they're pretty much everywhere. I'm talking about irregularly-shaped windows, of course. For a few years it seemed that every new application that came out had to take advantage of the ability to create windows that weren't the same ol', standard, boring rectangular windows that were so commonplace.
I believe that the first application I ever saw to leverage this capability of creating non-rectangular windows was a Norton Anti-Virus - it had a 'shield-shaped' dialog...or maybe it was McAfee. I was blown away at first by it. How on earth did they do that!? Then, predictably, almost every 'cool' tool and application had to have it - especially those in the multimedia arena. Well, there can be too much of a good thing, but I feel that they have their place.
This capability is made available by Windows via a GDI object known as a Region. Regions come in a variety of geometric shapes: rectangular, elliptical, polygonal. Once a region has been established, it can be assigned a window handle (hWnd) such as a Form, Button, etc via SetWindowRgn(). Regions, when applied to an hWnd determine which parts of the window are to be visible and which parts are transparent.
Not to be outdone, this capability is not lost on the .NET framework. In fact, it's alarmingly easy to create windows with non-rectangular borders. By using the TransparencyKey property of a Form you're pretty much there. However, this isn't without it's costs. When you create a Form that relies on a transparency key for this effect, the system recreates the region at runtime everytime the form loads. This can impact load time significantly. It would be much better if we could know before hand exactly what the region was and apply it without having to regenerate it repetitively.
And, as luck would have it...there is a way! We simply need to dip down into the Win32 using P/Invoke. Once a region is created (via any of the prescribed methods such as CreateRectRgn, CreateEllipticRgn, CombineRgn, etc), you can call the GetRegionData() GDI function. This function will return to the buffer that represents the region. The inverse of GetRegionData is the ExtCreateRegion function, which simply takes the buffer and creates a GDI Region object. This region object can then be applied to an hWnd via SetWindowRgn and we're off the races.
Back in my VB days about 5-6 years ago I had created a utility which I dubbed the 'Region Generator'. This utility would accept as parameters a bitmap image and a color to be treated as transparent. It would cycle through the pixels of the image and generate a complex Region via CombineRgn and the XOR flag which would effectively mark all pixels of the given color as transparent. Then it would output a binary file containing the buffer from GetRegionData. I would use this file by embedding it within an application's resource file to be read out and applied at runtime. It was pretty slick and pretty easy to use.
Unfortunately, I've since misplaced the tool (I know I have it somewhere), but rather than digging up the fossil, I thought I'd recreate the dinosaur. It only took a couple of hours and some excursions back into the long under-utilized recesses of my mind to remember how to do it. I have plans to add some other features to it (such as manually entered RGB/Web color values, a color tolerance so you don't have to get the color exactly right which might be especially useful for .jpg images, etc).
Once you have a region saved, it's a pretty simple task of taking the region data and loading it up to assign it to a window. This example demonstrates how to load the data from a file and applying it (reading it from a resource file is just as easy):
[DllImport("gdi32.dll")]
public static extern IntPtr ExtCreateRegion(int lpXform, int nCount, byte[] lpRgnData);
[DllImport("user32.dll")]
internal static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
// read the data from disk into a byte array
byte[] regionData = null;
using ( FileStream fs = new FileStream(@"C:\MyRegion.rgn", FileMode.Open, FileAccess.Read, FileShare.Read) ) {
BinaryReader reader = new BinaryReader(fs);
regionData = reader.ReadBytes((int)fs.Length);
}
// create the region from the byte array and assign it to the window
HandleRef hWnd = new HandleRef(this, this.Handle);
IntPtr hRgn = ExtCreateRegion(0, regionData.Length, regionData);
SetWindowRgn(hWnd.Handle, hRgn, true);
Of course in true .NET form, we can use the Region property of the Form rather than having to use SetWindowRgn thus:
this.Region = Region.FromHrgn(ExtCreateRegion(0, regionData.Length, regionData));
Feel free to download and use this tool and let me know what you think.