This documnet covers the process of converting a Windows bitmap handle (HBITMAP) into a standard BMP file on disk. The implementation handles all necessary structures including file headers, info headers, and optional palette data.
The export process follows these key steps:
- Open a file for binary writing
- Determine the color depth for the target bitmap
- Retrieve the bitmap dimensions and attributes
- Build the BITMAPINFOHEADER structure
- Build the BITMAPFILEHEADER structure
- Alllocate memory for the pixel data transfer
- Extract pixel data with proper palette handling
- Write all components to the output file
- Release allocated resources
BitmapWriter.h
#pragma once
#include <windows.h>
#include <string>
class BitmapWriter
{
public:
static bool ExportToBmp(HBITMAP sourceBitmap, const std::string& outputPath);
private:
static int CalculateColorDepth();
static void ExtractPaletteData(HBITMAP bitmap, const BITMAP& info,
DWORD paletteBytes, LPBITMAPINFOHEADER headerAddr);
};
BitmapWriter.cpp
#include "BitmapWriter.h"
#include <shlwapi.h>
bool BitmapWriter::ExportToBmp(HBITMAP sourceBitmap, const std::string& outputPath)
{
// Step 1: Open output file for writing
HANDLE fileHandle = CreateFileA(outputPath.c_str(), GENERIC_WRITE,
0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (fileHandle == INVALID_HANDLE_VALUE)
{
return false;
}
// Step 2: Determine color depth based on display settings
const int colorDepth = CalculateColorDepth();
// Step 3: Fetch bitmap metadata
BITMAP bitmapInfo;
GetObject(sourceBitmap, sizeof(bitmapInfo), &bitmapInfo);
// Calculate aligned scan line size
const DWORD alignedWidth = ((bitmapInfo.bmWidth * colorDepth + 31) / 32) * 4;
const DWORD imageDataSize = alignedWidth * bitmapInfo.bmHeight;
const DWORD paletteSize = 0;
// Step 4: Populate BITMAPINFOHEADER
BITMAPINFOHEADER infoHeader;
infoHeader.biSize = sizeof(BITMAPINFOHEADER);
infoHeader.biWidth = bitmapInfo.bmWidth;
infoHeader.biHeight = bitmapInfo.bmHeight;
infoHeader.biPlanes = 1;
infoHeader.biBitCount = colorDepth;
infoHeader.biCompression = BI_RGB;
infoHeader.biSizeImage = 0;
infoHeader.biXPelsPerMeter = 0;
infoHeader.biYPelsPerMeter = 0;
infoHeader.biClrImportant = 0;
infoHeader.biClrUsed = 0;
// Step 5: Populate BITMAPFILEHEADER
BITMAPFILEHEADER fileHeader;
fileHeader.bfType = 0x4D42; // "BM" marker
const DWORD totalFileSize = sizeof(BITMAPFILEHEADER) +
sizeof(BITMAPINFOHEADER) + paletteSize + imageDataSize;
fileHeader.bfSize = totalFileSize;
fileHeader.bfReserved1 = 0;
fileHeader.bfReserved2 = 0;
fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) +
sizeof(BITMAPINFOHEADER) + paletteSize;
// Step 6: Allocate transfer buffer
HGLOBAL memHandle = GlobalAlloc(GHND, imageDataSize + paletteSize + sizeof(BITMAPINFOHEADER));
LPBITMAPINFOHEADER headerPtr = static_cast<LPBITMAPINFOHEADER>(GlobalLock(memHandle));
*headerPtr = infoHeader;
// Step 7: Extract bitmap data with palette processing
ExtractPaletteData(sourceBitmap, bitmapInfo, paletteSize, headerPtr);
// Step 8: Write file to disk
DWORD bytesWritten = 0;
WriteFile(fileHandle, &fileHeader, sizeof(BITMAPFILEHEADER),
&bytesWritten, nullptr);
WriteFile(fileHandle, headerPtr, totalFileSize,
&bytesWritten, nullptr);
// Step 9: Cleanup
GlobalUnlock(memHandle);
GlobalFree(memHandle);
CloseHandle(fileHandle);
return true;
}
int BitmapWriter::CalculateColorDepth()
{
HDC screenDC = CreateDCA("DISPLAY", nullptr, nullptr, nullptr);
const int bitsPerPixel = GetDeviceCaps(screenDC, BITSPIXEL) * GetDeviceCaps(screenDC, PLANES);
DeleteDC(screenDC);
if (bitsPerPixel <= 1) return 1;
if (bitsPerPixel <= 4) return 4;
if (bitsPerPixel <= 8) return 8;
return 24;
}
void BitmapWriter::ExtractPaletteData(HBITMAP bitmap, const BITMAP& info,
DWORD paletteBytes, LPBITMAPINFOHEADER headerAddr)
{
HGDIOBJ savedPalette = nullptr;
HDC dc = nullptr;
const HGDIOBJ defaultPalette = GetStockObject(DEFAULT_PALETTE);
if (defaultPalette != nullptr)
{
dc = GetDC(nullptr);
savedPalette = SelectPalette(dc, static_cast<HPALETTE>(defaultPalette), FALSE);
RealizePalette(dc);
}
LPSTR dataBuffer = reinterpret_cast<LPSTR>(headerAddr) + sizeof(BITMAPINFOHEADER) + paletteBytes;
GetDIBits(dc, bitmap, 0, static_cast<UINT>(info.bmHeight),
dataBuffer, reinterpret_cast<BITMAPINFO*>(headerAddr), DIB_RGB_COLORS);
if (savedPalette != nullptr)
{
SelectPalette(dc, static_cast<HPALETTE>(savedPalette), TRUE);
RealizePalette(dc);
ReleaseDC(nullptr, dc);
}
}
Usage Example
#include "BitmapWriter.h"
void CaptureScreen(const char* filename)
{
HDC screen = GetDC(nullptr);
HDC memDC = CreateCompatibleDC(screen);
HBITMAP screenBmp = CreateCompatibleBitmap(screen,
GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
SelectObject(memDC, screenBmp);
BitBlt(memDC, 0, 0, GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN), screen, 0, 0, SRCCOPY);
BitmapWriter::ExportToBmp(screenBmp, filename);
DeleteObject(screenBmp);
DeleteDC(memDC);
ReleaseDC(nullptr, screen);
}
The color depth calculation maps device capabilities to standard bitmap depths: 1, 4, 8, or 24 bits per pixel. For most modern displays returning 32-bit values, the result is 24-bit color depth.
Scan lines must be 32-bit aligned, which the calculation (width * bpp + 31) / 32 * 4 ensures. Without proper alignment, the resulting BMP file will display incorrectly in image viewers.