Writing HBITMAP to BMP File in C++

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:

  1. Open a file for binary writing
  2. Determine the color depth for the target bitmap
  3. Retrieve the bitmap dimensions and attributes
  4. Build the BITMAPINFOHEADER structure
  5. Build the BITMAPFILEHEADER structure
  6. Alllocate memory for the pixel data transfer
  7. Extract pixel data with proper palette handling
  8. Write all components to the output file
  9. 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.

Tags: C++ Windows HBITMAP BMP Bitmap

Posted on Wed, 26 Aug 2026 16:53:37 +0000 by rp2006