Image shaped forms

by Andy Walker

No doubt you have seen applications that contain dialogs or main windows that have unusual shapes and even have transparent areas that don't appear to be part of the form. This article will take you step-by-step through the process of creating a component that can be placed on a form and can be used to shape and define transparent areas of any visual component. That includes forms, buttons, list boxes, and, in fact, anything that has a window handle.

The component created in this article assumes that you are using C++Builder 4. The techniques, however, are generic and can be coded using any development tool. (Note: The code presented in this article uses the ScanLine property of TBitmap which first appeared in C++Builder 3).

Creating the component

To begin, choose Component|New Component from the C++Builder main menu. Select TComponent for the ancestor type and enter TTransparentWindow in the Class Name field. Leave the other settings as they are and click the OK button. Save the file as TRANSPARENTWINDOW.CPP.

Next you will need to add some properties and methods. In the header for your new component, add the following definitions to the private section:

AnsiString asTransparentImage;
HWND lWindowHandle;
HRGN rgnOpaque;
Graphics::TBitmap *bmTransparentImage;
void __fastcall SetTransparentImage 
  (System::AnsiString value);
bool __fastcall SetTransparentRegion 
  (Graphics::TBitmap *bmBitmap);

The first variable is the data member for a property called TransparentImage. Iíll explain this property later in this section. The next three variables are used internally by the component. The lWindowHandle variable will be used to store the window handle for the selected component. The rgnOpaque variable will hold the region handle for the opaque region of the window. The bmTansparentRegion variable will hold the bitmap that defines the region. The SetTransparentImage() function is the write method for the TransparentImage property. This function will create the transparency image and load it into memory. The SetTransparentBitmap() function creates the opaque region of the form and removes the transparent parts.

The public section of the component already defines a constructor, but you need to add definitions for a destructor and two public methods. Here are those declarations:

__fastcall ~TTransparentWindow();
bool __fastcall Activate();
void __fastcall SetWindowHandle 
  (HWND Handle) {lWindowHandle=Handle;}

The Activate() method is used to activate the component, and the SetWindowHandle() method is used to tell the component which windows handle to work with. Note that the SetWindowHandle() method is an inline function.

To finish off the header you need to add a property to the __published section:

__property AnsiString TransparentImage = 
  { read = asTransparentImage, 
    write = SetTransparentImage };

This property is used to specify the name of the required transparency image.

An overview of Windows regions

Before I get to the code, I need to take a moment to explain how Windows regions work. Any object in Windows that can be filled, painted, inverted, framed, or hit tested (testing the location of the cursor) is a region. The region can be any shape, including rectangles, ellipses, and polygons. Regions can be combined or compared with any other region. By combining multiple sources, it is possible to build up a region that is irregular in shape and contains gaps that do not form part of the original area. Regions can be combined using the API call CombineRgn(). CombineRgn() uses the following Boolean operators:

RGN_AND
RGN_OR
RGN_XOR
RGN_COPY
RGN_DIFF

Once a region has been created, it can be selected into a device context. At that point the region can be filled, inverted, painted, framed, or it can be used to replace an existing region. In this article, CombineRgn() is used to replace the window region for a given window handle. This is done by calling the Windows API SetWindowRgn() function.

I canít explain every possible use of regions in this article, but you can find out more about regions by starting with the ďRegionsĒ topic in the Win32 SDK Reference help file.

Setting the transparent region

Now that the header is finished, you can move on to the actual coding of the component. The most important method in the entire project is SetTransparentRegion(). This method creates an irregular shaped region from a given transparency bitmap. It creates a region from the bitmap by processing the transparency image line by line and creating the region from the non-black parts of the bitmap. If, for example, the bitmap contains a black background with a non-black circle, the routine will create a circular window region (see Figure A). It achieves this by creating regions for the non-black area on each line of the bitmap and then combining them all together to create one irregular region. Depending on the complexity of the image, the resulting region might consist of single opaque pixels. Of course, all of this takes processing time so the simpler the image the faster the processing will go.

Figure A

This bitmap will create a circular window.

The SetTransparentRegion() routine can be split into five distinct functional steps. I will discuss each of these steps in the following sections.

Step 1: Create a region

The first step is to create a region that is the same size as the original bitmap:

HRGN rgnOpaque;
rgnOpaque = CreateRectRgn(0, 0, 
  bmTransparentBitmap->Width, 
  bmTransparentBitmap->Height);

This code creates an opaque region that starts at co-ordinates (0,0) and extends to the width and height of the bitmap.

Step 2: Process each line of the bitmap

Once you have a region to work with, you can begin the process of scanning each line of the bitmap and manipulating the region to create transparent parts. Hereís the code for this step:

for (Y=0;
  Y<bmTransparentBitmap->Height;
  Y++)
{
  Line = (Byte*)
    bmTransparentBitmap->ScanLine[Y];
  // step 3 and 4 code here
}

You now have a variable called Line that will point to the next line of the bitmap each time through the loop.

Step 3: Process non-transparent region

Now you can use the Line variable to test the color of each pixel on the current scan line. Remember that black is the transparent color so I loop through the pixels until I either reach a black pixel (represented by a 0) or the final pixel on that line:

while (X < bmTransparentBitmap->Width 
  && Line[X] != 0)
{
  //loop until a black pixel is found
  X++;
}

Step 4: Process transparent region

At this point you have either reached the end of the scan line, or reached a black pixel. The next step is to make a note of the position of the first black pixel on the line and continue processing the rest of the line:

XTransparent=X;
while (X<bmTransparentBitmap->Width 
    && Line[X] == 0)
{
  //loop until a non-black pixel is found
  X++;
}

When this loop finishes, you will have the transparent region start position in the XTransparent variable, and the end position in the variable X. The end position could be the end of the line, or if another non-black pixel was encountered, could be partway through the line.

Step 5: Remove the transparent part

The final step is to check whether a transparent section exists and, if so, remove it from the original opaque region. (The original region was created in Step 1.) This is achieved by creating a temporary region that is one pixel high and XTransparent to X pixels wide. This region could potentially be zero pixels, one pixel, a whole line of pixels the width of the bitmap, or any number in between. Here is the code that creates the region:

rgnTransparent = 
  CreateRectRgn(XTransparent, Y, X, Y+1);

Once you have a new region of the correct size, you can remove it from the original. This is done by combining the original region and the newly created region using the CombineRgn() function. CombineRgn() takes four parameters. The first parameter is the handle of the region that will contain the result. The second and third parameters are the handles to the two regions to be combined. The last parameter is used to specify the region combine mode. In this case you will perform an exclusive OR region combine by passing the RGN_XOR constant for the final parameter:

CombineRgn(rgnOpaque, 
  rgnOpaque, rgnTransparent, RGN_XOR);

The result is the original opaque region with the transparent region cut out.

Now imagine that you have a bitmap whose first line consists of the pixels shown in Figure B. The code detailed in Steps 3, 4, and 5 would process the first set of white pixels (as non-transparent), then process the black pixels (as transparent), create a temporary region for the transparent area, and then XOR that region with the original.

Figure B

An example scan line from a transparent image bitmap.

In order to get as far as the final set of white pixels, it is necessary to enclose the entire process within another loop that ensures each scan line is processed completely:

for(X=0;X<bmTransparentBitmap->Width;X++)
{
  // step 3 code
  // step 4 code
  // step 5 code
}

The source file for the example component is contained in Listing A. Examine the SetTransparentRegion() function in this listing to see the entire set of steps in context.

Conclusion

After entering the code in Listing A (or downloading it if you prefer) you can compile and install the component. After the component is installed, place it on a form and set the TransparentImage property to a valid bitmap. In the formís OnCreate event handler, call the SetWindowHandle() method, passing the Handle property of the form. Finally, call the Activate() method to activate the component. The result is a form that is shaped like the non-black pixels of the transparency bitmap.

The CreateRectRgn() and CombineRgn() functions make it possible to create windows that take on irregular and unusual shapes. The techniques covered here can be applied to any component and could be used to create shaped buttons, list boxes, toolbars, and so on.

All of the code detailed in this article can be downloaded from the Bridges Publishing Web site, or from my site at http://www.iola.co.uk. Just follow the link for FreeStuff and download the zip file.

Listing A: TTransparentWindow.cpp

#include <vcl.h>
#pragma hdrstop

#include "TransparentImage.h"
#pragma package(smart_init)

static inline void
ValidCtrCheck(TTransparentWindow *)
{
  new TTransparentWindow(NULL);
}

__fastcall TTransparentWindow::TTransparentWindow(
  TComponent* Owner) : TComponent(Owner)
{
  //<AW>Make Sure Pointers Are NULL
  bmTransparentImage = NULL;
}

bool __fastcall
TTransparentWindow::Activate()
{
  bool bSuccess;

  /*<AW>If the image has been set and the
  Windows handle has been specified then
  we can continue and return success*/
  if (bmTransparentImage &&
  lWindowHandle!=0)
  {
    //<AW>Create an opaque region from a bitmap

    bSuccess=SetTransparentRegion
      (bmTransparentImage);
    if (bSuccess)
    //<AW>Set region for the supplied Windows handle
      SetWindowRgn (lWindowHandle, rgnOpaque, true);
  }
  else
  {
  //<AW>If something is missing then report
  // what and return failure
  if (bmTransparentImage==NULL)
    Application->MessageBox ("No Bitmap Selected",
      "Activate Cancelled", MB_OK);
  else if (lWindowHandle == 0)
    Application->MessageBox (
      "No Window Handle Selected",
      "Activate Cancelled", MB_OK);
  bSuccess=false;
  }
  return bSuccess;
}

void __fastcall
TTransparentWindow::SetTransparentImage(
  System::AnsiString value)
{
  //<AW>Set The Window Image
  try
  {
    //<AW>If The Bitmap Exists Then Delete It
    if (bmTransparentImage)
    {
      delete bmTransparentImage;
      bmTransparentImage = NULL;
    }
    //<AW>Load The Image
    bmTransparentImage = new Graphics::TBitmap();
    asTransparentImage = value;
    bmTransparentImage->
      LoadFromFile(asTransparentImage);
    bmTransparentImage->PixelFormat = pf8bit;
  }
  catch (Exception &e)
  {
    //<AW>Handle Any Errors And Delete The Pointer
    Application->MessageBox (e.Message.c_str(),
      "Couldn't Load Transparent Image", MB_OK);
    if (bmTransparentImage)
    {
      delete bmTransparentImage;
      bmTransparentImage = NULL;
      asTransparentImage = "";
    }
  }
}

__fastcall TTransparentWindow::~TTransparentWindow()
{
  //<AW>Clean Up And Delete The Bitmap
  if (bmTransparentImage)
    delete bmTransparentImage;
}

bool __fastcall
TTransparentWindow::SetTransparentRegion(
  Graphics::TBitmap *bmTransparentBitmap)
{
  //<AW>Set The Active Region Of The Window
  long X,Y,XTransparent;
  Byte *Line;
  HRGN rgnTransparent;
  bool bSuccess;
  try
  {
    bSuccess = true;
    //<AW>Loop Through Each ScanLine and Each Pixel
    rgnOpaque = CreateRectRgn (0, 0,
     bmTransparentBitmap->Width,
     bmTransparentBitmap->Height);
    for (Y=0;Y<bmTransparentBitmap->Height;Y++)
    {
      Line=(Byte*)bmTransparentBitmap->ScanLine[Y];
      for(X=0;X<bmTransparentBitmap->Width;X++)
      {
        while (X<bmTransparentBitmap->Width
          && Line[X]!=0)
        {
          //<AW>Process The Non-Transparent Region
          X++;
         }
        //<AW>Transparent Start
        XTransparent=X;

        while (X<bmTransparentBitmap->Width
          && Line[X]==0)
        {
          //<AW>Process The Transparent Region
          X++;
        }

        //<AW>If There Was A Transparent Region
        if (XTransparent<X)
        {
          //<AW>Create The Region
          //(Remove The Transparent Bit)
          rgnTransparent = CreateRectRgn
            (XTransparent, Y, X, Y+1);
          //<AW>If It's A Null Region, Return False
          if (CombineRgn (rgnOpaque,
            rgnOpaque, rgnTransparent,
            RGN_XOR) == NULLREGION)
          bSuccess=false;
         DeleteObject(rgnTransparent);
        }
      }
    }
  }
  catch (Exception &e)
  {
    //<AW>Catch The Errors
    Application->MessageBox(e.Message.c_str(),
      "Error Creating Transparency", MB_OK);
  }
  return bSuccess;
}

namespace Transparentimage
{
  void __fastcall PACKAGE Register()
  {
    TComponentClass classes[1] =
      {__classid(TTransparentWindow)};
    RegisterComponents("Sample", classes, 0);
  }
}