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.
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_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
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)
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.
- Recompile and you're done.
If you want to use
CImageButtonWithStyle in other situations, you will have to call
SubclassWindow() to associate the
CImageButtonWithStyle instance with a particular control (unless you create the control dynamically by calling the
Create() member function from a
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.
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_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
- Get a handle to the XP visual styles theme data by calling
- 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_NORMAL, and then draw it using
- Get the rectangle describing the interior of the button image from
- Close the theme handle with
- Use the button style bits
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
uItemState includes the flag
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).