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






4.91/5 (15投票s)
本文展示了如何在运行时控制各种 Ribbon 和命令属性
目录
引言
在之前的 Ribbon 文章中,我们看到所有属性都在 XML 中编译时指定的 Ribbon。这对于简单的演示程序来说效果很好,但更复杂的程序需要更多地控制 Ribbon 命令的各种属性。大多数命令属性可以在运行时控制,这将是本文的主题。
与之前一样,构建示例代码的最低系统要求是 Visual Studio 2008、WTL 8.0 和 Windows 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 文件的内容,那将非常无聊。命令属性可以通过几种方式在运行时设置
- 在 Ribbon XML 中不为属性指定值。
- 调用
IUIFramework::SetUICommandProperty()
并传入新值。 - 调用
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_LargeImage
和UI_PKEY_SmallImage
。这些属性必须用 32bpp 图形初始化,但您可以将任何其他类型的图像作为源并将其转换为 32bpp。此过程涉及两个 COM 接口:IUIImageFromBitmap
和IUIImage
。Ribbon 实现了UIRibbonImageFromBitmapFactory
对象的类工厂,该工厂公开了IUIImageFromBitmap
接口。IUIImageFromBitmap
有一个方法,CreateImage()
,具有以下参数
bitmap
:您已经创建的HBITMAP
。options
:UI_OWNERSHIP
枚举的成员,控制HBITMAP
的所有权。如果您传入UI_OWNERSHIP_TRANSFER
,则新的 COM 对象将获得HBITMAP
的所有权。如果您传入UI_OWNERSHIP_COPY
,则 Ribbon 会复制HBITMAP
。ppImage
:接收新 COM 对象上IUIImage
接口的IUIImage**
。
创建 Ribbon 可以使用的位图的步骤是
- 确定 Ribbon 请求的位图大小。
- 加载或创建源图形。
- 将其转换为 32bpp 位图。
- 创建
UIRibbonImageFromBitmapFactory
COM 对象的一个实例。 - 使用工厂创建一个实现
IUIImage
并引用位图的 COM 对象。 - 将
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_LargeHighContrastImage
和UI_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_DOWNLOADBEGIN
和DISPID_DOWNLOADCOMPLETE
事件,以了解 WebBrowser 何时正在下载数据。当活动下载数为零时,按钮充当“刷新”。当下载数大于零时,按钮充当“停止”。
当按钮在这两种模式之间切换时,需要使这些属性无效
UI_PKEY_Label
:按钮的文本。UI_PKEY_TooltipTitle
、UI_PKEY_TooltipDescription
:按钮的工具提示。UI_PKEY_LargeImage
、UI_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_TOP
或UI_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 日:文章首次发布