Click here to Skip to main content
15,867,286 members
Articles / Desktop Programming / ATL

Shell Extensibility - Explorer Desk Band, Tray Notification Icon et al.

Rate me:
Please Sign up or sign in to vote.
4.97/5 (21 votes)
28 Aug 2009CPOL12 min read 136K   3.2K   71   42
A simple Calendar utility that demonstrates basic Shell extensibility techniques: desk band, tray notification icon, locales.
Calendar utility main window, desk band and tray icon with a tooltip:

Main window

Options dialog window:

Options

Date string format window:

Date Format

Contents

Introduction

The Calendar example demonstrates the following techniques:

  • Add/remove/update an icon in the system notification area (a.k.a. system tray).
  • Handle user interaction with an icon in the system tray.
  • Implementation of a Windows Explorer desk band.
  • Add/remove/show/hide a desk band programmatically.
  • Formatting localized date strings using locale data available on the system.

The project uses the Microsoft Active Template Library (ATL) in order to avoid tedious COM spadework.

Project Organization

The output of the project consists of two executable modules:

  • Calendar.exe - the main program executable. This executable establishes the system tray icon, handles user requests, and installs and uninstalls a desk band object according to the user's settings. Also, this executable implements the main application window that shows the current calendar.
  • CalendarDeskBand.dll - the COM DLL that contains the implementation of the calendar desk band object. This DLL is embedded as a custom resource into Calendar.exe and extracted on the disk only if necessary.

Both executables are statically linked with CRT and ATL. The decision to link statically stems from the desire to make the executables as self-contained as possible. I have personally used the Calendar program for many years on many systems, so from the very beginning, I strived to avoid installations and other deployment hustles. Also, sometimes my friends who are less computer savvy ask me for small useful utilities for themselves. So, I would like to provide them with a single executable file that doesn't require any special preparations to run.

System Tray Icon

Basics

In order to manipulate system tray icons, all you need to do is to call the Shell_NotifyIcon function with the appropriate parameters and your window handle. This function adds, updates, and deletes the system tray icons for a given window. This is what you need before installing the system tray icon:

  • A handle to the existing window. This window will receive notification messages from the shell when the user clicks on the icon or another event occurs.
  • A running code that maintains a message loop in order to be able to get tray icon notifications.
  • An application-defined message identifier. The shell uses this message identifier to send icon notifications to the window. You can define the message using the WM_APP predefined constant from the Platform SDK, for example: const UINT WM_SYSTRAYNOTIFY = WM_APP + 1;.
  • An icon itself.

This is how the code may look like:

C++
// dwAction is one of the following:
//    - NIM_ADD
//    - NIM_MODIFY
//    - NIM_DELETE
bool CCalendarWindow::UpdateSysTrayIcon(DWORD dwAction)
{
    NOTIFYICONDATA nid = { 0 };
    nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.hWnd = m_hWnd;

    HICON hIconPrev = NULL;

    switch(dwAction)
    {
    case NIM_MODIFY:
        hIconPrev = m_hSysTrayIcon;
        // fall through
    case NIM_ADD:
        {
            SYSTEMTIME stToday = { 0 };
            ::GetLocalTime(&stToday);
            const CString& strDateStr =
                m_dlgOptions.m_dateFormat.FormatDateString(stToday);

            m_hSysTrayIcon = LoadDayIcon(stToday);

            nid.uFlags |= NIF_TIP | NIF_ICON | NIF_MESSAGE;
            nid.uCallbackMessage = WM_SYSTRAYNOTIFY;
            lstrcpyn(nid.szTip, strDateStr, ARRAYSIZE(nid.szTip));
            nid.hIcon = m_hSysTrayIcon;
        }
        break;
    case NIM_DELETE:
        hIconPrev = m_hSysTrayIcon;
        break;
    }

    BOOL bRes = ::Shell_NotifyIcon(dwAction, &nid);
    ATLASSERT(bRes);

    if(hIconPrev != NULL)
        ::DestroyIcon(hIconPrev);

    return (bRes != FALSE);
}

The notification handler function looks like this:

C++
LRESULT CCalendarWindow::OnSysTrayNotify(
    UINT /*uMsg*/,
    WPARAM /*wParam*/,
    LPARAM lParam,
    BOOL& /*bHandled*/)
{
    switch(lParam)
    {
    case WM_LBUTTONUP:
        ShowAndActivate();
        break;
    case WM_RBUTTONUP:
        HandleContextCommand(ShowContextMenu());
        break;
    }

    return 0;
}

where the ShowAndActivate function shows and activates the main application window, ShowContextMenu uses TrackPopupMenu in order to show the context menu, and HandleContextCommand handles the requested command.

Restoring the System Tray Icon

Sometimes the Explorer.exe process crashes or restarts during a logon session upon user request. We all know that not all tray icons reappear when the new Explorer.exe process creates the system taskbar. This happens because some lazy applications forget to handle the taskbar creation notification. (One of these lazy applications is the Windows Task Manager itself.)

On task bar creation, the system broadcasts a special message to all top-level windows: the message with the name "TaskbarCreated". In order to be able to handle that message, an application requires to register it first:

C++
UINT CCalendarWindow::s_uTaskbarRestart =
    ::RegisterWindowMessage(TEXT("TaskbarCreated"));

When the application receives that message, it should add all its tray icons again.

Header file:

C++
BEGIN_MSG_MAP(CCalendarWindow)
    // ...
    MESSAGE_HANDLER(s_uTaskbarRestart, OnTaskbarRestart)
END_MSG_MAP()

Implementation file:

C++
LRESULT CCalendarWindow::OnTaskbarRestart(
    UINT /*uMsg*/,
    WPARAM /*wParam*/,
    LPARAM /*lParam*/,
    BOOL& /*bHandled*/)
{
    // Add our icon to the newly created system tray.
    return (UpdateSysTrayIcon(NIM_ADD) ? 0L : -1L);
}

Desk Band

Overview

The Shell desk band is a regular COM object that implements a predefined set of interfaces required by the system. Once a desk band is registered, it can be shown and hidden by a user. The calendar desk band is very simple. It has only one child window that displays the current date string in localized format.

Registering a Desk Band

Registration of a desk band object requires the desk band's COM class GUID subentry to be written under HKEY_CLASSES_ROOT\CLSID. Following is the layout of the required Registry entries:

HKEY_CLASSES_ROOT
    CLSID
        {Desk band's class GUID}
            (Default) = Menu Text String
            InProcServer32
                (Default) = DLL Path Name
                ThreadingModel = Apartment
            Implemented Categories
                {CATID_DeskBand}

Notice that the Implemented Categories key and its subkey, namely CATID_DeskBand GUID value, are italic. It means that you don't need to create this key manually within the DllRegisterServer function. The common way to register a COM category is to call the ICatRegister::RegisterClassImplCategories method of the component categories manager object (CLSID_StdComponentCategoriesMgr), which is provided by the system. A generic function for registering a COM category may look like this:

C++
// CComPtr - smart pointer class from ATL
bool RegisterComCat(CLSID clsid, CATID CatID)
{
    CComPtr<ICatRegister> ptrCatRegister;
    HRESULT hr = ptrCatRegister.CoCreateInstance(CLSID_StdComponentCategoriesMgr);

    if(SUCCEEDED(hr))
        hr = ptrCatRegister->RegisterClassImplCategories(clsid, 1, &CatID);

    return SUCCEEDED(hr);
}

Fortunately, ATL already made this work for developers. All you need to do in order to register a COM category for your object is to specify a category GUID in the category map of your class:

C++
class CCalendarDeskBand
{
// ...
BEGIN_CATEGORY_MAP(CCalendarDeskBand)
    IMPLEMENTED_CATEGORY(CATID_DeskBand)
END_CATEGORY_MAP()

// ...
};

During the registration of a desk band COM object, the ICatRegister::RegisterClassImplCategories method will be called automatically by the library.

Implementing a Desk Band

Required Interfaces

According to the MSDN documentation, the following interfaces must be implemented by a desk band object (besides the standard IUnknown and IClassFactory interfaces, of course):

  • IObjectWithSite
  • IDeskBand (including the base interfaces IDockingWindow, IOleWindow)
  • IPersistStream (including the base interface IPersist)
  • Windows Vista and higher: IDeskBand2

Not all methods of these interfaces must be implemented. For example, if a desk band object doesn't use persistence, it can return E_NOTIMPL from most of IPersistStream methods. Following are the examples of implementation of the most interesting interfaces and methods.

IObjectWithSite Interface

The IObjectWithSite interface is implemented by the ATL library, so we don't need to do much here. The only method that requires other than the default implementation is IObjectWithSite::::SetSite:

C++
HRESULT STDMETHODCALLTYPE CCalendarDeskBand::SetSite( 
    /* [in] */ IUnknown *pUnkSite)
{
    HRESULT hr = __super::SetSite(pUnkSite);

    if(SUCCEEDED(hr) && pUnkSite)
    // pUnkSite is NULL when band is being destroyed
    {
        CComQIPtr<IOleWindow> spOleWindow = pUnkSite;

        if(spOleWindow)
        {
            HWND hwndParent = NULL;
            hr = spOleWindow->GetWindow(&hwndParent);

            if(SUCCEEDED(hr))
            {
                m_wndCalendar.Create(hwndParent);

                if(!m_wndCalendar.IsWindow())
                    hr = E_FAIL;
            }
        }
    }

    return hr;
}

IDockingWindow Interface

The IDockingWindow interface has three methods:

  • IDockingWindow::ShowDW - shows/hides the desk band window.
  • IDockingWindow::CloseDW - closes and destroys the desk band window.
  • IDockingWindow::ResizeBorderDW - according to MSDN, this method is never called and should always return E_NOTIMPL.

Even though MSDN doesn't require an implementation of the IDockingWindow::ResizeBorderDW method, I implemented it anyway. Maybe one day in the future, this method will be called by the system, so the code won't require any changes:

C++
HRESULT STDMETHODCALLTYPE CCalendarDeskBand::ResizeBorderDW( 
    /* [in] */ LPCRECT prcBorder,
    /* [in] */ IUnknown *punkToolbarSite,
    /* [in] */ BOOL /*fReserved*/)
{
    ATLTRACE(atlTraceCOM, 2, _T("IDockingWindow::ResizeBorderDW\n"));

    if(!m_wndCalendar) return S_OK;

    CComQIPtr<IDockingWindowSite> spDockingWindowSite = punkToolbarSite;

    if(spDockingWindowSite)
    {
        BORDERWIDTHS bw = { 0 };
        bw.top = bw.bottom = ::GetSystemMetrics(SM_CYBORDER);
        bw.left = bw.right = ::GetSystemMetrics(SM_CXBORDER);

        HRESULT hr = spDockingWindowSite->RequestBorderSpaceDW(
            static_cast<IDeskBand*>(this), &bw);

        if(SUCCEEDED(hr))
        {
            HRESULT hr = spDockingWindowSite->SetBorderSpaceDW(
                static_cast<IDeskBand*>(this), &bw);

            if(SUCCEEDED(hr))
            {
                m_wndCalendar.MoveWindow(prcBorder);
                return S_OK;
            }
        }
    }

    return E_FAIL;
}

IPersistStream Interface

At least one method of persistency interfaces must be implemented: IPersist::GetClassID. This method always returns the desk band COM class GUID. This is how the Shell distinguishes between desk band objects. Implementation of other methods is optional.

If a band object needs to store its internal data between logon sessions, then it implements the rest of the methods of the IPersistStream interface:

  • IPersistStream::IsDirty
  • IPersistStream::Load
  • IPersistStream::Save
  • IPersistStream::GetSizeMax

These methods are pretty straightforward, and it is not hard to implement them. The IPersistStream::Load and IPersistStream::Save methods accept a pointer to the IStream interface as a parameter, so the user can read/write to and from the stream, whatever is desired.

However, why bother if ATL already has the code that can be reused? Well, almost. ATL contains a handy class template IPersistStreamInitImpl<T>, which provides the implementation of the IPersistStreamInit interface. The problem is that technically the IPersistStreamInitImpl<T> implementation cannot be used, since the system requires IPersistStream rather than IPersistStreamInit to be implemented. Let's look at these interfaces closer:

IPersistStreamIPersistStreamInit
Inherits from IPersistInherits from IPersist
HRESULT IsDirty(void)HRESULT IsDirty(void)
HRESULT Load(IStream *pStm)HRESULT Load(IStream *pStm)
HRESULT Save(IStream *pStm, BOOL fClearDirty)HRESULT Save(IStream *pStm, BOOL fClearDirty)
HRESULT GetSizeMax(ULARGE_INTEGER *pcbSize)HRESULT GetSizeMax(ULARGE_INTEGER *pcbSize)
HRESULT InitNew(void)

We can easily see that both interfaces are almost identical, except that the IPersistStreamInit interface contains an additional method: IPersistStreamInit::InitNew. This method is cleverly declared at the end of the IPersistStreamInit interface. It means that the layout of the virtual table for both interfaces is identical, except that the IPersistStreamInit virtual table has an additional entry at the end.

The identical layout of both virtual tables up to the last method of IPersistStream enables us to pass an object that implements IPersistStreamInit wherever IPersistStream is required. Technically speaking, we rely here on the implementation detail: polymorphism in VC++ is implemented via a virtual table, which is an array of pointers to functions. However, there is considerable doubt that this implementation detail will ever change in the foreseeable future.

It is a mystery why Microsoft decided to declare the IPersistStreamInit interface as a separate interface and not by inheriting it from IPersistStream. Nonetheless, they were clever enough to make both interfaces identical for the IPersistStream part, and therefore made IPersistStreamInit backward compatible with the older IPersistStream.

That's why the Calendar desk band uses ATL's implementation of IPersistStreamInit even though the IPersistStream implementation is required:

C++
class CCalendarDeskBand :
    public IPersistStreamInitImpl<CCalendarDeskBand>,
    ...
{
    // ...

// IPersistStreamInitImpl requires property map.
BEGIN_PROP_MAP(CCalendarDeskBand)
    PROP_DATA_ENTRY("Locale", m_dateFormat.lcId, VT_UI4)
    PROP_DATA_ENTRY("Calendar", m_dateFormat.calId, VT_UI4)
    PROP_DATA_ENTRY("CalendarType", m_dateFormat.calType, VT_UI4)
    PROP_DATA_ENTRY("DateFormat", m_bstrDateFormat.m_str, VT_BSTR)
END_PROP_MAP()
};

Thanks to the existing implementation of persistency, all that a developer has to do is to fill entries in the class' property map. The rest is handled automatically by the library.

Note: The Shell calls IPersistStream::Save during logoff and/or on Explorer.exe exit. However, for some strange reason, Shell doesn't call IPersistStream::Save when a desk band is closed by the user. It means that all settings that the user may have changed would be lost when a desk band is closed manually. I consider this a bug, which should be fixed soon. Meanwhile, the Calendar desk band saves its properties in the application's .INI file, as well. But, this code should be removed as soon as normal persistency starts working.

Optional Interfaces

Optional interfaces include:

  • IInputObject - used by the Shell to activate a desk band window and set focus.
  • IContextMenu - used by the Shell when a taskbar context menu is created.

Although these interfaces are optional and aren't required by the desk band specification, you almost always will implement them since a desk band that cannot interact with the user has little value.

IContextMenu Interface and Command Handling

Here is the implementation of two important methods of the IContextMenu interface. The IContextMenu::QueryContextMenu method is called when a taskbar context menu is about to be shown. IContextMenu::InvokeCommand is called when a user selects a desk band menu item.

C++
const UINT IDM_SEPARATOR_OFFSET = 0;
const UINT IDM_SETTINGS_OFFSET = 1;

// ...

HRESULT STDMETHODCALLTYPE CCalendarDeskBand::QueryContextMenu(
    /* [in] */ HMENU hMenu,
    /* [in] */ UINT indexMenu,
    /* [in] */ UINT idCmdFirst,
    /* [in] */ UINT /*idCmdLast*/,
    /* [in] */ UINT uFlags)
{
    ATLTRACE(atlTraceCOM, 2, _T("IContextMenu::QueryContextMenu\n"));

    if(CMF_DEFAULTONLY & uFlags)
        return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0);

    // Add a seperator
    ::InsertMenu(hMenu, 
        indexMenu,
        MF_SEPARATOR | MF_BYPOSITION,
        idCmdFirst + IDM_SEPARATOR_OFFSET, 0);

    // Add the new menu item
    CString sCaption;
    sCaption.LoadString(IDS_DESKBANDSETTINGS);

    ::InsertMenu(hMenu, 
        indexMenu, 
        MF_STRING | MF_BYPOSITION,
        idCmdFirst + IDM_SETTINGS_OFFSET,
        sCaption);

    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, IDM_SETTINGS_OFFSET + 1);
}


HRESULT STDMETHODCALLTYPE CCalendarDeskBand::InvokeCommand( 
    /* [in] */ LPCMINVOKECOMMANDINFO pici)
{
    ATLTRACE(atlTraceCOM, 2, _T("IContextMenu::InvokeCommand\n"));

    if(!pici) return E_INVALIDARG;

    if(LOWORD(pici->lpVerb) == IDM_SETTINGS_OFFSET)
    {
        ATLASSERT(m_wndCalendar.IsWindow());

        CDateFormatSettings dlgSettings;
        const INT_PTR res = dlgSettings.DoModal(m_wndCalendar);

        if(res == IDOK)
        {
            // apply settings
            // ...

            const HRESULT hr = UpdateDeskband();
            ATLASSERT(SUCCEEDED(hr));
        }
    }
    return S_OK;
}

Once the desk band settings have changed, we need to update the desk band appearance. In order to notify the Shell that the desk band object needs update, we use the IOleCommandTarget interface of our OLE site. In response to the IOleCommandTarget::Exec call with the DBID_BANDINFOCHANGED command ID, the Shell will call our IDeskBand::GetBandInfo implementation.

C++
HRESULT CCalendarDeskBand::UpdateDeskband()
{
    CComPtr<IInputObjectSite> spInputSite;
    HRESULT hr = GetSite(IID_IInputObjectSite,
        reinterpret_cast<void**>(&spInputSite));

    if(SUCCEEDED(hr))
    {
        CComQIPtr<IOleCommandTarget> spOleCmdTarget = spInputSite;

        if(spOleCmdTarget)
        {
            // m_nBandID must be `int' or bandID variant must be explicitly
            // set to VT_I4, otherwise IDeskBand::GetBandInfo won't
            // be called by the system.
            CComVariant bandID(m_nBandID);

            hr = spOleCmdTarget->Exec(&CGID_DeskBand,
                DBID_BANDINFOCHANGED, OLECMDEXECOPT_DODEFAULT, &bandID, NULL);
            ATLASSERT(SUCCEEDED(hr));

            if(SUCCEEDED(hr))
                m_wndCalendar.UpdateCaption();
        }
    }
    return hr;
}

Showing and Hiding a Desk Band Programmatically

Actually, showing and hiding desk bands programmatically is considered harmful. The original intention of Windows Shell designers was that only the user can decide what desk band is shown/hidden, therefore there is no clean way to show a desk band in Windows versions before Windows Vista. However, system abuse by programmers was so rampant and dirty that eventually, starting from Windows Vista, the Windows Shell team provided programmatic control over desk band visibility. I think they have concluded that if programmers abuse the system anyway, at least there should be a less damaging way to do this.

Now, when a program tries to show a desk band on Windows Vista, the system presents a dialog box to the user prompting whether the user actually agrees to it.

Showing a Desk Band in Windows XP

There is no clean way to show a desk band programmatically in Windows XP unless the desk band cooperates. Luckily, the Calendar desk band cooperates with the main Calendar application, so they devised the following scheme:

  1. After registration of the desk band, the Calendar application adds a predefined unique string to the system's global atom table.
  2. MSDN: The global atom table is available to all applications. When an application places a string in the global atom table, the system generates an atom that is unique throughout the system. Any application that has the atom can obtain the string it identifies by querying the global atom table.

  3. Then, the main Calendar application calls the dreaded SHLoadInProc function to instantiate the Calendar desk band object within the context of the Explorer.exe process. The SHLoadInProc function is so bad and insecure that starting from Windows Vista, it is no longer available and rightfully so.
  4. As a result of the SHLoadInProc call, the Calendar desk band object is created. Immediately after that, it looks up the system's global atom table for the predefined unique string.
  5. If the string is there, then it means that the show request was issued by the main Calendar application. The Calendar desk band object adds itself to the list of visible desk bands by using the Tray Band Site Service object (CLSID_TrayBandSiteService).
  6. The desk band is successfully shown. The desk band removes the unique string from the global atom table.

This is how the CCalendarDeskBand::FinalConstruct method looks like:

C++
HRESULT CCalendarDeskBand::FinalConstruct()
{
    // ...

    HRESULT hr = HandleShowRequest();

    return hr;
}

HRESULT CCalendarDeskBand::HandleShowRequest()
{
    OLECHAR szAtom[MAX_GUID_STRING_LEN] = { 0 };
    ::StringFromGUID2(CLSID_CalendarDeskBand, szAtom, MAX_GUID_STRING_LEN);

    HRESULT hr = S_OK;
    const ATOM show = ::GlobalFindAtomW(szAtom);

    if(show)
    {
        CComPtr<IBandSite> spBandSite;
        hr = spBandSite.CoCreateInstance(CLSID_TrayBandSiteService);

        if(SUCCEEDED(hr))
        {
            LPUNKNOWN lpUnk = static_cast<IOleWindow*>(this);
            hr = spBandSite->AddBand(lpUnk);
        }

        ::GlobalDeleteAtom(show);
    }

    return hr;
}

Showing a Desk Band in Windows Vista and Higher

Starting from Windows Vista, there is no need in desk band cooperation anymore. The Shell provides a Tray Desk Band object (CLSID_TrayDeskBand) that implements the ITrayDeskBand interface. By using this interface, you can show and hide any desk band object programmatically. This is how the main Calendar application tries to show and hide its desk band object:

C++
bool CCalendarWindow::ShowDeskband() const
{
    CComPtr<ITrayDeskBand> spTrayDeskBand;
    HRESULT hr = spTrayDeskBand.CoCreateInstance(CLSID_TrayDeskBand);

    if(SUCCEEDED(hr))   // Vista and higher
    {
        hr = spTrayDeskBand->DeskBandRegistrationChanged();
        ATLASSERT(SUCCEEDED(hr));

        if(SUCCEEDED(hr))
        {
            hr = spTrayDeskBand->IsDeskBandShown(CLSID_CalendarDeskBand);
            ATLASSERT(SUCCEEDED(hr));

            if(SUCCEEDED(hr) && hr == S_FALSE)
                hr = spTrayDeskBand->ShowDeskBand(CLSID_CalendarDeskBand);
        }
    }
    else    // WinXP workaround
    {
        const CString& sAtom = ::StringFromGUID2(CLSID_CalendarDeskBand);

        if(!::GlobalFindAtom(sAtom))
            ::GlobalAddAtom(sAtom);

        // Beware! SHLoadInProc is not implemented under Vista and higher.
        hr = ::SHLoadInProc(CLSID_CalendarDeskBand);
        ATLASSERT(SUCCEEDED(hr));
    }

    return SUCCEEDED(hr);
}

bool CCalendarWindow::HideDeskband() const
{
    CComPtr<ITrayDeskBand> spTrayDeskBand;
    HRESULT hr = spTrayDeskBand.CoCreateInstance(CLSID_TrayDeskBand);

    if(SUCCEEDED(hr))   // Vista and higher
    {
        hr = spTrayDeskBand->IsDeskBandShown(CLSID_CalendarDeskBand);

        if(hr == S_OK)
            hr = spTrayDeskBand->HideDeskBand(CLSID_CalendarDeskBand);
    }
    else    // WinXP
    {
        CComPtr<IBandSite> spBandSite;
        hr = spBandSite.CoCreateInstance(CLSID_TrayBandSiteService);

        if(SUCCEEDED(hr))
        {
            DWORD dwBandID = 0;
            const UINT nBandCount = spBandSite->EnumBands((UINT)-1, &dwBandID);

            for(UINT i = 0; i < nBandCount; ++i)
            {
                spBandSite->EnumBands(i, &dwBandID);

                CComPtr<IPersist> spPersist;
                hr = spBandSite->GetBandObject(dwBandID, IID_IPersist, 
                                                 (void**)&spPersist);

                if(SUCCEEDED(hr))
                {
                    CLSID clsid = CLSID_NULL;
                    hr = spPersist->GetClassID(&clsid);

                    if(SUCCEEDED(hr) && ::IsEqualCLSID(clsid, CLSID_CalendarDeskBand))
                    {
                        hr = spBandSite->RemoveBand(dwBandID);
                        break;
                    }
                }
            }
        }
    }

    return SUCCEEDED(hr);
}

Visual Styles and Themes

Starting from Windows XP, the system's GUI supports visual styles and themes. Unfortunately, the visual styles API is severely under-documented and therefore practically unusable. By trial and error, I discovered almost the correct set of calls and parameters in order to draw the Calendar desk band object; however, it is still not ideal. The most obvious call:

C++
// Get tray area clock theme and styles
HTHEME hTheme = ::OpenThemeData(NULL, VSCLASS_CLOCK);

fails spectacularly no matter what. There is simply no way to get the system tray clock styles.

Obtaining the Taskbar Font

Here are two examples of how to obtain the taskbar font: for the classic GUI and for a themed GUI.

C++
// Classic visual scheme.
NONCLIENTMETRICS ncm = { 0 };

ncm.cbSize = sizeof(NONCLIENTMETRICS) -
    (::IsVistaOrHigher() ? 0 : sizeof(ncm.iPaddedBorderWidth));

HFONT hFont = NULL;
if(::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0))
    hFont = ::CreateFontIndirect(&ncm.lfMessageFont);

ATLASSERT(hFont);

There is nothing special about this code. Very straightforward and simple.

In order to obtain the themed default font for the taskbar, I queried VSCLASS_REBAR. I discovered with the Spy++ tool that the taskbar desk bands reside onto a rebar window actually. Here is the code:

C++
// Themed visual scheme.
HTHEME hTheme = ::OpenThemeData(NULL, VSCLASS_REBAR);
ATLASSERT(hTheme);

HFONT hFont = NULL;
if(hTheme)
{
    LOGFONT lf = { 0 };
    const HRESULT hr = ::GetThemeFont(hTheme, NULL, RP_BAND, 0, TMT_FONT, &lf);
    ATLASSERT(SUCCEEDED(hr));

    if(SUCCEEDED(hr))
    {
        hFont = ::CreateFontIndirect(&lf);
        ATLASSERT(m_hFont);
    }
    
    ::CloseThemeData(hTheme);
}

Obtaining the Taskbar Text Color

In order to discover the color of taskbar captions, I queried the VSCLASS_TASKBAND class. For some mysterious reason, this visual style can be queried only from within the Explorer.exe process. Any attempt to call OpenThemeData(NULL, VSCLASS_TASKBAND) outside the Explorer.exe process will fail.

C++
// Getting the text color used for taskbar captions.
HTHEME hTheme = ::OpenThemeData(NULL, VSCLASS_TASKBAND);
ATLASSERT(hTheme);

COLORREF clrText = 0;
if(hTheme)
{
    const HRESULT hr = ::GetThemeColor(hTheme, TDP_GROUPCOUNT, 0, 
                                       TMT_TEXTCOLOR, &clrText);
    ATLASSERT(SUCCEEDED(hr));

    ::CloseThemeData(hTheme);
}

Finally, drawing the background within the WM_PAINT message handler:

C++
// Classic visual scheme.
::FillRect(hdc, &rcPaint, ::GetSysColorBrush(CTLCOLOR_DLG));

// Themed visual scheme. Draw transparently.
::DrawThemeParentBackground(hWnd, hdc, &rcPaint);

If there is a better way to discover taskbar visual styles, then any suggestion is welcome.

That's all.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
More than ten years of C++ native development, and counting.

Smile | :)

Comments and Discussions

 
General32bit release build with 64, compatibility Pin
AhmedZI27-Nov-10 15:09
AhmedZI27-Nov-10 15:09 
GeneralRe: 32bit release build with 64, compatibility Pin
Alex Blekhman27-Nov-10 16:04
Alex Blekhman27-Nov-10 16:04 
GeneralRe: 32bit release build with 64, compatibility Pin
AhmedZI8-Dec-10 14:40
AhmedZI8-Dec-10 14:40 
GeneralRe: 32bit release build with 64, compatibility Pin
Alex Blekhman8-Dec-10 15:55
Alex Blekhman8-Dec-10 15:55 
GeneralRe: 32bit release build with 64, compatibility - killing of explorer Pin
hooger201721-Apr-14 14:03
hooger201721-Apr-14 14:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.