Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Correctly drawn themed dialogs in WinXP

0.00/5 (No votes)
29 Jan 2004 1  
This article shows a workaround to the graphics bug that a checkbox or radio button is displayed with a black background on themed dialogs.

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:

As you can see this is totally wrong

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):

Huh? A little hard to read this way

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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?

Woah! It works!

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!

Revision History

  • 29-01-2004: Updated the example and screenshots to show the difference between edit controls as well and the application detects if it's themed or not so you can use the same code on earlier versions of Windows. Also updated the article to reflect the changes in the example code.
  • 30-01-2004: Added Win32 code and sample.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here