The Windows Native Ribbon Part 2: Setting Ribbon Properties at Runtime

The Windows Native Ribbon Part 2: Setting Ribbon Properties at Runtime

 

Contents

Introduction

In the previous Ribbon articles, we've seen Ribbons where all properties were specified at compile time in XML. That works fine for simple demo programs, but more-complicated programs need more control over various properties of Ribbon commands. Most command properties can be controlled at runtime, and that will be the subject of this article.

As before, the minimum system requirements for building the sample code are Visual Studio 2008, WTL 8.0, and the Windows 7 SDK. If you are running Windows 7 or Server 2008 R2, you have everything you need. If you are using Vista or Server 2008, you must install service pack 2 and the platform update for your operating system to use the Ribbon.

This article's demo project is an ActiveX control container that contains a WebBrowser control. The WebBrowser is a good way to demonstrate updating a UI in response to events, and everyone has the WebBrowser already, so there's nothing to install. The app also demonstrates how to use graphics files that aren't 32bpp bitmaps. Finally, we'll see how to set the properties exposed by the Ribbon itself.

Command Properties

As we saw in the Introduction to the Ribbon article, properties are identified by their PROPERTYKEY. For example, a toggle button's toggled state is controlled by the UI_PKEY_BooleanValue property. Controls have many more properties, which are listed in the documentation. For an example, see the MSDN page on Button properties.

The Ribbon would be pretty boring if it were limited to the contents of the XML file. Command properties can be set at runtime in a few ways:

  1. Don't specify a value for a property in the Ribbon XML.
  2. Call IUIFramework::SetUICommandProperty() and pass the new value.
  3. Call IUIFramework::InvalidateUICommand() to invalidate a property.

If you omit a property from the XML file, the Ribbon will call your implementation of IUICommandHandler::UpdateProperty() when it needs to know the property's value. This is similar to the example in the Introduction to the Ribbon article, where the Ribbon queried for the initial state of a ToggleButton. (The button's initial state is not something that can be set in the XML, but the sequence of events is the same.)

The other two methods are ways that the app tells the Ribbon that a property's value has changed. Method 2 makes the new value take effect right away, whereas method 3 just tells the Ribbon that the value of a command's property has been updated. The next time the Ribbon needs to know that property's value, it will call UpdateProperty() to get the new value.

Method 2 is simpler, but has a limitation: Only certain properties can be set directly. You must consult the documentation to see which properties can be set this way. If you try to set a property that cannot be set directly, SetUICommandProperty() will return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED).

Method 3 is conceptually similar to invalidating a window's contents. Your app calls IUIFramework::InvalidateUICommand() to tell the Ribbon that the value of a command's property has changed, but you don't specify what the new value is. The Ribbon may not query for that property's value immediately. For example, if the command is not present in the current tab, the Ribbon won't query for invalidated properties until the user switches to the tab containing the command. All properties can be updated through invalidation.

Setting Button Images at Runtime

The first example we'll look at is how to set a property when that property is omitted from the Ribbon XML. This particular example shows how to set a button's image in UpdateProperty().

You may have noticed that I'm rubbish at drawing and creating graphics. Because of that, the earlier sample Ribbon apps have been limited to the handful of 32bpp bitmaps that come with Microsoft's Ribbon samples. Icon files are easier to come by, but the Ribbon won't read accept files directly. This section will demonstrate how to specify any image for a command.

There are two properties that control images: UI_PKEY_LargeImage and UI_PKEY_SmallImage. These properties must be initialized with 32bpp graphics, but you can use any other type of image as a source and convert it to 32bpp. There are two COM interfaces involved in this process: IUIImageFromBitmap and IUIImage. The Ribbon implements a class factory for the UIRibbonImageFromBitmapFactory object, and the factory exposes the IUIImageFromBitmap interface. IUIImageFromBitmap has one method, CreateImage(), with these parameters:

  • bitmap: The HBITMAP that you already created.
  • options: A member of the UI_OWNERSHIP enum that controls ownership of the HBITMAP. If you pass UI_OWNERSHIP_TRANSFER, the new COM object assumes ownership of the HBITMAP. If you pass UI_OWNERSHIP_COPY, the Ribbon makes a copy of the HBITMAP.
  • ppImage: A IUIImage** that receives an IUIImage interface on the new COM object.

The steps for creating a bitmap that the Ribbon can use are:

  1. Determine the size of the bitmap that the Ribbon is asking for.
  2. Load or create the source graphic.
  3. Convert it to a 32bpp bitmap.
  4. Create an instance of the UIRibbonImageFromBitmapFactory COM object.
  5. Use the factory to create a COM object that implements IUIImage and references the bitmap.
  6. Store the IUIImage interface in the output PROPVARIANT that is passed to OnUpdateProperty().

Here's an implementation of OnUpdateProperty() that uses an icon as the source graphic. Preserving the alpha channel proved to be difficult; I finally worked out that drawing the icon into a CImage object did the trick.

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

One thing to note is that the bitmap size comes from a system metric: the size of a small or large icon, depending on the property being queried. This code works the same for all DPI settings, and overcomes a problem in the code from the earlier articles, which didn't return larger icons in higher DPI settings.

Here's how the sample app's custom bitmaps look at 96 and 144 DPI:

Image 1

Image 2

If the high contrast accessibility mode is turned on, the Ribbon will query for UI_PKEY_LargeHighContrastImage and UI_PKEY_SmallHighContrastImage instead. The code for setting those properties is similar, but the Ribbon expects a 16-color bitmap instead of a 32bpp bitmap.

Setting Command Properties Directly

The second method of setting a property is to call IUIFramework::SetUICommandProperty() and pass the new value. The sample app uses this method to enable and disable the Back and Forward navigation buttons. The WebBrowser sends a DISPID_COMMANDSTATECHANGE event when the state of those controls changes, so the app listens for that event and sets the button states accordingly.

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

Remember to check the documentation before using SetUICommandProperty(). If you read the list of Button properties, you'll see that UI_PKEY_Enabled is the only property that can be updated this way.

If you run the sample app and click one link, you'll see that the Forward button is disabled, while Back becomes enabled, as shown here:

Image 3

Invalidating Command Properties

The last way to update properties is through invalidation. When you need to notify the Ribbon that a property has changed, you call IUIFramework::InvalidateUICommand()InvalidateUICommand() takes three parameters: the command ID, a set of UI_INVALIDATIONS flags, and a pointer to the property key of the property being invalidated. The flags are:

  • UI_INVALIDATIONS_STATE: All properties related to the state of the control.
  • UI_INVALIDATIONS_VALUE: All properties related to the value of the control.
  • UI_INVALIDATIONS_PROPERTY: Any individual property.
  • UI_INVALIDATIONS_ALLPROPERTIES: Invalidate all properties.

If you pass the UI_INVALIDATIONS_PROPERTY flag, you must also pass a pointer to the PROPERTYKEY that you are invalidating. If you use one of the other flags, you can pass NULL for that parameter.

In addition to the navigation buttons mentioned earlier, the demo app has one button that switches between Stop and Refresh. The app listens to the DISPID_DOWNLOADBEGIN and DISPID_DOWNLOADCOMPLETE events to know when the WebBrowser is downloading data. When the number of active downloads is zero, the button acts like Refresh. When the number of downloads is greater than zero, the button acts like Stop.

These properties need to be invalidated when the button changes between those two modes:

  • UI_PKEY_Label: The button's text.
  • UI_PKEY_TooltipTitleUI_PKEY_TooltipDescription: The button's tooltip.
  • UI_PKEY_LargeImageUI_PKEY_SmallImage: The button's icons.

Here's the code from OnDownloadBegin() that invalidates each property individually:

 
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() shows how to invalidate all properties:

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

Because the app invalidates properties, it needs to return values for those properties in OnUpdateProperty(). Here's how the app returns the button's label:

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

Here's how the button looks in the Refresh state:

Image 4

Setting Ribbon Properties

The Ribbon has a few properties of its own that you can set to control some of its visual aspects:

  • UI_PKEY_QuickAccessToolbarDock: Where the QAT is docked.
  • UI_PKEY_Minimized: Whether the Ribbon is minimized to show only the row of tabs.
  • UI_PKEY_Viewable: Whether the Ribbon is visible at all.

To set these properties, query the IUIRibbon interface for IPropertyStore, then use IPropertyStore::SetValue() and IPropertyStore::Commit() to set a new value for a property.

The value of UI_PKEY_QuickAccessToolbarDock is a member of the UI_CONTROLDOCK enum, either UI_CONTROLDOCK_TOP or UI_CONTROLDOCK_BOTTOM. The other properties are booleans.

The sample app has a second Properties tab with buttons that set the properties listed above.

Image 5

Here is code from Execute() that handles the buttons that change where the QAT is docked:

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

Notes on the Sample App

For this sample app, I moved the code that owns and manages the IUIRibbon interface to a separate class, CAppRibbon. This class owns the IUIRibbon interface, while CMainFrame is still responsible for initializing the Ribbon framework. CAppRibbon implements IUICommandHandler and calls CMainFrame methods when various events happen. For example, CAppRibbon::Execute() calls CMainFrame::OnExecuteCommand(). This makes CMainFrame a lot simpler, and removes the need for some COM-specific hacks like how CMainFrame had to be a COM object.

Conclusion

Several other Ribbon features rely on setting command properties at runtime, so now that we've seen how to do that, we'll be ready to tackle more-advanced features later on, like contextual tabs. Not to mention the graphics in my sample apps will be a whole lot better.

In the next article, we'll see how to use other types of buttons, split buttons and drop-down buttons, along with more complex menus.

Revision History

July 17, 2011: Article first published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.

Mike was a VC MVP from 2005 to 2009.
posted @ 2022-12-13 15:01  小风风的博客  阅读(31)  评论(0编辑  收藏  举报