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:
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:
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.
- 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).
- 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);
}
- 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;
- 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:
- 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).
- 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
.
- Get a handle to the XP visual styles theme data by calling
g_xpStyle.OpenThemeData()
.
- 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()
.
- Get the rectangle describing the interior of the button image from
g_xpStyle.GetThemeBackgroundContentRect()
.
- Close the theme handle with
g_xpStyle.CloseThemeData()
.
- 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).
- If
uItemState
includes the flag CDIS_FOCUS
, call DrawFocusRect()
to draw the focus rectangle just inside the border.
- 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).