Home > Back-end >  Win32 GDI color palette transparency bug
Win32 GDI color palette transparency bug

Time:07-13

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.


EDIT: Someone asked for a minimal reproducible example. All the source shall be included below:

/*****************
 *****************
 ** Resources.h **
 *****************
 *****************/

#ifndef RESOURCES_H
#define RESOURCES_H

// An enum {} apparently doesn't suffice!
#define ID_ICON         100
#define ID_LOGO         101
#define ID_LOGO_BMP     102
#define ID_LOGO_BMP_LQ  103

#endif /* RESOURCES_H */
/*************
 ************* compiled with:
 ** logo.rc **   windres --input=logo.rc --output=data/logo.res
 *************   output-format=coff
 *************/

#include "winuser.h"
#include "resources.h"

CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "data\\logo.exe.manifest"

ID_ICON         ICON    "data\\icon.ico"
ID_LOGO_BMP     BITMAP  "data\\logo-8bpp.bmp"
ID_LOGO_BMP_LQ  BITMAP  "data\\logo-4bpp.bmp"
/**************
 ************** compiled with:
 ** main.cpp **   x86_64-w64-mingw32-g   main.cpp *.res -O3 -s -static
 **************   -lgdi32 -lcomctl32 -mwindows -o logo.exe
 **************/

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#define UNICODE
#include <cwchar>
#include <windows.h>
#include <commctrl.h>

#include "resources.h"

#define WND_NAME_MENU  L"Logo Window"
#define WND_NAME_CLASS L"LOGO_WINDOW"

#define DESTROY_WINDOW_SAFELY(window)\
    if (window) {\
        DestroyWindow(window);\
        window = (HWND)NULL;\
    }

/* Uncomment below to compile with WndProc fix */
//#define COMPILE_WITH_FIX



/************
** Globals **
************/

HINSTANCE   g_hInst = (HINSTANCE)NULL;
HWND        g_hWnd  = (HWND)     NULL;
HWND        g_hLogo = (HWND)     NULL;

long        g_lScrW,
            g_lScrH;

bool        g_bBmpExchange = false;



/*************************
** Forward Declarations **
*************************/

bool init(int nCmdShow);
void bye();
bool openWindow(int nCmdShow);

RECT suggestWindowRect();

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

bool armStaticControl(int rscID);
void displayError(const wchar_t *__restrict precede);



/****************
** Entry Point **
****************/

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg = {};
    BOOL bResult;
    
    
    /* In order to avoid C   static clean-up. */
    atexit(bye);
    
    /* Initialising evil non-const globals (sorry). */
    g_hInst = hInst;
    
    
    
    /* This will initialise everything. */
    if (!init(nCmdShow)) return EXIT_FAILURE;
    
    
    
    /* Message loop */
    while ( (bResult = GetMessage(&msg, (HWND)NULL, 0, 0)) != 0 )
    {
        if (bResult < 0)
        {
            displayError(L"GetMessage() error");
            return EXIT_FAILURE;
        }
        else
        {
            TranslateMessage(&msg);
            DispatchMessage (&msg);
        }
    }
    
    return EXIT_SUCCESS;
}



bool init(int nCmdShow)
{
    /* More initialisation of evil non-const globals (sorry). */
    g_lScrW = GetSystemMetrics(SM_CXSCREEN);
    g_lScrH = GetSystemMetrics(SM_CYSCREEN);
    
    return openWindow(nCmdShow);
}

void bye()
{
    armStaticControl(-1); /* Releases any objects when arg is -1 */
    DESTROY_WINDOW_SAFELY(g_hLogo);
    DESTROY_WINDOW_SAFELY(g_hWnd);
}




LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMsg, WPARAM wp, LPARAM lp)
{
    switch (uiMsg)
    {
    case WM_PAINT:
        if (hWnd == g_hWnd)
        {
            PAINTSTRUCT ps  = {};
            RECT        rct = {};
            
            HDC hDC;
            
            hDC = BeginPaint(hWnd, &ps);
            if (hDC)
            {
                TCHAR const *dt = g_bBmpExchange ?
                    TEXT("Press ENTER to swap between bit-depths!\n")
                    TEXT("Currently displaying 4-bit bitmap.")
                    :
                    TEXT("Press ENTER to swap between bit-depths!\n")
                    TEXT("Currently displaying 8-bit bitmap.")
                    ;
                
                DrawText(hDC, dt, -1, &rct, DT_NOCLIP);
                
                EndPaint(hWnd, &ps);
            }
            
            return 0;
        }
        break;
    
#ifdef COMPILE_WITH_FIX
    case WM_CTLCOLORSTATIC:
        if (lp == (LPARAM)g_hLogo)
        {
            SetBkMode((HDC)wp, TRANSPARENT);
            return (LRESULT)GetSysColorBrush(COLOR_WINDOW);
        }
        break;
#endif /* COMPILE_WITH_FIX */
    
    case WM_KEYDOWN:
        switch (wp)
        {
        case VK_RETURN:
            /* Toggle between high and low bit-depth bitmaps. */
            InvalidateRect(g_hWnd, NULL, TRUE);
            if ( (g_bBmpExchange = !g_bBmpExchange) )
                armStaticControl(ID_LOGO_BMP_LQ);
            else
                armStaticControl(ID_LOGO_BMP);
            return 0;
        }
        break;
    
    case WM_CLOSE:
        DestroyWindow(g_hWnd);
        return 0;
    
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    
    return DefWindowProc(hWnd, uiMsg, wp, lp);
}



bool initClass();

bool openWindow(int nCmdShow)
{
    RECT rct;
    
    
    
    /* Class initialisation */
    if (!initClass()) return false;
    
    
    /* Get suggested window rectangle and adjust.
     * This will only return a centred rectangle that is 3/4 the size of the
     * screen along x and y (not 3/4 of the geometric area). */
    rct = suggestWindowRect();
    
    if (!AdjustWindowRectEx(
        &rct,
        WS_CAPTION | WS_SYSMENU | WS_THICKFRAME |
            WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
        FALSE,
        WS_EX_OVERLAPPEDWINDOW))
    {
        displayError(L"Adjusting window rectangle failed");
    }
    rct.right   = GetSystemMetrics(SM_CXHSCROLL);
    rct.bottom  = GetSystemMetrics(SM_CYVSCROLL);
    
    /* Create window */
    g_hWnd = CreateWindowEx(
        WS_EX_OVERLAPPEDWINDOW, /* Extended Styles */
        WND_NAME_CLASS, WND_NAME_MENU, /* Class   display names */
        WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, /* Styles */
        rct.left,             /* X */
        rct.top,              /* Y */
        rct.right - rct.left, /* Width */
        rct.bottom - rct.top, /* Height */
        NULL,                 /* Parent */
        NULL,                 /* Menu */
        g_hInst,              /* Instance */
        NULL);
    if (!g_hWnd)
    {
        displayError(L"Failed to create window");
        return false;
    }
    
    /* Create static bitmap control */
    g_hLogo = CreateWindowEx(
        0, /* Extended Styles */
        L"STATIC", NULL, /* Class   display names */
        SS_BITMAP | WS_CHILD | WS_VISIBLE, /* Styles */
        32,        /* X */
        32,        /* Y */
        0,         /* Width */
        0,         /* Height */
        g_hWnd,    /* Parent window */
        (HMENU)ID_LOGO,   /* hMenu, repurposed as ID */
        g_hInst,          /* Instance */
        NULL);
    if (!g_hLogo)
    {
        displayError(L"Failed to create logo static control");
        return false;
    }
    
    /* Arm with transparent bitmap */
    if (!armStaticControl(ID_LOGO_BMP)) return false;
    
    /* Finally, show and update the window */
    ShowWindow(g_hWnd, nCmdShow);
    UpdateWindow(g_hWnd);
    
    return true;
}

bool initClass()
{
    WNDCLASSEX wc =
    {
        sizeof(WNDCLASSEX),           /* cbSize */
        CS_HREDRAW | CS_VREDRAW,      /* style */
        WndProc,                      /* callback */
        0, 0,                         /* class & window extra space */
        g_hInst,                      /* handle to instance */
        (HICON)NULL,                  /* handle to program's icon */              
        
        /* handle to cursor image, to display when mouse enters */
        (HCURSOR)LoadImage( NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0,
                            LR_DEFAULTSIZE | LR_SHARED),
        
        (HBRUSH)(COLOR_WINDOW 1),       /* window background colour */
        WND_NAME_MENU,                  /* menu name */
        WND_NAME_CLASS,                 /* class name */
        (HICON)NULL,                    /* small icon */
    };
    
    INITCOMMONCONTROLSEX cc_ex =
    {
        sizeof(INITCOMMONCONTROLSEX),
        ICC_STANDARD_CLASSES,
    };
    
    
    
    /* Initialise common controls */
    if (!InitCommonControlsEx(&cc_ex))
    {
        displayError(L"Failed to initialise common controls");
        return false;
    }
    
    /* Load program icon */
    wc.hIcon = (HICON)LoadImage(
        GetModuleHandle(NULL),
        MAKEINTRESOURCE(ID_ICON), IMAGE_ICON,
        0, 0,
        LR_SHARED | LR_DEFAULTSIZE);
    if (!wc.hIcon)
    {
        displayError(L"Failed to load icon");
    }
    wc.hIconSm = wc.hIcon;
    
    /* Register main window class */
    if (!RegisterClassEx(&wc))
    {
        displayError(L"Failed to initialise window class");
        
        DestroyIcon(wc.hIcon);
        return false;
    }
    
    return true;
}



/* Call with -1 as argument to destroy bitmap */
bool armStaticControl(int rscID)
{
    static HGDIOBJ hOldObject = (HGDIOBJ)NULL;
    
    HBITMAP hLogoBMP, hOldBMP;
    
    
    /* Check first that we have created the control */
    if (!g_hLogo) return false;
    
    /* If we must release objects */
    if (rscID == -1)
    {
        if (hOldObject == (HGDIOBJ)NULL) return false;
        
        /* Load original GDI object back into static control. */
        hOldBMP = (HBITMAP)SendDlgItemMessage(
            g_hWnd,
            ID_LOGO,
            STM_SETIMAGE,
            (WPARAM)IMAGE_BITMAP,
            (LPARAM)hOldObject);
        
        /* Then destroy the bitmap */
        DeleteObject((HGDIOBJ)hOldBMP);
        
        return true;
    }
    
    
    
    /* Load static control bitmap */
    hLogoBMP = (HBITMAP)LoadImage(
        GetModuleHandle(NULL),
        MAKEINTRESOURCE(rscID),
        IMAGE_BITMAP,
        0, 0,
        LR_CREATEDIBSECTION | LR_LOADTRANSPARENT);
    if (!hLogoBMP)
    {
        displayError(L"Failed to load logo");
    }
    
    /* Arm static control with bitmap. */
    hOldBMP = (HBITMAP)SendDlgItemMessage(
        g_hWnd,
        ID_LOGO,
        STM_SETIMAGE,
        (WPARAM)IMAGE_BITMAP,
        (LPARAM)hLogoBMP);
    
    if (!hOldObject)    hOldObject = (HGDIOBJ)hOldBMP;
    else                DeleteObject((HGDIOBJ)hOldBMP);
    
    return true;
}



void displayError(const wchar_t *__restrict precede)
{
    /* I am aware that this procedure could accept uiErr as an argument
     * and this would probably be safer. */
    DWORD const uiErr = GetLastError();
    
    /* Hopefully big enough. */
    wchar_t errbuf[1024];
    
    /* Using a buffer pointer is probably slightly faster than calling
     * wcslen(). */
    wchar_t *pErr = errbuf;
    
    
    
    /* Nice formatter. The 'stop' is returned to us too. */
    pErr  = wsprintf(pErr, L"%s [0x%x]: ", precede, uiErr);
    
    /* Print windows error at the 'stop'. */
    FormatMessageW(
        FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,  /* source / format */
        uiErr, /* Message ID */
        0,     /* language ID */
        pErr,
        1024 - (pErr - errbuf),  /* Size */
        NULL   /* Args */);
    
    /* Display error. */
    MessageBoxW(NULL, errbuf, L"Error", MB_ICONHAND);
}



RECT suggestWindowRect()
{
    RECT ret;
    
    /* Not (3/4) screen area, just width and height separately */
    ret.right  = (LONG)((size_t)g_lScrW*3)>>2; /* (3/4)screen_width */
    ret.bottom = (LONG)((size_t)g_lScrH*3)>>2; /* (3/4)screen_height */
    
    /* Centering */
    ret.left = (g_lScrW>>1) - (ret.right >>1);
    ret.top  = (g_lScrH>>1) - (ret.bottom>>1);
    
    ret.right   = ret.left;
    ret.bottom  = ret.top;
    
    return ret;
}

Manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
    version="1.0.0.0"
    processorArchitecture="*"
    name="buggy.bugged.logo"
    type="win32"
/>
<description>Transparent Logo.</description>
<dependency>
    <dependentAssembly>
        <assemblyIdentity
            type="win32"
            name="Microsoft.Windows.Common-Controls"
            version="6.0.0.0"
            processorArchitecture="*"
            publicKeyToken="6595b64144ccf1df"
            language="*"
        />
    </dependentAssembly>
</dependency>
</assembly>

8-bit logo: Logo (8-bpp), magenta is transparent

4-bit logo: Logo (4-bpp), magenta is transparent

CodePudding user response:

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'.

  • Related