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

如何支持同一可执行文件中的功能区和菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (37投票s)

2010年12月29日

公共领域

17分钟阅读

viewsIcon

97813

downloadIcon

6971

您可以在 Windows 7 中提供 Ribbon,同时仍然支持菜单输入,并且只发布一个可执行文件。

引言

我一直在开发一个应用程序,打算在 Windows XP 和 Windows 7 客户端上使用,但我想通过 Windows Ribbon Framework 支持新的 Ribbon 控件。我也想为那些更喜欢传统菜单而不是 Ribbon 风格交互的用户提供传统菜单。

在研究了 API 一段时间后,我意识到在一个可执行文件中同时支持 Ribbon 和传统菜单是相当简单的。在本文中,我将描述我组装的一个示例应用程序,它展示了如何仅使用 Windows API 和 Windows Ribbon Framework 来实现对两种命令选择方法的支持。

关于 Ribbon 的几句话

Ribbon 最初是在 Microsoft Office 2007 中引入的,作为一种方法来管理庞大的菜单和工具栏层级结构,这些结构已经发展到向用户展示套件的所有功能。然后它进入了 Windows 7 的 Paint 和 WordPad 应用中,现在可以在多个第三方应用程序中找到。

正如微软的大多数事物一样,用户似乎要么喜欢要么讨厌 Ribbon。我碰巧喜欢它,因为我一直不喜欢工具栏,我觉得菜单充其量很笨拙。由于并非所有人都持有此观点,而且包含菜单相对容易,并且我希望通过单个可执行文件支持 XP、Vista 和 Windows 7,因此我将尝试通过允许用户选择他们喜欢的命令呈现方式来满足尽可能多的人。

项目

我们将要使用的应用程序最初改编自 Raymond Chen 在他的文章中用作示例程序框架的一个“草稿程序”。我将其改编为我自己的非常精简的 C++ 包装器,并添加了一些我喜欢在功能齐全的应用程序中拥有的东西,然后我着手让它与 Ribbon 一起工作。您可以下载 Visual C++ 2010 Express 项目,并按照下面的说明进行操作。

必备组件

首先,您需要 Visual C++ 2010 Express(免费)或其更高级的版本(收费)才能编译项目。如果您想使用不同的编译器,则需要根据需要调整代码和项目。您还需要 Windows 7 SDK 版本 7.1。最后,为了查看 Ribbon 界面,您需要在 Windows 7 或 Windows Vista SP2 或更高版本上运行该应用程序,并安装 Platform Update。当应用程序在 Vista SP1 或更早版本上运行时,它只会显示传统菜单。

配置 Visual Studio 项目以支持 Ribbon

在 Visual Studio 中支持 Ribbon 需要一些设置。首先,安装 SDK 后,您需要在 Visual Studio 2010 的任何新项目中更改一个设置,以利用 SDK。在项目属性的“配置属性”>“常规”下,将“平台工具集”属性更改为“Windows7.1SDK”。

接下来,您需要向项目中添加一个新的 XML 文件来包含您的 Ribbon 标记。Ribbon 由 MSDN 上详细描述的 XML 标记模式定义。此 XML 由 Windows 7.1 SDK 提供的工具 UICC 编译。在将 XML 文件添加到项目后,打开该文件的属性页,并在“常规”属性中将“项类型”属性更改为“自定义生成工具”。应用设置,然后在“自定义生成工具”>“常规”设置中,将命令行更改为以下内容。

"$(WindowsSdkToolsDir)\bin\uicc" "%(Identity)" 
   "%(Filename).bin" /header:"%(Filename).h" /res:"%(Filename).rc2"

接下来,在“输出”属性中定义 UICC 编译器的输出。

%(Filename).bin;%(Filename).h;%(Filename).rc2;%(Outputs)

请确保为 Debug 和 Release 构建都应用了这些更改。

最后,您需要将 UICC 编译器的输出添加到项目的“资源脚本”中。这些需要放在资源文件中的特定位置。在 Visual Studio 2010 Professional 中,切换到资源视图(Ctrl + Shift + E),右键单击资源脚本,然后从上下文菜单中选择“资源包含”。在“只读符号指令”窗口的顶部添加 UICC 生成的头文件(例如,Ribbon.h),并在“编译时指令”窗口中添加 UICC 生成的资源脚本(例如,Ribbon.rc2),如下图所示。

在 Visual C++ 2010 Express 中,您需要直接编辑资源脚本代码,因为没有资源编辑器。在“解决方案资源管理器”中右键单击资源脚本,然后从上下文菜单中选择“查看代码”。找到 TEXTINCLUDE 部分并按如下方式进行编辑。

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
 
1 TEXTINCLUDE
BEGIN
    "resource.h\0"
END
 
2 TEXTINCLUDE
BEGIN
    "#include ""Ribbon.h""\r\n"
    "#ifndef APSTUDIO_INVOKED\r\n"
    "#include ""targetver.h""\r\n"
    "#endif\r\n"
    "#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "#include ""windows.h""\r\n"
    "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "\0"
END
 
3 TEXTINCLUDE
BEGIN
    "#include ""Ribbon.rc2""\r\n"
    "\0"
END
 
#endif    // APSTUDIO_INVOKED

通过这些更改,资源脚本将包含 UICC 编译器创建的控件 ID,以便您可以在菜单资源中重用它们。

运行应用程序

如我之前提到的,这个项目开始时是一个草稿应用程序。我的想法是为新应用程序提供一个起点,它会处理很多乏味的、样板化的东西,这样我就可以专注于新应用程序中有趣的部分。弄清楚如何集成 Ribbon 并使其正常工作并不完全直接,这让我很失望。我问题的答案通常在互联网的某个地方,但从未在一个地方找到。这就是为什么我决定写这篇文章来帮助那些可能和我一样经历同样挣扎的人。

下载 Visual C++ 2010 Express 项目后,在 Express 中打开它,构建并运行它。或者,如果您愿意,也可以下载可执行文件并运行它(如果您还没有安装,则需要安装 Visual C++ 2010 Redistributable)。在 Windows 7 上,以及在安装了 Platform Update 的 Windows Vista SP2 或更高版本上,您会看到一个顶部带有 Ribbon 控件的窗口。在 Ribbon 的“视图”选项卡中有一个按钮,允许用户切换到传统的菜单栏。“视图”菜单有一个相应的选项,允许用户再次显示 Ribbon。大多数其他命令将简单地在客户端区域输出一行文本,描述所选的选项。

在 Windows XP 和没有 Platform Update 的 Windows Vista 上,您将看到一个带有传统菜单的窗口。“显示 Ribbon”菜单选项被禁用,因为应用程序检测到这些平台不支持 Ribbon API。

检查代码

我不会详细介绍代码中常见的 Windows API 元素,也不会详细介绍它使用的极简 C++ 框架。我也不会详细介绍 Ribbon 标记的细节,这些细节在 MSDN Ribbon 文档中有详细介绍。相反,我将重点介绍与支持 Ribbon 和菜单相关的代码部分。

需要包含两个头文件才能获取 Ribbon 接口及其关联 GUID 的定义。

#include <UIRibbon.h>
#include <UIRibbonPropertyHelpers.h>

此外,还需要调用 Desktop Window Manager 中的一个函数来解决删除 Ribbon 时的一些绘制问题。也将这些函数定义包含进来。

#include <dwmapi.h>

应用程序的初始化在 App::Initialize 方法中执行。首先需要初始化 COM。

if (FAILED(CoInitialize(NULL)))
{
  ReportError(IDS_COINITIALIZE_FAILED);
  retVal = false;
  /* Fear of goto is highly irrational. Get over it. */
  goto exitinit;
}

关于 goto 的注释是另一篇文章的主题,所以我们暂时忽略它。初始化 COM 是必需的,因为 Ribbon API 是一组 COM 对象,它们实现了各种 Ribbon 接口(IUIFrameworkIUIRibbon 等)。在初始化方法中再往下一点,代码会读取保存的用户设置(如果存在),然后检查应用程序是否应该使用 Ribbon。无论应用程序运行在什么操作系统上,该设置默认都将是 true,因此应用程序将调用 CreateRibbon 方法。

LoadAppSettings();
 
if (settings.isRibbon)
{
  CreateRibbon();
}

CreateRibbon 方法使用 Ribbon API 的 COM 对象来初始化和显示 Ribbon。

bool App::CreateRibbon()
{
  /* Attempt to create the ribbon framework interface. */
  HRESULT hr = CoCreateInstance(
    CLSID_UIRibbonFramework,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_PPV_ARGS(&pFramework));
 
  if (SUCCEEDED(hr))
  {
    /* Framework creation succeeded, so initialize the framework and
    create the ribbon. */
 
    /* The killRibbon flag controls the activation 
    of message handling to fix a repaint
    problem that occurs when the ribbon is removed. */
    killRibbon = false;
 
    hr = pFramework->Initialize(GetHWND(), this);
 
    if (SUCCEEDED(hr))
    {
      hr = pFramework->LoadUI(GetModuleHandle(NULL), L"APPLICATION_RIBBON");
 
      if (SUCCEEDED(hr))
      {
        hr = pFramework->GetView(0, IID_PPV_ARGS(&pRibbon));
 
        if (SUCCEEDED(hr))
        {
          settings.isRibbon = true;
        }
      }
    }
  }
 
  if (FAILED(hr))
  {
    /* If ribbon creation or initialization failed, 
    make sure that any interfaces are released
    and set to null so that the UI will fall back 
    and use the menu instead. */
    CloseRibbon();
  }
 
  return SUCCEEDED(hr);
}

如果 COM 对象创建成功并且其方法被调用,API 将删除主窗口的菜单,并在其位置显示 Ribbon。此方法在不支持 Ribbon API 的平台上会失败,由于应用程序默认在主窗口上显示菜单,因此该菜单将保留在那里。

接下来,设置各种命令选项的初始状态。

SetDirty(false);
SetRedo(false);
EnableMenuItem(GetMenu(GetHWND()), ID_SHOW_RIBBON,
  IsRibbonSupported() ? MF_ENABLED : MF_GRAYED);

ID_SHOW_RIBBON 常量代表菜单选择,当 UI 显示菜单栏时,该选择会启用 Ribbon。此函数使用 IsRibbonSupported 方法的返回值来启用或禁用菜单项。该方法实现如下。

bool App::IsRibbonSupported()
{
  bool isRibbonSupported = false;
  IUIFramework* pTmp = 0;
 
  /* Attempt to create the ribbon framework interface. */
  HRESULT hr = CoCreateInstance(
    CLSID_UIRibbonFramework,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_PPV_ARGS(&pTmp));
 
  if (SUCCEEDED(hr))
  {
    isRibbonSupported = true;
    pTmp->Release();
  }
 
  return isRibbonSupported;
}

该方法所做的就是尝试创建一个 Ribbon API 提供的 COM 对象。如果尝试失败,则该方法返回 false。这总会在 XP 和没有 Platform Update 的 Vista 上失败,除非某个有进取心的开发人员在这些平台上实现了所有必需的 COM 接口和对象。

实现 COM 接口

Ribbon API 要求使用 Ribbon 的应用程序实现两个 COM 接口:IUIApplicationIUICommandHandlerIUIApplication 接口定义了 Ribbon API 回调到应用程序的方法。IUICommandHandler 接口在 Ribbon 上公开的每个命令都会被调用。

Scratch Ribbon Project 在 App 对象中实现了这两个接口。并非必须如此,但这使得与菜单栏的共享实现更容易完成。另一种实现方式是为每个命令创建一个唯一的 IUICommandHandler 实现。

ScratchRibbonProject.hApp 类声明中,声明了相关的方法。定义在 ScratchRibbonProject.cpp 中,我们将依次检查每个方法。

首先,我们需要实现 IUnknown,因为所有 COM 接口都派生自该接口。AddRefRelease 方法相当直接。

ULONG STDMETHODCALLTYPE App::AddRef()
{
  return InterlockedIncrement(&refCount);
}
 
ULONG STDMETHODCALLTYPE App::Release()
{
  return InterlockedDecrement(&refCount);
}

接下来是 QueryInterface,如果对象实现了请求的接口,则它应返回指向该接口的指针。

HRESULT STDMETHODCALLTYPE App::QueryInterface(
  REFIID riid,
  void **ppvObject)
{
  if (!ppvObject)
  {
    return E_INVALIDARG;
  }
 
  if (riid == IID_IUnknown)
  {
    *ppvObject = static_cast<IUnknown*>(
                      static_cast<IUIApplication*>(this));
  }
  else if (riid == __uuidof(IUICommandHandler))
  {
    *ppvObject = static_cast<IUICommandHandler*>(this);
  }
  else if (riid == __uuidof(IUIApplication))
  {
    *ppvObject = static_cast<IUIApplication*>(this);
  }
  else
  {
    *ppvObject = 0;
    return E_NOINTERFACE;
  }
 
  AddRef();
  return S_OK;
}

完成这项繁琐的工作后,我们就可以继续处理有趣的接口了。IUIApplication 接口指定了三个方法:OnViewChangedOnCreateUICommandOnDestroyUICommand。当 Ribbon 视图的状态发生变化时,Ribbon 框架会调用 OnViewChanged 方法。verb 参数指定了视图执行的操作。

HRESULT STDMETHODCALLTYPE App::OnViewChanged(
  UINT32 viewId,
  UI_VIEWTYPE typeID,
  IUnknown* pView,
  UI_VIEWVERB verb,
  INT32 uReasonCode)
{
  HRESULT hr = E_NOTIMPL;
 
  if (UI_VIEWVERB_CREATE == verb)
  {
    IUIRibbon* pRibbon = NULL;
    hr = pView->QueryInterface(IID_PPV_ARGS(&pRibbon));
 
    if (SUCCEEDED(hr))
    {
      LoadRibbonSettings(pRibbon);
      pRibbon->Release();
    }
  }
  else if (UI_VIEWVERB_SIZE == verb)
  {
    RECT rect = {};
    GetClientRect(GetHWND(), &rect);
    OnSize(GetHWND(), 0, rect.right - rect.left, rect.bottom - rect.top);
  }
  else if (UI_VIEWVERB_DESTROY == verb)
  {
    IUIRibbon* pRibbon = NULL;
    hr = pView->QueryInterface(IID_PPV_ARGS(&pRibbon));
 
    if (SUCCEEDED(hr))
    {
      SaveRibbonSettings(pRibbon);
      pRibbon->Release();
    }
  }

  return hr;
}

在 Ribbon 初始化和拆卸时,该方法分别使用 UI_VIEWVERB_CREATEUI_VIEWVERB_DESTROY verb 常量被调用。此实现使用这些调用在创建 Ribbon 视图时加载 Ribbon 设置,并在销毁时保存它们。我们稍后会查看 LoadRibbonSettingsSaveRibbonSettings 方法。

UI_VIEWVERB_SIZE 表示 Ribbon 的大小已更改(例如,Ribbon 已最小化)。应用程序可能需要响应此通知来调整任何需要根据新的 Ribbon 大小进行移动或调整大小的其他窗口。在此示例中,该方法通过调用 OnSize 消息处理程序来调整客户端区域。

Ribbon 框架会为 Ribbon 标记中指定的每个命令调用 OnCreateUICommand 方法。应用程序必须返回一个指向 IUICommandHandler 接口的指针,该接口将处理每个特定命令。在此应用程序中,所有命令都由 App 对象实例服务,因此我们只需返回请求的指针并增加引用计数。

HRESULT STDMETHODCALLTYPE App::OnCreateUICommand(
  UINT32 commandId,
  UI_COMMANDTYPE typeID,
  IUICommandHandler** commandHandler)
{
  if (commandHandler)
  {
    *commandHandler = static_cast<IUICommandHandler*>(this);
    AddRef();
    return S_OK;
  }
 
  return E_INVALIDARG;
}

OnDestroyUICommand 方法在每次销毁命令时被调用。这给了应用程序一个清理其命令处理程序的機会(如果需要),但在我们的例子中,没有什么可做的。

HRESULT STDMETHODCALLTYPE App::OnDestroyUICommand(
  UINT32 commandId,
  UI_COMMANDTYPE typeID,
  IUICommandHandler* commandHandler)
{
  return E_NOTIMPL;
}

最后,我们需要实现 IUICommandHandler 接口的两个方法,这些方法将由我们应用程序中的所有 Ribbon 命令共享。UpdateProperty 方法由框架调用,以请求更新命令的状态。作为修改启用/禁用状态的示例,此应用程序根据 App 对象维护的标志更改“保存”和“重做”命令的状态。

HRESULT STDMETHODCALLTYPE App::UpdateProperty(
  UINT32 commandId,
  REFPROPERTYKEY key,
  const PROPVARIANT *currentValue,
  PROPVARIANT *newValue)
{
  if (newValue)
  {
    if (key.fmtid == UI_PKEY_Enabled.fmtid)
    {
      if (commandId == ID_CMD_SAVE)
      {
        (*newValue).boolVal = IsDirty() ? VARIANT_TRUE : VARIANT_FALSE;
      }
      else if (commandId == ID_CMD_REDO)
      {
        (*newValue).boolVal = CanRedo() ? VARIANT_TRUE : VARIANT_FALSE;
      }
    }
  }
  return S_OK;
}

最后,我们来到了将 Ribbon 命令的激活连接到实际应用程序代码的方法。当应用程序需要响应命令事件时,会使用 UI_EXECUTIONVERB_EXECUTE 执行动词常量调用 Execute。此应用程序将一个相当于如果命令已从菜单中选择时会发送的 WM_COMMAND 消息发布到消息队列中。这反过来又会触发 WndProc 方法中的 WM_COMMAND 处理程序。

HRESULT STDMETHODCALLTYPE App::Execute(
  UINT32 commandId,
  UI_EXECUTIONVERB verb,
  const PROPERTYKEY *key,
  const PROPVARIANT *currentValue,
  IUISimplePropertySet *commandExecutionProperties)
{
  if (verb == UI_EXECUTIONVERB_EXECUTE)
  {
    PostMessage(GetHWND(), WM_COMMAND, commandId, 0);
  }
 
  return S_OK;
}

Ribbon XML

Ribbon.xml 文件包含定义应用程序 Ribbon 的标记。此文件由 UICC 编译并包含在资源脚本中。每个命令都有一个 Id 属性,指定在激活命令时发送到应用程序的数字值。此值与 UICC 生成的 Ribbon.h 文件中的 Symbol 属性相关联。以下是“新建”和“打开”命令的定义。

<Command Name="cmdNew" Id="0x0100" Symbol="ID_CMD_NEW" Keytip="N">
   <Command.LabelTitle>New</Command.LabelTitle>
   <Command.TooltipTitle>New (Ctrl+N)</Command.TooltipTitle>
   <Command.TooltipDescription>Create a new document</Command.TooltipDescription>
   <Command.LargeImages>
      <Image Source="images/New-icon-32.bmp" Id="101" 
         Symbol="ID_NEW_LARGEIMAGE1" MinDPI="96" />
   </Command.LargeImages>
   <Command.SmallImages>
      <Image Source="images/New-icon-16.bmp" Id="102" 
        Symbol="ID_NEW_SMALLIMAGE1" MinDPI="96" />
   </Command.SmallImages>
</Command>
<Command Name="cmdOpen" Id="0x0103" 
          Symbol="ID_CMD_OPEN" Keytip="O">
   <Command.LabelTitle>Open</Command.LabelTitle>
   <Command.TooltipTitle>Open (Ctrl+O)</Command.TooltipTitle>
   <Command.TooltipDescription>Open a document</Command.TooltipDescription>
   <Command.LargeImages>
      <Image Source="images/Open-icon-32.bmp" Id="103" 
            Symbol="ID_OPEN_LARGEIMAGE1" MinDPI="96" />
   </Command.LargeImages>
   <Command.SmallImages>
      <Image Source="images/Open-icon-16.bmp" Id="104" 
         Symbol="ID_OPEN_SMALLIMAGE1" MinDPI="96" />
   </Command.SmallImages>
</Command>

编译此 XML 后,将在 Ribbon.h 中定义以下常量。

#define ID_CMD_NEW 0x0100
#define ID_CMD_NEW_LabelTitle_RESID 60010
#define ID_CMD_NEW_Keytip_RESID 60011
#define ID_CMD_NEW_TooltipTitle_RESID 60012
#define ID_CMD_NEW_TooltipDescription_RESID 60013
#define ID_NEW_SMALLIMAGE1 102
#define ID_NEW_LARGEIMAGE1 101
#define ID_CMD_OPEN 0x0103
#define ID_CMD_OPEN_LabelTitle_RESID 60014
#define ID_CMD_OPEN_Keytip_RESID 60015
#define ID_CMD_OPEN_TooltipTitle_RESID 60016
#define ID_CMD_OPEN_TooltipDescription_RESID 60017
#define ID_OPEN_SMALLIMAGE1 104
#define ID_OPEN_LARGEIMAGE1 103

其中大多数常量用于各种字符串和图像资源,但将在菜单中使用的常量是 ID_CMD_NEWID_CMD_OPEN

菜单资源

我们之前修改了 ScratchRibbonProject.rc 以包含 Ribbon.h,以便 Ribbon 编译器在该头文件中定义的常量也可以被菜单资源使用。

IDC_APP_MENU MENU
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New\tCtrl+N",                ID_CMD_NEW
        MENUITEM "&Open\tCtrl+O",               ID_CMD_OPEN
        MENUITEM "&Save\tCtrl+S",               ID_CMD_SAVE
        MENUITEM "Save &As...\tCtrl+Shift+S",   ID_CMD_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "E&xit\tAlt+F4",               ID_CMD_EXIT
    END
    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo\tCtrl+Z",               ID_CMD_UNDO
        MENUITEM "&Redo\tCtrl+Y",               ID_CMD_REDO
        MENUITEM SEPARATOR
        MENUITEM "Cu&t\tCtrl+X",                ID_CMD_CUT
        MENUITEM "&Copy\tCtrl+C",               ID_CMD_COPY
        MENUITEM "&Paste\tCtrl+V",              ID_CMD_PASTE
        MENUITEM "&Delete\tDel",                ID_CMD_DELETE
    END
    POPUP "&View"
    BEGIN
        MENUITEM "Zoom &In\tCtrl+Plus",         ID_CMD_ZOOMIN
        MENUITEM "Zoom &Out\tCtrl+Minus",       ID_CMD_ZOOMOUT
        MENUITEM "&Normal Zoom\t Ctrl+0",       ID_CMD_NORMALZOOM
        MENUITEM SEPARATOR
        MENUITEM "&Show Ribbon",                ID_SHOW_RIBBON
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&View Help\tF1",              ID_CMD_VIEWHELP
        MENUITEM "&About\tCtrl+?",              ID_CMD_ABOUT
    END
END

菜单资源中定义的菜单项现在共享与 Ribbon.xml 中定义的相同的数字标识符。激活 Ribbon 或菜单上的任何这些项都将触发 OnCommand 消息处理程序中的相同处理程序。

void App::OnCommand(
  HWND hwnd,
  int id,
  HWND hwndCtl,
  UINT codeNotify)
{
  switch (id)
  {
  case ID_CMD_NEW:
    AppendText(L"New document\r\n");
    SetDirty(true);
    break;
 
  case ID_CMD_OPEN:
    AppendText(L"Open document\r\n");
    break;
 
    /* ... */
  }
}

切换 Ribbon 和菜单

当应用程序第一次在支持 Ribbon 的平台上运行时,应用程序默认会显示 Ribbon。激活“视图”选项卡中的“显示 Ribbon”命令将导致从窗口中删除 Ribbon,并在其位置显示菜单栏。该命令的标识符是 ID_HIDE_RIBBON,激活它时,将在 OnCommand 方法中执行以下代码。

case ID_HIDE_RIBBON:
  /* I use PostMessage here because when I tried to remove the
  ribbon immediately after pressing the corresponding ribbon button,
  the UIRibbon code would die trying to process the WM_LBUTTONUP
  message on a ribbon window that no longer existed. This gets
  around that. */
  PostMessage(hwnd, AM_SHOW_MENU, 0, 0);
  break;

这会将 AM_SHOW_MENU 消息(应用程序定义的 ist message)放入消息队列,并在 App::WndProc 中的以下代码中进行处理。

case AM_SHOW_MENU:
  {
    /* Set this flag to trigger the repaint hack for DWM environments
    when the ribbon is removed. */
    killRibbon = true;
    CloseRibbon();
    PostMessage(GetHWND(), AM_RESTORE_MENU, 0, 0);
  }
  return 0;

此代码设置一个标志(killRibbon)以激活特殊的 ist message 处理,以解决一些绘制问题(我们稍后会检查)。然后它调用 CloseRibbon 方法。

void App::CloseRibbon()
{
  /* If we have a ribbon, release it so that it will uninitialize cleanly. */
  if (pRibbon)
  {
    pRibbon->Release();
    pRibbon = 0;
  }
 
  /* Likewise, destroy and release the ribbon framework. */
  if (pFramework)
  {
    pFramework->Destroy();
    pFramework->Release();
    pFramework = 0;
  }
 
  settings.isRibbon = false;
}

此方法释放(如果设置了)pRibbon 对象,调用 pFramework 对象的 Destroy 方法,释放 pFramework 对象,并将指针设置为 null。最后,它更新 settings 对象以指出 Ribbon 未在使用中。

AM_SHOW_MENU 处理程序的最后一件事是发布一个 AM_RESTORE_MENU 消息,该消息按如下方式处理。

case AM_RESTORE_MENU:
  {
    SetMenu(GetHWND(), hMenu);
 
    /* This is hackish, but it sets the menu items to the proper state. In a real app
    I'd probably use some sort of signal/slots implementation to wire up this stuff. */
    SetDirty(IsDirty());
    SetRedo(CanRedo());
    EnableMenuItem(GetMenu(GetHWND()), ID_SHOW_RIBBON,
      IsRibbonSupported() ? MF_ENABLED : MF_GRAYED);
  }
  return 0;

使用 SetMenu 将菜单栏恢复到窗口,并根据需要启用或禁用菜单项。

反过来,如果用户从“视图”菜单中选择了“显示 Ribbon”选项,则会触发 OnCommand 方法中的 ID_SHOW_RIBBON 情况。

case ID_SHOW_RIBBON:
  PostMessage(hwnd, AM_SHOW_RIBBON, 0, 0);
  break;

这反过来会激活 WndProc 中的一个情况。

case AM_SHOW_RIBBON:
  {
    if (IsRibbonSupported())
    {
      /* Don't need the repaint hack anymore. */
      killRibbon = false;
 
      ShowWindow(GetHWND(), SW_HIDE);
      CreateRibbon();
      ShowWindow(GetHWND(), SW_SHOW);
 
      RECT rect = {};
      GetClientRect(GetHWND(), &rect);
 
      /* You aren't supposed to post a WM_SIZE message, but it's the only hack
      I could make work consistently to get the window to repaint correctly in
      all situations. */
      PostMessage(GetHWND(), WM_SIZE, 0,
        MAKELPARAM(rect.right - rect.left, rect.bottom - rect.top));
    }
  }
  return 0;

我在代码中遇到了一些绘制问题,最终决定只是隐藏窗口,恢复 Ribbon,然后再次显示窗口。这有点刺眼,但直到我找到解决方法为止,它仍然有效。(如果您找到更好的解决方法,请告诉我。)我特别感觉在发布 WM_SIZE 消息时很糟糕,但这是我找到正确调整客户端区域的唯一可靠方法。当我看到 WTL 8.0 中使用了相同的技术时,我感觉好多了。

保存和恢复 Ribbon 设置

上面我们看到 OnViewChanged 方法在初始化 Ribbon 时调用 LoadRibbonSettings,在销毁 Ribbon 时调用 SaveRibbonSettingsSaveRibbonSettings 方法在用户应用程序数据目录中的文件中创建一个 IStream 对象,并将该流传递给 pRibbon->SaveSettingsToStream 方法。Ribbon 框架会将设置写入此流,然后 SaveRibbonSettings 方法释放 IStreamIStorage 对象。

bool App::SaveRibbonSettings(
  IUIRibbon* pRibbon)
{
  /* Build a path to an app-specific directory in the user's application
  data storage directory. */
  HRESULT hr = E_FAIL;
  WCHAR pPath[MAX_PATH] = {};
 
  if (BuildSettingsPath(pPath, L"ScratchRibbonProjectSettings.bin"))
  {
    IStorage* pStorage = 0;
    hr = StgCreateStorageEx(pPath, STGM_CREATE|STGM_SHARE_EXCLUSIVE|STGM_READWRITE,
      STGFMT_STORAGE, 0, NULL, NULL, __uuidof(IStorage), (void**)&pStorage);
 
    if (SUCCEEDED(hr))
    {
      IStream* pStream = 0;
 
      hr = pStorage->CreateStream(L"Ribbon",
        STGM_CREATE|STGM_READWRITE|STGM_SHARE_EXCLUSIVE,
        0, 0, &pStream);
 
      if (SUCCEEDED(hr))
      {
        hr = pRibbon->SaveSettingsToStream(pStream);
        pStream->Release();
      }
 
      pStorage->Release();
    }
  }
 
  return SUCCEEDED(hr);
}

在 Ribbon 初始化时,LoadRibbonSettingsSaveRibbonSettings 创建的文件中打开 Ribbon 流,并将其传递给 pRibbon->LoadSettingsFromStream。这将恢复 Ribbon 的先前保存状态。

bool App::LoadRibbonSettings(
  IUIRibbon* pRibbon)
{
  HRESULT hr = E_FAIL;
  WCHAR pPath[MAX_PATH] = {};
 
  if (BuildSettingsPath(pPath, L"ScratchRibbonProjectSettings.bin"))
  {
    IStorage* pStorage = 0;
 
    hr = StgOpenStorageEx(
      pPath,
      STGM_READ|STGM_SHARE_DENY_WRITE,
      STGFMT_STORAGE,
      0, NULL, NULL,
      __uuidof(IStorage),
      (void**)&pStorage);
 
    if (SUCCEEDED(hr))
    {
      IStream* pStream = 0;
      hr = pStorage->OpenStream( L"Ribbon", NULL,
        STGM_READ|STGM_SHARE_EXCLUSIVE,0, &pStream);
 
      if (SUCCEEDED(hr))
      {
        LARGE_INTEGER liStart = {0, 0};
        ULARGE_INTEGER ulActual;
        pStream->Seek(liStart, STREAM_SEEK_SET, &ulActual);
        hr = pRibbon->LoadSettingsFromStream(pStream);
        pStream->Release();
      }
 
      pStorage->Release();
    }
  }
 
  return SUCCEEDED(hr);
}

启用和禁用 Ribbon 命令

为了演示如何禁用 Ribbon 命令,“保存”和“重做”命令将根据用户选择的其他命令更改状态。“保存”命令在激活“新建”或“撤销”命令之前是禁用的,“重做”命令在激活“撤销”命令之前是禁用的。

“保存”命令的状态由 App 对象中设置的脏标志控制。如果当前“文档”被标记为脏,因此是保存操作的候选,则应启用“保存”命令。要设置或清除脏标志,应用程序会调用 App 对象的 SetDirty 方法。

void App::SetDirty(bool isDirtyInit)
{
  isDirty = isDirtyInit;
 
  if (pFramework)
  {
    pFramework->InvalidateUICommand(ID_CMD_SAVE, UI_INVALIDATIONS_STATE, NULL);
  }
 
  HMENU hMenu = GetMenu(GetHWND());
 
  if (hMenu)
  {
    EnableMenuItem(hMenu, ID_CMD_SAVE, isDirty ? MF_ENABLED : MF_GRAYED);
  }
}

此方法同时修改菜单和 Ribbon 命令。它首先检查 pFramework 指针是否为非空。如果是,则它指向 Ribbon API 实现的 IUIFramework 接口的一个实例。它使用该指针调用 InvalidateUICommand 方法,指定 ID_CMD_SAVE 命令应被无效化。这会指示框架调用应用程序的 IUICommandHandler::UpdateProperty 实现,该实现将根据脏标志设置命令的状态。

各种清理任务

我们剩下的就是清理工作,以恢复客户端区域并处理一些绘制问题。首先,主窗口的客户端区域有一个只读编辑控件,用于显示所选 Ribbon 或菜单命令的描述。当主窗口大小调整或 Ribbon 大小更改时,此控件需要调整大小以适应 Ribbon 控件。这可以通过处理 WM_SIZE 消息来完成。

void App::OnSize(
  HWND hwnd,
  UINT state,
  int cx,
  int cy)
{
  /* Adjust any child windows in the client area to match the new size. */
  AdjustClientArea(cx, cy);
 
  /* Hack to correctly repaint in DWM environments when the ribbon is removed. */
  if (killRibbon && state != SIZE_MINIMIZED)
  {
    SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
      SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
  }
}

AdjustClientArea 函数接受客户端宽度和高度,并调整编辑窗口以适应 Ribbon。

void App::AdjustClientArea(
  int cx,
  int cy)
{
  /* Adjust the size of the edit window to fit into the client area.
  Take the size of the ribbon into consideration, if there is one. */
  UINT32 ribbonHeight = 0;
 
  if (pRibbon)
  {
    pRibbon->GetHeight(&ribbonHeight);
  }
 
  SendMessage(hEdit, WM_SETREDRAW, 0, 0);
  MoveWindow(
    hEdit,
    0, ribbonHeight,
    cx,
    cy - ribbonHeight,
    TRUE);
  int textLen = GetWindowTextLength(hEdit);
  SendMessage(hEdit, EM_SETSEL, static_cast<WPARAM>(textLen), 
              static_cast<LPARAM>(textLen));
  SendMessage(hEdit, WM_SETREDRAW, 1, 0);
  RedrawWindow(hEdit, NULL, NULL, 
    RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
  SendMessage(hEdit, EM_SCROLLCARET, 0, 0);
}

AdjustClientArea 调用 pRibbon->GetHeight 来获取 Ribbon 的高度,然后相应地调整编辑窗口的大小。

OnSize 方法还会检查 killRibbon 标志是否已设置。当用户隐藏 Ribbon 并恢复菜单时,会设置此标志。如果设置了该标志,OnSize 会调用 SetWindowPos 来触发 WM_NCCALCSIZE 消息。

还有 WM_SIZINGWM_ACTIVATE 的处理程序,它们会检查 killRibbon 标志并设置时调用 SetWindowPos。如果未设置,它们会将处理推迟到 WndProc 的基类版本,后者又会调用默认处理函数 DefWindowProc

case WM_SIZING:
  {
    /* Hack to correctly repaint in DWM environments 
       when the ribbon is removed. */
    if (killRibbon)
    {
      switch (wParam)
      {
      case WMSZ_TOP:
      case WMSZ_TOPLEFT:
      case WMSZ_TOPRIGHT:
        {
          PRECT pRect = reinterpret_cast<PRECT>(lParam);
          SetWindowPos(GetHWND(), NULL,
            pRect->left, pRect->top,
            pRect->right - pRect->left, pRect->bottom - pRect->top,
            SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED);
        }
        break;
 
      default:
        WinApp::WndProc(msg, wParam, lParam);
      }
 
      return TRUE;
    }
 
    return WinApp::WndProc(msg, wParam, lParam);
  }
 
case WM_ACTIVATE:
  {
    /* Hack to correctly repaint in DWM environments 
       when the ribbon is removed. */
    if (killRibbon)
    {
      if (wParam != WA_INACTIVE)
      {
        SetWindowPos(GetHWND(), NULL,
          0, 0, 0, 0,
          SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
      }
 
      return FALSE;
    }
 
    return WinApp::WndProc(msg, wParam, lParam);
  }

WM_NCCALCSIZE 处理程序执行一些额外的工作来正确绘制框架。

case WM_NCCALCSIZE:
  {
    LRESULT result = WinApp::WndProc(msg, wParam, lParam);
 
    /* Hack to correctly repaint in DWM environments 
       when the ribbon is removed. */
    if (killRibbon && wParam)
    {
      MARGINS margins = {};
      DwmExtendFrameIntoClientArea(GetHWND(), &margins);
 
      RECT adjustedRect = {};
      AdjustWindowRectEx(&adjustedRect, GetWindowStyle(GetHWND()),
        TRUE, GetWindowExStyle(GetHWND()));
 
      LPNCCALCSIZE_PARAMS pParams = (LPNCCALCSIZE_PARAMS)lParam;
      pParams->rgrc[0].top = pParams->rgrc[1].top + (-adjustedRect.top);
    }
 
    return result;
  }

当 Ribbon 恢复时,killRibbon 标志会被清除,并且这些消息会正常处理。

请注意,WM_NCCALCSIZE 处理程序调用一个 Desktop Window Manager 函数 DwmExtendFrameIntoClientArea,该函数在 DWM API 库 dwmapi.dll 中实现。但是,此库在 Windows XP 上不可用。我们可以使用 LoadLibrary 函数按需加载 DLL,但这很麻烦。相反,我们将使用 Visual Studio 的延迟加载功能,让链接器在第一次调用其导出函数时生成必要的代码来加载 DLL。

编辑项目设置,将 dwmapi.lib 添加到“附加依赖项”属性的文件列表中,然后将 dwmapi.dll 添加到“延迟加载 DLL”属性。

ScratchRibbonProject_Link_Properties.png

由于我们在 Windows XP 上永远不会进入此代码路径,因此应用程序永远不会尝试加载 dwmapi.dll

最后的想法,未来更改

最让我恼火的是 Ribbon 图形增加了可执行文件的大小。对于更喜欢使用菜单的用户来说,这是内存的浪费,但我可以通过将 Ribbon 资源保留在单独的 DLL 中来解决这个问题。我还没有尝试实现这一点,但当我做到时,我会发布一个更新。

我在这里涵盖了很多内容,所以如果还有什么不清楚的地方,请留下评论。

注意:此项目使用了 VisualPharm 的 Must Have Icons。

© . All rights reserved.