Using the GDI to Take DirectDraw Screenshots

by Michael Fötsch
May 31, 2000 (Last Update: June 25, 2000)

This article was originally published at Gary Simmons’ Mr GameMaker.

Sample  screenshot created with the code from this tutorial and converted to GIF  format afterwards

Table of Contents

  1. Introduction
  2. The Method
  3. Step 1: Creating an Offscreen Bitmap
  4. Step 2: Converting from DDB to DIB
  5. Step 3: Saving the File
  6. Cleaning up and chilling out
  7. A few words from the author
  8. Code Listing

Introduction

Every game should have a screenshot function! When people go to Disneyland, they bring their cameras. When they play your game, they must rely on you to provide a camera. Screenshots let you show off the beauty of your game to others, e.g. on your game’s homepage. Similarly, magazine reviewers will take screenshots for their articles, and users might wish to use them (the shots, not the reviewers!) as desktop wallpapers. You wouldn’t want to prevent either by being lazy, would you.

Laziness is said to be one of the programmer’s virtues, but it will not deter us from writing a screenshot function! After all, there are Windows and its Graphics Device Interface, the GDI, that we can delegate to. In this tutorial we’ll write a screenshot function that can be used to capture DirectDraw surfaces. Our goal is to have a single function for all resolutions and pixel formats that is as easy to call as this:

...
case WM_KEYDOWN:
if ((int)wParam) == VK_F11) Screenshot("shot.bmp", lpDDSBack);
...

Note: While we’ll be working with DirectDraw surfaces, the presented method can be used to capture any GDI device context.

The Method

The bitmap on a DirectDraw surface is stored in a device-dependent format. You could not copy it to a surface of a different pixel format with a simple memcpy. IDirectDrawSurface::Blt, which does not do more than that either, will fail if surface formats do not match.
BMP files, on the other hand, store device-independent bitmaps (DIBs). The format stays the same no matter which device the bitmap is intended to be displayed on, be it a printer, a 24-bit display, a 16-bit display, etc. In addition to the bitmap bits, the file contains an information header that describes the format of the data.
Our task when creating a screenshot is to convert the DDB on the surface to a DIB. The GDI function GetDIBits will do that for us. Before we can call it, however, we need to obtain a GDI-compatible device context from the surface and obtain the handle of a DDB. After the conversion, we simply dump the converted data into a file. The beauty of GetDIBits is that it can convert to any format. We could, for example, convert a 32-bit surface to a 16-bit BMP. GetDIBits will also generate a palette if needed.

Step 1: Creating an Offscreen Bitmap

We need a GDI bitmap that GetDIBits can work with. That bitmap should be compatible with the DirectDraw surface, as we will copy the surface’s contents to it. A compatible bitmap is created with the GDI function CreateCompatibleBitmap. That function does not expect an LPDIRECTDRAWSURFACE but the handle of a GDI device context, which, in turn, we will create by calling the surface’s GetDC method. Following is the code to do all this:

HDC SurfDC;
lpDDS->GetDC(&SurfDC);
HBITMAP OffscrBmp = CreateCompatibleBitmap(SurfDC, Width, Height);

Note: We can query the surface’s width and height with its GetSurfaceDesc method. See the code listing at the end of this article.

We have created a bitmap that is compatible with the surface, but it does not yet contain any picture. We are going to use BitBlt to copy the image from the surface to our offscreen bitmap. Before we can do so, we need to “select the bitmap into a device context”, i.e. connect it to the device driver that will actually perform the transfer. It is wise to create a DC that is compatible with the surface’s DC:

HDC OffscrDC = CreatecompatibleDC(SurfDC);

A GDI object, which can be a bitmap but also a pen, a brush, a clipping region, etc., is selected into a device context using a function call like the following:

HBITMAP OldBmp = (HBITMAP)SelectObject(OffscrDC, OffscrBmp);

The first parameter specifies the device context, the second specifies the object that is to be selected. The return value is the handle of the previously selected object of the same type. When you select a pen, the return value is an HPEN. When you select a bitmap, the return value is an HBITMAP. The return value should be stored to be able to de-select the object later on. For obvious reasons, de-selecting an object by passing NULL would not work because NULL gives no information whether the pen, the font, or the bitmap should be de-selected.

Now we can finally copy the bitmap:

BitBlt(OffscrDC, 0, 0, Width, Height, SurfDC, 0, 0, SRCCOPY);

Step 2: Converting from DDB to DIB

The actual DDB-to-DIB conversion will be handled entirely by GetDIBits. The only thing we’ll have to do is to inform GetDIBits about the desired format of the DIB. I’d suggest we use the same format as the surface, so that no information is lost and we do not waste memory either. As it turns out, GetDIBits can also be used to obtain the format information from an HBITMAP. In other words, we’ll use GetDIBits to fill a structure that we, in a second call, will use as a paramater for that very same function.

The structure in question is BITMAPINFO, which consists of a BITMAPINFOHEADER and an optional palette. When taking a look at the declaration, you’ll notice that there is only memory for one palette entry. We need to reserve more:

LPBITMAPINFO lpbi = (LPBITMAPINFO)(new char[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)]);
lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

The structure is now big enough for the biggest possible palette, which is that of an 8-bit or 256-color bitmap.

The Win32 Programmer’s Reference says that the bitmap must not be selected into the device context when GetDIBits is called. We select OldBmp (remember?) to de-select OffscrBmp:

SelectObject(OffscrDC, OldBmp);

When calling GetDIBits to query bitmap info, do not pass an array for the bitmap bits but do pass a BITMAPINFOHEADER:

GetDIBits(OffscrDC, OffscrBmp, 0, Height, NULL, lpbi, DIB_RGB_COLORS);

Once the information has been obtained, we can allocate memory for the bitmap bits. The required size is returned in the biSizeImage member of the info header.

LPVOID lpvBits = new char[lpbi->bmiHeader.biSizeImage];

Now we can have GetDIBits convert the bitmap:

GetDIBits(OffscrDC, OffscrBmp, 0, Height, lpvBits, lpbi, DIB_RGB_COLORS);

Step 3: Saving the File

The last step is saving the buffers to a file. The file is created the recommended way:

HANDLE BmpFile = CreateFile(FileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

The first structure within a .bmp file must be a BITMAPFILEHEADER:

BITMAPFILEHEADER bmfh;
bmfh.bfType = 19778; // always the same, 'BM'
// bmfh.bfSize = ??? // we'll write that later
bmfh.bfReserved1 = bmfh.bfReserved2 = 0; // must be zero
// bmfh.bfOffBits = ??? // we'll write that later
WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL);

Note: The missing two fields, biOffBits and biSize, will be updated later when we have that information. It would be possible to calculate these values now, but for me it looks a bit cleaner my way. Be patient.

Immediately following the file header is the BITMAPINFOHEADER, which we can dump as is:

WriteFile(BmpFile, &lpbi->bmiHeader, sizeof(BITMAPINFOHEADER), &Written, NULL);

Following the info header could be a palette. Palettes are only needed for certain bit depths. On the one hand, modes with a bit count <= 8 will require a palette. On the other hand, 16-bit and 32-bit modes require a palette as well. In these modes the first three entries of the palette are used as bit masks for the red, green, and blue components of the color. These modes have a biCompression of BI_BITFIELDS. (Not all paint programs will handle such files!)

Note: 24-bit bitmaps do not need such information, because the masks are always 0x0000FF, 0x00FF00, and 0xFF0000, respectively.

if (lpbi->bmiHeader.biCompression == BI_BITFIELDS) PalEntries = 3;
else PalEntries = (lpbi->bmiHeader.biBitCount <= 8 ) ?
(int)(1 << lpbi->bmiHeader.biBitCount) : 0;
// Check biClrUsed: (See the note below)
if (lpbi->bmiHeader.biClrUsed) PalEntries = lpbi->bmiHeader.biClrUsed);

Note: I am not sure whether GetDIBits will ever make use of the biClrUsed member. I take it into account just to be on the safe side. The purpose of this member is to make it possible for a BMP file to store less palette entries than would be possible for the bit depth. For example, if a bitmap is stored in 8-bit format but uses, say, three colors only, biClrUsed could be set to three and only three RGBQUADs would have to be saved. The last time when I wrote a bitmap loader, some Windows wallpapers were using this feature. (Making my bitmap loader fail ungracefully, by the way…)

It’s time to store the calculated number of palette entries now:

if (PalEntries)
WriteFile(BmpFile, lpbi->bmiColors, PalEntries * sizeof(RGBQUAD), &Written, NULL);

The current position of the file pointer is the value that should be stored in the biOffBits member of the file header. We save it:

bmfh.bfOffBits = GetFilePointer(BmpFile);

Note: GetFilePointer is a wrapper for the following:
SetFilePointer(FileHandle, 0, 0, FILE_CURRENT);
Thus, GetFilePointer returns the current location of the file pointer.

Following the (optional) palette is the array of bitmap bits:

WriteFile(BmpFile, lpvBits, lpbi->bmiHeader.biSizeImage, &Written, NULL);

The current position of the file header is the biSize member of the file header. Let’s overwrite the file header that we have stored before and we are all done:

bmfh.bfSize = GetFilePointer(BmpFile);
SetFilePointer(BmpFile, 0, 0, FILE_BEGIN);
WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL);

Cleaning up…

The cleaning up is a necessary evil. Lazy programmers can copy and paste the following code:

CloseHandle(BmpFile);
if (SurfDC) lpDDS->ReleaseDC(SurfDC);
if (OffscrDC) DeleteDC(OffscrDC);
if (OffscrBmp) DeleteObject(OffscrBmp);
if (lpbi) delete[] lpbi;
if (lpvBits) delete[] lpvBits;
if (BmpFile != INVALID_HANDLE_VALUE) CloseHandle(BmpFile);

The preceding code has been written with an error checking mechanism in mind. It can be called at any time from the Screenshot function, provided that the variables have been properly initialized with NULL.

Note: The inline code samples do not perform any error checking while the listing at the end of this article does.

…and chilling out

We have created a flexible Screenshot function without much work. The GDI will handle DirectDraw surfaces of all formats without us having to modify the code. This is how it should always be!
The lesson we learned is that not even in times of DirectDraw one should forget about the rest of the WinAPI. For some reason the GDI has a reputation of being slow compared to DirectDraw. Being honest, this is true in a certain respect. On the other hand, DirectDraw is not even capable of blitting between surfaces of different formats!
The flexibility of functions like GetDIBits comes at a price. But the time you save for not having to code them yourself can be used for other things: Creating worlds that are worth taking screenshots of, for example.

A few words from the author

This has been my first article for mr-gamemaker.com. I was trying to make it useful for both beginners of GDI programming and those who are already familiar with the concepts of device contexts, DDBs, etc. Let me know whether you think I succeeded! I am looking forward to receiving your suggestions, questions, and comments: foetsch@yahoo.com.


Code Listing

//---------------------------------------------------------------------------
//
//    Screenshot - Author: Michael Fötsch; Date: May 31, 2000
//
//---------------------------------------------------------------------------

#include <ddraw.h>
#include <math.h>

//---------------------------------------------------------------------------

// Helper function to retrieve current position of file pointer:
inline int GetFilePointer(HANDLE FileHandle)
{
    return SetFilePointer(FileHandle, 0, 0, FILE_CURRENT);
}
//---------------------------------------------------------------------------

// Helper macro to return from function when error occurs:
#define ERROR_BREAK(x) throw (int)(x);

// Screenshot
//    -> FileName: Name of file to save screenshot to
//    -> lpDDS: DirectDraw surface to capture
//    <- Result: Success
//
bool Screenshot(LPCTSTR FileName, LPDIRECTDRAWSURFACE7 lpDDS)
{
    if (!FileName || !lpDDS) return false;

    bool Success=false;
    HDC SurfDC=NULL;        // GDI-compatible device context for the surface
    HBITMAP OffscrBmp=NULL; // bitmap that is converted to a DIB
    HDC OffscrDC=NULL;      // offscreen DC that we can select OffscrBmp into
    LPBITMAPINFO lpbi=NULL; // bitmap format info; used by GetDIBits
    LPVOID lpvBits=NULL;    // pointer to bitmap bits array
    HANDLE BmpFile=INVALID_HANDLE_VALUE;    // destination .bmp file
    BITMAPFILEHEADER bmfh;  // .bmp file header

try
{
    // Get dimensions of Surface:
    DDSURFACEDESC2 ddsd;
    ZeroMemory(&ddsd, sizeof(ddsd));
    ddsd.dwSize = sizeof(ddsd);
    if (FAILED(lpDDS->GetSurfaceDesc(&ddsd))) ERROR_BREAK(0);
    int Width = ddsd.dwWidth;
    int Height = ddsd.dwHeight;

    // Create a GDI-compatible device context for the surface:
    if (FAILED(lpDDS->GetDC(&SurfDC))) ERROR_BREAK(1);

    // We need an HBITMAP to convert it to a DIB:
    if ((OffscrBmp = CreateCompatibleBitmap(SurfDC, Width, Height)) == NULL)
        ERROR_BREAK(2);

    // The bitmap is empty, so let's copy the contents of the surface to it.
    // For that we need to select it into a device context. We create one.
    if ((OffscrDC = CreateCompatibleDC(SurfDC)) == NULL) ERROR_BREAK(3);
    // Select OffscrBmp into OffscrDC:
    HBITMAP OldBmp = (HBITMAP)SelectObject(OffscrDC, OffscrBmp);
    // Now we can copy the contents of the surface to the offscreen bitmap:
    BitBlt(OffscrDC, 0, 0, Width, Height, SurfDC, 0, 0, SRCCOPY);

    // We don't need SurfDC anymore. Free it:
    lpDDS->ReleaseDC(SurfDC); SurfDC = NULL;

    // GetDIBits requires format info about the bitmap. We can have GetDIBits
    // fill a structure with that info if we pass a NULL pointer for lpvBits:
    // Reserve memory for bitmap info (BITMAPINFOHEADER + largest possible
    // palette):
    if ((lpbi = (LPBITMAPINFO)(new char[sizeof(BITMAPINFOHEADER) +
        256 * sizeof(RGBQUAD)])) == NULL) ERROR_BREAK(4);
    ZeroMemory(&lpbi->bmiHeader, sizeof(BITMAPINFOHEADER));
    lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    // Get info but first de-select OffscrBmp because GetDIBits requires it:
    SelectObject(OffscrDC, OldBmp);
    if (!GetDIBits(OffscrDC, OffscrBmp, 0, Height, NULL, lpbi, DIB_RGB_COLORS))
        ERROR_BREAK(5);

    // Reserve memory for bitmap bits:
    if ((lpvBits = new char[lpbi->bmiHeader.biSizeImage]) == NULL)
        ERROR_BREAK(6);

    // Have GetDIBits convert OffscrBmp to a DIB (device-independent bitmap):
    if (!GetDIBits(OffscrDC, OffscrBmp, 0, Height, lpvBits, lpbi,
        DIB_RGB_COLORS)) ERROR_BREAK(7);

    // Create a file to save the DIB to:
    if ((BmpFile = CreateFile(FileName,
                              GENERIC_WRITE,
                              0, NULL,
                              CREATE_ALWAYS,
                              FILE_ATTRIBUTE_NORMAL,
                              NULL)) == INVALID_HANDLE_VALUE) ERROR_BREAK(8);

    DWORD Written;    // number of bytes written by WriteFile

    // Write a file header to the file:
    bmfh.bfType = 19778;        // 'BM'
    // bmfh.bfSize = ???        // we'll write that later
    bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
    // bmfh.bfOffBits = ???     // we'll write that later
    if (!WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL))
        ERROR_BREAK(9);
    if (Written < sizeof(bmfh)) ERROR_BREAK(9);

    // Write BITMAPINFOHEADER to the file:
    if (!WriteFile(BmpFile, &lpbi->bmiHeader, sizeof(BITMAPINFOHEADER),
        &Written, NULL)) ERROR_BREAK(10);
    if (Written < sizeof(BITMAPINFOHEADER)) ERROR_BREAK(10);

    // Calculate size of palette:
    int PalEntries;
    // 16-bit or 32-bit bitmaps require bit masks:
    if (lpbi->bmiHeader.biCompression == BI_BITFIELDS) PalEntries = 3;
    else
        // bitmap is palettized?
        PalEntries = (lpbi->bmiHeader.biBitCount <= 8 ) ?
            // 2^biBitCount palette entries max.:
            (int)(1 << lpbi->bmiHeader.biBitCount)
        // bitmap is TrueColor -> no palette:
        : 0;
    // If biClrUsed use only biClrUsed palette entries:
    if (lpbi->bmiHeader.biClrUsed) PalEntries = lpbi->bmiHeader.biClrUsed;

    // Write palette to the file:
    if (PalEntries)
    {
        if (!WriteFile(BmpFile, &lpbi->bmiColors, PalEntries * sizeof(RGBQUAD),
            &Written, NULL)) ERROR_BREAK(11);
        if (Written < PalEntries * sizeof(RGBQUAD)) ERROR_BREAK(11);
    }

    // The current position in the file (at the beginning of the bitmap bits)
    // will be saved to the BITMAPFILEHEADER:
    bmfh.bfOffBits = GetFilePointer(BmpFile);

    // Write bitmap bits to the file:
    if (!WriteFile(BmpFile, lpvBits, lpbi->bmiHeader.biSizeImage,
        &Written, NULL)) ERROR_BREAK(12);
    if (Written < lpbi->bmiHeader.biSizeImage) ERROR_BREAK(12);

    // The current pos. in the file is the final file size and will be saved:
    bmfh.bfSize = GetFilePointer(BmpFile);

    // We have all the info for the file header. Save the updated version:
    SetFilePointer(BmpFile, 0, 0, FILE_BEGIN);
    if (!WriteFile(BmpFile, &bmfh, sizeof(bmfh), &Written, NULL))
        ERROR_BREAK(13);
    if (Written < sizeof(bmfh)) ERROR_BREAK(13);

    Success = true;
}
catch (int &errorcode)
{
    char Buf[100];
    wsprintf(Buf, "Screenshot error #%i", errorcode);
    OutputDebugString(Buf);
}
catch (...)
{
    OutputDebugString("Screenshot error");
}

    if (SurfDC) lpDDS->ReleaseDC(SurfDC);
    if (OffscrDC) DeleteDC(OffscrDC);
    if (OffscrBmp) DeleteObject(OffscrBmp);
    if (lpbi) delete[] lpbi;
    if (lpvBits) delete[] lpvBits;
    if (BmpFile != INVALID_HANDLE_VALUE) CloseHandle(BmpFile);

    return Success;
}
//---------------------------------------------------------------------------

Google+