Without a doubt, one of the single most appealing new features of Windows Vista is the new desktop composition engine, dubbed Aero Glass. Aero Glass is enabled if the computer running Vista has a DirectX 9 capable graphics adapter. It allows for the fancy translucency effect that shows the windows that are ordered behind a window as a blurred background image. However, one problem with Aero Glass is that if you want to use it in your application, your application won't run on older Windows versions such as Windows XP or Windows 2000. That is, unless you go to great lengths with delay-loading of the DLLs that implement Aero Glass and carefully implement different code paths for the different operating system versions that your application is supposed to run on. However, an even bigger obstacle is the fact that traditional GDI- and USER-based applications don't play nicely together with Aero Glass. This point will be discussed later.
All of this means that the majority of old applications written in C/C++ cannot easily use Aero Glass in their UI. For those of us who, like me, still have to stick with VC6 at work, using Aero Glass in our applications is next to impossible. This is because in order to use it you have to link against new import libraries of the Vista SDK that the VC6 linker cannot use without problems.
The goal of this article and the accompanied source code is therefore to help alleviate a lot of these problems. In fact, this article's source code is more or less development system agnostic: The sample application for this article has been developed not only with Visual Studio 2005, but also with VC6 and the two latest Platform SDKs that are compatible with VC6 (the XP SP2 Platform SDK and the W2K3 Server Platform SDK). The x64 build can be compiled with VC6 and the x64 headers and libraries that come with the W2K3 Server SP1 PlatSDK. It goes without saying that a Visual Studio 2005 solution and workspace for both x86 and x64 builds -- as well as for ANSI- and Unicode-based builds -- is included also. All build targets for all platforms, all character set representations (ANSI vs. Unicode) and all compiler versions should build cleanly without errors or warnings arising under the highest warning levels.
One particularly nice thing about the binaries you can create using this article's source code, is that they still run on older Windows versions. You can simply build the demo application that comes with this article and run it on Vista with Aero Glass enabled, as well as on Windows XP or 2000. The only difference is that on the older operating system versions you will have the standard window background instead of a translucent background.
The problem with GDI and USER based controls on a translucent background
The image at the very top of this article shows this article's sample application, an MFC-based dialog application. It has the standard control types that we have all known since the advent of Windows 95 and its then-new common controls, plus the additions to the common controls that came with Internet Explorer 4. Now consider how this application would look if we naively only enabled the background of the window to have the Aero Glass translucency effect:
Ugly, huh? The problem with GDI and USER is that neither of these system DLLs know anything about the alpha channel that Aero Glass uses. What's even worse is that Aero considers black as being its transparent color. This is why, in the ugly screenshot above, you see the background image shining through where black text is supposed to appear. From this screenshot, it should be clear that the approach of just applying a translucent background to a window will fail with traditional child window controls. However, the code in this article strives to make it really easy to apply Aero Glass to such a window. It subclasses the window and all of its child windows with a single function call. In this way, it applies Aero Glass to the window itself and its child controls in one fell swoop. The function you have to call on a per-window level -- per instance of a window, that is -- has the following prototype:
BOOL AeroAutoSubclass(HWND hWnd, DWORD dwFlags, DWORD dwReserved);
The hWnd
parameter is the window handle to the window itself. The parameter dwFlags
specifies which types of the window's controls the function should subclass. It also determines whether Aero Glass should be applied to the whole window or if you instead want to be in control of applying the translucent portion to the window yourself. In the easiest case, as in the screenshot at the very top of this article, you would call this function like so, in an MFC based and CDialog
-derived window:
BOOL CAutoaeroDlg::OnInitDialog()
{
CDialog::OnInitDialog();
if(!AeroAutoSubclass(m_hWnd, ASC_SUBCLASS_ALL_CONTROLS, 0L))
{
TRACE(_T("Failed to autosubclass with %lu\n"), GetLastError());
}
return TRUE;
}
Problematic controls
All of the controls that you can see in the screenshot at the very top have been specifically sub-classed in order to be displayed nicely with a partially translucent background. Some implementations in this source code's article go to great lengths in order to look good on Aero Glass. A prime example is the sub-classing code for the animation control, which allows you to show an animation on a translucent background without flickering, using the low-level Video for Windows routines and GDI+. Other controls, such as the listbox control, are mere wrappers around a call to SendMessage(..., WM_PRINTCLIENT,...)
in their sub-classing code and their handler for WM_PAINT
. However, two of the standard control types are somewhat problematic, namely the MonthCalendar control and the RichText control:
The MonthCalendar control uses animation effects if you drill up or down calendar dates. During and after the animation, the black text on the control will always let the background windows shine through because, as we already know by now, black is considered the alpha channel. I found no way to control this behaviour because there are no documented ways to either turn off the animations or to be notified when the animations start and stop.
The other problematic control, the RichText control, simply doesn't support the WM_PRINTCLIENT
message. Most of the other control sub-classing code uses it in order to render the control in the paint buffers that the desktop composition engine uses. In order to demonstrate how these problematic controls can be used anyway, the demo application has the two buttons labeled "MonthCal" and "RichEdit." If you press these buttons, dialogs will appear that demonstrate how these controls can be used in a parent window that otherwise is completely translucent:
The idea behind these two dialogs is that the translucent area of the window encompasses the entire client area of the window, except for the area that the MonthCalendar control or the RichEdit control uses. However, because the translucency of the window is expressed using pixel values that the translucent area grows from the border into the client area, you are limited to only one such problematic control per window. That means you cannot have multiple RichEdit controls on one window with Aero Glass applied on the window's background. If you really need this or if you want to display a number of controls with Aero Glass applied and another number of controls with the standard battleship grey background, you can use the dialog's technique that it employs if you press the "Mixed controls" button:
As you can see, there are a number of controls outside the gray background area which have a translucent background. A few others are inside a rectangle where a standard background appears. The code that does all this looks like the following:
const UINT g_dwAeroCtrlIds[] =
{
IDOK,
IDCANCEL,
IDC_STATIC_GLASS,
IDC_STATIC_GLASS2,
IDC_STATIC_ICO_GLASS,
IDC_RADIO4,
IDC_RADIO5,
IDC_CHECK1
};
BOOL CMixedCtrlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
if(!AeroAutoSubclass(m_hWnd, ASC_NO_FRAME_EXTENSION, 0L))
{
TRACE(_T("Failed to autosubclass with %lu\n"), GetLastError());
}
else
{
size_t stIdx = 0;
for (;stIdx<dimof(g_dwAeroCtrlIds);stIdx++)
{
HWND hCtrl = ::GetDlgItem(m_hWnd, g_dwAeroCtrlIds[stIdx]);
ASSERT(hCtrl);
VERIFY(AeroSubClassCtrl(hCtrl));
}
}
return TRUE;
}
As you can see, this time AeroAutoSubclass
is called with the flag ASC_NO_FRAME_EXTENSION
. This basically says, "Allow for individual controls to be sub-classed, but don't apply the translucent background to this window or any of its child controls. I will do everything on my own." The code above then loops through an array of resource IDs. You already guessed it: these are the controls outside the grey background. Next, it calls AeroSubClassCtrl
with the appropriate window handles. But how then do you apply the translucent background to the dialog? One way is to designate an area that should not be translucent and that is occupied by an invisible control. This allows you to conveniently move controls around in VC's resource editor. In my case, this control has the ID IDC_STATIC_PLACEHOLDER
. Applying Aero Glass to this window is then accomplished from a single method -- CMixedCtrlDlg::ApplyAeroGlass
, in my case -- of the class implementing this dialog. The method is called from the dialog's WM_PAINT
handler. The next code snippet shows the implementation of this method:
void CMixedCtrlDlg::ApplyAeroGlass(CDC *pDC)const
{
RECT rcPlaceholder;
ASSERT(pDC);
RECT rcClient;
GetClientRect(&rcClient);
MARGINS marGlassInset = {-1, -1, -1, -1};
GetDlgItem(IDC_STATIC_PLACEHOLDER)->GetWindowRect(&rcPlaceholder);
::ScreenToClient(m_hWnd, &rcPlaceholder);
marGlassInset.cxLeftWidth = rcPlaceholder.left;
marGlassInset.cxRightWidth = rcClient.right - rcPlaceholder.right;
marGlassInset.cyBottomHeight = rcClient.bottom - rcPlaceholder.bottom;
marGlassInset.cyTopHeight = rcPlaceholder.top;
CDwmApiImpl dwmApi;
if (dwmApi.Initialize() && dwmApi.IsDwmCompositionEnabled() &&
SUCCEEDED(dwmApi.DwmExtendFrameIntoClientArea(m_hWnd,
&marGlassInset)))
{
RECT rcScratch = rcClient;
rcScratch.right = marGlassInset.cxLeftWidth;
pDC->PatBlt(rcScratch.left, rcScratch.top, RECTWIDTH(rcScratch),
RECTHEIGHT(rcScratch), BLACKNESS);
rcScratch = rcClient;
rcScratch.bottom = marGlassInset.cyTopHeight;
pDC->PatBlt(rcScratch.left, rcScratch.top, RECTWIDTH(rcScratch),
RECTHEIGHT(rcScratch), BLACKNESS);
rcScratch = rcClient;
rcScratch.left = rcScratch.right - marGlassInset.cxRightWidth;
pDC->PatBlt(rcScratch.left, rcScratch.top, RECTWIDTH(rcScratch),
RECTHEIGHT(rcScratch), BLACKNESS);
rcScratch = rcClient;
rcScratch.top = rcScratch.bottom - marGlassInset.cyBottomHeight;
pDC->PatBlt(rcScratch.left, rcScratch.top, RECTWIDTH(rcScratch),
RECTHEIGHT(rcScratch), BLACKNESS);
}
}
The CDwmApiImpl
class used in the code snippet above implements Vista's desktop window manager API without using its import library. So, it is more or less a bundle of LoadLibrary
/GetProcAddress
calls. If the CDwmApiImpl
object can be initialized and if extending the translucent background using CDwmApiImpl::DwmExtendFrameIntoClientArea
succeeds, the code simply paints filled black rectangles around the place holder control, thereby making the background translucent. The dialogs that show how to deal with the MonthCalendar control and the RichText control work in pretty much the same way.
Using the code and building the code sample
In order to use the code, you need VC6 or Visual Studio 2005. In case you only have Visual Studio 2002 or 2003, I assume that you can simply open the VC6 .dsw file with these development environments. In that case, everything in the following paragraph that applies to VC6 will probably apply to Visual Studio 2002 and 2003, as well. However, I don't know if using the VC6 workspace with these development environments really works, simply because I don't have a copy or a license for these development systems and therefore have never tried it.
You also definitely need the Vista Software Development Kit, which you can download for free. This SDK is needed in any case, because we need a bunch of header files from this SDK, even if we use VC6. If you use VC6, for x86 builds you also need the Windows Server 2003 Platform SDK or the Windows XP SP2 Platform SDK. The directories for these two Platform SDKs also need to be integrated into VC6. For x64 builds with VC6, you need the Windows Server 2003 SP1 Platform SDK.
The usage of this article's code and demo project differ, depending on which configuration your development environment has. I will distinguish the four cases as:
- Visual Studio 2005 with Vista SDK integrated
- Visual Studio 2005 without Vista SDK integrated
- Visual C/C++ 6 x86 Build
- Visual C/C++ 6 x64 Build
Visual Studio 2005 with Vista SDK integrated
If you have this configuration, using this article's source code is a no-brainer. Simply unpack the accompanying .zip file with directory preservation, delete the aerohdr
subdirectory, build and run the demo application.
Visual Studio 2005 without Vista SDK integrated
This is the out-of-the-box configuration of Visual Studio 2005. Unpack the accompanying .zip file with directory preservation and notice the directory aerohdr
s, "Aero headers." In the meantime, you should have installed the Vista SDK on your machine. Now copy the following files from the Vista SDK's include
directory into the aerohdr
s directory:
- uxtheme.h
- vsstyle.h
- vssym32.h
- specstrings.h
I could have easily added these files from the Vista SDK to this article's demo application, but I am not allowed to redistribute these files. It is therefore up to you to download these files and move them to the proper locations. Next, delete the header file sal.h from the aerohdr
subdirectory. Now choose a proper build target, build and run the demo application.
Visual C/C++ 6 x86 Build
First, unpack the accompanied .zip file with directory preservation. I assume that you have either the Windows 2003 Server Platform SDK or the Windows XP SP2 Platform SDK installed and integrated into VC6. For both x86 builds and x64 builds, you now have to copy the same files as in Visual Studio 2005, without the Vista SDK integrated procedure, into the aerohdr
subdirectory (uxtheme.h, vsstyle.h, vssym32.h, specstrings.h). However, this time we will leave the sal.h file in the aerohdr
subdirectory. Now simply start VC6, load the autoaero.dsw workspace file, choose one of the non-x64 targets, build and run the sample.
Visual C/C++ 6 x64 Build
Like before, unpack the accompanied .zip file with directory preservation. For x64 builds using VC6, you first have to download and install the Windows Server 2003 SP1 Platform SDK. This Platform SDK contains the necessary libraries and development tools for x64 binaries. Once downloaded, I suggest creating a batch file with the following content:
call <PLATSDKDIR>\setenv.cmd /X64 /RETAIL
start <MSVC6DIR>\Common\MSDev98\Bin\MSDEV.COM /useenv
In this batch file, <PLATSDKDIR>
stands for the directory where you installed the Server 2003 SP1 Platform SDK. <MSVC6DIR>
stands for your VC6 installation directory. If you fire up this batch file, the VC6 development environment will be started for you with the proper directory settings for header files and libraries, as well as compiler files for the generation of native x64 binaries. However, this will only work if you have one of the latest VC6 Service Packs installed. I therefore suggest that you download and install Visual Studio 6 Service Pack 6, if you haven't done so yet. Next, make sure that you copied the four header files (uxtheme.h, vsstyle.h, vssym32.h, specstrings.h) from the Vista SDK into the aerohdr
s subdirectory. Now choose one of the x64 targets and build an x64 version of the demo application.
What's the futz with this sal.h file?
The sal.h file that I added to the aerohdr
subdirectory makes it possible to use header files from the Vista SDK with compiler versions that don't know anything about SAL annotation, such as the one which ships with VC6. SAL annotation is the decoration that has been added to recent SDK header files. They allow the PREfast tool (aka /analyze option) to do static code analysis. sal.h -- along with specstrings_strict.h and specstrings_adt.h -- gets included implicitly by the specstrings.h header file that we have to copy from the Vista SDK into the aerohdr
directory. The sal.h file that I provide simply contains empty macro definitions for all the of the SAL annotation macros used in the Vista SDK header files we copied to the aerohdr
s subdirectory. Visual Studio 2005 ships with a built-in sal.h file. In that case, you will definitely want to use the official Visual Studio 2005 version of this file and delete the one that I added to the aerohdr
subdirectory.
Retrofitting/adding the code to your own project
When retrofitting the functionality from the sample application to your own code, follow these steps:
- Add a per-project include path to the projects' preprocessor settings to the directory where the unpacked common directory resides.
- Add a per-project include path to the projects' preprocessor settings to the directory where the unpacked
aerohdr
s directory resides. If you use VS2005 with the Vista SDK integrated, you should omit this step. - Add the per-process initialization of GDI+ to your application. See the sample application and its use of the
GdiplusStartup
and GdiplusShutdown
functions if you don't know how to do this. If you plan to make your application work on Windows 2000, as well, and if you don't ship the GDI+ redistributable with your application, delay-load gdiplus.dll in your application. I refrained from internal initialization and uninitialization of GDI+ on a per-function level in the functions that use GDI+. This is because I read in various usenet postings that the initialization/uninitialization of GDI+ doesn't work reliably on a per-call level and should really be a per-process initialization/uninitialization. - Make sure you have a manifest for comctl32.dll version 6.0 added. For VC6, you can do this by adding a manifest file to your resource file like the one in this article's sample application. For VS2005, you can add it with a bunch of
pragma
s, like I did in the sample's stdafx.h file:
#if _MSC_VER>=1400
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32'
name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
processorArchitecture='x86' publicKeyToken='6595b64144ccf1df'
language='*'\"")
#elif defined _M_IA64
#pragma comment(linker,"/manifestdependency:\"type='win32'
name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df'
language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32'
name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df'
language='*'\"")
#else
#pragma comment(linker,"/manifestdependency:\"type='win32'
name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
processorArchitecture='*' publicKeyToken='6595b64144ccf1df'
language='*'\"")
#endif
#endif /// _MSC_VER>=1400
Note that for some obscure reason that really escapes me, new projects created in Visual Studio 2005 don't get a version 6 common controls manifest. That is, unless you choose to create the project with the Unicode version of the runtime or MFC libraries. Using the snippet above works around this problem. - Add all the files from the common directory to your application's project. Set the settings for all of the .cpp files from this directory to "Not using precompiled headers."
- If you use an animation control on a window with Aero Glass using the code from this article, make sure you set the file name or resource ID for the animation -- using
SendMessage(..., ACM_OPEN,...)
, Animate_Open
, Animate_OpenEx
or MFC's CAnimateCtrl::Open(...)
-- after having sub-classed the animation control's parent window using AeroAutoSubclass
or after having sub-classed the animation control using AeroSubClassCtrl
. This is necessary because there is no documented way to retrieve the animation control's animation file name or resource ID after having set it. Therefore, in order to load the animation from secondary storage, the sub-classing code for animation controls needs to intercept either the ACM_OPEN
message or a resource using the Video for Windows routines. - Make the changes to the
aerohdr
s directory as outlined above, depending on your development environment. - Now think long and hard about where it makes sense to add the fancy Aero Glass effect in your application. Note that using an Aero Glass background will not improve the readability of your application's dialogs or windows at all. There is a reason that standard color schemes and standard dialogs show text in black on a white or gray background. This is because the high contrast allows for better readability. Do not use Aero Glass just because you now have a hammer and suddenly everything looks like a nail. I think that Aero Glass will look good on a few selected windows or dialogs, such as those modeless sorts of dialogs with animation controls or those progress bars that appear when lengthy operations are performed. A good place could also be your application's About box or splash window. Applications that typically appear as autostart applications of your installation CD are also good candidates. Don't add Aero Glass to windows or dialogs that your users work with all the time, as over time it can get very tedious to read text on translucent background.
Implementation details
Most of the individual controls' sub-classing code relies on the WM_PRINTCLIENT
message. You can use this message and send to a control, along with a device context handle (an HDC
). If the control supports this message, it will render itself into the device context. For static controls, on the other hand, the sub-classing code employs the theme-ing API, most notably the DrawThemeTextEx
API. Using this API, it is possible to add a glow effect to the text being rendered by the control. This is critically important for readability because the glow effect creates additional contrast that is independent of the translucent background. The button control sub-classing code also makes heavy usage of the theme-ing API, most notably DrawThemeBackground
, GetThemeBitmap
and DrawThemeTextEx
. As already briefly mentioned, the sub-classing code for the animation control uses the low-level Video for Windows API. In addition, the animation sub-classing code contains a fallback implementation in case one of the various initialization steps for the Video-for-Windows-based implementation fails. This fallback code flickers on low-end machines and requires much more CPU utilization than the Video-for-Windows-based implementation. However, one problem that the Video-for-Windows-based implementation has is the fact that for each animation control that exists, a temporary file needs to be created. I would be grateful for any hint to an alternative implementation that doesn't require a temporary file.
The glue for all drawing code in this article's sub-classing code is provided by GDI+, as the routines from this library are inherently alpha-channel-aware. Windows XP and all subsequent Windows operating system versions ship with GDI+ preinstalled. In addition, there is a redistributable for GDI+ on older operating system versions, so I decided not to write a wrapper for GDI+ like I did for dwmapi.dll and uxtheme.dll. The drawback of this decision is that for Windows 2000, you will have to delay-load gdiplus.dll explicitly in your project if you plan to ship your application using the code from this article.
But...
...what about .NET code?
Honestly, I do not know if you can use the code from this article for .NET-based code. Most probably, you can create a native DLL with AeroAutoSubclass
and AeroSubClassCtrl
as its exported functions. Then it should be possible to create a P/Invoke wrapper and call this from your Windows-Forms-based application. Maybe I will add this to a future version of this article.
...what about my ActiveX controls?
If you want to add Aero Glass to a window that hosts an ActiveX control, then this control will probably suffer from the same problems as we see in the screenshot above for the naive Aero Glass implementation. You probably have to sub-class this control -- or a placeholder control, such as a hidden static window -- and try the WM_PRINTCLIENT
method in order to have the control render itself into a paint buffer. You will also have to understand the implementation details of this article's sub-classing code and be prepared to debug it as needed.
...can I run the code from this article on Windows 9x or ME?
Frankly speaking, I did not try. However, I am not aware of any functions in this article's source code that should prevent the code from running on these old consumer Windows versions. If there is a problem, see if it goes away by delay-loading the DLL that implements the offending API. If you want to run the demo application on such a consumer Windows version, you'll probably have to install either the update for the common controls DLL or the latest version of Internet Explorer. This is simply because the demo application uses controls that are not part of the RTM versions of these consumer Windows versions.
Kudos and motivation
Kudos for inspiration go to Michael Dunn and Jens Schacherl. Michael writes excellent articles on CodeProject and his article on Aero Glass was my introduction to programming with Aero Glass. Jens wrote an excellent article on CodeGuru that shows how to render an .avi file on a transparent background using the low-level Video for Windows APIs. Until I had incorporated routines like the ones that Jens uses in his article, the animation sub-classing code needed a fair amount of CPU usage, relied on undocumented behaviour of the animation control and flickered a lot on slow machines.
The main motivation for the code in this article, though, comes from the folks at Microsoft who were responsible for creating the then-famous ctl3d.dll way back in Win16-ish times, some 15 years ago or so. Back then, Windows UI had a white background on dialogs and everything looked flat and boring. However, by using ctl3d.dll -- or its successor, ctl3dv2.dll -- it became possible to add a stylish battleship grey background and so controls looked three-dimensional. Using one magic function, Ctl3dAutoSubclass
, it was even possible to apply the 3D-look to all dialogs and controls within an entire process... oops... uh... task. So I asked myself, "Wouldn't it be possible to enable Aero Glass for an application in a similar way as Ctl3dAutoSubclass
enabled the 3D-look back then?" The code in this article is certainly not as convenient, comprehensive or polished as ctl3d.dll was at the time, but I hope it comes as close as possible.
Summary
The routines I provide with this article are probably far from being perfect, as I most likely missed a lot of subtleties in the way standard Windows controls and their gazillion of different style flags work. My hope is that they are good enough for people to use them in their applications and that bugs and annoyances will be weeded out over time. I also hope that alternative implementations will be provided by others in order to solve problems in the code or simply to provide a better or more elegant solution. It took me well over two months of my spare time to experiment, debug and write the code associated with this article and so I truly hope that you enjoy it.
History
- 05/21/2007: Initial version
- 06/19/2007: Fixed a dangling pointer problem in header control and combobox destruction code