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

C++ 中的 Windows 7 实用程序:Ribbon 简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (38投票s)

2011年2月22日

CPOL

22分钟阅读

viewsIcon

141058

downloadIcon

3323

本教程展示了如何在您的应用程序中开始使用 Ribbon。

目录

引言

功能区最初由 Office 团队设计,旨在取代其应用程序中复杂的菜单和工具栏系统。在 Windows 7 中,Microsoft 发布了一个可重用的基于 COM 的功能区组件,用于本机 Win32 应用程序。本机功能区与 Office、WPF 和 MFC 中的其他功能区组件略有不同,但它们都具有相同的基本布局和功能。

由于功能区具有许多功能,因此本文将仅介绍功能区及其 COM 接口的基础知识,并演示从头开始创建使用简单功能区的应用程序所需的步骤。如果您想直接深入功能区开发,请查看 CodeProject 上的其他功能区文章,或即将推出的 WTL 8.1 中的功能区类。

构建示例代码的系统要求是 Visual Studio 2008、WTL 8.0Windows 7 SDK。如果您运行的是 Windows 7 或 Server 2008 R2,则拥有所需的一切。如果您使用的是 Vista 或 Server 2008,则必须安装 Service Pack 2 和操作系统的平台更新才能使用功能区。

启动新应用

CMainFrame 中的准备工作

我们将从 WTL AppWizard 创建的基本 SDI 应用程序开始。此应用程序没有工具栏(因为功能区取代了工具栏),但它有一个状态栏。它为视图窗口使用一个单独的类,但视图窗口不会做太多事情,因为我们的重点是让功能区启动和运行。稍后,在布局主框架的子控件时,我们将对视图窗口做更多的工作。

第一步是包含 UIRibbon.h,其中包含我们将使用功能区所需的类型和接口定义。我们的应用程序将实现其中两个接口,实现细节在本文的其余部分中进行了概述。首先,我们只需要将 IUIApplication 添加到 CMainFrame 的继承列表中,然后添加三个方法的存根实现

// In stdafx.h:
#include <UIRibbon.h>
 
class CMainFrame :
  public CFrameWindowImpl<CMainFrame>,
  public CMessageFilter,
  public CComObjectRootEx<CComSingleThreadModel>,
  public IUIApplication
{
  // IUIApplication methods
  STDMETHODIMP OnCreateUICommand (
                 UINT32 uCmdID, UI_COMMANDTYPE nType,
                 IUICommandHandler** ppHandler )
  { return E_NOTIMPL; }
 
  STDMETHODIMP OnDestroyUICommand (
                 UINT32 uCmdID, UI_COMMANDTYPE nType,
                 IUICommandHandler* pHandler )
  { return E_NOTIMPL; }
 
  STDMETHODIMP OnViewChanged (
                 UINT32 uViewID, UI_VIEWTYPE nType, IUnknown* pView,
                 UI_VIEWVERB nVerb, INT32 nReason )
  { return E_NOTIMPL; }
};

由于 CMainFrame 需要是一个 COM 对象,因此它也派生自 CComObjectRootEx。此外,Run() 函数中的单个 CMainFrame 实例是使用 CComObjectStackEx 创建的,以便使用 ATL 对 IUnknown 的实现

  // In the global Run() function:
  CComObjectStackEx<CMainFrame> wndMain;

初始化 UI 框架

我们与功能区的第一次交互是通过 IUIFramework 接口。CMainFrame 使用此接口初始化和关闭框架,因此它需要为应用程序的生命周期保留一个接口指针

  // In MainFrm.h:
  CComPtr<IUIFramework> m_pFramework;

CMainFrame::OnCreate() 中,我们共同创建框架的 COM 对象

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // Standard frame initialization here...
 
  // Note: Error handling has been omitted.
  // Cocreate the Ribbon framework COM object.
  m_pFramework.CoCreateInstance ( CLSID_UIRibbonFramework );
 
  // Initialize the framework:
IUIApplication* pApp = this;
 
  m_pFramework->Initialize ( m_hWnd, pApp );
 
  return 0;
}

IUIFramework::Initialize() 接受包含功能区的窗口的 HWND 和框架将用于与应用程序通信的 IUIApplication 接口。框架会更改窗口的外观以适应功能区的视觉准则。例如,框架会删除窗口的菜单并在标题栏中绘制控件。为了确保这些功能平稳运行,框架要求窗口是具有 WS_CAPTION 样式且没有 WS_EX_TOOLWINDOW 样式的顶级窗口。如果窗口不符合这些条件,IUIFramework::Initialize() 将返回 ERROR_INVALID_WINDOW_HANDLE

为了正确关闭框架并释放它正在使用的任何资源,我们还需要在 WM_DESTROY 处理程序中调用 IUIFramework::Destroy()

void CMainFrame::OnDestroy()
{
  m_pFramework->Destroy();
  m_pFramework.Release();
}

添加对 IUIFramework::Initialize() 的调用后,我们的框架窗口如下所示

当然,现在还没有功能区,因为我们还没有定义功能区的内容,但框架已经进行了必要的视觉调整,例如删除了菜单。

向应用程序添加功能区

功能区的工作方式与您以前使用过的其他 UI 小部件不同。其设计原则之一是功能区内容的定义应与其视觉设计分开。应用程序可以指定功能区中出现哪些命令、它们如何组织和分组等。但是应用程序对功能区的实际外观控制有限。这使得 Microsoft 将来可以更改功能区的外观,而兼容性问题比现有创建 GUI(如对话框资源)的方式要少。

创建功能区定义的步骤是

  1. 创建一个定义功能区内容的 XML 文件,并将该文件添加到您的项目中。
  2. 使用 uicc 工具编译 XML。
  3. 将 uicc 生成的文件添加到应用程序的资源中。
  4. 调用 IUIFramework::LoadUI() 使用编译的定义创建功能区。

在深入了解 XML 之前,我们需要熟悉功能区的元素。

功能区组件

功能区在许多地方使用命令的概念。命令不仅仅是一个按钮,它基本上是功能区中您可以单击并执行操作的任何东西。命令具有各种属性,例如文本标签、图标和快捷键。一旦您列出应用程序中的命令,您就可以开始使用这些命令构建功能区的其他部分。

这是 Paint 窗口的屏幕截图,显示了功能区的各个部分

  • 应用程序菜单:第一个选项卡左侧的按钮打开应用程序菜单。这本身就是一个命令,尽管它不允许自定义其外观。菜单包含其他命令。
  • 快速访问工具栏 (QAT):QAT 是功能区在窗口标题区域绘制的工具栏。您可以通过右键单击它们并在上下文菜单上选择添加到快速访问工具栏来将其他命令添加到工具栏。QAT 右边缘的向下箭头显示一个菜单,您可以在其中自定义工具栏中的命令并更改 QAT 和功能区的外观。
  • 选项卡:选项卡行显示您在 XML 文件中定义的所有选项卡。每个选项卡都是一个命令,它包含一个或多个组。
  • 组:每个是命令的集合。组底部有一个可选标签,功能区在组之间绘制分隔符。您可能偶尔会看到使用代替组。这不是官方术语,但它在 Office 团队构建第一个功能区时使用,并且仍用于博客文章等非官方书面材料中。

启动 XML 文件

这是我们起点的骨架 XML 文件

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns='http://schemas.microsoft.com/windows/2009/Ribbon'>
  <Application.Commands>
  </Application.Commands>
  <Application.Views>
    <Ribbon>
      <Ribbon.Tabs>
      </Ribbon.Tabs>
    </Ribbon>
  </Application.Views>
</Application>

请注意,一些属性是使用子标签编写的,采用“object.property”命名约定,类似于 XAML。例如,<Application.Commands> 定义 <Application> 标签的 Commands 属性。

第一步是创建一些我们将在 <Ribbon> 部分中使用的命令。每个命令都在 <Command> 标签中定义。<Command> 有几个我们稍后会看到的属性,但现在,我们将使用两个:NameLabelTitleName 属性是标识 XML 文件其余部分中命令的字符串,LabelTitle 是设置 UI 元素文本的字符串。

为了创建一个按钮,我们还需要一个组和一个选项卡来放置它,所以我们从三个命令开始

<Application.Commands>
  <Command Name="tabMain" LabelTitle="Main" />
  <Command Name="grpHello" LabelTitle="Hello Ribbon!" />
  <Command Name="cmdClickMe" LabelTitle="Click me" />
</Application.Commands>

(我看到的 Microsoft 示例在所有命令名称中使用“cmd”前缀,但我选择使用更具描述性的前缀来指示每个命令的功能。)然后我们在组中创建一个选项卡、一个组和一个按钮控件

<Application.Views>
  <Ribbon>
    <Ribbon.Tabs>
      <Tab CommandName="tabMain">
        <Group CommandName="grpHello" SizeDefinition="OneButton">
          <Button CommandName="cmdClickMe" />
        </Group>
      </Tab>
    </Ribbon.Tabs>
  </Ribbon>
</Application.Views>

这将创建一个包含一个组的选项卡。选项卡的大多数属性来自关联的 <Command> 标签。这里有一个间接级别:您没有说明选项卡的文本是什么,您说明了选项卡是哪个命令,功能区会在关联的 <Command> 标签中查找文本。同样,组的标签来自 grpHello 命令的 LabelTitle 属性。现在不用担心 SizeDefinition 属性,它只是必需的,以便功能区知道如何在组中排列控件。

最后,有一个 <Button> 标签用于创建一个普通的按钮。将属性分离到 <Command> 标签对于按钮更有用,因为同一个命令可能出现在多个地方,并且这种分离意味着您不必在命令出现的每个地方重复相同的属性。

编译功能区 XML 文件

现在我们已经创建了功能区定义文件,我们需要将其合并到应用程序中。如果您将上述 XML 保存到名为 ribbon.xml 的文件中,您可以通过单击 项目|添加现有项 并选择 ribbon.xml 将其添加到 Visual Studio 项目。

下一步是调用编译器 (uicc.exe),它将 XML 转换为功能区可以理解的二进制格式。打开 ribbon.xml 的属性,并在 自定义生成步骤 设置中,将 命令行 字段设置为

uicc.exe $(InputFileName) $(InputName).bml /header:$(InputName)ids.h
  /res:$(InputName).rc /name:HelloRibbon

uicc 在编译 ribbon.xml 时会生成三个文件

  • ribbon.bml:编译后的输出。Microsoft 的示例使用 .bml 扩展名,我照做了。
  • ribbonids.h:一个 C 头文件,其中包含 XML 中定义的所有命令和字符串的 #define
  • ribbon.rc:一个资源定义文件,其中包含 XML 中引用的字符串和图标的定义,以及一个引入 ribbon.bml 的自定义资源类型。/name 开关控制此资源的名称。在此示例中,名称为 HELLORIBBON_RIBBON。也就是说,名称是 /name 开关中指定的任何内容,大写,并附加 "_RIBBON"。

我们还应该通过将 输出 字段设置为 "$(InputName).bml;$(InputName)ids.h;$(InputName).rc" 来告诉 Visual Studio uicc 创建了哪些文件。

接下来,我们将 ribbon.rc 中的资源添加到应用程序的资源中。在资源视图选项卡中,右键单击 HelloRibbon.rc 并选择 资源包括。在 编译时指令 编辑框中,添加

#include "ribbon.rc"

现在,当应用程序编译时,应用程序的资源将包括 ribbon.rc 中列出的任何资源。uicc 还会验证 XML 是否不包含任何错误,并且不违反任何功能区的布局规则(例如,同一个命令在同一个组中出现多次)。如果 uicc 发现任何错误,它将无法编译 XML,并且项目将无法构建。

加载功能区

最后一步是使用 XML 的编译版本初始化功能区。我们通过在 CMainFrame::OnCreate() 中添加对 IUIFramework::LoadUI() 的调用来完成此操作

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // Note: Error handling has been omitted.
  // Cocreate the Ribbon framework COM object.
  m_pFramework.CoCreateInstance ( CLSID_UIRibbonFramework );
 
  // Initialize the framework:
IUIApplication* pApp = this;
 
  m_pFramework->Initialize ( m_hWnd, pApp );
 
  // Load the Ribbon.
  m_pFramework->LoadUI ( _Module.GetResourceInstance(),
                         L"HELLORIBBON_RIBBON" );
 
  return 0;
}

IUIFramework::LoadUI() 接受两个参数,包含编译后的 XML 的 HMODULE 和资源名称。

如果我们在进行这些更改后编译应用程序,并忽略 uicc 关于缺少 XML 标签的几个警告,我们将看到我们的第一个功能区!

修复 uicc 警告

在我们继续之前,让我们修复 uicc 关于缺少 ApplicationMenuQuickAccessToolbar 属性的警告。这些元素中的每一个都需要一个命令

<Application.Commands>
  <Command Name="cmdApplicationMenu" />
  <Command Name="cmdQAT" />
<Application./Commands>

然后我们在 <Ribbon> 标签下添加两个新标签

<Application.Views>
  <Ribbon>
    <Ribbon.ApplicationMenu>
      <ApplicationMenu CommandName="cmdApplicationMenu" />
    </Ribbon.ApplicationMenu>
    <Ribbon.QuickAccessToolbar>
      <QuickAccessToolbar CommandName="cmdQAT" />
    </Ribbon.QuickAccessToolbar>
  </Ribbon>
</Application.Views>

通过这些更改,所有必需的元素都已到位,uicc 不会再发出任何警告。

与功能区通信

我们已经看到了 IUIFramework 接口,我们的应用程序使用它来设置和拆除功能区框架。我们的应用程序还使用另一个接口直接与功能区通信:IUIRibbon。功能区通过 IUIApplication::OnViewChanged() 将此接口传递给我们的应用程序,因此我们将从填充该方法开始。

首先,向 CMainFrame 添加一个成员,该成员将保存 IUIRibbon 接口

  // In MainFrm.h:
  CComQIPtr<IUIRibbon> m_pRibbon;

当功能区被创建、销毁或调整大小时,会调用 CMainFrame::OnViewChanged()。(技术上讲,它处理应用程序视图,而不是专门针对功能区,但功能区是目前唯一定义的视图。)OnViewChanged 的参数是

  • uViewID:视图 ID,目前始终为 0。
  • nType:视图类型,目前始终为 UI_VIEWTYPE_RIBBON
  • pView:视图提供的 IUnknown 接口。
  • nVerb:视图正在执行的操作:UI_VIEWVERB_CREATEUI_VIEWVERB_DESTROYUI_VIEWVERB_SIZE
  • nReason:目前未使用。

nVerbUI_VIEWVERB_CREATE 时,功能区正在创建中,我们可以查询它以获取 IUIRibbon 接口。当 nVerbUI_VIEWVERB_DESTROY 时,功能区将消失,因此我们释放 IUIRibbon 接口

STDMETHODIMP CMainFrame::OnViewChanged (
  UINT32 uViewID, UI_VIEWTYPE nType, IUnknown* pView, UI_VIEWVERB nVerb,
  INT32 nReason )
{
  switch ( nVerb )
    {
    case UI_VIEWVERB_CREATE:
      m_pRibbon = pView;
    break;
 
    case UI_VIEWVERB_DESTROY:
      m_pRibbon.Release();
    break;
  }
 
  return S_OK;
}

一个更有趣的情况是 UI_VIEWVERB_SIZE。这告诉我们功能区正在改变大小、被隐藏或被显示。由于这会影响主框架的布局,因此我们获取功能区的新高度(以像素为单位)并布局框架窗口

    // Added to the switch statement above:
    case UI_VIEWVERB_SIZE:
      if ( m_pRibbon )
        {
        HRESULT hr = m_pRibbon->GetHeight ( &m_cyRibbon );
 
        if ( SUCCEEDED(hr) )
          UpdateLayout();
        }
    break;

CFrameWindowImpl::UpdateLayout() 定位 WTL 支持的各种类型的条(rebar、工具栏和状态栏),然后定位视图窗口以占用剩余空间。由于 WTL 不了解功能区,因此我们需要重写 UpdateLayout() 并使用功能区的高度作为计算的一部分。我们的重写与 CFrameWindowImpl::UpdateLayout() 非常相似,但它通过查看 m_cyRibbon 来确定在客户端区域顶部保留多少像素

void CMainFrame::UpdateLayout ( BOOL bResizeBars )
{
CRect rect;
 
  GetClientRect ( rect );
  UpdateBarsPosition ( rect, bResizeBars );
 
  // 'rect' now holds the rect of the client area that WTL calculated,
  // but it doesn't include the space needed by the Ribbon.  Move the top
  // coordinate of the rect down so the view window doesn't overlap the Ribbon.
  rect.top += m_cyRibbon;
 
  // Resize the view window
  if ( m_hWndClient != NULL )
    CWindow(m_hWndClient).SetWindowPos ( NULL, rect,
                                         SWP_NOZORDER | SWP_NOACTIVATE );
}

添加此 UpdateLayout() 重写后,您可以尝试最小化功能区,视图窗口将相应地调整大小

如果将窗口缩小到足够小,功能区将完全隐藏自己。我们的 UpdateLayout() 也处理这种情况。

处理来自命令的通知

IUICommandHandler

我们将使用的下一个接口是 IUICommandHandler。应用程序实现此接口,功能区调用其方法以读取每个命令的属性并通知应用程序它应该执行一个命令。

当功能区解析编译后的 XML 时,它会为每个命令向应用程序请求一个 COM 接口。应用程序必须返回一个 IUICommandHandler 接口,才能使命令正常工作。此接口可以由任何 COM 对象实现,但对于我们的示例应用程序,我们将直接将接口添加到 CMainFrame

class CMainFrame :
  ...
  public IUICommandHandler
{
  // IUICommandHandler methods
  STDMETHODIMP Execute (
                UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
                const PROPVARIANT* pCurrVal,
                IUISimplePropertySet* pCmdProperties );
 
  STDMETHODIMP UpdateProperty (
                 UINT32 uCmdID, REFPROPERTYKEY key,
                 const PROPVARIANT* pCurrVal,
                 PROPVARIANT* pNewVal );
};

创建和销毁 IUICommandHandlers

功能区将为每个命令调用 IUIApplication::OnCreateUICommand() 一次以获取 IUICommandHandler 接口,并在不再需要该接口时调用 IUIApplication::OnDestroyUICommand() 一次。由于 CMainFrame 为每个命令实现了 IUICommandHandler,因此 CMainFrame::OnCreateUICommand() 很简单

STDMETHODIMP CMainFrame::OnCreateUICommand (
    UINT32 uCmdID, UI_COMMANDTYPE nType, IUICommandHandler** ppHandler )
{
  // This object implements IUICommandHandler.
  return QueryInterface ( IID_PPV_ARGS(ppHandler) );
}

当命令被销毁时,我们不需要进行任何清理,所以 CMainFrame::OnDestroyUICommand() 只返回一个成功的 HRESULT

STDMETHODIMP CMainFrame::OnDestroyUICommand (
    UINT32 uCmdID, UI_COMMANDTYPE nType, IUICommandHandler* pHandler )
{
  // No cleanup needed.
  return S_OK;
}

将 C 命令标识符插入功能区 XML

请注意,OnCreateUICommand()OnDestroyUICommand() 具有命令 ID 参数。默认情况下,uicc 会自动生成 ID。如果您查看 ribbonids.h 文件,您会看到如下行

#define cmdClickMe 7

其中 uicc 使用 <Command> 标签的 Name 属性来创建 C 标识符。让我们看看如何控制这些标识符并使它们遵循全部大写的 C 约定。

回到 Click Me 按钮的 <Command> 标签

  <Command Name="cmdClickMe" LabelTitle="Click me" />

我们可以添加一个 Symbol 属性来设置标识符的名称,以及一个 Id 属性来设置其值。例如

  <Command Name="cmdClickMe" Symbol="RIDC_CLICK_ME"
           Id="42" LabelTitle="Click me" />

(我通过在传统前缀 IDC 前添加 R(功能区)来使用 RIDC 作为前缀。)通过此更改,ribbonids.h 现在将包含

#define RIDC_CLICK_ME 42

Name 可以是任何合法的 C 标识符,Id 可以是 2 到 59999(包括)之间的整数。Id 值可以写成十进制或十六进制。

执行命令

当用户单击按钮时,功能区会调用关联的 IUICommandHandler::Execute() 方法。Execute() 的参数是

  • uCmdID:命令 ID。
  • nVerb:一个常量,指示用户正在执行何种操作。在示例应用程序中,动词始终为 UI_EXECUTIONVERB_EXECUTE
  • pKeypCurrVal:对于某些命令,功能区会在此参数中传递属性及其新值。
  • pCmdProperties:一个 IUISimplePropertySet 集合,包含有关命令的其他属性。示例应用程序不使用此参数。

根据命令的类型,功能区可能会将有关命令状态的一些信息传递给 Execute()。例如,当用户单击切换按钮时,功能区会在 pCurrVal 参数中传递按钮的新状态(关闭或打开)。我们稍后会看到一个使用此信息的示例。一个简单的按钮没有额外的信息,所以我们只需查看命令 ID 即可知道正在执行哪个命令

STDMETHODIMP CMainFrame::Execute (
UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
const PROPVARIANT* pCurrVal, IUISimplePropertySet* pCmdProperties )
{
  switch ( uCmdID )
    {
    case RIDC_CLICK_ME:
      MessageBox ( _T("Thanks for clicking me!"), _T("Hello Ribbon!") );
    break;
    }
 
  return S_OK;
}

请注意,case 语句使用我们在 <Command> 标签中设置的 RIDC_CLICK_ME 标识符。

使用切换按钮

可以放入功能区的另一种控件是切换按钮。这些按钮的功能类似于复选框,每次单击都会在开(按下)和关(未按下)之间切换状态。

添加 ToggleButton 控件

我们将使用切换按钮来显示和隐藏状态栏,类似于 WTL AppWizard 创建的 视图|状态栏 命令。我们需要两个新命令,一个用于新组,一个用于按钮

<Application.Commands>
  <Command Name="grpStatusBar" LabelTitle="Status bar" />
  <Command Name="cmdToggleStatusBar" Symbol="RIDC_TOGGLE_STATUS_BAR"
           LabelTitle="Show status bar" />
</Application.Commands>

然后我们在主选项卡中添加一个包含 ToggleButton 控件的新组

<Tab CommandName="tabMain">
  <Group CommandName="grpHello" SizeDefinition="OneButton">
    <Button CommandName="cmdClickMe" />
  </Group>
  <Group CommandName="grpStatusBar" SizeDefinition="OneButton">
    <ToggleButton CommandName="cmdToggleStatusBar" />
  </Group>
</Tab>

新按钮在切换到开启状态时如下所示

为 ToggleButton 编写 IUICommandHandler::Execute

IUICommandHandler::Execute() 的实现会传递一个键/值对,其形式为 PROPERTYKEYPROPVARIANT。当用户单击切换按钮而调用 Execute() 时,键是 UI_PKEY_BooleanValue,值是一个布尔值,表示按钮的新状态:true 表示按钮现在已切换为开启,false 表示已切换为关闭。

头文件 UIRibbonPropertyHelpers.h 包含一些帮助函数,使获取和设置 PROPVARIANT 中的值变得更容易。由于为切换按钮发送的值是布尔值,我们可以使用 UIPropertyToBoolean() 函数从 PROPVARIANT 读取值

// Add to stdafx.h:
#include <UIRibbonPropertyHelpers.h>
 
STDMETHODIMP CMainFrame::Execute (
  UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
  const PROPVARIANT* pCurrVal, IUISimplePropertySet* pCmdProperties )
{
  switch ( uCmdID )
    {
    case RIDC_CLICK_ME:
      // ...
    break;
 
    case RIDC_TOGGLE_STATUS_BAR:
      {
      BOOL bShowBar;
 
      // We can get the new state of the toggle button by reading pKey/pCurrVal.
      if ( NULL != pKey && UI_PKEY_BooleanValue == *pKey && NULL != pCurrVal )
        {
        BOOL bToggledOn;
 
        UIPropertyToBoolean ( *pKey, *pCurrVal, &bToggledOn );
 
        // Show the bar if the button is now toggled on.
        bShowBar = bToggledOn;
        }
      else
        // Show the bar if the bar is not currently visible.
        bShowBar = !::IsWindowVisible ( m_hWndStatusBar );
 
      ShowStatusBar ( !bShowBar );
      }
 
    break;
    }
}
 
void CMainFrame::ShowStatusBar ( BOOL bShow )
{
  CWindow(m_hWndStatusBar).ShowWindow ( bShow ? SW_HIDE : SW_SHOW );
  UpdateLayout();
}

将此代码添加到 CMainFrame::Execute() 后,您可以单击切换按钮,状态栏将随着每次单击而显示或隐藏。

设置命令属性

由于切换按钮默认是关闭的,所以我们按钮的初始状态不是我们想要的;状态栏一开始是可见的,所以按钮应该以开启状态开始。然而,XML 文件中没有用于设置按钮初始状态的元素。相反,功能区在该按钮的 IUICommandHandler 创建后立即查询该属性。

当功能区需要查询命令的属性时,它会调用该命令的 IUICommandHandler::UpdateProperty() 方法,该方法可以在 PROPVARIANT 中返回属性的新值。以下是我们设置切换按钮初始状态的方法

STDMETHODIMP CMainFrame::UpdateProperty (
    UINT32 uCmdID, REFPROPERTYKEY key, const PROPVARIANT* pCurrVal,
    PROPVARIANT* pNewVal )
{
  if ( RIDC_TOGGLE_STATUS_BAR == uCmdID && UI_PKEY_BooleanValue == key &&
       NULL != pNewVal )
    {
    // Set the "Show status bar" button to be initially toggled on.
    UIInitPropertyFromBoolean ( key, TRUE, pNewVal );
    return S_OK;
    }
 
  // We don't respond to queries for other buttons or properties.
  return E_NOTIMPL;
}

这本质上是与我们刚刚在 Execute() 中看到的代码相反。它使用 UIRibbonPropertyHelpers.h 中的另一个帮助函数 UIInitPropertyFromBoolean() 将布尔值存储在 PROPVARIANT 中。所有 UIInitPropertyFromXxx() 帮助函数都接受 PROPERTYKEY 和新值,以便它们可以检查该值是否与属性的数据类型兼容。如果您传递不兼容的数据类型,编译器将发出错误。

如果您在 CMainFrame::UpdateProperty() 的开头设置一个断点,您会发现它被调用了很多次。功能区 XML 中有很多我们没有设置的可选属性。功能区会调用 UpdateProperty() 来查询所有这些属性,如果函数返回 E_NOTIMPL,则会回退到合理的默认值。这就是为什么我们必须检查 key 参数,以便我们知道功能区何时正在查询按钮的切换状态,而不是其他默认值就足够的属性。

命令的其他属性

添加工具提示和按键提示

功能区内置了键盘导航支持。如果您按下 Alt 键,您将看到工具提示,告诉您如何浏览功能区元素

这些导航工具提示称为按键提示,可以通过 <Command> 标签的 Keytip 属性进行自定义。例如,要将主选项卡的导航键更改为“M”,请对选项卡的 <Command> 标签进行此更改

  <Command Name="tabMain" Symbol="RIDT_MAIN" LabelTitle="Main" Keytip="M" />

此字符串以及其他字符串属性都以资源形式出现在 uicc 创建的 ribbon.rc 文件中。这样,字符串就可以本地化。字符串资源 ID 从 60001 开始,但 ID 可以像命令 ID 一样自定义。要自定义字符串属性,您需要创建一个名为 <Command.Keytip> 的子标签来保存按键提示属性。在该标签中,创建一个包含 C 标识符和资源 ID 的 <String> 标签

  <Command Name="tabMain" Symbol="RIDT_MAIN" LabelTitle="Main">
    <Command.Keytip>
      <String Symbol="RIDS_MAIN_KEYTIP" Id="54321">M</String>
    </Command.Keytip>
  </Command>

每个命令还可以有一个工具提示,当鼠标悬停在按钮上时显示。工具提示可以有一个标题(以粗体显示)和一个描述。这些字符串通过 <Command> 标签的 TooltipTitleTooltipDescription 属性设置。以下是如何向切换按钮添加工具提示

  <Command Name="cmdToggleStatusBar" Symbol="RIDC_TOGGLE_STATUS_BAR"
           Keytip="T" LabelTitle="Show status bar"
           TooltipTitle="Toggle the status bar"
           TooltipDescription="Show or hide the status bar" />

Keytip 一样,您可以通过使用 <String> 标签更改工具提示字符串的 ID。以下是自定义工具提示的外观

为按钮分配图像

我们的按钮没有任何图形看起来很无聊,所以让我们看看如何添加一些。功能区只支持两种图形格式:用于普通图像的 32bpp BMP 文件和用于高对比度图像的 4bpp BMP 文件。如果您使用任何其他类型的图形,uicc 将会报错。(我必须提前为我制作的糟糕图形道歉。我自己画不出任何好东西,所以我从 Windows SDK 中的示例功能区应用程序中借用了图形。)

每个命令可以有四组图像:大、小、大高对比度和小高对比度。功能区根据命令出现的位置选择使用大图像还是小图像。我们到目前为止添加的两个按钮是大按钮,因此它们将使用大图像。如果您将这些命令添加到 QAT,这些按钮将使用小图像。当系统使用高对比度视觉主题时,会使用两组高对比度图像;图像尺寸的选择保持不变。

图像通过 <Command> 标签的属性设置。以下是为“显示状态栏”按钮设置大图像和小图像的 XML 代码

  <Command Name="cmdToggleStatusBar" ... >
    <Command.LargeImages>
      <Image Source="res/StatusBar_lg.bmp" />
    </Command.LargeImages>
    <Command.SmallImages>
      <Image Source="res/StatusBar_sm.bmp" />
    </Command.SmallImages>
  </Command>

Source 属性是相对于 XML 文件所在位置的文件路径,因此在本例中,文件位于 res 子目录中。uicc 将这些图像包含在 ribbon.rc 文件中并为其生成 ID。与字符串一样,您可以通过向 <Image> 标签添加属性来自定义图像的资源 ID 和 C 标识符

  <Image Source="res/StatusBar_lg.bmp" Id="12345" Symbol="RIDI_STATUS_BAR_LG" />

在四组图像中的每一组中,最多可以有四个 <Image> 标签,用于不同的 DPI 设置。在此示例程序中,我们只有一个用于默认 DPI 96 的图像。这很好,因为如果系统使用更高的 DPI 设置,功能区将根据需要缩放图像。

这是功能区在两个按钮上带有图像时的样子

设置应用程序菜单

应用程序菜单是功能区元素,类似于具有常规菜单的应用程序中的文件菜单。可以通过选项卡左侧的下拉按钮访问应用程序菜单。其内容列在 <Ribbon> 标签的子标签 <Ribbon.ApplicationMenu> 中。

应用程序菜单包含两个子元素,一个最近使用的文件列表和一个命令菜单。MRU 列表通过 <ApplicationMenu.RecentItems> 子标签设置,菜单内容由一个或多个 <MenuGroup> 子标签控制。

以下是显示我们示例应用程序中应用程序菜单内容的 XML 代码

<Application.Commands>
  <!-- New commands: -->
  <Command Name="cmdAbout" Symbol="RIDC_ABOUT" LabelTitle="&About" />
  <Command Name="cmdExit" Symbol="RIDC_EXIT" LabelTitle="E&xit" />
  <Command Name="cmdMRUList" LabelTitle="(MRU goes here)" />
</Application.Commands>
<Application.Views>
  <Ribbon>
    <Ribbon.ApplicationMenu>
      <ApplicationMenu CommandName="cmdApplicationMenu">
        <ApplicationMenu.RecentItems>
          <RecentItems CommandName="cmdMRUList" />
        </ApplicationMenu.RecentItems>
        <MenuGroup>
          <Button CommandName="cmdAbout" />
          <Button CommandName="cmdExit" />
        </MenuGroup>
      </ApplicationMenu>
    </Ribbon.ApplicationMenu>
  </Ribbon>
</Application.Views>

菜单看起来像这样

菜单有几点需要注意

  • MRU 列表只是一个空占位符,用于显示其外观。
  • 菜单在一个组中包含两个命令。您可以通过关闭一个 <MenuGroup> 标签并开始另一个来在菜单中创建分隔符。
  • 菜单中出现的命令的助记符通过在菜单项文本中放置一个&号来设置,与传统菜单一样。&号必须在 XML 中转义并写为 "&"。如果命令在功能区的其他地方使用,&号将被忽略,并改用 Keytip 属性。

保存功能区设置

我们将添加到示例应用程序的最后一件事是应用程序关闭时保存功能区设置的功能。如果您更改 QAT 的内容或最小化功能区,这些更改将在您关闭应用程序时丢失。为了解决这个问题,我们将使用两个 IUIRibbon 方法来加载和保存设置。首先,向 CMainFrame 添加一个字符串成员,用于保存数据文件路径

  // In MainFrm.h:
  CString m_sSettingsFilePath;

由于此路径在应用程序运行时不会改变,我们可以在 CMainFrame 构造函数中初始化它

CMainFrame::CMainFrame()
{
TCHAR szTempDir[MAX_PATH] = {0};
 
  // Build the path to the file where we'll store the ribbon settings.
  GetTempPath ( _countof(szTempDir), szTempDir );
  PathAddBackslash ( szTempDir );
 
  m_sSettingsFilePath.Format ( _T("%sHelloRibbonSettings.dat"), szTempDir );
}

然后我们在功能区创建时加载设置,并在功能区销毁时保存设置。这通过在 CMainFrame::OnViewChanged() 中添加几行代码来完成

STDMETHODIMP CMainFrame::OnViewChanged(...)
{
  switch ( nVerb )
    {
    case UI_VIEWVERB_CREATE:
      m_pRibbon = pView;
      LoadRibbonSettings();
    break;
 
    case UI_VIEWVERB_DESTROY:
      m_pRibbon.Release();
      SaveRibbonSettings();
    break;
  }
 
  return S_OK;
}

LoadRibbonSettings() 调用 IUIRibbon::LoadSettingsFromStream() 加载设置。由于该方法接受数据上的 IStream 接口,因此我们调用 SHCreateStreamOnFileEx() 获取可用于读取文件的 IStream 接口。

void CMainFrame::LoadRibbonSetings()
{
HRESULT hr;
CComPtr<IStream> pStrm;
 
  hr = SHCreateStreamOnFileEx ( m_sSettingsFilePath, STGM_READ,
                                0, FALSE, NULL, &pStrm );
 
  if ( SUCCEEDED(hr) )
    m_pRibbon->LoadSettingsFromStream ( pStrm );
}

如果文件不存在或无法打开,我们不会调用 LoadSettingsFromStream(),并且功能区将以默认状态启动。

SaveRibbonSettings() 类似。我们获取一个可用于写入文件的 IStream 接口,并将文件大小重置为零,以便擦除任何现有内容。然后我们调用 IUIRibbon::SaveSettingsToStream() 将设置保存到文件。

void CMainFrame::SaveRibbonSetings()
{
HRESULT hr;
CComPtr<IStream> pStrm;
 
  hr = SHCreateStreamOnFileEx ( m_sSettingsFilePath, STGM_WRITE|STGM_CREATE,
                                FILE_ATTRIBUTE_NORMAL, TRUE, NULL, &pStrm );
 
  if ( SUCCEEDED(hr) )
    {
    LARGE_INTEGER liPos;
    ULARGE_INTEGER uliSize;
 
    liPos.QuadPart = 0;
    uliSize.QuadPart = 0;
 
    pStrm->Seek ( liPos, STREAM_SEEK_SET, NULL );
    pStrm->SetSize ( uliSize );
 
    m_pRibbon->SaveSettingsToStream ( pStrm );
    }
}

现在,当您关闭应用程序时,您对功能区大小和 QAT 所做的任何更改都将在下次运行应用程序时保留。

结论

在本文中,我们了解了在应用程序中包含功能区所需的基本步骤。下次,我们将深入探讨命令如何在组中组织以及如何自定义组的布局。

修订历史

2011年2月22日:文章首次发布

© . All rights reserved.