May 1998

Capturing a form to disk

by Kent Reisdorph

Did you ever want to capture a portion of your application as a bitmap file on disk? If so, you'll be glad to learn that doing so isn't difficult. Getting it right, though, takes a little work. In this article, we'll show you how to capture a VCL form to a Windows bitmap (BMP) file on disk. We'll also exlain how to get the palette for your form so you are sure the image will be saved properly.

The big picture

When you come right down to it, the whole Windows desktop display is one big bitmap. Sure, it looks like you have lots of windows onscreen, but it's just your mind (and Microsoft) playing tricks on you. You perceive a two-dimensional (or even three-dimensional) plane with multiple windows floating around, but it's no more than visual trickery. Capturing a form to disk should just be a matter of copying bitmap bits from the appropriate part of the desktop display to memory, then to a disk file. Sound complicated? It would be if VCL didn't do most of the work for you. But thanks to VCL, most of the operation is fairly easy. The required steps are as follows:
bullet Create a TCanvas object to represent the desktop device context.
bullet Create a TBitmap object to hold the bitmap in memory.
bullet Copy the bits representing the form onscreen from the desktop canvas to the bitmap in memory.
bullet Save the bitmap in memory to disk
Most of this process is straightforward; we'll show you exactly how to perform each step. However, one item doesn't appear on our list: grabbing the form's palette. We'll leave that step for a little later.

A canvas for the desktop

VCL doesn't provide a built-in canvas for drawing directly on the desktop. After all, that's Windows' job--Windows provides a way of getting to the desktop device context. You can loosely think of a Windows device context (commonly know as a DC) as a drawing surface. Working with DCs at the API level can be messy. If you don't jump through all the hoops--and in the right order--your application will leak GDI resources. Fortunately, VCL provides the TCanvas class to make working with DCs easier. The trick, then, is to create a TCanvas object that allows access to the desktop DC. Hold that thought for a moment, and we'll get back to it in just a bit.

As you know, some VCL components come with a Canvas property and some don't. A PaintBox component has a Canvas property, naturally, so you can draw on the paint box. A Memo component, on the other hand, has no Canvas property. Windows is responsible for drawing the contents of a Memo component, and most mere mortals shouldn't dabble in that area (nor would you want to, in most cases). The good news is that you can create a Canvas for any window, provided you know the window handle.

For example, suppose you were to tempt fate and draw on a Memo component. You could create a TCanvas object for that purpose with this code:

TCanvas* canvas = new TCanvas;
canvas->Handle = GetDC(Memo->Handle);
// do some stuff
delete canvas;
When you call delete on the TCanvas object, VCL performs all the nasty cleanup tasks required to properly dispose of a device context. Clean and easy (that's what C++Builder is all about). But the desktop isn't a window, per se. So how do you create a device context for the desktop? The Windows API provides a way. When you call GetDC() with a window handle of 0, Windows happily creates a device context for the desktop. Combining that with the previous code snippet, you can create a canvas for the desktop as follows:
TCanvas* canvas = new TCanvas;
canvas->Handle = GetDC(0);
Now you can draw on the desktop to your heart's content--but you shouldn't, of course. You should usually consider a desktop canvas a read-only object. That's how we'll use the desktop canvas in this case.

How's your memory (bitmap)?

In order to create a working bitmap in memory at the API level, you need to create a memory device context and a bitmap. You then select the bitmap into the memory device context. Here again, VCL does a great deal of behind-the-scenes work for you. The TBitmap class contains both a bitmap and a memory DC. Creating a TBitmap object is simple:
Graphics::TBitmap* 
    bitmap = new Graphics::TBitmap;
As you can see, you must specify the Graphics namespace so the compiler knows you're creating a VCL TBitmap object (as opposed to the "other" TBitmap, which is a typedef for a Windows HBITMAP handle). That's not the whole story, though, because you also need to set the height and width of the bitmap prior to writing data to it. Since you'll be copying the form's image to the bitmap, you can use the height and width of the form to size the bitmap:
bitmap->Width = Width;
bitmap->Height = Height;
You're now ready to get on with the business of copying the form's bitmap bits from the desktop canvas to the bitmap in memory.

Copy and save to disk

Copying the bits is a simple process. You need only three lines to accomplish this part of the operation:
TRect src = BoundsRect;
TRect dest = Rect(0, 0, Width, Height);
bitmap->Canvas->CopyRect(dest, dtCanvas, src);
The first line sets the rectangle called src to the size and position of the form's outer dimensions (the BoundsRect property provides these dimensions). You'll use this rectangle as the source of your copy operation. The second line sets the destination rectangle. In this case, the destination rectangle is the entire size of the memory bitmap.

The last line in the code snippet copies the bitmap bits from the source rectangle to the destination rectangle. The CopyRect function of TCanvas does all the work. Behind the scenes, this step translates to a call to the Windows API function BitBlt().

All that remains is to save the bitmap in memory to a disk file. Don't blink or you'll miss it:

bitmap->SaveToFile("form.bmp");
Yes, that's really all there is to it. Now you have a copy of your form captured on disk.

But wait, there's more!

If your form contains only standard Windows controls, then you can stop reading--you're finished. If, however, your form contains bitmaps composed of more than 16 colors, you'll need to do a little more work to ensure that the form is saved with the correct colors--you must delve into the mysterious world of palettes. Let's say you have a form with an Image component, and that the Image component contains a 256-color bitmap (for example, the HANDSHAKE.BMP file that comes with C++Builder). You can save the form to disk using the code we presented earlier. When you do, however, it will come out looking like the image shown in Figure A.

Figure A: This saved form has a bad palette.
[ Figure A ]

Even in black and white, you can see that something is drastically wrong with this picture. The reason is that the palette information for the 256-color bitmap wasn't saved when the file was saved. To make the bitmap display correctly, you have to obtain the form's palette and assign it to the memory bitmap's Palette property before saving the file.

In this case, VCL doesn't provide an easy way to obtain a form's palette. You'll have to get your hands dirty and use the API. Here's the code:

int nColors =
  GetDeviceCaps(Canvas->Handle, SIZEPALETTE);
LOGPALETTE* logPal = (LOGPALETTE*)new Byte[
  sizeof(LOGPALETTE) + (nColors - 1) *
  sizeof(PALETTEENTRY)];
logPal->palVersion  = 0x300;
logPal->palNumEntries = (Word)nColors;
GetSystemPaletteEntries(Canvas->Handle,
  0, nColors, logPal->palPalEntry);
bitmap->Palette = CreatePalette(logPal);
delete[] logPal;
Yikes! Yes, folks, this is the way people used to write Windows programs. In summary, the code snippet gets the number of colors for the current display driver, creates a LOGPALETTE structure using that information, and then gets the palette for the form by calling GetSystemPaletteEntries(). This Windows API function requires a handle to a device context, so you pass the Handle property for the form's canvas. After that, you create a new palette with the CreatePalette() function and assign the result to the memory bitmap's Palette property. Now the color information will be saved when the bitmap is saved to disk.

Note that if you have a single 256-color object on a form, you can take a shortcut and assign that component's Palette to the Palette of the bitmap. For example:

bitmap->Palette = Image1->Picture->Palette;
This is simplest method of preserving palette information, but it isn't always the most reliable. The method we've outlined ensures that the entire form's palette will be preserved rather than relying on the palette of a single component. Figure B shows the result when the palette information is saved properly. As you can see, this image shows a dramatic improvement.

Figure B: When we save the form with the proper palette, it looks much better.
[ Figure B ]

A full example

You can save the image of any form to disk using the method we've outlined in this article. Note, however, that the form must be fully visible onscreen for this technique to work. Listing A contains the source unit of a program that saves its main form to disk. We didn't list the header to save space, but the pertinent details are provided. As usual, you can obtain the entire project from www.cobb.com/cpb, as part of the file may98.zip.

Listing A: FORMCAPU.CPP

//---------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop

#include "FormCapU.h"
//---------------------------------------------
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
  : TForm(Owner)
{
}
//---------------------------------------------
void __fastcall TForm1::Button1Click(
                TObject *Sender)
{
  // Create TCanvas object for desktop DC.
  TCanvas* dtCanvas = new TCanvas;
  dtCanvas->Handle = GetDC(0);

  // Create new TBitmap object and set its
  // size to the size of the form.
  Graphics::TBitmap*
    bitmap = new Graphics::TBitmap;
  bitmap->Width = Width;
  bitmap->Height = Height;

  // Create palette from form's Canvas; assign
  // that palette to bitmap's Palette property.
  int nColors =
    GetDeviceCaps(Canvas->Handle, SIZEPALETTE);
  LOGPALETTE* logPal = (LOGPALETTE*)new Byte[
     sizeof(LOGPALETTE) + (nColors - 1) *
     sizeof(PALETTEENTRY)];
  logPal->palVersion  = 0x300;
  logPal->palNumEntries = (Word)nColors;
  GetSystemPaletteEntries(Canvas->Handle,
    0, nColors, logPal->palPalEntry);
  bitmap->Palette = CreatePalette(logPal);
  delete[] logPal;

  // Copy section of screen from 
  // desktop canvas to the bitmap.
  TRect src = BoundsRect;
  TRect dest = Rect(0, 0, Width, Height);
  bitmap->Canvas->CopyRect(dest, dtCanvas, src);

  // Save it to disk.
  bitmap->SaveToFile("form.bmp");

  // Clean up and go home.
  delete bitmap;
  delete dtCanvas;
}