Introduction
After Office 2007 introduced the fluent Ribbon, a lot of commercial expensive solutions were available. But finally, in Windows 7 Microsoft released the native Ribbon control. The article here explains my first try to use the ribbon in two of my projects, my light TurboIRC and my less light Turbo Play's Score Editor, and will help you use the ribbon in your application in no time at all. A sample project ready-to-run is also included.
Features
- Wraps the ribbon ActiveX in a simple C++ interface
- Works in x86/x64
You Need
Draw It Visually
If you do not want to mess with XML, or if you want a quick visual design tool you can try out my Visual Ribbon Creator which creates the ribbon for you.
Overview
The Ribbon is an ActiveX object that is attached to a window. It takes the entire width of the window, and its height is configurable. It sizes itself automatically when needed, so you need not handle stuff in WM_SIZE
. By default, the ribbon removes the menu, but you can insert it again later if you need it.
The ribbon gets its display data from an XML. This means that you only use a few function calls, some to initialize and load the data, and an event handler to get notified when something happens to the ribbon.
Because the ribbon is an ActiveX interface, an application that uses it will simply fail to initialize it under XP, but it will continue to run.
When you create the XML (see how below), you pass it to uicc.exe which creates the .BIN, the .H, and the .RC. Just include the .RC file inside your RC project, and the necessary stuff (the binary and the stuff defined in the ribbon RC and H) will be included.
Initialization
- You create an IUIFramework
IUIFramework* u_f = 0;
HRESULT hr = CoCreateInstance(CLSID_UIRibbonFramework,
0,CLSCTX_ALL,__uuidof(IUIFramework),(void**)&u_f);
You implement an IUIApplication
This interface has (in addition to the IUnknown
members), the following three functions:
- To be called from the ribbon when a command is created:
virtual HRESULT __stdcall OnCreateUICommand(UINT32 commandId,
UI_COMMANDTYPE typeID,IUICommandHandler **commandHandler);
To be called from the ribbon when a command is destroyed:
virtual HRESULT __stdcall OnDestroyUICommand(UINT32 commandId,
UI_COMMANDTYPE typeID,IUICommandHandler *commandHandler);
To be called when the view of the ribbon has changed:
virtual HRESULT __stdcall OnViewChanged(UINT32 viewId,UI_VIEWTYPE typeID,
IUnknown *view,UI_VIEWVERB verb,INT32 uReasonCode);
You call IUIFramework::Initialize() with the parent window of the ribbon, and your implementation of IUIApplication
.
You call IUIFramework::LoadUI() with the HINSTANCE
and the resource name.
The ribbon is now displayed!
Command Handlers
OnCreateUICommand
is called for each "command". See what a command below is in XML format. You must return an implementation of an IUICommandHandler
(AddRef()
+!) that will handle the command with two member functions:
- This is called when the value of the command is changed - still under pending documentation:
HRESULT __stdcall UpdateProperty(UINT32 commandId,REFPROPERTYKEY key,
const PROPVARIANT *currentValue,PROPVARIANT *newValue);
This is called when the command is executed. The "verb" is one of the UI_EXECUTIONVERB
enumeration, and it notifies you what sort of an event did happen, for example, a font selection drop down, a button click, etc.
HRESULT __stdcall Execute(UINT32 commandId,UI_EXECUTIONVERB verb,
const PROPERTYKEY *key, const PROPVARIANT *currentValue,
IUISimplePropertySet *commandExecutionProperties)
OnDestroyUICommand
is called when the interface is not needed anymore, but do not call Release()
. The ribbon will call it. It is just a notification for you that the object is about to be destroyed, not a request to destroy it yourself.
OnViewChanged
allows you to request an interface from the IUIFramework
and query its state. If typeId == UI_VIEWTYPE_RIBBON
and verb == UI_VIEWVERB_CREATE
or UI_VIEWVERB_SIZE
for example, this tells you that the ribbon is created or resized (warning, the ribbon is *not* created by the time IUIFramework:: LoadUI()
returns), so you can query the IUnknown
pointer for an IUIRibbon
and get its height.
Using My Library
To avoid all the above stuff, my library wraps these interfaces. A class RIBBON
is there, and you use it as follows:
RIBBON(HWND hh = 0);
~RIBBON();
bool Initialize();
Returns true
on successful ribbon initialization.
bool LoadMarkup(HINSTANCE hInst,LPCWSTR resourceName);
Loads the ribbon from the resource. If an existing ribbon is loaded, it destroys it.
void DestroyMarkup()
;
Destroys the loaded ribbon.
My library sends your parent window a predefined MESSAGE_RIBBON
(const int MESSAGE_RIBBON = RegisterWindowMessage(L"{E733E4DA-904C-486b-B5FB-6201773D69DE}");
), with the WPARAM
set to the RIBBON*
class, and the LPARAM
set to a RIBBON_MESSAGE
structure:
struct RIBBON_MESSAGE
{
IUIFramework * u_f; UINT32 cmd; UINT32 reason; UI_COMMANDTYPE type; UI_VIEWTYPE vtype; UI_VIEWVERB vverb; UI_EXECUTIONVERB verb; const PROPERTYKEY* key; const PROPVARIANT* cv; IUISimplePropertySet* pset; void* view; bool update; };
So, if you simply want to redirect the message to WM_COMMAND
, you check for update == false
, verb == UI_EXECUTIONVERB_EXECUTE
, and type == UI_COMMANDTYPE_ACTION
.
LRESULT CALLBACK Main_DP(HWND hh,UINT mm,WPARAM ww,LPARAM ll)
{
if (mm == MESSAGE_RIBBON)
{
RIBBON_MESSAGE* rm = (RIBBON_MESSAGE*)ll;
if (!rm)
if (rm->update == false && rm->verb == UI_EXECUTIONVERB_EXECUTE
&& rm->type == UI_COMMANDTYPE_ACTION)
SendMessage(hh,WM_COMMAND,rm->cmd,0);
}
...
}
Understanding the XML Format of the Ribbon Data
Because a ribbon is basically a representation of an XML map, here is where you will actually spend your most time to develop a nice and working ribbon.
A ribbon basically has these elements:
- A set of commands, each of them can have an ID, a symbol, a label, a tooltip, and a set of images. You define these commands for both "push buttons" and other elements such as a tab or a group.
- A set of elements, including an "application menu", a "quick access toolbar", and the "ribbon" which contains a number of tabs. Each tab can have some groups, and each group can have some predefined controls. These groups must be marked with specific "sizes" and layouts; so for example, if you want to have, say, six buttons inside a group, you have only three predefined ways to arrange them.
Here is a set of sample commands:
<Command Name="cmdNew" LabelTitle="New..." Symbol="cmdNew"
Comment="New" Id="22001" TooltipTitle="Tooltip Title"
TooltipDescription="Tooltip Text">
<Command.SmallImages>
<Image>1-32a.bmp</Image>
</Command.SmallImages>
<Command.LargeImages>
<Image>1-32a.bmp</Image>
</Command.LargeImages>
</Command>
<Command Name="cmdOpen" LabelTitle="Open..."
Symbol="cmdOpen" Comment="Open" Id="22002" />
<Command Name="cmdSave" LabelTitle="Save..."
Symbol="cmdSave" Comment="Save" Id="22003" />
<Command Name="Tab1" LabelTitle="First Tab" Symbol="_44" Id="30001"/>
<Command Name="Tab2" LabelTitle="Second Tab" Id="30002"/>
<Command Name="cx1" LabelTitle="Check Box 1" />
<Command Name="Font1" LabelTitle="Font Selection1" />
<Command Name="cpick1" LabelTitle="Choose Color" />
<Command Name="cmdn1" LabelTitle="Main Menu" />
<Command Name="g1" LabelTitle="Group 1" />
Note that the "button" commands would need images (small 16x16, or large 32x32, or 64x64 depending on the DPI, but I've found out that the ribbon resizes big images pretty well), but other "commands" like labels for tabs or groups only need name and title, because there is no command ID associated with them.
Here is how to create an "application menu":
<Ribbon.ApplicationMenu>
<ApplicationMenu CommandName="cmdn1">
<MenuGroup Class="MajorItems">
<Button CommandName="cmdNew" />
<Button CommandName="cmdOpen" />
<Button CommandName="cmdSave" />
</MenuGroup>
</ApplicationMenu>
</Ribbon.ApplicationMenu>
And now, you have an application menu with three buttons: New, Open, Save.
Here is how to create a quick access toolbar:
<Ribbon.QuickAccessToolbar>
<QuickAccessToolbar CustomizeCommandName="cmdCustomize">
<QuickAccessToolbar.ApplicationDefaults>
<Button CommandName="cmdNew" />
</QuickAccessToolbar.ApplicationDefaults>
</QuickAccessToolbar>
</Ribbon.QuickAccessToolbar>
Now, you have a quick access toolbar with the cmdNew
inside, plus a button defined by "cmdCustomize
" that should be able to customize the quick access toolbar. Note that the ribbon allows the user to change the buttons that appear in the quick access toolbar.
And, here is how to create a tab with some groups:
<Ribbon.Tabs>
....
<Tab CommandName="Tab1" >
<Tab.ScalingPolicy>
<ScalingPolicy>
<ScalingPolicy.IdealSizes>
<Scale Group="g1" Size="Large" />
<Scale Group="g2" Size="Large" />
<Scale Group="g3" Size="Large" />
<Scale Group="g4" Size="Large" />
</ScalingPolicy.IdealSizes>
</ScalingPolicy>
</Tab.ScalingPolicy>
<Group CommandName="g1" SizeDefinition="OneButton">
<Button CommandName="cmdNew" />
</Group>
<Group CommandName="g2" SizeDefinition="ThreeButtons">
<Button CommandName="cmdNew" />
<Button CommandName="cmdOpen" />
<Button CommandName="cmdSave" />
<DialogLauncher CommandName="cmdSave" />
</Group>
<Group CommandName="g3" SizeDefinition="OneFontControl">
<FontControl CommandName = "Font1" FontType = "RichFont" />
</Group>
<Group CommandName="g4">
<DropDownColorPicker CommandName="cpick1" ChipSize = "Large" />
</Group>
</Tab>
Be careful now. The tab has a reference to the command "Tab1
" which contains the name of the tab (very weird that they don't simply allow <Tab Label="x" />
, perhaps they might want to associate images with the tab later). This tab has four groups, and there is a scaling policy for each of them. The scaling policy of the group is not arbitrary set to "Large
" ,"Medium
", or "Small
", but it depends on the number of controls and the layout templates (described in MSDN here). That means that if your group is "OneButton
", it must be set to the "Large
" scaling size.
There is a predefined number of templates, but you can also use the <SizeDefinition>
for custom templates. You can view a sample in my "g5" group which uses a custom template.
After the scaling policy, here are the group definitions along with the layout templates described in MSDN. Now, each group can have many things, including buttons, spin buttons, drop down, font control, color picker, dialog box starters, separators, and all the stuff described in the Markup Elements page. My four groups above have some push buttons, a font chooser, and a color picker.
How do we get the values from the color picker? Check for type == UI_COMMANDTYPE_COLORANCHOR
, and the PROPVARIANT
"cv
" value contains an integer that represents the RGB.
How do we get the value from the font chooser? The type
is UI_COMMANDTYPE_FONT
and the PROPVARIANT
"cv
" value contains an IUnknown
, but I haven't found yet how to get the font from it!
Application Modes
Depending on your application's context, you might need some tabs and/or groups to be visible or invisible. Instead of doing this explicitly for each group/tab, the ribbon provides the "Application modes", which is a 32-bit bitset of the modes that they should be "active". Application modes apply to groups and tabs.
For example, here is the definition of the "Tab1
":
<Tab CommandName="Tab1" ApplicationModes="0,2">
What this means is that when the bit 0 or the bit 2 in the current selected mode is set, then the tab is displayed. So when I call Ribbon::SetModes(0)
, this tab will be hidden. When I use 2 or 8 or any integer that has either the bit 0 or the bit 2 set, the tab will be displayed.
The same can be applied to groups easily.
<Group CommandName="g1" SizeDefinition="OneButton" ApplicationModes="3">
Contextual Tabs
These tabs appear only when your application needs them. Use them for context-specific applications - for example Turbo Play only displays the score editor when a MIDI track is loaded.
To specify the contextual tab, add the <Tab>
element after a special header in the ribbon:
<Ribbon.ContextualTabs>
<TabGroup CommandName='CoTab1'>
<Tab CommandName="Tab4">
As you see, there has to be a tabgroup that contains all contextual tabs to be visible at the same time.
To show the contextual tab, you have to invalidate the ribbon contextual tab group:
IUIFramework::InvalidateUICommand(
51999,
UI_INVALIDATIONS_PROPERTY,
&UI_PKEY_ContextAvailable);
The ribbon then sends a notification to reconfigure which contextual tabs are to be shown:
if (rm->key && *rm->key == UI_PKEY_ContextAvailable)
{
if (rm->cmd == 59009)
{
unsigned int gPR = (unsigned int)UI_CONTEXTAVAILABILITY_NOTAVAILABLE;
if (ShouldShowCtTab(rm->cmd))
gPR = (unsigned int)UI_CONTEXTAVAILABILITY_ACTIVE;
UIInitPropertyFromUInt32(*rm->key, gPR, rm->nv);
}
...
}
Property Keys
In order to get/set the state of any ribbon control, you can use its property keys documented in MSDN. You can either use the IUIFramework :: GetUICommandProperty
to get a property key, or you can query the "cv" IUnknown member of the passed RIBBON_MESSAGE
structure for an IPropertyStore
; Use GetValue()
/ SetValue()
/ Commit to read/write properties of the referenced control. There are also a number of "global" keys, for which you query the IUIFramework
directly for an IPropertyStore
.
For example, when my WndProc
gets notified that a color is selected, it applies it to a background color for the ribbon as follows:
PROPVARIANT val;
HRESULT hr = rm->u_f->GetUICommandProperty
(rm->cmd,UI_PKEY_Color,&val); IPropertyStore* st = 0;
rm->u_f->QueryInterface(__uuidof(IPropertyStore),(void**)&st);
if (st && SUCCEEDED(hr))
{
st->SetValue(UI_PKEY_GlobalBackgroundColor,val);
st->Commit();
}
In the case of a font control, you can simply query the IUnknown*
pointer for an IPropertyStore
and use the UI_PKEY_FontProperties_XXXX
keys.
Current Limitations of the Ribbon Implementation
- Ribbon bitmaps can only be 32-bit BMP files. No JPG/PNG/other BMP support yet.
- The ribbon does not allow you to initialize, resize, rearrange, delete, or otherwise interact with its members apart from the XML configuration - for example, you cannot yet specify the startup tab or change the tabs programmatically. Also, no notifications are provided for non-commands, such as tab selection changing.
- The ribbon bitmaps must be included within the same module that contains the .rc.
- No multilingual support yet (you must define multiple language ribbons separately).
- The ribbon does not allow hosting of any control except its predefined ones, so you can't add your own
HWND
to the ribbon.
When these issues are considered by Microsoft, I will modify the article to include new functions!
Acknowledgements
Special thanks to these Microsoft staff that guided me to understand the ribbon internals:
- Ryan Demopoulos
- Nicolas Brun
History
- 20-11-2009: Win 7 Final update, Added contextual Tabs and added Visual Ribbon Creator reference tool
- 07-5-2009: Article update with RC1 updates
- 05-2-2009: Added property keys, application modes and font detection
- 23-1-2009: First release