viksoe.dk

Alpha Play


This article was submitted .


I rarely use Windows 2000 or Windows XP-only API functions since my apps usually have to run on ancient (?) systems such as Windows 98. Hopefully this will change soon enough when every home-user decides to retire their Win98 box (yearh right; as of 2005 this still seems a few years into the future), but it's not like the latest versions of Windows introduced a lot of GUI stuff to play with anyway.
Since I don't use these functions much, I thought I'd write down some notes on their use.

TransparentBlt

Some of the few new APIs released were bundled in the msimg32 dll and were probably part of Internet Explorer's effort to paint pretty web pages. One of them is TransparentBlt which should have been added to Windows a long time ago. It simply does a standard blit but uses a colour-key to mask out all colour-key coloured pixels.


The function operates just like the regular BitBlt except that it takes a color-key so I'm not going into details with it.

GradientFill


Somewhat more interesting is GradientFill that can be instructed to paint a smoothly interpolated colour-band. I did that by hand in the old days, but this is much better as it even looks good on low-end displays (64K colours and below). GradientFill takes some arcane arguments and shades both triangles and rectangles. With this little setup we can draw a neat gradient band:
   TRIVERTEX triv[2] = 
   {
      { rc.left, rc.top, 
        GetRValue(clrFirst) << 8, GetGValue(clrFirst) << 8, 
        GetBValue(clrFirst) << 8, 0xFF00 },
      { rc.right, rc.bottom, 
        GetRValue(clrSecond) << 8, GetGValue(clrSecond) << 8, 
        GetBValue(clrSecond) << 8, 0xFF00 }
   };
   GRADIENT_RECT grc = { 0, 1 };
   ::GradientFill(hDC, triv, 2, &grc, 1, 
      bVertical ? GRADIENT_FILL_RECT_V : GRADIENT_FILL_RECT_H);

AlphaBlend

AlphaBlend is where the fun starts. This Win2000 API allows you to blit your image with dynamic transparency. You can blit an image with 30% transparency to an existing device context and blend the images. If you just alpha-blit an image to your device context, you will probably be disappointed.
   CDC dcCompat;
   dcCompat.CreateCompatibleDC(hDC);
   HBITMAP hOldBitmap = dcCompat.SelectBitmap(hBmp);
   ::SetStretchBltMode(hDC, COLORONCOLOR);
   BLENDFUNCTION bf;
   bf.BlendOp = AC_SRC_OVER; 
   bf.BlendFlags = 0; 
   bf.SourceConstantAlpha = iAlpha;
   bf.AlphaFormat = 0; 
   ::AlphaBlend(hDC, rc.left, rc.top, cx, cy, dcCompat, 0, 0, cx, cy, bf);
   dcCompat.SelectBitmap(hOldBitmap);
...which produces this result with 30% transparency:

The function actually behaves correctly because what you really want in this case is to specify a colour-key (masked pixels). Unfortunately the function doesn't take one as argument - but instead you can use the function with pr-pixel alpha. In this scenario the source bitmap itself contains a transparency value for each pixel (alpha-channel) where fully transparent pixels have 0% alpha and the visible parts could be 100% for fully opaque or 30% to blend it with the background.

This is really cool but also where the problems begin. First of all, you will either need to predefine the alpha values in your image or generate the alpha values on the fly.
To use the pr-pixel alpha your image must be 32bpp (RGBA codec; A is the alpha byte). With a BMP file that already includes the alpha values you are all set to paint some neat translucent graphics.

To load the bitmap resource always remember:

   CBitmap bmp;
   bmp.LoadBitmap(IDB_LOGO);  // Wrong
   bmp = AtlLoadBitmapImage(IDB_LOGO, LR_CREATEDIBSECTION);
...because LoadBitmap is stuck in Windows 3.1 mode and is now defunct.

Now, all this assumes that you are able to create the alpha layer and save a 32bpp BMP file in your favourite bitmap editor. Unfortunately not even my trusted Paint Shop Pro was able to do that. The suggestion is then to use another file format such as PNG, but that would require inclusion of yucky image libraries.
We can also do the alpha values ourselves. One way could be to keep two bitmaps: the source bitmap and an additional 8-bit bitmap with the alpha-mask and then merge them. I'd prefer to just programmatically construct the alpha values, but that only works when you have a simple formula to generate them from (if pixels randomly vary from, say, 30% to 50% alpha it may quickly prove too difficult).

Anyway, creating a bitmap with alpha-values by hand does require a bit of typing, so here are the steps to follow:

First we need to know the dimensions of our bitmap.

   BITMAPINFO bmi = { 0 };
   bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
   BOOL bRes = ::GetDIBits(hDC, hBmp, 0, cy, NULL, &bmi, DIB_RGB_COLORS);
   if( !bRes ) return;
The NULL argument value in there forces Windows to just return the bitmap size information.

We now have the width and height, so let's ask Windows to give us access to the pixel data, this time in a format we can work with:

   DWORD dwTotSize = bmi.bmiHeader.biWidth * 
      bmi.bmiHeader.biHeight * sizeof(DWORD);
   LPDWORD pSrcBits = (LPDWORD) ::LocalAlloc(LPTR, dwTotSize);
   bmi.bmiHeader.biBitCount = 32;  // Ask for ARGB codec
   bmi.bmiHeader.biCompression = BI_RGB;
   bRes = ::GetDIBits(hDC, hBmp, 0, cy, pSrcBits, &bmi, DIB_RGB_COLORS);
   if( !bRes || bmi.bmiHeader.biBitCount != 32 ) {
      ::LocalFree(pSrcBits);
      return;
   }
It is important to specify which colour codec and depth you want the pixels in otherwise Windows just returns the format as it was read from disk.

We then ask Windows to generate a new bitmap with the same dimensions and 32bpp codec which we will populate with the same pixel data and the new alpha values:

   LPDWORD pDest = NULL;
   CBitmap bmpDest = ::CreateDIBSection(hDC, &bmi, DIB_RGB_COLORS, 
      (LPVOID*) &pDest, NULL, 0);
   if( bmpDest.IsNull() ) {
      ::LocalFree(pSrcBits);
      return;
   }
The DIB Section gives us access to each individual pixel in a memory buffer. We even asked for 32bpp so Windows kindly made room for the extra Alpha byte in the pixel data.

Now we can generate the alpha values. In this sample we just scan all the pixels and manipulate a colour-key. Since we just created a new empty bitmap we'll have to copy the source image pixels too as we go.

   DWORD dwKey = RGB(GetBValue(clrTrans), 
     GetGValue(clrTrans), 
     GetRValue(clrTrans));
   LPDWORD pSrc = pSrcBits;
   DWORD dwShowColor = 0xFF000000;
   for( int y = 0; y < abs(bmi.bmiHeader.biHeight); y++ ) {
      for( int x = 0; x < bmi.bmiHeader.biWidth; x++ ) {
         if( *pSrc != dwKey ) *pDest++ = *pSrc | dwShowColor; 
         else *pDest++ = 0x00000000;
         pSrc++;
      }
   }
Here we just set the alpha value to 255 (0xFF000000 in ARGB pixel-format) to allow pixels to become opaque.

Finally we can blit the new image to the device context. We'll need to tweak the arguments for AlphaBlend slightly because we are now using pr-pixel alpha.

   CDC dcCompat;
   dcCompat.CreateCompatibleDC(hDC);
   HBITMAP hOldBitmap = dcCompat.SelectBitmap(bmpDest);
   ::SetStretchBltMode(hDC, COLORONCOLOR);
   BLENDFUNCTION bf;
   bf.BlendOp = AC_SRC_OVER; 
   bf.BlendFlags = 0; 
   bf.AlphaFormat = AC_SRC_ALPHA;
   bf.SourceConstantAlpha = iAlpha;
   bRes = ::AlphaBlend(hDC, rc.left, rc.top, cx, cy, 
      dcCompat, 0, 0, cx, cy, bf);
   dcCompat.SelectBitmap(hOldBitmap);
   ::LocalFree(pSrcBits);
This does a pr-pixel alpha blit. The argument iAlpha defines how semitransparent the blit should be. 255 is opaque; at 30 the image is blended with the background and barely visible.

Working with alpha may not always be as fast as you could hope. You can detect if the system has acceleration on the graphics card by using GetDeviceCaps with the SHADEBLENDCAPS flag.

I bundled the code and polished it a bit in the download available below.
The sample code dynamically loads the alpha functions so it runs on Windows 95 and similar too, but won't paint stuff on these platforms unless you install a proper msimg32 library. To make it work you'll need to initialize the msimg32 dll once when your application starts.

::LoadLibrary("msimg32.dll")

Alternatively, you may consider using the GDI+ library. After all it does support many of these operations with trivial implementation. But since Microsoft doesn't seem to want to push for graphics card acceleration for the entire library, you will probably have to live with its sluggish and slow performance for some time to come.

Premultiply your bitmap

So the AlphaBlend API is cool, and I did mention that if you already had a bitmap with the proper alpha-channel you were good to go. Well, it's not always that simple. The AlphaBlend expects your bitmap data to have its RGB data premultiplied with the alpha channel. That basically means that the bitmap must be prepared for quick blitting to the screen and the process of doing so is very similar to the sample code I just showed above. There is source code in the sample download. Just remember that if your alpha bitmap doesn't look as you expected it, you may need to preprocess the data before you use the bitmap handle.

Source Code Dependencies

Microsoft Visual C++ 6.0
Microsoft WTL 7.5 Library

Download Files

DownloadSource Code (40 Kb)

To the top