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:
- 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
: 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:
- 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 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 )
{
bool bLargeIcon = ( UI_PKEY_LargeImage == key );
int cx = GetSystemMetrics ( bLargeIcon ? SM_CXICON : SM_CXSMICON );
int cy = GetSystemMetrics ( bLargeIcon ? SM_CYICON : SM_CYSMICON );
CIcon icon;
CImage img;
img.Create ( cx, cy, 32,
CImage::createAlphaChannel );
DrawIconEx ( CImageDC(img), 0, 0, icon, cx, cy, 0, 0, DI_NORMAL );
CComPtr<IUIImageFromBitmap> pifb;
pifb.CoCreateInstance ( CLSID_UIRibbonImageFromBitmapFactory );
CComPtr<IUIImage> pImage;
pifb->CreateImage ( img, UI_OWNERSHIP_COPY, &pImage );
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()
{
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:
{
CComQIPtr<IPropertyStore> pps = m_pAppRibbon->m_pRibbon;
bool bOnTop = (RIDC_RIBBON_QAT_ON_TOP == uCommandID);
PROPVARIANT pv;
UIInitPropertyFromUInt32 (
UI_PKEY_QuickAccessToolbarDock,
bOnTop ? UI_CONTROLDOCK_TOP : UI_CONTROLDOCK_BOTTOM, &pv );
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