The Windows Native Ribbon Part 2: Setting Ribbon Properties at Runtime
The Windows Native Ribbon Part 2: Setting Ribbon Properties at Runtime
Contents
- Introduction
- Command Properties
- Setting Button Images at Runtime
- Setting Command Properties Directly
- Invalidating Command Properties
- Setting Ribbon Properties
- Notes on the Sample App
- Conclusion
- Revision History
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:
- Don't specify a value for a property in the Ribbon XML.
- Call
IUIFramework::SetUICommandProperty()
and pass the new value. - 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
: TheHBITMAP
that you already created.options
: A member of theUI_OWNERSHIP
enum that controls ownership of theHBITMAP
. If you passUI_OWNERSHIP_TRANSFER
, the new COM object assumes ownership of theHBITMAP
. If you passUI_OWNERSHIP_COPY
, the Ribbon makes a copy of theHBITMAP
.ppImage
: AIUIImage**
that receives anIUIImage
interface on the new COM object.
The steps for creating a bitmap that the Ribbon can use are:
- Determine the size of the bitmap that the Ribbon is asking for.
- Load or create the source graphic.
- Convert it to a 32bpp bitmap.
- Create an instance of the
UIRibbonImageFromBitmapFactory
COM object. - Use the factory to create a COM object that implements
IUIImage
and references the bitmap. - Store the
IUIImage
interface in the outputPROPVARIANT
that is passed toOnUpdateProperty()
.
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:
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:
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_TooltipTitle
,UI_PKEY_TooltipDescription
: The button's tooltip.UI_PKEY_LargeImage
,UI_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:
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.
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)
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.