Calendar utility main window, desk band and tray icon with a tooltip:
Options dialog window:
Date string format window:
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:
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;
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:
LRESULT CCalendarWindow::OnSysTrayNotify(
UINT ,
WPARAM ,
LPARAM lParam,
BOOL& )
{
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:
UINT CCalendarWindow::s_uTaskbarRestart =
::RegisterWindowMessage(TEXT("TaskbarCreated"));
When the application receives that message, it should add all its tray icons again.
Header file:
BEGIN_MSG_MAP(CCalendarWindow)
MESSAGE_HANDLER(s_uTaskbarRestart, OnTaskbarRestart)
END_MSG_MAP()
Implementation file:
LRESULT CCalendarWindow::OnTaskbarRestart(
UINT ,
WPARAM ,
LPARAM ,
BOOL& )
{
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:
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:
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
:
HRESULT STDMETHODCALLTYPE CCalendarDeskBand::SetSite(
IUnknown *pUnkSite)
{
HRESULT hr = __super::SetSite(pUnkSite);
if(SUCCEEDED(hr) && pUnkSite)
{
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:
HRESULT STDMETHODCALLTYPE CCalendarDeskBand::ResizeBorderDW(
LPCRECT prcBorder,
IUnknown *punkToolbarSite,
BOOL )
{
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:
IPersistStream | IPersistStreamInit |
---|
Inherits from IPersist | Inherits 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:
class CCalendarDeskBand :
public IPersistStreamInitImpl<CCalendarDeskBand>,
...
{
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.
const UINT IDM_SEPARATOR_OFFSET = 0;
const UINT IDM_SETTINGS_OFFSET = 1;
HRESULT STDMETHODCALLTYPE CCalendarDeskBand::QueryContextMenu(
HMENU hMenu,
UINT indexMenu,
UINT idCmdFirst,
UINT ,
UINT uFlags)
{
ATLTRACE(atlTraceCOM, 2, _T("IContextMenu::QueryContextMenu\n"));
if(CMF_DEFAULTONLY & uFlags)
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0);
::InsertMenu(hMenu,
indexMenu,
MF_SEPARATOR | MF_BYPOSITION,
idCmdFirst + IDM_SEPARATOR_OFFSET, 0);
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(
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)
{
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.
HRESULT CCalendarDeskBand::UpdateDeskband()
{
CComPtr<IInputObjectSite> spInputSite;
HRESULT hr = GetSite(IID_IInputObjectSite,
reinterpret_cast<void**>(&spInputSite));
if(SUCCEEDED(hr))
{
CComQIPtr<IOleCommandTarget> spOleCmdTarget = spInputSite;
if(spOleCmdTarget)
{
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:
- After registration of the desk band, the Calendar application adds a predefined unique string to the system's global atom table.
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.
- 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. - 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. - 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
). - 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:
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:
bool CCalendarWindow::ShowDeskband() const
{
CComPtr<ITrayDeskBand> spTrayDeskBand;
HRESULT hr = spTrayDeskBand.CoCreateInstance(CLSID_TrayDeskBand);
if(SUCCEEDED(hr)) {
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 {
const CString& sAtom = ::StringFromGUID2(CLSID_CalendarDeskBand);
if(!::GlobalFindAtom(sAtom))
::GlobalAddAtom(sAtom);
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)) {
hr = spTrayDeskBand->IsDeskBandShown(CLSID_CalendarDeskBand);
if(hr == S_OK)
hr = spTrayDeskBand->HideDeskBand(CLSID_CalendarDeskBand);
}
else {
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:
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.
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:
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.
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:
::FillRect(hdc, &rcPaint, ::GetSysColorBrush(CTLCOLOR_DLG));
::DrawThemeParentBackground(hWnd, hdc, &rcPaint);
If there is a better way to discover taskbar visual styles, then any suggestion is welcome.
That's all.