cwinapitransparencygdiloadimage

Win32 GDI color palette transparency bug


This question could be considered more of a bug report on an unsightly and time-wasting issue I've recently encountered while using Win32/GDI:

That is, loading a bitmap image into static control (a bitmap static control, not icon). I'll demonstrate with the following code (this follows the creation of the main window):

HBITMAP hbmpLogo;

/* Load the logo bitmap graphic, compiled into the executable file by a resource compiler */
hbmpLogo = (HBITMAP)LoadImage(
        wc.hInstance,             /* <-- derived from GetModuleHandle(NULL) */
        MAKEINTRESOURCE(ID_LOGO), /* <-- ID_LOGO defined in a header */
        IMAGE_BITMAP,
        0, 0,
        LR_CREATEDIBSECTION | LR_LOADTRANSPARENT);

/* We have a fully functioning handle to a bitmap at this line */
if (!hbmpLogo)
{
    /* Thus this statement is never reached */
    abort();
}

We then create the control, which is a child of the main window:

/* Add static control */
m_hWndLogo = CreateWindowExW(
        0,            /* Extended styles, not used */
        L"STATIC",    /* Class name, we want a STATIC control */
        (LPWSTR)NULL, /* Would be window text, but we would instead pass an integer identifier
                       * here, formatted (as a string) in the form "#100" (let 100 = ID_LOGO) */
        SS_BITMAP | WS_CHILD | WS_VISIBLE, /* Styles specified. SS = Static Style. We select
                                            * bitmap, rather than other static control styles. */
        32,  /* X */
        32,  /* Y */
        640, /* Width. */
        400, /* Height. */
        hMainParentWindow,
        (HMENU)ID_LOGO, /* hMenu parameter, repurposed in this case as an identifier for the
                         * control, hence the obfuscatory use of the cast. */
        wc.hInstance,   /* Program instance handle appears here again ( GetModuleHandle(NULL) )*/
        NULL);
if (!m_hWndLogo)
{
    abort(); /* Also never called */
}

/* We then arm the static control with the bitmap by the, once more quite obfuscatory, use of
 * a 'SendMessage'-esque interface function: */

SendDlgItemMessageW(
        hMainParentWindow, /* Window containing the control */
        ID_LOGO,           /* The identifier of the control, passed in via the HMENU parameter
                            * of CreateWindow(...). */
        STM_SETIMAGE,      /* The action we want to effect, which is, arming the control with the
                            * bitmap we've loaded. */
        (WPARAM)IMAGE_BITMAP, /* Specifying a bitmap, as opposed to an icon or cursor. */
        (LPARAM)hbmpLogo);    /* Passing in the bitmap handle. */

/* At this line, our static control is sufficiently initialised. */

What is not impressive about this segment of code is the mandated use of LoadImage(...) to load the graphic from the program resources, where it is otherwise seemingly impossible to specify that our image will require transparency. Both flags LR_CREATEDIBSECTION and LR_LOADTRANSPARENT are required to effect this (once again, very ugly and not very explicit behavioural requirements. Why isn't LR_LOADTRANSPARENT good on its own?).

I will elaborate now that the bitmap has been tried at different bit-depths, each less than 16 bits per pixel (id est, using colour palettes), which incurs distractingly unaesthetical disuniformity between them. [Edit: See further discoveries in my answer]

What exactly do I mean by this?

A bitmap loaded at 8 bits per pixel, thus having a 256-length colour palette, renders with the first colour of the bitmap deleted (that is, set to the window class background brush colour); in effect, the bitmap is now 'transparent' in the appropriate areas. This behaviour is expected.

I then recompile the executable, now loading a similar bitmap but at (a reduced) 4 bits per pixel, thus having a 16-length colour palette. All is good and well, except I discover that the transparent region of the bitmap is painted with the WRONG background colour, one that does not match the window background colour. My wonderful bitmap has an unsightly grey rectangle around it, revealing its bounds.

What should the window background colour be? All documentation leads back, very explicitly, to this (HBRUSH)NULL-inclusive eyesore:

WNDCLASSEX wc = {}; /* Zero initialise */
/* initialise various members of wc
 * ...
 * ... */

wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); /* Here is the eyesore. */

Where a certain colour preset must be incremented, then cast to a HBRUSH typename, to specify the desired background colour. 'Window colour' is an obvious choice, and a fragment of code very frequently recurring and reproducible.

You may note that when this is not done, the Window instead assumes the colour of its preceding number code, which on my system happens to be the 'Scroll' colour. Indeed, and alas, if I happen to forget the notorious and glorious +1 appended to the COLOR_WINDOW HBRUSH, my window will become the unintended colour of a scroll bar.

And it seems this mistake has propagated within Microsofts own library. Evidence? That a 4-bpp bitmap, when loaded, will also erase the bitmap transparent areas to the wrong background color, where a 8-bpp bitmap does not.

TL;DR

It seems the programmers at Microsoft themselves do not fully understand their own Win32/GDI interface jargon, especially regarding the peculiar design choice behind adding 1 to the Window Class WNDCLASS[EX] hbrBackground member (supposedly to support (HBRUSH)NULL).

This is unless, of course, anyone can spot a mistake on my part?

Shall I submit a bug report?

Many thanks.


Solution

  • As though to patch over a hole in a parachute, there is a solution that produces consistency, implemented in the window callback procedure:

    LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMsg, WPARAM wp, LPARAM lp)
    {
        /* ... */
    
        switch (uiMsg)
        {
        /* This message is sent to us as a 'request' for the background colour
         * of the static control. */
        case WM_CTLCOLORSTATIC:
            /* WPARAM will contain the handle of the DC */
            /* LPARAM will contain the handle of the control */
            if (lp == (LPARAM)g_hLogo)
            {
                SetBkMode((HDC)wp, TRANSPARENT);
                return (LRESULT)GetSysColorBrush(COLOR_WINDOW); /* Here's the magic */
            }
            break;
        }
    
        return DefWindowProc(hWnd, uiMsg, wp, lp);
    }
    

    It turns out the problem was not reproducible when other transparent bitmaps of varying sizes (not only bit depths) were loaded.

    This is horrible. I am not sure why this happens. Insights?


    EDIT: All classes have been removed to produce a neat 'minimal reproducible example'.