65.9K
CodeProject 正在变化。 阅读更多。
Home

Shell 扩展 - Explorer 桌面工具栏、任务栏通知图标等

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (20投票s)

2009 年 8 月 28 日

CPOL

12分钟阅读

viewsIcon

156703

downloadIcon

3249

一个简单的日历实用工具,演示了基本的 Shell 扩展技术:桌面工具栏、任务栏通知图标、区域设置。

日历实用工具主窗口、桌面工具栏和带工具提示的任务栏图标

Main window

选项对话框窗口

Options

日期字符串格式窗口

Date Format

目录

引言

日历示例演示了以下技术

  • 添加/删除/更新系统通知区域(又称系统托盘)中的图标。
  • 处理用户与系统托盘图标的交互。
  • 实现 Windows Explorer 桌面工具栏。
  • 以编程方式添加/删除/显示/隐藏桌面工具栏。
  • 使用系统中可用的区域设置数据格式化本地化日期字符串。

该项目使用 Microsoft Active Template Library (ATL) 来避免繁琐的 COM 工作。

项目组织

项目输出包含两个可执行模块

  • Calendar.exe - 主程序可执行文件。此可执行文件创建系统托盘图标,处理用户请求,并根据用户设置安装和卸载桌面工具栏对象。此外,此可执行文件实现了显示当前日历的主应用程序窗口。
  • CalendarDeskBand.dll - 包含日历桌面工具栏对象实现的 COM DLL。此 DLL 作为自定义资源嵌入到 Calendar.exe 中,仅在必要时才提取到磁盘。

两个可执行文件都与 CRT 和 ATL 静态链接。选择静态链接是为了使可执行文件尽可能自包含。我个人多年来在许多系统上使用 Calendar 程序,因此从一开始,我就努力避免安装和其他部署麻烦。此外,有时我那些不太懂电脑的朋友会向我索要一些小巧实用的工具。所以,我想提供一个无需特殊准备即可运行的单个可执行文件。

系统托盘图标

基本知识

要操作系统托盘图标,您只需使用适当的参数和窗口句柄调用 Shell_NotifyIcon 函数即可。此函数为给定窗口添加、更新和删除系统托盘图标。这是在安装系统托盘图标之前需要做的:

  • 指向现有窗口的句柄。当用户单击图标或其他事件发生时,此窗口将从 Shell 接收通知消息。
  • 一个正在运行的代码,它维护一个消息循环,以便能够获取托盘图标通知。
  • 一个应用程序定义的**消息标识符**。Shell 使用此消息标识符将图标通知发送到窗口。您可以使用 Platform SDK 中的 WM_APP 预定义常量来定义消息,例如:const UINT WM_SYSTRAYNOTIFY = WM_APP + 1;
  • 一个图标本身。

代码可能看起来像这样

// 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);
}

通知处理函数如下所示

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;
}

其中 ShowAndActivate 函数显示并激活主应用程序窗口,ShowContextMenu 使用 TrackPopupMenu 来显示上下文菜单,HandleContextCommand 处理请求的命令。

恢复系统托盘图标

有时 Explorer.exe 进程在用户请求的登录会话中崩溃或重启。我们都知道,当新的 Explorer.exe 进程创建系统任务栏时,并非所有托盘图标都会重新出现。这是因为有些惰性应用程序会忘记处理任务栏创建通知。(Windows 任务管理器本身就是这些惰性应用程序之一。)

在任务栏创建时,系统会将一条特殊消息广播给所有顶层窗口:名为“TaskbarCreated”的消息。为了能够处理该消息,应用程序需要先注册它

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

当应用程序收到该消息时,它应该重新添加所有托盘图标。

头文件

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

实现文件

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);
}

桌面工具栏

概述

Shell 桌面工具栏是一个常规的 COM 对象,它实现了系统所需的预定义接口集。一旦桌面工具栏被注册,用户就可以显示和隐藏它。日历桌面工具栏非常简单。它只有一个子窗口,以本地化格式显示当前日期字符串。

注册桌面工具栏

注册桌面工具栏对象需要在 HKEY_CLASSES_ROOT\CLSID 下写入桌面工具栏的 COM 类 GUID 子项。以下是所需注册表项的布局

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

请注意,已实现的类别键及其子键,即 CATID_DeskBand GUID 值,是斜体的。这意味着您不需要在 DllRegisterServer 函数中手动创建此键。注册 COM 类别的通用方法是调用组件类别管理器对象(CLSID_StdComponentCategoriesMgr)的 ICatRegister::RegisterClassImplCategories 方法,该对象由系统提供。注册 COM 类别的通用函数可能如下所示

// 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);
}

幸运的是,ATL 已经为开发人员完成了这项工作。您需要做的就是为您的对象注册一个 COM 类别,只需在类别的类别映射中指定类别 GUID 即可

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

// ...
};

在注册桌面工具栏 COM 对象期间,库将自动调用 ICatRegister::RegisterClassImplCategories 方法。

实现桌面工具栏

所需接口

根据 MSDN 文档,除了标准的 IUnknownIClassFactory 接口之外,桌面工具栏对象还必须实现以下接口

  • IObjectWithSite
  • IDeskBand (包括基接口 IDockingWindowIOleWindow)
  • IPersistStream (包括基接口 IPersist)
  • Windows Vista 及更高版本:IDeskBand2

并非所有这些接口的方法都必须实现。例如,如果桌面工具栏对象不使用持久化,它可以从大多数 IPersistStream 方法返回 E_NOTIMPL。以下是最有趣的接口和方法的实现示例。

IObjectWithSite 接口

IObjectWithSite 接口由 ATL 库实现,因此我们在这里不需要做太多。唯一需要非默认实现的**方法**是 IObjectWithSite::::SetSite

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 接口

IDockingWindow 接口有三个**方法**

  • IDockingWindow::ShowDW - 显示/隐藏桌面工具栏窗口。
  • IDockingWindow::CloseDW - 关闭并销毁桌面工具栏窗口。
  • IDockingWindow::ResizeBorderDW - 根据 MSDN,此方法永远不会被调用,并且应始终返回 E_NOTIMPL

尽管 MSDN 不要求实现 IDockingWindow::ResizeBorderDW 方法,但我还是实现了它。也许将来有一天,此方法会被系统调用,因此代码将不需要任何更改

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 接口

至少必须实现一种持久化接口的方法:IPersist::GetClassID。此方法始终返回桌面工具栏 COM 类 GUID。这就是 Shell 区分桌面工具栏对象的方式。其他方法的实现是可选的。

如果条带对象需要在登录会话之间存储其内部数据,则它会实现 IPersistStream 接口的其余方法

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

这些方法非常直接,实现它们并不困难。IPersistStream::LoadIPersistStream::Save 方法接受指向 IStream 接口的指针作为参数,因此用户可以根据需要从流中读取/写入任何内容。

但是,为什么还要费力呢,如果 ATL 已经有了可以重用的代码?嗯,差不多。ATL 包含一个方便的类模板 IPersistStreamInitImpl<T>,它提供了 IPersistStreamInit 接口的实现。问题是,从技术上讲,不能使用 IPersistStreamInitImpl<T> 的实现,因为系统要求实现 IPersistStream 而不是 IPersistStreamInit。让我们更仔细地看一下这些接口

IPersistStream IPersistStreamInit
继承自 IPersist 继承自 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)

我们可以很容易地看到这两个接口几乎相同,除了 IPersistStreamInit 接口包含一个附加**方法**:IPersistStreamInit::InitNew。此方法巧妙地声明在 IPersistStreamInit 接口的末尾。这意味着这两个接口的虚拟表布局是相同的,除了 IPersistStreamInit 虚拟表在末尾有一个额外的条目。

这两个虚拟表在 IPersistStream 的最后一个方法之前具有相同的布局,这使得我们可以将实现 IPersistStreamInit 的对象传递给需要 IPersistStream 的任何地方。从技术上讲,我们依赖于实现细节:VC++ 中的多态性是通过虚拟表实现的,虚拟表是指向函数的指针数组。但是,对于这种实现细节将来是否会改变,存在很大的疑问。

为什么 Microsoft 决定将 IPersistStreamInit 接口声明为一个单独的接口,而不是从 IPersistStream 继承它,这仍然是个谜。尽管如此,他们足够聪明,使得两个接口的 IPersistStream 部分相同,从而使 IPersistStreamInit 向后兼容旧的 IPersistStream

这就是为什么日历桌面工具栏使用 ATL 的 IPersistStreamInit 实现,即使需要 IPersistStream 实现。

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()
};

由于存在持久性实现,开发人员所要做的就是填写类属性映射中的条目。其余的由库自动处理。

注意: Shell 在注销和/或退出 Explorer.exe 时调用 IPersistStream::Save。然而,出于某种奇怪的原因,当用户关闭桌面工具栏时,Shell 不会调用 IPersistStream::Save。这意味着用户可能更改的所有设置在桌面工具栏被手动关闭时都会丢失。我认为这是一个应该尽快修复的 bug。在此期间,日历桌面工具栏也会将其属性保存在应用程序的 .INI 文件中。但是,一旦正常的持久性开始工作,就应该删除这段代码。

可选接口

可选接口包括

  • IInputObject - 由 Shell 用于激活桌面工具栏窗口并设置焦点。
  • IContextMenu - 当创建任务栏上下文菜单时由 Shell 使用。

尽管这些接口是可选的,并且桌面工具栏规范不要求它们,但您几乎总会实现它们,因为无法与用户交互的桌面工具栏几乎没有价值。

IContextMenu 接口和命令处理

以下是 IContextMenu 接口两个重要**方法**的实现。当即将显示任务栏上下文菜单时,会调用 IContextMenu::QueryContextMenu 方法。当用户选择桌面工具栏菜单项时,会调用 IContextMenu::InvokeCommand

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;
}

一旦桌面工具栏设置发生更改,我们就需要更新桌面工具栏的外观。为了通知 Shell 桌面工具栏对象需要更新,我们使用 OLE 站点 OLE 命令目标接口 (IOleCommandTarget)。响应 IOleCommandTarget::Exec 调用(带有 DBID_BANDINFOCHANGED 命令 ID)的 Shell 将调用我们的 IDeskBand::GetBandInfo 实现。

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;
}

以编程方式显示和隐藏桌面工具栏

实际上,以编程方式显示和隐藏桌面工具栏被认为是**有害的**。Windows Shell 设计者的初衷是只有用户才能决定显示/隐藏哪个桌面工具栏,因此在 Windows Vista 之前的版本中没有干净的方法来显示桌面工具栏。然而,程序员对系统的滥用如此猖獗且肮脏,以至于从 Windows Vista 开始,Windows Shell 团队提供了对桌面工具栏可见性的编程控制。我认为他们得出的结论是,既然程序员无论如何都会滥用系统,那么至少应该有一种损害较小的方法来做到这一点。

现在,当程序尝试在 Windows Vista 上显示桌面工具栏时,系统会向用户显示一个对话框,询问用户是否确实同意。

在 Windows XP 中显示桌面工具栏

在 Windows XP 中,没有干净的方法可以以编程方式显示桌面工具栏,除非桌面工具栏进行配合。幸运的是,日历桌面工具栏与主日历应用程序配合,因此他们设计了以下方案

  1. 注册桌面工具栏后,日历应用程序将一个预定义的唯一字符串添加到系统的全局原子表。
  2. MSDN:全局原子表可供所有应用程序使用。当应用程序将字符串放入全局原子表时,系统会生成一个在整个系统中唯一的原子。具有该原子的任何应用程序都可以通过查询全局原子表来获取它标识的字符串。

  3. 然后,主日历应用程序调用令人憎恶的 SHLoadInProc 函数,在 Explorer.exe 进程的上下文中实例化日历桌面工具栏对象。SHLoadInProc 函数如此糟糕且不安全,以至于从 Windows Vista 开始,它就不再可用,这是理所当然的。
  4. 作为 SHLoadInProc 调用**的结果**,日历桌面工具栏对象被创建。创建后,它立即在系统的全局原子表中查找预定义的唯一字符串。
  5. 如果字符串存在,则意味着显示请求是由主日历应用程序发出的。日历桌面工具栏对象通过使用托盘工具栏站点服务对象 (CLSID_TrayBandSiteService) 将自己添加到可见桌面工具栏列表中。
  6. 桌面工具栏已成功显示。桌面工具栏从全局原子表中删除唯一字符串。

以下是 CCalendarDeskBand::FinalConstruct 方法的样子

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;
}

在 Windows Vista 及更高版本中显示桌面工具栏

从 Windows Vista 开始,不再需要桌面工具栏配合。Shell 提供了一个托盘桌面工具栏对象 (CLSID_TrayDeskBand),它实现了 ITrayDeskBand 接口。通过使用此接口,您可以以编程方式显示和隐藏任何桌面工具栏对象。这就是主日历应用程序尝试显示和隐藏其桌面工具栏对象的方式

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);
}

视觉样式和主题

从 Windows XP 开始,系统的 GUI 支持视觉样式和主题。不幸的是,视觉样式 API 的文档严重不足,因此实际上无法使用。通过反复试验,我几乎发现了绘制日历桌面工具栏对象的正确调用集和参数;然而,它仍然不理想。最明显的调用

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

无论如何都会**惨败**。根本无法获取系统托盘时钟的样式。

获取任务栏字体

这里有两个获取任务栏字体的**示例**:一个用于经典 GUI,一个用于主题 GUI。

// 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);

这段代码没有什么特别之处。非常直接和简单。

为了获取任务栏的主题默认字体,我查询了 VSCLASS_REBAR。我用 Spy++ 工具发现任务栏桌面工具栏实际上驻留在 rebar 窗口上。这是代码

// 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);
}

获取任务栏文本颜色

为了**发现**任务栏标题的颜色,我查询了 VSCLASS_TASKBAND 类。出于某种神秘原因,此视觉样式**只能**在 Explorer.exe 进程**内部**查询。任何在 Explorer.exe 进程**外部**调用 OpenThemeData(NULL, VSCLASS_TASKBAND) 的尝试都将失败。

// 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);
}

最后,在 WM_PAINT 消息处理程序中绘制背景

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

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

如果发现任务栏视觉样式的更好方法,欢迎任何建议。

就是这样。

© . All rights reserved.