Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

CImageButtonWithStyle - Buttons using Images with XP Visual Styles

4.50/5 (27 votes)
24 Nov 2005CPOL6 min read 1   4.8K  
How to get buttons using an icon or bitmap to use XP visual styles.

Introduction

Imagine you've finally gotten around to adding a manifest file to your MFC application so all your controls will take advantage of the new XP visual styles. One of your dialogs mixes buttons with ordinary text captions along with buttons using images as captions, and it ends up looking like this:

Sample Dialog without CImageButtonWithStyle class

That's not what you wanted! The buttons with text labels came out the way you expected, but those using images are ignoring the XP visual style setting and are being drawn with the old 3D-effect. You want all your buttons to use the XP visual styles, like this:

Sample Dialog using CImageButtonWithStyle class

CImageButtonWithStyle is a small class that makes this easy, and it won't change how your application runs on pre-XP Windows versions.

Background

Windows applications don't automatically use the new "theme aware" version of the common controls library comctl32.dll when running under Windows XP. Your application has to include a manifest file as one of its resources to tell Windows you want to use the newer version of the library, since there are some minor incompatibilities. See the article Add XP Theme Style to your current projects by Jian Hong for more details (or look over the demo app for this article).

If you just want to test out what happens without making a permanent change, just copy a suitable manifest file to same directory as your executable appname.exe and rename it to appname.exe.manifest.

You'll notice that buttons using icons or bitmaps (using window style flags BS_ICON or BS_BITMAP) are rendered with the 3D-effect you would expect if visual styles weren't in use. This behavior makes sense in some cases, like bitmaps that completely fill the face of the button. In other cases, like when mixing symbolic and text captions, it just looks ugly.

You would think there would be a simple solution to choose the new appearance (like an extended style flag), but I searched in vain. There were a number of published solutions that used the BS_OWNERDRAW style to completely take over all button rendering, but this means re-implementing most of the basic behavior of the button control (see the articles CXPStyleButtonST v1.2 by Davide Calabro or Native Win32 Theme aware Owner-draw Controls without MFC by Ewan Ward). It's very hard to be sure these classes will behave just like normal Windows button controls in all other respects.

Forget BS_OWNERDRAW, Just Use NM_CUSTOMDRAW Instead

Fortunately, SfaeJ had added a comment to Ewan Ward's article that put me on the right path. When running with the newer version of comctl32.dll, the button control sends NM_CUSTOMDRAW notifications. Once I knew what to look for, I was able to find some small tidbits in Microsoft's documentation and get a working solution.

Using the code

Using the CImageButtonWithStyle is simple.

  1. First, add the source files for the class CImageButtonWithStyle and its helper class CVisualStylesXP to your project (the four files: ImageButtonWithStyle.h, ImageButtonWithStyle.cpp, VisualStylesXP.h and VisualStylesXP.cpp).
  2. Add a CButton member to your dialog for each button that will display an image and associate it with the Windows control. If you use the Visual Studio class wizard, it will both add the member variable and add a call to DDX_Control( in your CDialog derived class' DoDataExchange() override). This will associate the CButton instance with the Windows control created by the dialog template.
    void CSampleDlg::DoDataExchange(CDataExchange* pDX)
    {
        CDialog::DoDataExchange(pDX);
        DDX_Control(pDX, IDC_BUTTON, m_wnd_button);
        // other DDX_ and DDV_ calls
    }
  3. Now, add the line #include "ImageButtonWithStyle.h" to the header for your CDialog derived class, and change the declaration of the CButton members to use the CImageButtonWithSyle class instead.
    CImageButtonWithStyle m_wnd_button;
  4. Recompile and you're done.

If you want to use CImageButtonWithStyle in other situations, you will have to call SubclassDlgItem() or SubclassWindow() to associate the CImageButtonWithStyle instance with a particular control (unless you create the control dynamically by calling the Create() member function from a CImageButtonWithStyle instance).

The Demo Application

The demo application was produced using the Visual Studio App-Wizard. I created a minimal SDI application and added a menu command "View|View Dialog..." to invoke the sample dialog shown at the top of the article.

Under the Hood

I derived my new CImageButtonWithStyle class from the MFC CButton class and added a handler for the NM_CUSTOMDRAW notification. If an older version of comctl32.dll is being used (i.e. pre Windows XP), then no NM_CUSTOMDRAW notifications will be sent to button controls, so my handler won't even be invoked. There should be no worries about backwards compatibility, since the new code isn't active in this case.

Rather than calling the uxtheme.dll visual styles API functions directly, I used the CVisualStylesXP class from David A. Zhao's article Add XP Visual Style Support to OWNERDRAW Controls to load the uxtheme.dll dynamically. When running on older Windows versions where uxtheme.dll isn't present, CVisualStylesXP is unable to load the library, so it provides stub versions of the functions that always fail. If I called the uxtheme.dll functions directly, applications using CImageButtonWithStyle wouldn't be able to load on older Windows versions.

The NM_CUSTOMDRAW handler OnNotifyCustomDraw() is passed a pointer to an NMCUSTOMDRAW structure. The CImageButtonWithStyle handler does the following:

  1. If the button control doesn't have either of the style flags BS_ICON or BS_BITMAP set, or if XP visual style themes aren't in use, then the handler simply returns CDRF_DODEFAULT. This causes the default window procedure (provided by comctl32.dll) to render the button just like it normally would (just as if I hadn't handled the NM_CUSTOMDRAW in the first place).
  2. If the dwDrawStage member of the NMCUSTOMDRAW structure has the value CDDS_PREERASE, erase the background by calling the DrawThemeParentBackground() member of the global CVisualStylesXP instance g_xpStyle.
  3. Get a handle to the XP visual styles theme data by calling g_xpStyle.OpenThemeData().
  4. Use the button's window style and the uItemState member of the NMCUSTOMDRAW structure to figure out which button state to use for drawing the background: PBS_DISABLED, PBS_PRESSED, PBS_HOT, PBS_DEFAULTED or PBS_NORMAL, and then draw it using g_xpStyle.DrawThemeBackground().
  5. Get the rectangle describing the interior of the button image from g_xpStyle.GetThemeBackgroundContentRect().
  6. Close the theme handle with g_xpStyle.CloseThemeData().
  7. Use the button style bits BS_LEFT, BS_RIGHT, BS_TOP and BS_BOTTOM to determine the image position, and then draw the bitmap or icon image using the Windows DrawState() function (passing in the flag DSS_DISABLED if the button's window style includes the WS_DISABLED flag).
  8. If uItemState includes the flag CDIS_FOCUS, call DrawFocusRect() to draw the focus rectangle just inside the border.
  9. Finally, set the return result to CDRF_SKIPDEFAULT to tell the default window procedure (from comctl32.dll) not to draw the button (since my code already has).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)