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

Vista Goodies in C++: Using Glass in Your UI

4.86/5 (99 votes)
29 Dec 200611 min read 2   4.2K  
How to add glass to your application's main window.

Contents

Introduction

This is the first article in a series that will demonstrate how to use various new features of Vista from native C++ code. The sample code is built with Visual Studio 2005, WTL 7.5, and the Windows SDK. I've classified these articles as Intermediate because I won't be covering the basics of Win32 APIs and WTL. See my series of articles on WTL if you need to get up to speed on WTL. I also won't be going step-by-step through the Visual Studio wizards, which were also covered in the WTL series. (Those articles show the wizards in VC 2003, but the wizards in 2005 are similar.)

The Aero theme and glass effects, along with the desktop window manger (DWM), are major new features in Vista that Microsoft is pushing heavily. Here in this first article, I'll demonstrate how to use Aero glass in a frame window-based app and a dialog-based app. Incorporating glass into your app is one way to make it distinctive (and, let's face it, look cool) when the Aero theme is enabled.

Glass in the Aero Theme

When Aero is the active theme, and Vista determines that your video card can handle it, the desktop is drawn using the DWM. DWM renders the desktop using a process called composition. The DWM automatically uses Aero theme elements in the non-client area of top-level windows. (This is similar to how XP automatically themes top-level windows.) This does not always add the glass effects, though; if the computer is running on batteries, or the user just decides to turn transparency off, the non-client areas will not be glass:

Image 1

If you do enable transparent glass in the Personalization|Visual Appearance Control Panel applet, then the non-client areas will be transparent:

Image 2

Notice how the frame has a green hue (that's the wallpaper showing through), and a couple of desktop icons are visible in the caption bar.

The key thing to remember is that your code only has to worry about whether composition is enabled, not what the glass settings are, because the DWM handles drawing the glass itself.

Starting the Project

The first sample program is an SDI app with no view window, toolbar, or status bar. After running the WTL AppWizard, the first thing we need to do is set up the #defines in stdafx.h so we can use Vista features. Vista is Windows version 6, and the IE version in Vista is 7, so the beginning of stdafx.h should look like this:

#define WINVER         0x0600
#define _WIN32_WINNT   0x0600
#define _WIN32_IE      0x0700

Then we include the ATL and WTL header files:

#define _WTL_NO_WTYPES // Don't define CRect/CPoint/CSize in WTL headers
 
#include <atlbase.h>
#include <atltypes.h>   // shared CRect/CPoint/CSize
#include <atlapp.h>
extern CAppModule _Module;
#include <atlwin.h>
#include <atlframe.h>
#include <atlmisc.h>
#include <atlcrack.h>
#include <atltheme.h>   // XP/Vista theme support
#include <dwmapi.h>     // DWM APIs

If you make these changes and compile now, you'll get four errors in atltheme.h. For example, here is the code for CTheme::GetThemeTextMetrics() which won't compile:

HRESULT GetThemeTextMetrics(..., PTEXTMETRICW pTextMetric)
{
  ATLASSERT(m_hTheme != NULL);
 
  // Note: The cast to PTEXTMETRIC is because uxtheme.h
  // incorrectly uses it instead of PTEXTMETRICW
  return ::GetThemeTextMetrics(m_hTheme, ..., (PTEXTMETRIC) pTextMetric);
}    

The cast in the call to the GetThemeTextMetrics() API is a workaround for a mistake in uxtheme.h in the Platform SDK. However, the Windows SDK does not have this mistake, so the cast causes an error. You can remove the cast in that function and the other three that have the same workaround.

Adding Glass to the Frame

Adding glass is done by extending the glass effect from the non-client area into the client area. The API that does this is DwmExtendFrameIntoClientArea(). DwmExtendFrameIntoClientArea() takes two parameters, the HWND of our frame window, and a MARGINS struct that says how far the glass should be extended on each of the four sides of the window. We can call this API in OnCreate():

LRESULT CMainFrame::OnCreate(LPCREATESTRUCT lpcs)
{
  // frame initialization here...
 
  // Add glass to the bottom of the frame.
MARGINS mar = {0};
 
  mar.cyBottomHeight = 100;
  DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
 
  return 0;
}

If you run this code, you won't notice any difference:

Image 3

This happens because the glass effect relies on the transparency of the window being correct. In order for the glass to appear, the pixels in the region (in this case, 100 pixels at the bottom of the client area) must have their alpha values set to 0. The easiest way to do this is to paint the area with a black brush, which sets the color values (red, green, blue, and alpha) of the pixels to 0. We can do this in OnEraseBkgnd():

BOOL CMainFrame::OnEraseBkgnd ( HDC hdc )    
{
CDCHandle dc = hdc;
CRect rcClient;
 
  GetClientRect(rcClient);
  dc.FillSolidRect(rcClient, RGB(0,0,0));
 
  return true;
}    

With this change, the frame window looks like this:

Image 4

The bottom 100 pixels are now glass!

Adding Text to the Glass Area

Adding glass to the window is the easy part, adding your own UI on top of the glass is a bit trickier. Since the alpha values of the pixels have to be maintained properly, we have to use drawing APIs that understand alpha and set the alpha values properly. The bad news is that GDI almost entirely ignores alpha - the only API that maintains it is BitBlt() with the SRCCOPY raster operation. Therefore, apps have to use GDI+ or the theme APIs for drawing, since those APIs were written with alpha in mind.

A common use of glass in the apps that ship with Vista is for a status area (replacing the status bar common control). For example, Windows Media Player 11 shows the play controls and current track information in the glass area at the bottom of the window:

Image 5

In this section, I'll demonstrate how to draw text on the glass area, and how to add the glow effect so the text is readable against any background.

Using the Right Font

Vista has broken away from the old look of MS Sans Serif and Tahoma, and now uses Segoe UI as the default UI font. Our app should also use Segoe UI (or whatever other fonts might come in the future), so we create a font based on the current theme. If themes are disabled (for example, the user is running the Windows Classic color scheme), then we fall back to the SystemParametersInfo() API.

We'll first need to add theme support to CMainFrame. This is pretty simple since WTL has a class for dealing with themes: CThemeImpl. We add CThemeImpl to the inheritance list, and chain messages to CThemeImpl so that code can handle notifications when the active theme changes.

class CMainFrame :
  public CFrameWindowImpl<CMainFrame>,
  public CMessageFilter,
  public CThemeImpl<CMainFrame>
{
  // ...
 
  BEGIN_MSG_MAP(CMainFrame)
    CHAIN_MSG_MAP(CThemeImpl<CMainFrame>)
    // ...
  END_MSG_MAP()
 
protected:
  CFont m_font;  // font we'll use to draw text
};    

In the CMainFrame constructor, we call CThemeImpl::SetThemeClassList(), which specifies the window class whose theme we'll be using. For plain windows (that is, windows that are not common controls), use the name "globals":

CMainFrame::CMainFrame()
{
  SetThemeClassList ( L"globals" );
}    

Finally, in OnCreate(), we can read the font info from the theme and create a font for our own use:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // ...
 
  // Determine what font to use for the text.
LOGFONT lf = {0};
 
  if ( !IsThemeNull() )
    GetThemeSysFont ( TMT_MSGBOXFONT, &lf );
  else
    {
    NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) };
  
    SystemParametersInfo (
        SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS),
        &ncm, false );
 
    lf = ncm.lfMessageFont;
    }
 
  m_font.CreateFontIndirect ( &lf );
 
  return 0;
}

Drawing the Text

Drawing text on glass involves these steps:

  1. Create a memory DC like you would with double-buffered drawing.
  2. Create a 32-bpp DIB and select it into the DC.
  3. Draw the text onto the in-memory DIB with DrawThemeTextEx().
  4. Copy the text to the screen with BitBit().

Since our drawing code will be different depending on whether composition is enabled, we'll need to check the composition state during the drawing process. The API that checks the state is DwmIsCompositionEnabled(). Since that API can fail, and the enabled state isn't indicated in the return value, CMainFrame has a wrapper called IsCompositionEnabled() that is easier to use:

bool CMainFrame::IsCompositionEnabled() const
{
HRESULT hr;
BOOL bEnabled;
 
  hr = DwmIsCompositionEnabled(&bEnabled);
  return SUCCEEDED(hr) && bEnabled;
}    

Now let's go through OnEraseBkgnd() and see how each step is done. Since this app is a clock, we first get the current time with GetTimeFormat().

BOOL CMainFrame::OnEraseBkgnd(HDC hdc)
{
CDCHandle dc = hdc;
CRect rcClient, rcText;
 
  GetClientRect ( rcClient );
  dc.FillSolidRect ( rcClient, RGB(0,0,0) );
 
  rcText = rcClient;
  rcText.top = rcText.bottom - 100;
 
  // Get the current time.
TCHAR szTime[64];
 
  GetTimeFormat ( LOCALE_USER_DEFAULT, 0, NULL, NULL,
                  szTime, _countof(szTime) );

If composition is enabled, then we'll do the composited drawing steps. We first set up a memory DC:

if ( IsCompositionEnabled() )
  {
  // Set up a memory DC and bitmap that we'll draw into
  CDC dcMem;
  CBitmap bmp;
  BITMAPINFO dib = {0};

  dcMem.CreateCompatibleDC ( dc );

Next, we fill in the BITMAPINFO struct to make a 32-bpp bitmap, with the same width and height as the glass area. One important thing to note is that the bitmap height (the biHeight member of the BITMAPINFOHEADER) is negative. This is done because normally, BMPs are stored in bottom-to-top order in memory; however, DrawThemeTextEx() needs the bitmap to be in top-to-bottom order. Setting the height to a negative value does this.

dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
dib.bmiHeader.biWidth = rcText.Width();
dib.bmiHeader.biHeight = -rcText.Height();
dib.bmiHeader.biPlanes = 1;
dib.bmiHeader.biBitCount = 32;
dib.bmiHeader.biCompression = BI_RGB;

bmp.CreateDIBSection ( dc, &dib, DIB_RGB_COLORS,
                       NULL, NULL, 0 );

Now that our graphics objects are created, we can draw the text.

// Set up the DC
dcMem.SelectBitmap ( bmp );
dcMem.SelectFont ( m_font );

// Draw the text!
DTTOPTS dto = { sizeof(DTTOPTS) };
const UINT uFormat = DT_SINGLELINE|DT_CENTER|DT_VCENTER|DT_NOPREFIX;
CRect rcText2 = rcText;

dto.dwFlags = DTT_COMPOSITED|DTT_GLOWSIZE;
dto.iGlowSize = 10;
rcText2 -= rcText2.TopLeft(); // same rect but with (0,0) as the top-left

DrawThemeTextEx ( m_hTheme, dcMem, 0, 0, CT2CW(szTime), -1,
                  uFormat, rcText2, &dto );

The DTTOPTS struct controls how the text is drawn. The flags indicate that we're drawing composited text, and we want the text to have a glow effect added. Finally, we blit from the in-memory bitmap to the screen:

// Blit the text to the screen.
BitBlt ( dc, rcText.left, rcText.top, rcText.Width(), rcText.Height(),
         dcMem, 0, 0, SRCCOPY );
}  // end if (IsCompositionEnabled())

If composition isn't enabled, we draw the text with GDI calls:

  else
    {
    const UINT uFormat = DT_SINGLELINE|DT_CENTER|DT_VCENTER|DT_NOPREFIX;
 
    // Set up the DC
    dc.SetTextColor ( RGB(255,255,255) );
    dc.SelectFont ( m_font );
    dc.SetBkMode ( TRANSPARENT );
 
    // Draw the text!
    dc.DrawText ( szTime, -1, rcText, uFormat );
    }
 
  return true;  // we drew the entire background
}

Here's what the composited text looks like:

Image 6

Just to illustrate the usefulness of the glow effect, here's the text against the same background, but without the glow:

Image 7

Handling Composition-Related Notifications

When DWM composition is enabled or disabled, the system broadcasts a WM_DWMCOMPOSITIONCHANGED message to all top-level windows. If composition is being turned on, we need to call DwmExtendFrameIntoClientArea() again to tell the DWM what part of our window should be glass:

LRESULT CMainFrame::OnCompositionChanged(...)
{
  if ( IsCompositionEnabled() )
    {
    MARGINS mar = {0};
 
    mar.cyBottomHeight = 100;
    DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
    }
 
  return 0;
}

Using Glass in a Dialog-Based App

The process for adding glass to a dialog is similar to the frame window case, but there are a few differences that require some slightly different code. The sample dialog-based app adds glass to the top of the window; in the text below, code that is changed or added compared to the previous sample is shown in bold.

Setting Up the Dialog

As before, we tell CThemeImpl which window class theme to use, and call DwmExtendFrameIntoClientArea() to add glass to the window frame.

CMainDlg::CMainDlg()
{
  SetThemeClassList ( L"globals" );
}
 
BOOL CMainDlg::OnInitDialog ( HWND hwndFocus, LPARAM lParam )
{
  // (wizard-generared init code omitted)
 
  // Add glass to the top of the window.
  if ( IsCompositionEnabled() )
    {
    MARGINS mar = {0};
 
    mar.cyTopHeight = 150;
    DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
    }

Notice that we need to explicitly call OpenThemeData(). We didn't need to call it in the frame window example because CThemeImpl calls it in its WM_CREATE handler. Since dialogs receive WM_INITDIALOG instead, and CThemeImpl doesn't handle WM_INITDIALOG, we need to call OpenThemeData() ourselves.

Next, we construct the font to use for the text. We also make the font larger, just to show how the glow looks on larger text.

  // Determine what font to use for the text.
LOGFONT lf = {0};
 
  OpenThemeData();
 
  if ( !IsThemeNull() )
    GetThemeSysFont ( TMT_MSGBOXFONT, &lf );
  else
    {
    NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) };
 
    SystemParametersInfo (
        SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS),
        &ncm, false );
 
    lf = ncm.lfMessageFont;
    }
 
  lf.lfHeight *= 3;
  m_font.CreateFontIndirect ( &lf );

The dialog has a large static text control at the top of the window, which is where we'll draw the time. This code sets the owner-draw style on the control, so we can put all our text-drawing code in OnDrawItem().

  // Set up the owner-draw static control
  m_wndTimeLabel.Attach ( GetDlgItem(IDC_CLOCK) );
  m_wndTimeLabel.ModifyStyle ( SS_TYPEMASK, SS_OWNERDRAW );

Finally, we call EnableThemeDialogTexture() so the dialog's background is drawn using the current theme.

  // Other initialization
  EnableThemeDialogTexture ( ETDT_ENABLE );
 
  // Start a 1-second timer so we update the clock every second.
  SetTimer ( 1, 1000 );
 
  return TRUE;
}

Enabling Glass

As before, we need to fill the glass area with a black brush so the glass shows through. Since the built-in dialog window proc draws the dialog's background in response to WM_ERASEBKGND, and handles details like non-square or semi-transparent controls, we need to do our painting on OnPaint() instead of OnEraseBkgnd().

void CMainDlg::OnPaint ( HDC hdc )
{
CPaintDC dc(m_hWnd);
CRect rcGlassArea;
 
  if ( IsCompositionEnabled() )
    {
    GetClientRect ( rcGlassArea );
 
    rcGlassArea.bottom = 150;
    dc.FillSolidRect(rcGlassArea, RGB(0,0,0));
    }
}

Drawing the Text

In OnTimer(), we get the current time, then set the static control's text to that string:

void CMainDlg::OnTimer ( UINT uID, TIMERPROC pProc )
{
  // Get the current time.
TCHAR szTime[64];
 
  GetTimeFormat ( LOCALE_USER_DEFAULT, 0, NULL, NULL,
                  szTime, _countof(szTime) );
 
  m_wndTimeLabel.SetWindowText ( szTime )
}

The SetWindowText() call makes the static control redraw, which results in a call to OnDrawItem(). The code in OnDrawItem() is just like the frame window example, so I won't repeat it here. Here's what the clock looks like:

Image 8

Drawing Graphics on Glass

As mentioned earlier, any drawing on the glass area needs to use alpha-aware APIs such as GDI+. The sample project uses the GDI+ Image class to draw a logo in the top-left corner of the dialog, as shown here:

Image 9

The logo is read from the mylogo.png file in the same directory as the EXE. Notice that the alpha transparency around the logo is preserved, since the code uses GDI+ to draw the logo.

Making the Entire Window Glass

Another option is to make the entire window glass. There is a shortcut for this, just set the first member of the MARGINS struct to -1:

MARGINS mar = {-1};
 
  DwmExtendFrameIntoClientArea ( m_hWnd, &mar );    

If we did this in our dialog, the results wouldn't be that good:

Image 10

Notice how the text in the four buttons is the wrong color, and there's an opaque rect around each button. In general, transparency and child windows don't mix very well. If you do want an all-glass dialog, the parts with controls should be drawn with an opaque background, as in the Mobility Center app:

Image 11

Conclusion

Adding glass to your apps is a good way to make them visually distinctive, and it can provide a richer status area than what can be accomplished with the status bar common control. This article should give you a good starting point, and an understanding of the DWM APIs that you'll use when adding glass to an app written in native C++.

References

Much of this info was gleaned from the PRS319 session at PDC 2005 ("Building Applications That Look Great in Windows Vista").

I only discovered it after finishing the article, but Kenny Kerr has a huge blog post covering many glass topics. Check out his whole Vista for Developers series, too, they're well worth the time.

Copyright and License

This article is copyrighted material, ©2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

  • October 2, 2006: Article first published.
  • October 6, 2006: Added link to Kenny Kerr's blog post on using glass.
  • December 26, 2006: Code tested on the RTM Vista build, intro updated accordingly. Minor wording changes for clarity.

Series navigation: Monitoring the Computer's Power Status »

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