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

Vista themed Owner-Drawn and Full-Custom Push/Menu/Image Buttons

4.91/5 (43 votes)
7 May 2007Public Domain10 min read 1   7K  
Classes reproducing Vista fading transitions & animated glowing-default-state effects

Introduction

This article describes a set of themed owner-drawn & second set of themed full-custom push buttons. On Vista, they provide visual effects similar to standard push buttons - fading transitions and the subtle glowing default-state effects.

I've refined these for my own apps for awhile now, recently upgraded them for Vista, and figured others might benefit. The button classes are compatible with Windows 98, 2000, XP, and Vista. They are implemented as C++/MFC controls. Standard push buttons, menu dropdown buttons, and image (bitmap or icon) buttons are provided.

Screenshot - example.png

Screenshot - disabled.png

Background

Windows support for themed controls is quite good. Creating custom controls matching system flavors is easy. Vista goes one step further by animating certain controls - notably push buttons. Vista push buttons smoothly transition between states and have subtle glowing default-state buttons to catch the eye. Cool stuff!

Screenshot - default1.png Not Glowing Screenshot - default2.png Glowing

Unfortunately the theme API's don't obviously allow custom controls to match this new behavior. The NM_CUSTOMDRAW technique for custom drawing (see Stephen Steel's CImageButtonWithStyle article) doesn't support the glowing-effect, although other transitions work. However, on Vista the NM_CUSTOMDRAW technique isn't needed for simple themed bitmap/icon buttons. They are supported natively.

Custom Draw

If you really need owner or custom draw however, read on. The included button classes offer a consistent drawing framework across platforms, and replicate Vista's glowing-effects. Examples are a plain push, menu, and image+text buttons.

Just to be clear, these visual effects appear only on Vista. On 98/2k/XP these buttons are like any other. There is no owner-drawn support for checkboxes or radio buttons (they're not animated, so no point).

Since I needed these backwards compatible with Win98, I used VC6 here (horrors!). Fortunately, running Win98 under VirtualPC or VMWare makes such support trivial. It also compiles with VS7/2003 or VS8/2005 fine - just open the DSW project file. VS8 users must remove entries from the RT_MANIFEST resource category beforehand.

David Zhao's visual styles class is used to avoid DLL problems on Win98/2k. You'll also need a platform SDK with the XP themes header files.

Alpha Blending

The AlphaBlend API is used to merge bitmaps of rendered button states. What is alpha blending? References to "alpha" means the degree to which two images are merged. The following equation might help illustrate:

output = (old_pixel * (255-alpha) + new_pixel * alpha)/255

An alpha of zero means the output equals the old pixel. An alpha of 255 means the output equals the new pixel. Other values produce a blended combination of the two. Expand the concept to RGB colors, add image scaling, and you have the AlphaBlend API.

Vista Button Transitions

The theme-API supports five states for push buttons: disabled, normal, hot, defaulted, and pushed. Vista performs transitions between these states at varying speeds. Some are fast - like when pushing a button. Others slower - like fading from the hot or disabled states.

There are also differences depending on how long the mouse has hovered over the button. Such is life. I don't claim to perfectly reproduce Vista, warts and all, but these classes are pretty close. Now to code...

First, we determine the new button state with a prioritized check:

C++
int old_stateid = m_stateid; 
if (!button_enabled) m_stateid = PBS_DISABLED; 
else if (button_pressed) m_stateid = PBS_PRESSED; 
else if (button_hot) m_stateid = PBS_HOT; 
else if (button_default) m_stateid = PBS_DEFAULTED;
else m_stateid = PBS_NORMAL;

If the state changes, then we setup the transition:

C++
if (UseVistaEffects() && (m_stateid != old_stateid)) 
{
    switch (m_stateid) 
    {
    case PBS_HOT : 
        m_transition_tickcount = (old_stateid==PBS_PRESSED) ? 4 : 2;
        break;
    case PBS_NORMAL : 
        m_transition_tickcount = (old_stateid==PBS_HOT) ? 20 : 4;
        break;
    case PBS_PRESSED : 
        m_transition_tickcount = 2; 
        break;
    case PBS_DEFAULTED : 
        m_transition_tickcount = (old_stateid==PBS_HOT) ? 20 : 2; 
        break;
    default : m_transition_tickcount = 4; break; 
    }
    m_transition_tickscale = 250/m_transition_tickcount;
    // Get snapshot of button in old state...
    CDCBitmap tempDC(dc,m_oldstate_bitmap);
    g_xpStyle.DrawThemeParentBackground(m_hWnd, tempDC.GetSafeHdc(), &rc);
    g_xpStyle.DrawThemeBackground (
        hTheme, tempDC.GetSafeHdc(), BP_PUSHBUTTON, 
        old_stateid, &rc, NULL);
}

The tickcount variable holds the number of 50ms timer ticks required. The tickscale variable when multiplied by tickcount, provides the 0 to 250 range "alpha" for merging bitmaps. We also record a snapshot of the old button state here. CDCBitmap is a helper class for drawing into bitmaps.

Next, the new button state background is rendered:

C++
// Draw themed button background...
if (g_xpStyle.IsThemeBackgroundPartiallyTransparent(hTheme, BP_PUSHBUTTON, 
    m_stateid))
{
    g_xpStyle.DrawThemeParentBackground(m_hWnd, mDC.GetSafeHdc(), &rc);
}
g_xpStyle.DrawThemeBackground (
    hTheme, mDC.GetSafeHdc(), BP_PUSHBUTTON,  
    m_stateid, &rc, NULL);
// Get content rectangle...
CRect border(rc);
g_xpStyle.GetThemeBackgroundContentRect (
    hTheme, mDC.GetSafeHdc(), BP_PUSHBUTTON, m_stateid, 
    &border, &border);

Standard stuff. Nothing new there.

Lastly, AlphaBlend the old with the new:

C++
if (UseVistaEffects() && (m_transition_tickcount>0)) 
{
    CDCBitmap tempDC(dc,m_oldstate_bitmap);
    BLENDFUNCTION bf;
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = m_transition_tickcount*m_transition_tickscale;
    bf.AlphaFormat = 0; // AC_SRC_ALPHA;
    AlphaBlend(mDC.GetSafeHdc(), rc.left, rc.top, rc.Width(), rc.Height(), 
        tempDC.GetSafeHdc(), rc.left, rc.top, rc.Width(), rc.Height(), bf);
}

Tada! Mostly its just setting up the BLENDFUNCTION struct for AlphaBlend. Initially the alpha is near 100%, so the old-state dominates visually. As the timer decrements tickcount & forces window refreshes, the alpha reduces to 0% so the new-state prevails. Simple eh?

You might wonder why I'm not using NM_CUSTOMDRAW. It's incompatible with timer based updates, since Windows already provides state transitions. The methods fight - a joy to behold.

Note: The above code only deals button backgrounds. Center content is drawn afterwards (text, menu arrows, bitmaps, etc.)

Vista Default/Focused Buttons

The glowing/pulsing default button is the neatest effect. Unfortunately the theme-API doesn't know about it. Doh. However, its apparently just a combination of the default & hot states.

We'll reproduce the effect as follows:

  • Copy the default-state image to a temporary bitmap.
  • Thicken the button border with a 50% AlphaBlend from the temp bitmap.
  • Render the hot button state to the temp bitmap.
  • AlphaBlend the content area from the temp bitmap.

The AlphaBlend steps are alpha-scaled over a two-second interval providing the pulsing effect.

C++
if (UseVistaEffects() && (m_transition_tickcount==0) && 
    (m_stateid == PBS_DEFAULTED)) 
{
    // Copy "default" button state...
    CDCBitmap tempDC(dc,rc);
    AlphaBlt (tempDC, rc, mDC, rc, 255); // lazy bitblt srccopy
    // Compute "glow" alpha...  0->250->0 over 40 ticks.
    int alpha = (int)(m_defaultbutton_tickcount*12.5);
    if (m_defaultbutton_tickcount>=20) alpha = 500-alpha;
    // Thicken content border...
    CRect rect(border);
    rect.InflateRect(1,1);
    AlphaBlt (mDC, border, tempDC, rect, alpha/2);
    // Render hot button state...
    g_xpStyle.DrawThemeParentBackground(m_hWnd, tempDC.GetSafeHdc(), &rc);
    g_xpStyle.DrawThemeBackground (
        hTheme, tempDC.GetSafeHdc(), BP_PUSHBUTTON, PBS_HOT, &rc, NULL);
    // Blend the hot-state content area (avoiding thick border)...
    border.DeflateRect(1,1);
    AlphaBlt (mDC, border, tempDC, border, alpha);
    border.InflateRect(1,1);
}

Here the AlphaBlend function was moved off into a standalone function AlphaBlt cleaning things up. The timer cycles tickcount from 0 to 40 endlessly, from which we compute the alpha. Ticks 0 to 19 become alpha 0 to 237, and ticks 20 to 40 become alpha 250 to 0.

Using the code

To evaluate the Vista Effects, replace or subclass any push-button instance of CButton with the owner-drawn CButtonVE. Add the source files to your project and you're in business. The following classes are provided:

  • CButtonVE / CButtonVE2 - Owner drawn & full-custom push button.
  • CButtonVE_Menu / CButtonVE2_Menu - Owner drawn & full-custom menu button.
  • CButtonVE_Image / CButtonVE2_Image - Owner drawn & full-custom image button (bitmap or icons).

All provide the following content control functions:

  • SetOwner - Specify window to receive button clicks (& menu button commands). Defaults to parent.
  • SetContentHorz - Specify horizontal alignment for button image/text content (ModifyStyle can be used also).
  • SetContentVert - Specify vertical alignment for button image/text content (ModifyStyle can be used also).
  • SetContentMargin - Specify spacing between button border & content.
  • SetBackgroundColor - Force button background color (defaults to polling parent with WM_CTLCOLOR).

The menu button classes add the following setup/notify functions:

  • SetMenu - Preload a menu from resource ID or CMenu (can be changed with following functions before display).
  • AddMenuItem - Add a menu item manually (can append to loaded menu resource).
  • RemoveMenuItem - Remove a menu item (can remove items from a loaded menu resource).
  • RemoveAllMenuItems - Removes all menu items.
  • NotifyMenuPopup - Called before menu appears for dynamic updating (default polls owner for UI updates).

The image button classes provide these:

  • SetImagePosition - Specify position of image relative to text (left, right, above, or below the text).
  • SetImageSpacing - Specify spacing between image and text.
  • SetImageShadow - Control if Gaussian blurred drop shadow shown under images.
  • SetTransparentColor - Specify bitmap background color. Default is the upper-left corner pixel.
  • SetHotImage - Specify a bitmap or icon to display when the button is hot (mouse hovering over it).
  • SetDisabledImage - Specify a bitmap or icon to display when the button is disabled. By default the image button creates a shaded version of the source image.

Adding Owner Drawn Buttons

Add a standard button in the dialog editor, setting the "owner draw" style. Add a control type member variable (subclassing it), and replace the "CButton" header instance with your choice of button class.

Screenshot - ownerdraw.png

Adding Full Custom Buttons

Add a custom control in the dialog editor, and specify the desired "Class" name in the properties. ie:

Screenshot - custctrl.png

In your WM_INITDIALOG handler, you'll need to configure font & window text for the full-custom controls (see demo code).

Customizing Button Content

To customize button drawing, derive a new class & replace "DrawContent". The CButtonVE framework handles the background & transition effects.

Several parameters provide useful info for drawing, including three different rectangle coordinates: the button outline, the safe content border, and the recommended text rectangle. If hTheme is valid, using the theme-API's whenever possible is recommended. The uistate mask controls hiding of focus & accelerator marks.

C++
virtual void DrawContent (
                          CDC &dc,         // Drawing context
                          HTHEME hTheme,   // Vista theme (if available). 
                              // Use g_xpStyle global var for drawing.
                          int uistate,     // Windows keyboard/mouse ui styles.
                              // If UISF_HIDEACCEL set, hide underscores.
                          CRect rclient,   // Button outline rectangle.
                          CRect border,    // Safe content rectangle.
                          CRect textrc,    // Text rectangle.
                          int text_format, // DrawText API formatting.
                          BOOL enabled)    // Set if button enabled.

The image buttons (for example) just override this one function drawing within the "textrc" coordinates.

Points of Interest

Two versions of each button are provided. Owner-drawn (VE) and a fully custom (VE2) implemented button.

Why both you ask? Owner drawn buttons are great! They simplify life in a number of ways (despite the headache mentioned later). However, there is a singular show stopper. In order to draw itself, a child window depends on the parent cooperating & reflecting back messages. Thus the "owner" part of "owner-drawn". This also applies to NM_CUSTOMDRAW.

Some parent windows don't reflect notify messages, such as CFileDialog (GetOpenFileName/GetSaveFileName). For these uncooperative parents, you can't use the owner draw or NM_CUSTOMDRAW approaches. Deriving from CButton and replacing the WM_PAINT handler doesn't work, because Windows repaints button controls outside of WM_PAINT on button clicks. Therefore the full-custom push buttons derived from CWnd option.

The Owner Draw Headache

One irritating problem with owner draw buttons is worth mentioning. In dialogs, Windows tracks the "default" button - the one "clicked" when pressing Enter. The default button is drawn with a heavy border.

Windows tracks the "default" state by querying a window with WM_GETDLGCODE. Push buttons should return DLGC_DEFPUSHBUTTON (if the default) or DLGC_UNDEFPUSHBUTTON (if not). Failing to do so means you can't become the "default" as far as Windows is concerned. OK, simple enough.

Now the irritating problem. Returning the values in WM_GETDLGCODE has a side effect - they make Windows remove the BS_OWNERDRAW button style! Huh? Windows sends a BS_SETSTYLE message setting the BS_DEFPUSHBUTTON style on default buttons, and BS_DEFPUSHBUTTON is mutually exclusive to the owner draw style. Doh. Fortunately, its possible to override BS_SETSTYLE and restore owner draw. Doing so though means losing the default state. Nice.

My workaround involves tracking the default state locally. A BS_SETSTYLE handler records when Windows specifies BS_DEFPUSHBUTTON, then forces owner draw instead. The recorded state is used for drawing and returning the correct value in WM_GETDLGCODE. Works great for both keyboard navigation & mouse clicks! Subsequent research found Paolo Messina's COddButton article which solves the problem similarly.

Customized File Dialog Demo

The CFileDialogVE demo class (opened with the "Normal Push" button) shows use of the custom image button:

Screenshot - filedialog.png

An OnInitDialog handler creates an instance of CButtonVE2_Image, and adjusts the dialog size so its visible:

C++
// Create & setup full-custom image button...
CButtonVE2_Image *btn = new CButtonVE2_Image(); // note: self deletes 
btn->Create(_T("Custom Drawn Bitmap\n with Glow"), 
    WS_VISIBLE|WS_CHILD|WS_TABSTOP|BS_MULTILINE, 
    rve, CWnd::FromHandle(ofn_hWnd), CUSTOM_IMAGEBTN_ID);

HBITMAP hBitmap = (HBITMAP)(LoadImage(theApp.m_hInstance, 
    MAKEINTRESOURCE(IDB_BITMAP1), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR));
btn->SetBitmap(hBitmap);

// Update dialog size...  
CRect rw; 
::GetWindowRect(ofn_hWnd,&rw);
int adjust = rve.bottom-rcombo.bottom;

// tweak height if no places bar visible...
if (m_ofn.lStructSize == OPENFILENAME400SIZE) adjust -= rcombo.Height();
::SetWindowPos(ofn_hWnd, NULL, 0, 0, rw.Width(), rw.Height()+adjust, 
    SWP_NOMOVE|SWP_NOZORDER);

CRect rve holds the button coords, rcombo the coords of the type combobox, and ofn_hWnd the file dialog handle. The dialog size adjust depends on if the places bar is visible (detected by size of the structure).

Vista of course, already provides limited customization support in its new file dialogs (see Michael Dunn's Vista File Dialogs article).

Other Goodies

The code includes various useful routines. Supporting the Vista 9pt Segoe UI font, and 8pt MS Sans Serif for 98/2k/XP is handled by CFontOccManager (see ButtonVE_demo.cpp). Someone posted it on MSDN forums, but its too good to languish there. If running Vista, it queries SystemParametersInfo and initializes a LOGFONT, then computes the correct scaling for an MFC CDialogTemplate.

The image buttons runtime compute disabled & normal images supporting a transparent background color. For those who enjoy BitBlt, see DrawDisabledImage, DrawTransparentImage, and DrawBluredShadowImage (in ButtonVE_Helper.h). Monochrome bitmaps are created to mask off the background, then images are drawn to an output context.

Enjoy!

Copyright and License

This article is Copyright © 2007 by Ian E Davis. The demo code and source code accompanying this article are hereby released to the public domain.

History

  • April 24th, 2007 - First release. My first Code Project article!
  • April 27th, 2007 - Patched problems found by Hans Dietrich (themed WinXP default state on full-custom buttons), and Jerry Evans (uistate not handled properly). Also added better Enter handling in full-custom buttons.
  • May 4th, 2007 - Added support for BS_PUSHLIKE style for toggle pushbuttons (query with normal GetCheck), Gaussian blurred image drop shadows, and hot image hover support. Also added WM_CTLCOLOR polling for background color. Fixed rare and difficult to reproduce visual flicker with animated default button.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication