Correctly drawn themed dialogs in WinXP
Correctly drawn themed dialogs in WinXP
Introduction
Ever needed or wanted to write an application which uses the 'new' and nifty Windows XP themes? Well, you might just have stumbled on a little problem when using tab controls and static controls in such an application. In case you create a dialog which has a tab control and on this tab control static controls or a radio button or a checkbox, the background color of those controls is not drawn correctly. It simply draws the good old COLOR_BTNFACE
as the background of those controls, as shown in the following example:
The first idea: Property sheets
So what are we going to do about this? Well first of all, there just must be a way to correctly draw this as there are certain applications such as Internet Explorer which draw it the right way. After a bit of investigation, I figured out they are using property sheets. So I decided to try a property sheet myself and to my surprise it's drawing correctly using a property sheet. However, it is just drawing correctly with the one and only tab control provided by the property sheet itself! Hmmm, so that's not too nice either and besides, sometimes property sheets are just not what you want and it's much too much work to configure one properly. I attempted to figure out how they got the property sheet to draw correctly, but unfortunately I could not find anything. If however you don't need anything fancy or custom, a property sheet might just do for you.
The second idea: EnableThemeDialogTexture
Well, there must be some way Windows is drawing that nice tab control background, so I decided to take a look in the UxTheme library. I found this function in there and it did show some interesting effects. This function enables you to automatically draw a background similar to the tab background on your dialog. So why doesn't this work? Well, it draws the background on the dialog itself. So everything that actually -should- be COLOR_BTNFACE
is now drawn as if it has the background of a tab control as well, plus the background of the children are not aligned correctly, so you still see an ugly dialog.
The third idea: Transparency
You ever noticed the property 'Transparent' on your static controls? Well, if you did, you might also have noticed that it simply doesn't work... This doesn't mean however that we can't make it work. We actually can! How, you ask? Well, by subclassing the WM_CTLCOLORSTATIC
message. This message gets send to the dialog box every time a child window requests the background brush to be drawn with. Well, that sounds perfect doesn't it? If we could just return a transparent brush, it's all fine and you know what? There is just such a thing! If we use the following code, it actually -seems- to work:
LRESULT OnCtlColor(UINT /* uMsg */, WPARAM wParam,
LPARAM /* lParam */, BOOL& /* bHandled */) throw()
{
// Set the background mode to transparent
::SetBkMode((HDC)(wParam), TRANSPARENT);
// Return the brush return
(LRESULT)(::GetStockObject(HOLLOW_BRUSH));
}
So, if we run the program, the static controls actually work. But, and there's always a but isn't there... The radio button and checkbox display black backgrounds! (On this screenshot the radio button even keeps its grey background somehow):
So, now what? Well, I did some extensive searching on the Internet for this problem and I actually found some sites mentioning this problem, but no one really offered a solution besides using a property sheet. So what I did next was to look how it is achieved to display bitmaps as background of a dialog while being transparent. I got a sample of Microsoft which gave me a very nice, al-be-it a sort of 'hack' idea.
The last and final idea: We use a bitmap as background
What? I hear you say. We use a bitmap? So how are we doing that? Well, I answer, we simply take a sort of screenshot from the tab control at creation and every time it resizes, and we have the perfect background bitmap for the transparent controls. Interesting idea, so how does it work? What we desire is to get an image of the background and the background only of the tab control. There are a couple of requirements for this idea:
- We want an image of the background of the tab control and just the background and not the children, as oddities might occur if, for instance, a child control changes its caption.
- We need to create a brush of this bitmap and we don't want to recreate it all the time, because it's rather inefficient. So we only update this brush when the tab control gets created and when the tab control gets resized.
- Now we have the background brush, it needs to be aligned for each control, else we see the top left corner of the tab control as the background of each child.
- It would be nice to have an easy way of implementing this on a dialog box without having to copy-paste anything to recreate the effect.
- As noted by A sleepy one, the code controls don't display correctly with earlier versions of Windows, so we need to find a way to only use it if the themes are enabled.
Well, the solution is near. I created a template class which does the following. It intercepts the WM_INITDIALOG
message and subclasses the tab control. It calls the UpdateBackgroundBrush()
function each time the tab control resizes and after we've subclassed it for the initial state. The function looks like this:
void UpdateBackgroundBrush() throw()
{
HMODULE hinstDll;
// Check if the application is themed
hinstDll = ::LoadLibrary(_T("UxTheme.dll"));
if (hinstDll)
{
typedef BOOL (*ISAPPTHEMEDPROC)();
ISAPPTHEMEDPROC pIsAppThemed;
pIsAppThemed =
(ISAPPTHEMEDPROC) ::GetProcAddress(hinstDll, "IsAppThemed");
if(pIsAppThemed)
m_bThemeActive = pIsAppThemed();
::FreeLibrary(hinstDll);
}
// Destroy old brush
if (m_hBrush)
::DeleteObject(m_hBrush);
m_hBrush = NULL;
// Only do this if the theme is active
if (m_bThemeActive)
{
RECT rc;
// Get tab control dimensions
m_wndTab.GetWindowRect(&rc);
// Get the tab control DC
HDC hDC = m_wndTab.GetDC();
// Create a compatible DC
HDC hDCMem = ::CreateCompatibleDC(hDC);
HBITMAP hBmp = ::CreateCompatibleBitmap(hDC,
rc.right - rc.left, rc.bottom - rc.top);
HBITMAP hBmpOld = (HBITMAP)(::SelectObject(hDCMem, hBmp));
// Tell the tab control to paint in our DC
m_wndTab.SendMessage(WM_PRINTCLIENT, (WPARAM)(hDCMem),
(LPARAM)(PRF_ERASEBKGND | PRF_CLIENT | PRF_NONCLIENT));
// Create a pattern brush from the bitmap selected in our DC
m_hBrush = ::CreatePatternBrush(hBmp);
// Restore the bitmap
::SelectObject(hDCMem, hBmpOld);
// Cleanup
::DeleteObject(hBmp);
::DeleteDC(hDCMem);
m_wndTab.ReleaseDC(hDC);
}
}
So, what exactly do I do? It's quite simple. First of all, I dynamically load the IsAppThemed
function so this code is compatible with earlier versions of Windows. Per default, the m_bThemeActive
is set to false
. If the function can get loaded, it executes it to verify if the themes are active. In case the themes are active, I create a memory dc and a bitmap in the memory dc of the same size as the tab control. When I've created the memory dc, we send the WM_PRINTCLIENT
message to the tab control. What does this message do? Well, we can send this message to the tab control and it will draw itself inside the dc specified by us. Pretty nifty message huh? The biggest advantage when we would use BitBlt
to copy the bitmap from the tab control to our bitmap is that this way we don't include the children. Well, after that, we simply create a brush from the bitmap and clean up the resources we used.
So now we have the brush to use, but how do we use it for the children of the tab control? Well, we go back to our third idea and subclass the WM_CTLCOLORSTATIC
message. The handler for this message will look like this:
LRESULT OnCtlColor(UINT /* uMsg */, WPARAM wParam,
LPARAM lParam, BOOL& /* bHandled */) throw()
{
if (m_bThemeActive)
{
RECT rc;
// Set the background mode to transparent
::SetBkMode((HDC)(wParam), TRANSPARENT);
// Get the controls window dimensions
::GetWindowRect((HWND)(lParam), &rc);
// Map the coordinates to coordinates with the upper
// left corner of dialog control as base
::MapWindowPoints(NULL, m_wndTab, (LPPOINT)(&rc), 2);
// Adjust the position of the brush for this control
// (else we see the top left of the brush as background)
::SetBrushOrgEx((HDC)(wParam), -rc.left, -rc.top, NULL);
// Return the brush
return (LRESULT)(m_hBrush);
}
return FALSE;
}
It looks a lot like the one from my third idea. It sets the background mode to transparent again and it returns the created brush from the UpdateBackgroundBrush()
function. There is a little added code though, to make sure the brush is aligned correctly so we don't see the top left of the tab control as the background of the children. Also, it checks if the application is themed so it doesn't perform unnecessary tasks. So how does this look in reality?
Well, as you can see it works! It's a rather dirty way of solving an ugly glitch :-) But it works and this far I haven't seen another solution just yet. So how do you use this class in your code? It's fairly easy:
class CSomeDialog: public CThemedDialog<CSomeDialog, IDD_DIALOG1, IDC_TAB1>
{
public:
typedef CThemedDialog<CSomeDialog, IDD_DIALOG1, IDC_TAB1> SUPERCLASS;
BEGIN_MSG_MAP(CNiceDialog)
CHAIN_MSG_MAP(SUPERCLASS)
ALT_MSG_MAP(1)
CHAIN_MSG_MAP_ALT(SUPERCLASS, 1)
END_MSG_MAP()
};
This is all you need to do. In case you don't want to subclass the dialog, you can drop the message map all together. In case you do, you have to remind to chain the alternate message map as well. This alternate message map is used for subclassing the tab control. As parameters for the template you have to pass the class itself, the resource identifier of the dialog box and the resource identifier of the tab control inside the dialog box. It only supports one tab control however, and if you desire to support more, you'll have to adjust the code a bit. Enjoy!