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