Hacking the Overall Background Color of a Windows Tab Control
2003-11-03 :: Glenn Slaydenfigure 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_castand in parent window on WM_DRAWITEM:(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; }; TCITEM tabitem; TCHAR buff[30] = {0}; HWND htab = GetDlgItem(hwnd, IDC_TABCTL); if ( htab == dis->hwndItem ) { FillRect(dis->hDC, &dis->rcItem, reinterpret_castThanks See you...(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));
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 }