Hacking the Overall Background Color of a Windows Tab Control

2003-11-03 :: Glenn Slayden


figure a.
Q: How do you change the background color of a Windows tab control? I don't mean the background of the individual tabs but rather the overall background behind the control, such as the empty tab space (figure a.).

A: The short answer is, you can't. This part of the painting is integrated into the WM_PAINT processing with the rest of the control, so there's no message you can subclass to affect only the background painting. That area will always be painted using the COLOR_BTNFACE color, with the exceptions as follows: I found some evidence that people have gotten this to work by using property sheets instead of the tab control, which suggests that the (internal) tab control code is not exactly independent of wrapping bodies, as it should be. Similar observations have been made regarding XP "theming" support and the tab control (offsite link: PN Devlog). Intercepting the WM_ERASEBKGND message is of no use with this control.

After having pulled out most of the stops regarding subclassing, to no avail, I had to resort to a much more hideous solution, as follows:
...
WNDPROC g_pfnTab;
HBRUSH g_hbrBkgnd = CreateSolidBrush(0);	// desired background brush
...

LRESULT CALLBACK SubclassTabProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	if (uMsg==WM_PAINT)
	{
		LRESULT l = CallWindowProc(g_pfnTab,hwnd,uMsg,wParam,lParam);

		HDC hdc = GetDC(hwnd);
		COLORREF cr = GetSysColor(COLOR_BTNFACE);
		SelectObject(hdc,g_hbrBkgnd);
		RECT r;
		GetClientRect(hwnd,&r);
		ExtFloodFill(hdc,r.right-3,3,cr,FLOODFILLSURFACE);
		SelectObject(hdc,GetStockObject(WHITE_BRUSH));
		ReleaseDC(hwnd,hdc);

		return l;
	}
	return CallWindowProc(g_pfnTab,hwnd,uMsg,wParam,lParam);
}

...
HWND hwnd_tab = CreateWindow(WC_TABCONTROL,L"",WS_CHILD,10,10,300,300,hWnd,(HMENU)ID_TABCTRL,g_hInst,0);
g_pfnTab = (WNDPROC)SetWindowLong(hwnd_tab,GWL_WNDPROC,(long)SubclassTabProc);
...
This solution is not perfect, but it was easy and I'm moving on. No, I'm not proud to post this. It's really sad. I originally was going to try to get CallWindowProc to do its painting into a memory DC, in order to eliminate any flicker, but then I realized that there's no way to replace the DC which the internal WM_PAINT processing will fetch when it calls BeginPaint. And I theorized that the XP GDI caching hopefully would take care of any flicker, and by my observation, it does a good enough job. Pronounced flicker is visible when drag-resizing the control.

If somebody finds out a better way to do this, please contact me.

2007-11-02 Update

Somebody did contact me to suggest a solution, and he invited me to share it here.
-----Original Message-----
From: ****** 
Sent: Saturday, November 03, 2007 5:24 PM
To: glenn@glennslayden.com
Subject: Re:Tab Control

I coded something like that and it's working ;]

first:
	

WNDPROC tabctl = reinterpret_cast(SetWindowLong(htab, GWL_WNDPROC, reinterpret_cast(TabProc)));

in tabctl proc:

switch (imsg)
{
case WM_ERASEBKGND:
  GetClientRect(hwnd, &rect);
  FillRect(((HDC)wParam), &rect, CreateSolidBrush(RGB(255,255,255)));
  UpdateWindow(hwnd);
  return TRUE;
};
and in parent window on WM_DRAWITEM:
TCITEM tabitem;
TCHAR  buff[30] = {0};
HWND   htab = GetDlgItem(hwnd, IDC_TABCTL);

if ( htab == dis->hwndItem )
{
  FillRect(dis->hDC, &dis->rcItem, reinterpret_cast(COLOR_WINDOW));
  SetBkMode(dis->hDC, TRANSPARENT);

  memset(&tabitem, 0, sizeof(TCITEM));

  tabitem.mask = TCIF_TEXT;
  tabitem.pszText = buff;
  tabitem.cchTextMax = 30;

  SendMessage(htab1, TCM_GETITEM, static_cast(dis->itemID), reinterpret_cast(&tabitem));

 TextOut(dis->hDC, (dis->rcItem.left + 6), (dis->rcItem.top + 2), tabitem.pszText, lstrlen(tabitem.pszText));
Thanks See you...

2008-08-29 Another Update

And thanks to Paul Sanders at AlpineSoft for the following:

From: Paul Sanders, AlpineSoft
Sent: Thursday, August 28, 2008 12:07 PM
Subject: Hacking the Overall Background Color of a Windows Tab Control (Hideous)

Hi,
 
This can be achieved using WM_PRINTCLIENT.  *Almost* all of the Windows controls support 
it (Rich Edit does not, for some unfathomable reason). I stumbled across WM_PRINTCLIENT 
by accident when I was trying to get AnimateWindow to work.  This area is a bit of a 
mess actually, in that there are three ways (or maybe more...) of drawing a window to 
an arbitrary DC:
 
1.  Send it a WM_PRINT message (which kinda works, but with a few glitches / gotchas)
 
2.  Send it a WM_PAINT message, passing an HDC in wParam  (which works for some common
controls, but not all, and is not generally that useful)
 
3.  Call PrintWindow (hWnd, hDC), which came along in Windows XP and appears to be a 
low-level hack somewhere inside GDI in that normal WM_PAINT messages are sent to the 
window but BeginPaint returns the DC you passed into PrintWindow rather than a 
normal screen DC
 
Of course, everything works slightly differently under Aero.

==============
 
I do this (in my WM_PAINT handler):
 
1.  Create a memory DC to draw into
 
2.  Send a WM_PRINTCLIENT message to the tab control to get it to draw the tabs into your memory DC
 
3.  Create a region which mirrors the shape of the tabs
 
4.  Fill the parts of the memory DC outside this region (RGN_DIFF) with the desired background brush
 
5.  Blt the result into the DC returned by BeginPaint
 
6.  Call EndPaint and return, *without* calling the tab control's own WndProc of course :)
 
Step 3 is a bit fiddly as you have to know the location and shape of the tabs, but other 
than that it's a pretty clean solution (see image below the following sample code).  You 
could probably use TransparentBlt to replace the system background colour instead.
 
Example code follows (sorry, it's all a bit tied up with my [proprietary] class library).


INT_PTR TransparentTabWindow::OnPaint (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hDC = SmartBeginPaint (hWnd, &ps, wParam);                            // accepts an HDC in wParam
    
    CallOriginalWndProc (hWnd, WM_PRINTCLIENT, (WPARAM) hDC, PRF_CLIENT);     // as the name suggests
 
    HRGN hRgn = CreateRectRgn (0, 0, 0, 0);
    int n_items = TabCtrl_GetItemCount (hWnd);
    int current_item = TabCtrl_GetCurSel (hWnd);
    RECT r;
    RECT lh_corner = { 0 }, rh_corner = { 0 };
    bool xp_themed = !gIsVista && this->theme_active;                        // or call IsThemeActive () and GetVersionEx
 
    for (int i = 0; i < n_items; ++i)
    {
        TabCtrl_GetItemRect (hWnd, i, &r);
        if (i == current_item)
        {
            r.left -= 1;
            r.right += 1;
            r.top -= 2;
            if (i == 0)
            {
                r.left -= 1;
                if (!xp_themed)
                    r.right += 1;
            }
            if (i == n_items - 1)
                r.right += 1;
        }
        else
        {
            r.right -= 1;
            if ((xp_themed || gIsVista) && i == n_items - 1)
                r.right -= 1;
        }
 
        if (xp_themed)
        {
            if (i != current_item + 1)
            {
                lh_corner = r;
                lh_corner.bottom = lh_corner.top + 1;
                lh_corner.right = lh_corner.left + 1;
            }
            
            rh_corner = r;
            rh_corner.bottom = rh_corner.top + 1;
            rh_corner.left = rh_corner.right - 1;
        }
 
        HRGN hTabRgn = CreateRectRgn (r.left, r.top, r.right, r.bottom);
        CombineRgn (hRgn, hRgn, hTabRgn, RGN_OR);
        BOOL ok = DeleteObject (hTabRgn);
        assert (ok);
 
        if (lh_corner.right > lh_corner.left)
        {
            HRGN hRoundedCorner = CreateRectRgn
                (lh_corner.left, lh_corner.top, lh_corner.right, lh_corner.bottom);
            CombineRgn (hRgn, hRgn, hRoundedCorner, RGN_DIFF);
            ok = DeleteObject (hRoundedCorner);
            assert (ok);
        }
 
        if (rh_corner.right > rh_corner.left)
        {
            HRGN hRoundedCorner = CreateRectRgn
                (rh_corner.left, rh_corner.top, rh_corner.right, rh_corner.bottom);
            CombineRgn (hRgn, hRgn, hRoundedCorner, RGN_DIFF);
            ok = DeleteObject (hRoundedCorner);
            assert (ok);
        }
    }
 
    GetClientRect (hWnd, &r);
    HRGN hFillRgn = CreateRectRgn (r.left, r.top, r.right, r.bottom);
    CombineRgn (hFillRgn, hFillRgn, hRgn, RGN_DIFF);
    SelectClipRgn (hDC, hFillRgn);
    bool must_delete = true;
    HBRUSH hBGBrush = GetCtlBGBrush (this, hDC, &must_delete);            // actually sends a WM_CTLCOLORBTN
    FillRgn (hDC, hFillRgn, hBGBrush);
    if (must_delete)
    {
        BOOL ok = DeleteObject (hBGBrush);
        assert (ok);
    }
 
    BOOL ok = DeleteObject (hFillRgn);
    assert (ok);
    ok = DeleteObject (hRgn);
    assert (ok);
 
    if (wParam == 0)
        EndPaint (hWnd, &ps);
    return MH_NODEFWNDPROC;                                            // tells my class library not to pass the message on
}