windowswinapiwin32gui

Controls on top of a tab have the incorrect background in Win32 GUI


I'm trying to create a Win32 application (with no other dependencies for GUI) that has a set of tabs and controls within those tabs. I have created the following reasonably minimal example following Raymond Chen's guidelines, which seem to contradict Microsoft's documentation, but either way it doesn't seem to affect drawing:

#include <Windows.h>
#include <windowsx.h>
#include <CommCtrl.h>
#include <uxtheme.h>

#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_DESTROY: {
            PostQuitMessage(0);
            return 0;
        }
        case WM_PAINT: {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            // All painting occurs here, between BeginPaint and EndPaint.

            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW));

            EndPaint(hwnd, &ps);

            return 0;
        }
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int main(int argc, char **argv) {
    HINSTANCE hinst = GetModuleHandle(NULL);

    INITCOMMONCONTROLSEX icex;
    icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icex.dwICC = ICC_TAB_CLASSES;
    InitCommonControlsEx(&icex);

    LOGFONT lf;
    GetObject (GetStockObject(DEFAULT_GUI_FONT), sizeof(LOGFONT), &lf); 
    HFONT hfont = CreateFont (lf.lfHeight, lf.lfWidth, 
        lf.lfEscapement, lf.lfOrientation, lf.lfWeight, 
        lf.lfItalic, lf.lfUnderline, lf.lfStrikeOut, lf.lfCharSet, 
        lf.lfOutPrecision, lf.lfClipPrecision, lf.lfQuality, 
        lf.lfPitchAndFamily, lf.lfFaceName); 

    const wchar_t CLASS_NAME[]  = L"Window Class";

    WNDCLASS wc = { 0,
        wndproc,
        0,
        0,
        hinst,
        NULL,
        NULL,
        NULL,
        NULL,
        (LPCSTR)CLASS_NAME
    };

    RegisterClass(&wc);

    HWND window_hwnd = CreateWindow((LPCSTR)CLASS_NAME, "Tabs Example", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, NULL, NULL, hinst, NULL);
    SendMessage(window_hwnd, WM_SETFONT, (WPARAM)hfont, TRUE);

    HWND tab_hwnd = CreateWindow(WC_TABCONTROL, "", WS_CHILD | WS_VISIBLE, 0, 0, 640, 480, window_hwnd, NULL, hinst, NULL);
    SendMessage(tab_hwnd, WM_SETFONT, (WPARAM)hfont, TRUE);

    TCITEM item;
    item.mask = TCIF_TEXT | TCIF_IMAGE;
    item.iImage = -1;
    
    item.pszText = "One!";
    TabCtrl_InsertItem(tab_hwnd, 0, &item);

    item.pszText = "Two!";
    TabCtrl_InsertItem(tab_hwnd, 1, &item);

    item.pszText = "Three!";
    TabCtrl_InsertItem(tab_hwnd, 2, &item);

    HWND groupbox_hwnd = CreateWindow(WC_BUTTON, "Groupbox", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_GROUPBOX, 10, 32, 600, 400, window_hwnd, NULL, hinst, NULL);
    SendMessage(groupbox_hwnd, WM_SETFONT, (WPARAM)hfont, TRUE);

    HWND text_hwnd = CreateWindow(WC_STATIC, "Static Text", WS_CHILD | WS_VISIBLE | SS_CENTER, 100, 100, 100, 100, window_hwnd, NULL, hinst, NULL);
    SendMessage(text_hwnd, WM_SETFONT, (WPARAM)hfont, TRUE);

    ShowWindow(window_hwnd, SW_NORMAL);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

}

The problem is that, when it is run, it draws the tab content incorrectly. Each control in the tab has a background with the window background color:

(note the incorrect background behind the "Groupbox" text and "Static Text")

image

I have tried using a transparent background using WM_CTLCOLORSTATIC like so:

case WM_CTLCOLORSTATIC: {
    HDC hEdit = (HDC)wParam;
    SetBkMode(hEdit, TRANSPARENT);
    SetTextColor(hEdit, RGB(0, 0, 0));
    return GetStockObject(HOLLOW_BRUSH);
}

But that results in incorrect rendering of the groupbox:

(note the groupbox divider being drawn behind the "Groupbox" text)

image

Also, I have tried reordering the tab control to the bottom, which makes the Groupbox display incorrectly:

SetWindowPos(tab_hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);

(note the grey background under the whole groupbox)

image


Solution

  • For most controls WM_PRINTCLIENT will do.

    case WM_PRINTCLIENT:
        {
            // Paint area outside tabs control here if needed.
            ...
            // Now paint tabs area
            RECT    r1, r2;
            GetWindowRect(hwnd, &r1);
            GetWindowRect(tags_hwnd, &r2);
            POINT   old;
            SetWindowOrgEx((HDC)wparam, r1.left - r2.left, r1.top - r2.top, &old);
            LRESULT result = SendMessage(tabs_hwnd, WM_PRINTCLIENT, wparam, lparam);
            SetWindowOrgEx((HDC)wparam, old.x, old.y, 0);
            return result;
        }
    

    WM_PRINTCLIENT should paint whole client area, so there is a reason that child dialog or helper window are often used for this.

    Unfortunately STATIC control doesn't call WM_PRINTWINDOW, WM_CTLCOLORSTATIC can be used instead.

    We can delegate some work to tabs control, but it will not work correctly for gradient or bitmap backround..

    case WM_CTLCOLORSTATIC:
        return SendMessage(tabs_hwnd, WM_CTLCOLORSTATIC, wparam, lparam);
    

    Edit:
    Removed simplified (and controversial) implementation for gradient and or bitmapped background.

    Full version needs following steps:

    1. Create compatible DC.
    2. Create bitmap.
    3. Select bitmap into dc.
    4. Adjust origin.
    5. Render with WM_PRINTCLIENT.
    6. Create brush from bitmap.
    7. Cleanup and return brush.

    Screenshot:

    This program doesn't use dialog templates. Controls are created by code and automaticly layed out by geometry manager in resizable window.

    Usung XP because it has tabs with gradient background.

    enter image description here

    EDIT: Full Approach In Context:

    For the program presented in the question, the WndProc function can be replaced with the following (note the global variables, as well) to show correct output (I did not have an XP environment to fully confirm correct rendering with gradients, but it seems fine on 11):

    HWND tab_hwnd;
    HBRUSH hBrush = NULL;
    
    LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
        switch (uMsg) {
            case WM_DESTROY: {
                PostQuitMessage(0);
                return 0;
            }
            case WM_PAINT: {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hwnd, &ps);
    
                // All painting occurs here, between BeginPaint and EndPaint.
    
                FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW));
    
                EndPaint(hwnd, &ps);
    
                return 0;
            }
            case WM_CTLCOLOR:
            case WM_CTLCOLORBTN:
            case WM_CTLCOLORSTATIC: {
                // create a brush that copies the tab's body
                // NOTE: this brush would have to be recreated when the window is resized, this is just an example
                if (!hBrush) {
                    RECT rc;
    
                    GetWindowRect(tab_hwnd, &rc);
                    HDC hdc = GetDC(tab_hwnd);
                    HDC hdc_new = CreateCompatibleDC(hdc);  // create a new device context to draw our tab into
                    HBITMAP hbmp = CreateCompatibleBitmap(hdc, rc.right - rc.left, rc.bottom - rc.top); // create a new bitmap to draw the tab into
                    HBITMAP hbmp_old = (HBITMAP)(SelectObject(hdc_new, hbmp));  // replace the device context's bitmap with our new bitmap
    
                    SendMessage(tab_hwnd, WM_PRINTCLIENT, hdc_new, (LPARAM)(PRF_ERASEBKGND | PRF_CLIENT | PRF_NONCLIENT));  // draw the tab into our bitmap
                    hBrush = CreatePatternBrush(hbmp);  // create a brush from the bitmap
                    SelectObject(hdc_new, hbmp_old);    // replace the bitmap in the device context
    
                    DeleteObject(hbmp);
                    DeleteDC(hdc_new);
                    ReleaseDC(tab_hwnd, hdc);
                }
    
                // use our previously created brush as a background for the control
                RECT rc2;
    
                HDC hEdit = (HDC)wParam;
                SetBkMode(hEdit, TRANSPARENT);
    
                GetWindowRect(hwnd, &rc2);  // get control's position
                MapWindowPoints(NULL, tab_hwnd, (LPPOINT)(&rc2), 2);    // convert coordinates into tab's space
                SetBrushOrgEx(hEdit, rc2.left, rc2.top, NULL);  // set brush origin to our control's position
    
                return hBrush;
            }
        }
    
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    

    Example application with groupbox and static text drawing correctly