winapi

WM_CTLCOLORSTATIC not working from subclassed Dialog procedure


I know from this answer that the WM_CTLCOLORSTATIC message goes to the parent window of Dialog controls.

Regarding the parent window, if I put it in the direct Dialog Proc of a dialog I create below with DialogBox, it works correctly.

But if I subclass the Dialog and move all the CTLCOLOR code there instead of the direct proc, then WM_CTLCOLORSTATIC stops working for me. WM_CTLCOLORDLG still works. With this code, my Dialog is white but the Static labels are no longer white, they go back to gray. I'm still dealing with the Parent Window (Dialog), so it should work.

FULL REPRODUCIBLE EXAMPLE

DlgSubClassTest.cpp

#include "resource.h"

#include <Windows.h>

#include <CommCtrl.h>

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


INT_PTR CALLBACK MyDlgProc(HWND hWndDlg, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK Subclassed_DialogMakeWhite(HWND hWndDlg, UINT message, WPARAM wParam, LPARAM lParam,
                                            UINT_PTR uIdSubclass, DWORD_PTR dwRefData);


int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ LPWSTR /*lpCmdLine*/,
                      _In_ int /*nCmdShow*/)
{
    return (int)DialogBoxW(hInstance, MAKEINTRESOURCEW(IDD_TEST), nullptr, MyDlgProc);
}


// Dialog Proc
INT_PTR CALLBACK MyDlgProc(HWND hWndDlg, UINT message, WPARAM /*wParam*/, LPARAM /*lParam*/)
{
    switch (message)
    {
    case WM_INITDIALOG:
        SetWindowSubclass(hWndDlg, Subclassed_DialogMakeWhite, 0, 0);
        return TRUE;

    case WM_CLOSE:
        EndDialog(hWndDlg, 0);
        return TRUE;

    default:
        return FALSE;
    }
}

// Subclass proc
LRESULT CALLBACK Subclassed_DialogMakeWhite(HWND hWndDlg, UINT message, WPARAM wParam, LPARAM lParam,
                                            UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/)
{
    LRESULT lRes = DefSubclassProc(hWndDlg, message, wParam, lParam);

    if (message == WM_CTLCOLORDLG)
    { // THIS WORKS
        return (INT_PTR)GetStockObject(WHITE_BRUSH);
    }
    else if (message == WM_CTLCOLORSTATIC)
    { // THIS DOES NOT WORK
        return (INT_PTR)GetStockObject(WHITE_BRUSH);
    }
    else
    {
        return lRes;
    }
}

Dialog.rc

#include "resource.h"
#include <winres.h>

IDD_TEST DIALOGEX 0, 0, 173, 75
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_SYSMENU
CAPTION "DlgSubClassTest"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LTEXT           "Static1",IDC_STATIC,20,25,29,12
    LTEXT           "Static2",IDC_STATIC,20,46,31,12
    EDITTEXT        IDC_EDIT1,70,24,82,15,ES_AUTOHSCROLL
    EDITTEXT        IDC_EDIT2,70,42,82,16,ES_AUTOHSCROLL
END

resource.h

#define IDD_TEST 100
#define IDC_EDIT1 1000
#define IDC_EDIT2 1001

You can use the following command script from a Visual Studio Command Prompt to build the executable.

build.cmd

@ECHO OFF

if not exist .\bin mkdir .\bin
if not exist .\obj mkdir .\obj

rc /nologo /fo.\obj\Dialog.res /l 409 Dialog.rc
cl /nologo /Fo.\obj\ /Fe.\bin\program.exe DlgSubClassTest.cpp /EHsc /std:c++17 /W4 /O2 /link Gdi32.lib User32.lib Dialog.res /LIBPATH:.\obj /MANIFEST:EMBED

Executing build && bin\program.exe from a Visual Studio Command Prompt shows the following result:

Actual result

The dialog's background is white (as expected), but the labels are rendered using the default background color (this is unexpected).

Moving the WM_CTLCOLORSTATIC message handler implementation to MyDlgProc produces the desired result:

Expected result

How do I get the labels to render onto a white background while handling the WM_CTLCOLORSTATIC message in a SUBCLASSPROC (and why is the result different when handling the message in a DLGPROC)?


Solution

  • The line of code that reads

    // THIS DOES NOT WORK
    

    isn't actually true. You can verify this by commenting out the WM_CTLCOLORSTATIC branch and comparing the results:

    WM_CTLCOLORSTATIC handled WM_CTLCOLORSTATIC not handled
    With WM_CTLCOLORSTATIC handling Without WM_CTLCOLORSTATIC handling

    The image on the right shows that the static controls' entire client areas are rendered using the default color, whereas the image on the left uses the custom brush for the background that isn't occupied by text.

    This is the clue to derive a solution. Incidentally, the documentation for WM_CTLCOLORSTATIC provides the instructions in plain text:

    By responding to this message, the parent window can use the specified device context handle to set the text foreground and background colors of the static control.

    The default foreground color is fine, but we need to set the background color to match the brush's color:

    LRESULT CALLBACK Subclassed_DialogMakeWhite(HWND hWndDlg, UINT message, WPARAM wParam, LPARAM lParam,
                                                UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/)
    {
        LRESULT lRes = DefSubclassProc(hWndDlg, message, wParam, lParam);
    
        if (message == WM_CTLCOLORDLG)
        {
            return (INT_PTR)GetStockObject(WHITE_BRUSH);
        }
        else if (message == WM_CTLCOLORSTATIC)
        {
            // Set the text background color
            SetBkColor((HDC)wParam, RGB(255, 255, 255));
            return (INT_PTR)GetStockObject(WHITE_BRUSH);
        }
        else
        {
            return lRes;
        }
    }
    

    This produces the desired output:

    Fixed text background


    Curiously, since you mentioned that you also get the desired output when handling the WM_CTLCOLORSTATIC message in your dialog procedure, I investigated what's up with that. Indeed, adding a case WM_CTLCOLORSTATIC: to MyDlgProc somehow adjusts the text background color to match the background brush.

    The documentation for WM_CTLCOLORSTATIC doesn't describe this behavior and I want to believe that this hasn't always been the case. Regardless, I wanted to understand why the (undocumented) behavior was different between the two dialog procedures. As it turned out, the unconditional/early call to DefSubclassProc seems to interfere with the automatic text background color adjustments.

    The following implementation produces the same output (even without setting the text background color explicitly):

    LRESULT CALLBACK Subclassed_DialogMakeWhite(HWND hWndDlg, UINT message, WPARAM wParam, LPARAM lParam,
                                                UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/)
    {
        switch (message)
        {
        case WM_CTLCOLORDLG:
            return (INT_PTR)GetStockObject(WHITE_BRUSH);
    
        case WM_CTLCOLORSTATIC:
            return (INT_PTR)GetStockObject(WHITE_BRUSH);
    
        default:
            return DefSubclassProc(hWndDlg, message, wParam, lParam);
        }
    }
    

    This is relying on undocumented behavior and I would suggest setting the text background color explicitly regardless.


    Addendum: One could accomplish the same effect by calling SetBkMode with a value of TRANSPARENT instead of setting the text background color. This may seem attractive as it avoids the opportunity of failing to match colors. It is, however, difficult to coordinate with the control's default rendering to arrange for the text background to be rendered as expected. Plus, it affects other rendering operations (such as line drawing) and is computationally more expensive.

    Therefore, always prefer SetBkColor over SetBkMode unless you have a very specific reason to use the latter.