Displaying a Splash Screen with C++ (Part I)

This is Part I of a series on creating a splash screen application in native code. For more information, see the Introduction and the Code License.

Part I: Creating a HBITMAP

In order to display an image on-screen, we need to have it available as a HBITMAP. The Windows Imaging Component allows us to decode a PNG image into a 32 bits-per- pixel bitmap (with an alpha channel) and extract its pixels into a DIB.

In this code, I’ll assume the image is embedded in the resources of the splash screen EXE (using a statement similar to the following in the .RC file):

IDI_SPLASHIMAGE PNG splash.png

The first step is to create an IStream on the resource data. This involves loading the resource, copying its data into a memory buffer, then creating a stream on that buffer. (Note that the following code has been changed for pedagogical purposes; production code could be improved by using smart pointers for the COM interfaces and Windows handles, and by using exceptions to handle error conditions.) COM should have been initialised (by calling CoInitialize or CoInitializeEx) before calling this method.

// Creates a stream object initialized with the data from an executable resource.
IStream * CreateStreamOnResource(LPCTSTR lpName, LPCTSTR lpType)
{
    // initialize return value
    IStream * ipStream = NULL;

    // find the resource
    HRSRC hrsrc = FindResource(NULL, lpName, lpType);
    if (hrsrc == NULL)
        goto Return;

    // load the resource
    DWORD dwResourceSize = SizeofResource(NULL, hrsrc);
    HGLOBAL hglbImage = LoadResource(NULL, hrsrc);
    if (hglbImage == NULL)
        goto Return;

    // lock the resource, getting a pointer to its data
    LPVOID pvSourceResourceData = LockResource(hglbImage);
    if (pvSourceResourceData == NULL)
        goto Return;

    // allocate memory to hold the resource data
    HGLOBAL hgblResourceData = GlobalAlloc(GMEM_MOVEABLE, dwResourceSize);
    if (hgblResourceData == NULL)
        goto Return;

    // get a pointer to the allocated memory
    LPVOID pvResourceData = GlobalLock(hgblResourceData);
    if (pvResourceData == NULL)
        goto FreeData;

    // copy the data from the resource to the new memory block
    CopyMemory(pvResourceData, pvSourceResourceData, dwResourceSize);
    GlobalUnlock(hgblResourceData);

    // create a stream on the HGLOBAL containing the data
    if (SUCCEEDED(CreateStreamOnHGlobal(hgblResourceData, TRUE, &ipStream)))
        goto Return;

FreeData:
    // couldn't create stream; free the memory
    GlobalFree(hgblResourceData);

Return:
    // no need to unlock or free the resource
    return ipStream;
}

Now that we have an IStream pointer to the data of the image, we can use WIC to load that image. An important step in this process is to use WICConvertBitmapSource to ensure that the image is in a 32bpp format suitable for direct conversion into a DIB. This method assumes that the input image is in the PNG format; for a splash screen, this is an excellent choice because it allows an alpha channel as well as lossless compression of the source image. (To make the splash screen image as small as possible, I highly recommend the PNGOUT compression utility.)

// Loads a PNG image from the specified stream (using Windows Imaging Component).
IWICBitmapSource * LoadBitmapFromStream(IStream * ipImageStream)
{
    // initialize return value
    IWICBitmapSource * ipBitmap = NULL;

    // load WIC's PNG decoder
    IWICBitmapDecoder * ipDecoder = NULL;
    if (FAILED(CoCreateInstance(CLSID_WICPngDecoder, NULL, CLSCTX_INPROC_SERVER, __uuidof(ipDecoder), reinterpret_cast<void**>(&ipDecoder))))
        goto Return;

    // load the PNG
    if (FAILED(ipDecoder->Initialize(ipImageStream, WICDecodeMetadataCacheOnLoad)))
        goto ReleaseDecoder;

    // check for the presence of the first frame in the bitmap
    UINT nFrameCount = 0;
    if (FAILED(ipDecoder->GetFrameCount(&nFrameCount)) || nFrameCount != 1)
        goto ReleaseDecoder;

    // load the first frame (i.e., the image)
    IWICBitmapFrameDecode * ipFrame = NULL;
    if (FAILED(ipDecoder->GetFrame(0, &ipFrame)))
        goto ReleaseDecoder;

    // convert the image to 32bpp BGRA format with pre-multiplied alpha
    //   (it may not be stored in that format natively in the PNG resource,
    //   but we need this format to create the DIB to use on-screen)
    WICConvertBitmapSource(GUID_WICPixelFormat32bppPBGRA, ipFrame, &ipBitmap);
    ipFrame->Release();

ReleaseDecoder:
    ipDecoder->Release();
Return:
    return ipBitmap;
}

Next is to use CreateDIBSection to allocate a DIB that can be written to directly. By setting up a BITMAPINFO structure with the right values, the DIB will be of the same format as the 32bpp BGRA image loaded by WIC, and the pixels can be copied directly from the WIC bitmap to the DIB.

// Creates a 32-bit DIB from the specified WIC bitmap.
HBITMAP CreateHBITMAP(IWICBitmapSource * ipBitmap)
{
    // initialize return value
    HBITMAP hbmp = NULL;

    // get image attributes and check for valid image
    UINT width = 0;
    UINT height = 0;
    if (FAILED(ipBitmap->GetSize(&width, &height)) || width == 0 || height == 0)
        goto Return;

    // prepare structure giving bitmap information (negative height indicates a top-down DIB)
    BITMAPINFO bminfo;
    ZeroMemory(&bminfo, sizeof(bminfo));
    bminfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bminfo.bmiHeader.biWidth = width;
    bminfo.bmiHeader.biHeight = -((LONG) height);
    bminfo.bmiHeader.biPlanes = 1;
    bminfo.bmiHeader.biBitCount = 32;
    bminfo.bmiHeader.biCompression = BI_RGB;

    // create a DIB section that can hold the image
    void * pvImageBits = NULL;
    HDC hdcScreen = GetDC(NULL);
    hbmp = CreateDIBSection(hdcScreen, &bminfo, DIB_RGB_COLORS, &pvImageBits, NULL, 0);
    ReleaseDC(NULL, hdcScreen);
    if (hbmp == NULL)
        goto Return;

    // extract the image into the HBITMAP
    const UINT cbStride = width * 4;
    const UINT cbImage = cbStride * height;
    if (FAILED(ipBitmap->CopyPixels(NULL, cbStride, cbImage, static_cast<BYTE *>(pvImageBits))))
    {
        // couldn't extract image; delete HBITMAP
        DeleteObject(hbmp);
        hbmp = NULL;
    }

Return:
    return hbmp;
}

Finally, these three functions can be put together to load the PNG image from the EXE’s resources and convert it to a HBITMAP:

// Loads the PNG containing the splash image into a HBITMAP.
HBITMAP LoadSplashImage()
{
    HBITMAP hbmpSplash = NULL;

    // load the PNG image data into a stream
    IStream * ipImageStream = CreateStreamOnResource(MAKEINTRESOURCE(IDI_SPLASHIMAGE), _T("PNG"));
    if (ipImageStream == NULL)
        goto Return;

    // load the bitmap with WIC
    IWICBitmapSource * ipBitmap = LoadBitmapFromStream(ipImageStream);
    if (ipBitmap == NULL)
        goto ReleaseStream;

    // create a HBITMAP containing the image
    hbmpSplash = CreateHBITMAP(ipBitmap);
    ipBitmap->Release();

ReleaseStream:
    ipImageStream->Release();
Return:
    return hbmpSplash;
}

The next installment will show how to create a layered window that can display this image.

Posted by Bradley Grainger on September 23, 2008