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

Windows 原生 Ribbon 第二部分:运行时设置 Ribbon 属性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (15投票s)

2011 年 7 月 18 日

CPOL

8分钟阅读

viewsIcon

44129

downloadIcon

2682

本文展示了如何在运行时控制各种 Ribbon 和命令属性

目录

引言

在之前的 Ribbon 文章中,我们看到所有属性都在 XML 中编译时指定的 Ribbon。这对于简单的演示程序来说效果很好,但更复杂的程序需要更多地控制 Ribbon 命令的各种属性。大多数命令属性可以在运行时控制,这将是本文的主题。

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

本文的演示项目是一个 ActiveX 控件容器,其中包含一个 WebBrowser 控件。WebBrowser 是演示响应事件更新 UI 的好方法,而且每个人都已经拥有 WebBrowser,因此无需安装任何东西。该应用程序还演示了如何使用非 32bpp 位图的图形文件。最后,我们将看到如何设置 Ribbon 本身公开的属性。

命令属性

正如我们在Ribbon 简介文章中看到的那样,属性由其PROPERTYKEY标识。例如,切换按钮的切换状态由UI_PKEY_BooleanValue属性控制。控件具有更多属性,这些属性在文档中列出。例如,请参阅MSDN 上的按钮属性页面

如果 Ribbon 仅限于 XML 文件的内容,那将非常无聊。命令属性可以通过几种方式在运行时设置

  1. 在 Ribbon XML 中不为属性指定值。
  2. 调用IUIFramework::SetUICommandProperty()并传入新值。
  3. 调用IUIFramework::InvalidateUICommand()以使属性无效。

如果您从 XML 文件中省略某个属性,则当 Ribbon 需要知道该属性的值时,它将调用您对IUICommandHandler::UpdateProperty()的实现。这类似于Ribbon 简介文章中的示例,其中 Ribbon 查询 ToggleButton 的初始状态。(按钮的初始状态无法在 XML 中设置,但事件序列是相同的。)

另外两种方法是应用程序告知 Ribbon 属性值已更改的方式。方法 2 使新值立即生效,而方法 3 只是告知 Ribbon 命令属性的值已更新。下次 Ribbon 需要知道该属性的值时,它将调用UpdateProperty()来获取新值。

方法 2 更简单,但有一个限制:只有某些属性可以直接设置。您必须查阅文档才能了解哪些属性可以通过这种方式设置。如果您尝试设置无法直接设置的属性,SetUICommandProperty()将返回HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)

方法 3 在概念上类似于使窗口内容无效。您的应用程序调用IUIFramework::InvalidateUICommand()来告知 Ribbon 命令属性的值已更改,但您没有指定新值是什么。Ribbon 可能不会立即查询该属性的值。例如,如果命令不存在于当前选项卡中,Ribbon 将不会查询无效属性,直到用户切换到包含该命令的选项卡。所有属性都可以通过失效来更新。

在运行时设置按钮图像

我们将要查看的第一个示例是如何在 Ribbon XML 中省略某个属性时设置该属性。这个特定示例展示了如何在UpdateProperty()中设置按钮的图像。

您可能已经注意到,我在绘图和创建图形方面很糟糕。因此,早期的示例 Ribbon 应用程序仅限于 Microsoft Ribbon 示例附带的少量 32bpp 位图。图标文件更容易获得,但 Ribbon 不会直接接受文件。本节将演示如何为命令指定任何图像。

有两个属性控制图像:UI_PKEY_LargeImageUI_PKEY_SmallImage。这些属性必须用 32bpp 图形初始化,但您可以将任何其他类型的图像作为源并将其转换为 32bpp。此过程涉及两个 COM 接口:IUIImageFromBitmapIUIImage。Ribbon 实现了UIRibbonImageFromBitmapFactory对象的类工厂,该工厂公开了IUIImageFromBitmap接口。IUIImageFromBitmap有一个方法,CreateImage(),具有以下参数

  • bitmap:您已经创建的HBITMAP
  • optionsUI_OWNERSHIP枚举的成员,控制HBITMAP的所有权。如果您传入UI_OWNERSHIP_TRANSFER,则新的 COM 对象将获得HBITMAP的所有权。如果您传入UI_OWNERSHIP_COPY,则 Ribbon 会复制HBITMAP
  • ppImage:接收新 COM 对象上IUIImage接口的IUIImage**

创建 Ribbon 可以使用的位图的步骤是

  1. 确定 Ribbon 请求的位图大小。
  2. 加载或创建源图形。
  3. 将其转换为 32bpp 位图。
  4. 创建UIRibbonImageFromBitmapFactory COM 对象的一个实例。
  5. 使用工厂创建一个实现IUIImage并引用位图的 COM 对象。
  6. IUIImage接口存储在传递给OnUpdateProperty()的输出PROPVARIANT中。

这是使用图标作为源图形的OnUpdateProperty()的实现。保留 Alpha 通道被证明是困难的;我终于发现将图标绘制到CImage对象中可以解决问题。

HRESULT CMainFrame::OnUpdateProperty (
  UINT32 uCommandId, REFPROPERTYKEY key,
  const PROPVARIANT* pCurrentValue, PROPVARIANT* pNewValue )
{
HRESULT hr = E_NOTIMPL;
 
  if ( UI_PKEY_LargeImage == key || UI_PKEY_SmallImage == key )
    {
    // Error-handling has been omitted for clarity.
 
    // 1. Determine the size of the bitmap that we need to return.
    bool bLargeIcon = ( UI_PKEY_LargeImage == key );
    int cx = GetSystemMetrics ( bLargeIcon ? SM_CXICON : SM_CXSMICON );
    int cy = GetSystemMetrics ( bLargeIcon ? SM_CYICON : SM_CYSMICON );
    CIcon icon;  // This is our source graphic
 
    // 2. Not shown: Initialize 'icon', for example load it from a resource.
 
    // Create a CImage of the right size and bit depth.
    CImage img;
 
    img.Create ( cx, cy, 32,  // dimensions and bpp
                 CImage::createAlphaChannel );  // use an alpha channel
 
    // 3. Convert the icon to 32bpp by drawing it into the CImage.
    DrawIconEx ( CImageDC(img), 0, 0, icon, cx, cy, 0, 0, DI_NORMAL );
 
    // 4. Create a UIRibbonImageFromBitmapFactory COM object and get an
    // IUIImageFromBitmap interface.
    CComPtr<IUIImageFromBitmap> pifb;
 
    pifb.CoCreateInstance ( CLSID_UIRibbonImageFromBitmapFactory );
 
    // 5. Create a new IUIImage, telling it to copy the HBITMAP that we pass in.
    CComPtr<IUIImage> pImage;
 
    pifb->CreateImage ( img, UI_OWNERSHIP_COPY, &pImage );
 
    // 6. Store the IUIImage interface in the output PROPVARIANT.
    hr = UIInitPropertyFromInterface ( key, pImage, pNewValue );
    }
 
  return hr;
}

需要注意的一点是,位图大小来自系统度量:小图标或大图标的大小,具体取决于正在查询的属性。此代码适用于所有 DPI 设置,并克服了早期文章中代码的一个问题,该代码在更高的 DPI 设置中没有返回更大的图标。

以下是示例应用程序的自定义位图在 96 和 144 DPI 下的外观

如果启用了高对比度辅助功能模式,Ribbon 将改为查询UI_PKEY_LargeHighContrastImageUI_PKEY_SmallHighContrastImage。设置这些属性的代码类似,但 Ribbon 期望 16 色位图而不是 32bpp 位图。

直接设置命令属性

设置属性的第二种方法是调用IUIFramework::SetUICommandProperty()并传入新值。示例应用程序使用此方法启用和禁用“后退”和“前进”导航按钮。WebBrowser 在这些控件的状态更改时发送DISPID_COMMANDSTATECHANGE事件,因此应用程序侦听该事件并相应地设置按钮状态。

void __stdcall CMainFrame::OnCommandStateChange (
  long lCommand, VARIANT_BOOL bEnable )
{
PROPVARIANT pv;
 
  UIInitPropertyFromBoolean ( UI_PKEY_Enabled, (bEnable != VARIANT_FALSE), &pv );
 
  if ( CSC_NAVIGATEBACK == lCommand )
    m_pFramework->SetUICommandProperty ( RIDC_NAV_BACK, UI_PKEY_Enabled, pv );
  else if ( CSC_NAVIGATEFORWARD == lCommand )
    m_pFramework->SetUICommandProperty ( RIDC_NAV_FORWARD, UI_PKEY_Enabled, pv );
 
  PropVariantClear ( &pv );
}

在使用SetUICommandProperty()之前,请记住查阅文档。如果您阅读按钮属性列表,您会看到UI_PKEY_Enabled是唯一可以通过这种方式更新的属性。

如果您运行示例应用程序并单击一个链接,您会看到“前进”按钮被禁用,而“后退”按钮变为启用状态,如下所示

使命令属性无效

更新属性的最后一种方法是通过失效。当您需要通知 Ribbon 属性已更改时,您会调用IUIFramework::InvalidateUICommand()InvalidateUICommand()接受三个参数:命令 ID、一组UI_INVALIDATIONS标志,以及指向正在失效的属性的属性键的指针。这些标志是

  • UI_INVALIDATIONS_STATE:与控件状态相关的所有属性。
  • UI_INVALIDATIONS_VALUE:与控件值相关的所有属性。
  • UI_INVALIDATIONS_PROPERTY:任何单独的属性。
  • UI_INVALIDATIONS_ALLPROPERTIES:使所有属性无效。

如果您传入UI_INVALIDATIONS_PROPERTY标志,您还必须传入指向您正在失效的PROPERTYKEY的指针。如果您使用其他标志之一,您可以为该参数传入 NULL。

除了前面提到的导航按钮之外,演示应用程序还有一个按钮,可在“停止”和“刷新”之间切换。应用程序侦听DISPID_DOWNLOADBEGINDISPID_DOWNLOADCOMPLETE事件,以了解 WebBrowser 何时正在下载数据。当活动下载数为零时,按钮充当“刷新”。当下载数大于零时,按钮充当“停止”。

当按钮在这两种模式之间切换时,需要使这些属性无效

  • UI_PKEY_Label:按钮的文本。
  • UI_PKEY_TooltipTitleUI_PKEY_TooltipDescription:按钮的工具提示。
  • UI_PKEY_LargeImageUI_PKEY_SmallImage:按钮的图标。

这是OnDownloadBegin()中单独使每个属性无效的代码

void __stdcall CMainFrame::OnDownloadBegin()
{
  // m_cDownloadEvents holds the number of downloads being done right now.
  if ( ++m_cDownloadEvents == 1 )
    {
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_Label );
 
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_TooltipTitle );
 
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_TooltipDescription );
 
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_LargeImage );
 
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_SmallImage );
    }
}

OnDownloadEnd()展示了如何使所有属性无效

void __stdcall CMainFrame::OnDownloadComplete()
{
  if ( --m_cDownloadEvents == 0 )
    {
    m_pFramework->InvalidateUICommand (
      RIDC_STOP_OR_REFRESH, UI_INVALIDATIONS_ALLPROPERTIES, NULL );
    }
}

由于应用程序使属性无效,它需要在OnUpdateProperty()中返回这些属性的值。以下是应用程序如何返回按钮标签的代码

HRESULT CMainFrame::OnUpdateProperty (
  UINT32 uCommandId, REFPROPERTYKEY key,
  const PROPVARIANT* pCurrentValue, PROPVARIANT* pNewValue )
{
  if ( UI_PKEY_Label == key && RIDC_STOP_OR_REFRESH == uCommandId )
    {
    LPCWSTR pwszText = (m_cDownloadEvents > 0) ? L"Stop" : L"Refresh";
 
    return UIInitPropertyFromString ( key, pwszText, pNewValue );
    }
}

以下是按钮在“刷新”状态下的外观

设置 Ribbon 属性

Ribbon 拥有一些自己的属性,您可以设置这些属性来控制其某些视觉方面

  • UI_PKEY_QuickAccessToolbarDock:QAT 的停靠位置。
  • UI_PKEY_Minimized:Ribbon 是否最小化以仅显示选项卡行。
  • UI_PKEY_Viewable:Ribbon 是否可见。

要设置这些属性,请查询IUIRibbon接口以获取IPropertyStore,然后使用IPropertyStore::SetValue()IPropertyStore::Commit()为属性设置新值。

UI_PKEY_QuickAccessToolbarDock的值是UI_CONTROLDOCK枚举的成员,可以是UI_CONTROLDOCK_TOPUI_CONTROLDOCK_BOTTOM。其他属性是布尔值。

示例应用程序有一个第二个“属性”选项卡,其中包含设置上述属性的按钮。

以下是Execute()中处理更改 QAT 停靠位置的按钮的代码

HRESULT CMainFrame::Execute ( ... )
{
  switch ( uCommandID )
    {
    case RIDC_RIBBON_QAT_ON_TOP:
    case RIDC_RIBBON_QAT_ON_BOTTOM:
      {
      // Error-handling has been omitted for clarity.
      // See below for a description of m_pAppRibbon.
 
      CComQIPtr<IPropertyStore> pps = m_pAppRibbon->m_pRibbon;
      bool bOnTop = (RIDC_RIBBON_QAT_ON_TOP == uCommandID);
      PROPVARIANT pv;
 
      // Initialize the PROPVARIANT.
      UIInitPropertyFromUInt32 (
        UI_PKEY_QuickAccessToolbarDock,
        bOnTop ? UI_CONTROLDOCK_TOP : UI_CONTROLDOCK_BOTTOM, &pv );
 
      // Set the property and commit the change.
      pps->SetValue ( UI_PKEY_QuickAccessToolbarDock, pv );
      pps->Commit();
 
      return S_OK;
      }
    }
}

示例应用程序的注意事项

对于这个示例应用程序,我将拥有和管理IUIRibbon接口的代码移到了一个单独的类CAppRibbon中。这个类拥有IUIRibbon接口,而CMainFrame仍然负责初始化 Ribbon 框架。CAppRibbon实现了IUICommandHandler并在发生各种事件时调用CMainFrame方法。例如,CAppRibbon::Execute()调用CMainFrame::OnExecuteCommand()。这使得CMainFrame简单得多,并消除了对某些 COM 特定技巧的需要,例如CMainFrame必须是一个 COM 对象。

结论

其他几个 Ribbon 功能依赖于在运行时设置命令属性,所以既然我们已经看到了如何做到这一点,我们以后就可以准备好处理更高级的功能,例如上下文选项卡。更不用说我的示例应用程序中的图形会好很多。

在下一篇文章中,我们将看到如何使用其他类型的按钮、拆分按钮和下拉按钮,以及更复杂的菜单。

修订历史

2011 年 7 月 17 日:文章首次发布

© . All rights reserved.