Windows 7 Goodies in C++: Introduction to the Ribbon

Windows 7 Goodies in C++: Introduction to the Ribbon

 

Contents

Introduction

The Ribbon was originally designed by the Office team to replace their applications' complex systems of menus and toolbars. In Windows 7, Microsoft released a reusable COM-based Ribbon component for use in native Win32 applications. The native Ribbon is slightly different from the other Ribbon components in Office, WPF, and MFC, but they all share the same basic layout and functionality.

Because the Ribbon has lots of features, this first article will cover just the basics of the Ribbon and its COM interfaces, and will demonstrate the steps required to create an app from scratch that uses a simple Ribbon. If you want to dive right into Ribbon development, check out the other Ribbon articles here on CodeProject, or the Ribbon classes in the forthcoming WTL 8.1.

The 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.

Starting a New App

Prep work in CMainFrame

We'll start with a basic SDI app as created by the WTL AppWizard. This app does not have a toolbar (since the Ribbon replaces the toolbar), but it does have a status bar. It uses a separate class for the view window, but the view window won't do too much, since our focus is on getting the Ribbon up and running. Later on, we'll do more with the view window when it comes to laying out the main frame's child controls.

The first step is to include UIRibbon.h, which contains the type and interface definitions we'll need to use the Ribbon. Our app will implement two of those interfaces, and the implementation details are outlined throughout the rest of the article. For starters, all we need to do is add IUIApplication to the inheritance list of CMainFrame, then add stub implementations of the three methods:

 
// In stdafx.h:
#include <UIRibbon.h>
 
class CMainFrame :
  public CFrameWindowImpl<CMainFrame>,
  public CMessageFilter,
  public CComObjectRootEx<CComSingleThreadModel>,
  public IUIApplication
{
  // IUIApplication methods
  STDMETHODIMP OnCreateUICommand (
                 UINT32 uCmdID, UI_COMMANDTYPE nType,
                 IUICommandHandler** ppHandler )
  { return E_NOTIMPL; }
 
  STDMETHODIMP OnDestroyUICommand (
                 UINT32 uCmdID, UI_COMMANDTYPE nType,
                 IUICommandHandler* pHandler )
  { return E_NOTIMPL; }
 
  STDMETHODIMP OnViewChanged (
                 UINT32 uViewID, UI_VIEWTYPE nType, IUnknown* pView,
                 UI_VIEWVERB nVerb, INT32 nReason )
  { return E_NOTIMPL; }
};

Since CMainFrame needs to be a COM object, it also derives from CComObjectRootEx. Also, the single CMainFrame instance in the Run() function is created using CComObjectStackEx, in order to use ATL's implementation of IUnknown:

 
// In the global Run() function:
CComObjectStackEx<CMainFrame> wndMain;

Initializing the UI Framework

The first interaction we have with the Ribbon is through the IUIFramework interface. CMainFrame uses this interface to initialize and shut down the framework, so it needs to keep an interface pointer for the lifetime of the app:

 
// In MainFrm.h:
CComPtr<IUIFramework> m_pFramework;

In CMainFrame::OnCreate(), we cocreate the framework's COM object:

 
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // Standard frame initialization here...
 
  // Note: Error handling has been omitted.
  // Cocreate the Ribbon framework COM object.
  m_pFramework.CoCreateInstance ( CLSID_UIRibbonFramework );
 
  // Initialize the framework:
IUIApplication* pApp = this;
 
  m_pFramework->Initialize ( m_hWnd, pApp );
 
  return 0;
}

IUIFramework::Initialize() takes the HWND of the window that will contain the Ribbon, and the IUIApplication interface that the framework will use to communicate with the app. The framework changes the appearance of the window to fit in with the Ribbon's visual guidelines. For example, the framework removes the window's menu and draws controls in the caption bar. To ensure that those features work smoothly, the framework requires that the window be a top-level window with the WS_CAPTION style, and without the WS_EX_TOOLWINDOW style. IUIFramework::Initialize() will return ERROR_INVALID_WINDOW_HANDLE if the window does not meet those conditions.

In order to properly shut down the framework and release any resources it was using, we also need to call IUIFramework::Destroy() in the WM_DESTROY handler:

 
void CMainFrame::OnDestroy()
{
  m_pFramework->Destroy();
  m_pFramework.Release();
}

After adding the call to IUIFramework::Initialize(), our frame window looks like this:

Image 1

There is no Ribbon yet, of course, since we haven't defined the contents of our Ribbon, but the framework has made the necessary visual tweaks like removing the menu.

Adding a Ribbon to the Application

The Ribbon works differently than other UI widgets that you've used before. One of the design principles is that the definition of a Ribbon's contents should be separate from its visual design. An app can specify what commands appear in the Ribbon, how they are organized and grouped, and so on. But the app has limited control over how the Ribbon will actually look. This lets Microsoft change the Ribbon's appearance in the future with fewer compatibility problems than existing ways of creating GUIs like dialog resources.

The steps in creating a Ribbon definition are:

  1. Create an XML file that defines the Ribbon's contents and add that file to your project.
  2. Compile the XML with the uicc tool.
  3. Add the files that uicc generates to your app's resources.
  4. Call IUIFramework::LoadUI() to create a Ribbon using the compiled definition.

Before we dive into the XML, we need to become familiar with the elements of the Ribbon.

Parts of the Ribbon

The Ribbon uses the notion of a command in many places. A command is more than a button, it's essentially anything in the Ribbon that you can click and do things with. Commands have various properties, such as a text label, an icon, and a shortcut key. Once you've made the list of commands in your app, you start building other parts of the Ribbon out of those commands.

Here's a screen shot of the Paint window, showing the various parts of the Ribbon:

Image 2

  • Application menu: The button to the left of the first tab opens the Application menu. This is a command itself, although it does not allow its appearance to be customized. The menu contains other commands.
  • Quick access toolbar (QAT): The QAT is the toolbar that the Ribbon draws in the window's caption area. You can add other commands to the toolbar by right-clicking them and selecting Add to Quick Access Toolbar on the context menu. The down arrow at the right edge of the QAT shows a menu where you can customize the commands in the toolbar and change the appearance of the QAT and the Ribbon.
  • Tabs: The tab row shows all the tabs that you've defined in the XML file. Each tab is a command, and it contains one or more groups.
  • Groups: Each group is a collection of commands. A group has an optional label at the bottom, and the Ribbon draws dividers between the groups. You might occasionally see the term chunk used instead of group. This is not the official term, but it was used by the Office team when they were building the first Ribbon, and it still gets used in unofficial written material like blog posts.

Starting the XML file

Here's the skeleton XML file that's our starting point:

 
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns='http://schemas.microsoft.com/windows/2009/Ribbon'>
  <Application.Commands>
  </Application.Commands>
  <Application.Views>
    <Ribbon>
      <Ribbon.Tabs>
      </Ribbon.Tabs>
    </Ribbon>
  </Application.Views>
</Application>

Notice that some properties are written using child tags with the "object.property" naming convention, similar to XAML. For example, <Application.Commands> defines the Commands property of the <Application> tag.

The first step is to create a few commands that we'll use in the <Ribbon> sections. Each command is defined in a <Command> tag. <Command> has several properties that we'll see later, but for now, we'll use two: Name and LabelTitle. The Name property is a string that identifies the command in the rest of the XML file, and LabelTitle is a string that sets the text of the UI element.

In order to create a button, we also need a group and a tab to put it in, so we start with three commands:

 
<Application.Commands>
  <Command Name="tabMain" LabelTitle="Main" />
  <Command Name="grpHello" LabelTitle="Hello Ribbon!" />
  <Command Name="cmdClickMe" LabelTitle="Click me" />
</Application.Commands>

(The Microsoft samples I've seen use the "cmd" prefix in all command names, but I've chosen to use more descriptive prefixes that indicate the function of each command.) Then we create a tab, a group, and a button control in the group:

 
<Application.Views>
  <Ribbon>
    <Ribbon.Tabs>
      <Tab CommandName="tabMain">
        <Group CommandName="grpHello" SizeDefinition="OneButton">
          <Button CommandName="cmdClickMe" />
        </Group>
      </Tab>
    </Ribbon.Tabs>
  </Ribbon>
</Application.Views>

This creates a tab that contains one group. Most of the tab's properties come from the associated <Command> tag. There's a level of indirection here: You don't say what the text of a tab is, you say what command the tab is, and the Ribbon looks up the text in the associated <Command> tag. Similarly, the group's label comes from the LabelTitle property of the grpHello command. Don't worry about the SizeDefinition attribute for now, it's just something that's required so the Ribbon knows how to arrange the controls in the group.

Finally, there's a <Button> tag that creates a regular push button. The separation of properties into <Command> tags is more useful for buttons, since it's possible to have the same command in multiple places, and having that separation means you don't have to repeat the same properties in every place that the command appears.

Compiling the Ribbon XML file

Now that we've created the Ribbon definition file, we need to incorporate it into the app. If you've saved the above XML to a file called ribbon.xml, you can add it to the Visual Studio project by clicking Project|Add Existing Item and selecting ribbon.xml.

The next step is to invoke the compiler (uicc.exe) that converts the XML to a binary format that the Ribbon understands. Open the properties for ribbon.xml, and in the Custom build step settings, set the Command line field to:

 
uicc.exe $(InputFileName) $(InputName).bml /header:$(InputName)ids.h
  /res:$(InputName).rc /name:HelloRibbon

uicc generates three files when it compiles ribbon.xml:

  • ribbon.bml: The compiled output. Microsoft's samples use the .bml extension, and I've followed suit.
  • ribbonids.h: A C header file that has #defines for all commands and strings that are defined in the XML.
  • ribbon.rc: A resource definition file that has definitions for string and icons referenced in the XML, and one custom resource type that pulls in ribbon.bml. The /name switch controls the name of this resource. In this example, the name is HELLORIBBON_RIBBON. That is, the name is whatever is specified in the /name switch, uppercased, with "_RIBBON" appended.

We should also tell Visual Studio what files uicc creates by setting the Outputs field to "$(InputName).bml;$(InputName)ids.h;$(InputName).rc".

Next, we add the resources from ribbon.rc to our app's resources. In the Resource View tab, right-click HelloRibbon.rc and select Resource Includes. In the Compile-time directives edit box, add:

 
#include "ribbon.rc"

Now when the app is compiled, the app's resources will include any resources that were listed in ribbon.rc. uicc also verifies that the XML contains no errors, and doesn't break any of the Ribbon's layout rules (for example, having the same command appear more than once in a group). If uicc finds any errors, it will fail to compile the XML, and the project will not build.

Loading the Ribbon

The last step is to initialize the Ribbon using the compiled version of the XML. We do this by adding a call to IUIFramework::LoadUI() in CMainFrame::OnCreate():

 
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // Note: Error handling has been omitted.
  // Cocreate the Ribbon framework COM object.
  m_pFramework.CoCreateInstance ( CLSID_UIRibbonFramework );
 
  // Initialize the framework:
IUIApplication* pApp = this;
 
  m_pFramework->Initialize ( m_hWnd, pApp );
 
  // Load the Ribbon.
  m_pFramework->LoadUI ( _Module.GetResourceInstance(),
                         L"HELLORIBBON_RIBBON" );
 
  return 0;
}

IUIFramework::LoadUI() takes two parameters, the HMODULE that contains the compiled XML, and the resource name.

If we compile the app after making these changes, and ignore a couple of warnings from uicc about missing XML tags, we'll see our first Ribbon!

Image 3

Fixing uicc warnings

Before we move on, let's fix those warnings from uicc about missing ApplicationMenu and QuickAccessToolbar properties. We'll need one command for each of those elements:

 
<Application.Commands>
  <Command Name="cmdApplicationMenu" />
  <Command Name="cmdQAT" />
<Application./Commands>

Then we add two new tags under the <Ribbon> tag:

 
<Application.Views>
  <Ribbon>
    <Ribbon.ApplicationMenu>
      <ApplicationMenu CommandName="cmdApplicationMenu" />
    </Ribbon.ApplicationMenu>
    <Ribbon.QuickAccessToolbar>
      <QuickAccessToolbar CommandName="cmdQAT" />
    </Ribbon.QuickAccessToolbar>
  </Ribbon>
</Application.Views>

With these changes, all of the required elements are in place, and uicc won't issue any more warnings.

Communicating with the Ribbon

We've already seen the IUIFramework interface, which our app uses to set up and tear down the Ribbon framework. There is another interface that our app uses to communicate directly with the Ribbon: IUIRibbon. The Ribbon passes this interface to our app via IUIApplication::OnViewChanged(), so we'll start by filling in that method.

First, add a member to CMainFrame that will hold the IUIRibbon interface:

 
// In MainFrm.h:
CComQIPtr<IUIRibbon> m_pRibbon;

CMainFrame::OnViewChanged() is called when the Ribbon is created, destroyed, or resized. (Technically, it deals with application views, not the Ribbon specifically, but the Ribbon is the only view that has been defined so far.) The parameters to OnViewChanged are:

  • uViewID: The view ID, currently always 0.
  • nType: The view type, currently always UI_VIEWTYPE_RIBBON.
  • pView: An IUnknown interface provided by the view.
  • nVerb: The action that the view is performing: UI_VIEWVERB_CREATEUI_VIEWVERB_DESTROY, or UI_VIEWVERB_SIZE.
  • nReason: Not currently used.

When nVerb is UI_VIEWVERB_CREATE, the Ribbon is being created and we can query it for an IUIRibbon interface. When nVerb is UI_VIEWVERB_DESTROY, the Ribbon is going away, so we release the IUIRibbon interface:

 
STDMETHODIMP CMainFrame::OnViewChanged (
  UINT32 uViewID, UI_VIEWTYPE nType, IUnknown* pView, UI_VIEWVERB nVerb,
  INT32 nReason )
{
  switch ( nVerb )
    {
    case UI_VIEWVERB_CREATE:
      m_pRibbon = pView;
    break;
 
    case UI_VIEWVERB_DESTROY:
      m_pRibbon.Release();
    break;
  }
 
  return S_OK;
}

A more interesting case is UI_VIEWVERB_SIZE. This tells us that the Ribbon is changing size, being hidden, or being shown. Since that affects the layout of the main frame, we get the Ribbon's new height in pixels and lay out the frame window:

 
// Added to the switch statement above:
case UI_VIEWVERB_SIZE:
  if ( m_pRibbon )
    {
    HRESULT hr = m_pRibbon->GetHeight ( &m_cyRibbon );

    if ( SUCCEEDED(hr) )
      UpdateLayout();
    }
break;

CFrameWindowImpl::UpdateLayout() positions the various kinds of bars that WTL supports -- rebars, toolbars, and status bars -- and then positions the view window to take up the remaining space. Since WTL doesn't know about the Ribbon, we need to override UpdateLayout() and use the Ribbon's height as part of the calculations. Our override is pretty similar to CFrameWindowImpl::UpdateLayout(), but it determines how many pixels to reserve at the top of the client area by looking at m_cyRibbon:

 
void CMainFrame::UpdateLayout ( BOOL bResizeBars )
{
CRect rect;
 
  GetClientRect ( rect );
  UpdateBarsPosition ( rect, bResizeBars );
 
  // 'rect' now holds the rect of the client area that WTL calculated,
  // but it doesn't include the space needed by the Ribbon.  Move the top
  // coordinate of the rect down so the view window doesn't overlap the Ribbon.
  rect.top += m_cyRibbon;
 
  // Resize the view window
  if ( m_hWndClient != NULL )
    CWindow(m_hWndClient).SetWindowPos ( NULL, rect,
                                         SWP_NOZORDER | SWP_NOACTIVATE );
}

After adding this UpdateLayout() override, you can try minimizing the Ribbon and the view window will resize accordingly:

Image 4

If you make the window small enough, the Ribbon will hide itself entirely. Our UpdateLayout() handles that case as well.

Handling Notifications from Commands

IUICommandHandler

The next interface we'll use is IUICommandHandler. The app implements this interface, and the Ribbon calls its methods to read properties for each command and notify the app that it should execute a command.

When the Ribbon parses the compiled XML, it asks the app for a COM interface on each command. The app must return an IUICommandHandler interface for a command to be functional. This interface can be implemented by any COM object, but for our sample app, we'll add the interface directly to CMainFrame:

 
class CMainFrame :
  ...
  public IUICommandHandler
{
  // IUICommandHandler methods
  STDMETHODIMP Execute (
                UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
                const PROPVARIANT* pCurrVal,
                IUISimplePropertySet* pCmdProperties );
 
  STDMETHODIMP UpdateProperty (
                 UINT32 uCmdID, REFPROPERTYKEY key,
                 const PROPVARIANT* pCurrVal,
                 PROPVARIANT* pNewVal );
};

Creating and destroying IUICommandHandlers

The Ribbon will call IUIApplication::OnCreateUICommand() once per command to get an IUICommandHandler interface, and IUIApplication::OnDestroyUICommand() once when it no longer needs the interface. Since CMainFrame implements IUICommandHandler for every command, CMainFrame::OnCreateUICommand() is simple:

 
STDMETHODIMP CMainFrame::OnCreateUICommand (
    UINT32 uCmdID, UI_COMMANDTYPE nType, IUICommandHandler** ppHandler )
{
  // This object implements IUICommandHandler.
  return QueryInterface ( IID_PPV_ARGS(ppHandler) );
}

We don't have to do any cleanup when commands are destroyed, so CMainFrame::OnDestroyUICommand() just returns a successful HRESULT:

 
STDMETHODIMP CMainFrame::OnDestroyUICommand (
    UINT32 uCmdID, UI_COMMANDTYPE nType, IUICommandHandler* pHandler )
{
  // No cleanup needed.
  return S_OK;
}

Inserting C command identifiers into the Ribbon XML

Notice that OnCreateUICommand() and OnDestroyUICommand() have command ID parameters. By default, uicc generates IDs automatically. If you look at the ribbonids.h file, you'll see lines like the following:

 
#define cmdClickMe 7

where uicc has used the Name attribute of a <Command> tag to create a C identifier. Let's see how to control these identifiers and make them follow the C convention of being all uppercase.

Going back to the <Command> tag for the Click Me button:

 
<Command Name="cmdClickMe" LabelTitle="Click me" />

we can add a Symbol attribute that sets the identifier's name, and an Id attribute that sets its value. For example:

 
<Command Name="cmdClickMe" Symbol="RIDC_CLICK_ME"
         Id="42" LabelTitle="Click me" />

(I've used RIDC as a prefix by adding R (ribbon) in front of the conventional prefix IDC.) With this change, ribbonids.h will now have:

 
#define RIDC_CLICK_ME 42

Name can be any legal C identifier, and Id can be an integer between 2 and 59999 inclusive. The Id value can be written as decimal or hexadecimal.

Executing a command

When the user clicks a button, the Ribbon calls the associated IUICommandHandler::Execute() method. The parameters to Execute() are:

  • uCmdID: The command ID.
  • nVerb: A constant that indicates what kind of action the user is taking. In the sample app, the verb is always UI_EXECUTIONVERB_EXECUTE.
  • pKeypCurrVal: For some commands, the Ribbon passes a property and its new value in these parameters.
  • pCmdProperties: A IUISimplePropertySet collection containing additional properties about the command. The sample app does not use this parameter.

Depending on the type of command, the Ribbon may pass Execute() some information about the state of the command. For example, when the user clicks a toggle button, the Ribbon passes the new state of the button (toggled off or on) in the pCurrVal parameter. We'll see an example later that uses this info. A simple push button has no extra info, so we just look at the command ID to know which command is being executed:

 
STDMETHODIMP CMainFrame::Execute (
UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
const PROPVARIANT* pCurrVal, IUISimplePropertySet* pCmdProperties )
{
  switch ( uCmdID )
    {
    case RIDC_CLICK_ME:
      MessageBox ( _T("Thanks for clicking me!"), _T("Hello Ribbon!") );
    break;
    }
 
  return S_OK;
}

Notice that the case statement uses the RIDC_CLICK_ME identifier that we set in the <Command> tag.

Image 5

Using Toggle Buttons

Another type of control you can put in the Ribbon is a toggle button. These buttons function like check boxes, in that each click toggles the state between on (pressed) and off (not pressed).

Adding a ToggleButton control

We'll use a toggle button to show and hide the status bar, similar to the View|Status Bar command that's created by the WTL AppWizard. We'll need two new commands, one for a new group, and one for the button:

 
<Application.Commands>
  <Command Name="grpStatusBar" LabelTitle="Status bar" />
  <Command Name="cmdToggleStatusBar" Symbol="RIDC_TOGGLE_STATUS_BAR"
           LabelTitle="Show status bar" />
</Application.Commands>

Then we add a new group to the Main tab that contains a ToggleButton control:

 
<Tab CommandName="tabMain">
  <Group CommandName="grpHello" SizeDefinition="OneButton">
    <Button CommandName="cmdClickMe" />
  </Group>
  <Group CommandName="grpStatusBar" SizeDefinition="OneButton">
    <ToggleButton CommandName="cmdToggleStatusBar" />
  </Group>
</Tab>

And here's what the new button looks like, in the toggled-on state:

Image 6

Writing IUICommandHandler::Execute for a ToggleButton

The implementation of IUICommandHandler::Execute() is passed a key/value pair in the form of a PROPERTYKEY and a PROPVARIANT. When Execute() is called because the user clicked a toggle button, the key is UI_PKEY_BooleanValue, and the value is a boolean that indicates the new state of the button: true means the button is now toggled on, and false means it's toggled off.

The header file UIRibbonPropertyHelpers.h contains some helper functions that make it easier to get and set values in PROPVARIANTs. Since the value sent for a toggle button is a boolean, we can use the UIPropertyToBoolean() function to read the value from the PROPVARIANT:

 
// Add to stdafx.h:
#include <UIRibbonPropertyHelpers.h>
 
STDMETHODIMP CMainFrame::Execute (
  UINT32 uCmdID, UI_EXECUTIONVERB nVerb, const PROPERTYKEY* pKey,
  const PROPVARIANT* pCurrVal, IUISimplePropertySet* pCmdProperties )
{
  switch ( uCmdID )
    {
    case RIDC_CLICK_ME:
      // ...
    break;
 
    case RIDC_TOGGLE_STATUS_BAR:
      {
      BOOL bShowBar;
 
      // We can get the new state of the toggle button by reading pKey/pCurrVal.
      if ( NULL != pKey && UI_PKEY_BooleanValue == *pKey && NULL != pCurrVal )
        {
        BOOL bToggledOn;
 
        UIPropertyToBoolean ( *pKey, *pCurrVal, &bToggledOn );
 
        // Show the bar if the button is now toggled on.
        bShowBar = bToggledOn;
        }
      else
        // Show the bar if the bar is not currently visible.
        bShowBar = !::IsWindowVisible ( m_hWndStatusBar );
 
      ShowStatusBar ( !bShowBar );
      }
 
    break;
    }
}
 
void CMainFrame::ShowStatusBar ( BOOL bShow )
{
  CWindow(m_hWndStatusBar).ShowWindow ( bShow ? SW_HIDE : SW_SHOW );
  UpdateLayout();
}

Once you add this code to CMainFrame::Execute(), you can click the toggle button and the status bar will be shown or hidden with every click.

Setting command properties

Since a toggle button is toggled off by default, the initial state of our button isn't what we want; the status bar starts out visible, so the button should start in the toggled-on state. However, there is no element in the XML file for setting the button's initial state. Instead, the Ribbon queries for that property right after the IUICommandHandler for that button is created.

When the Ribbon needs to query a property of a command, it calls that command's IUICommandHandler::UpdateProperty() method, which can return the property's new value in a PROPVARIANT. Here's how we set the initial state of the toggle button:

 
STDMETHODIMP CMainFrame::UpdateProperty (
    UINT32 uCmdID, REFPROPERTYKEY key, const PROPVARIANT* pCurrVal,
    PROPVARIANT* pNewVal )
{
  if ( RIDC_TOGGLE_STATUS_BAR == uCmdID && UI_PKEY_BooleanValue == key &&
       NULL != pNewVal )
    {
    // Set the "Show status bar" button to be initially toggled on.
    UIInitPropertyFromBoolean ( key, TRUE, pNewVal );
    return S_OK;
    }
 
  // We don't respond to queries for other buttons or properties.
  return E_NOTIMPL;
}

This is essentially doing the opposite of the code we just saw in Execute(). It uses another helper function in UIRibbonPropertyHelpers.hUIInitPropertyFromBoolean(), to store a boolean value in a PROPVARIANT. All of the UIInitPropertyFromXxx() helpers take the PROPERTYKEY and the new value so they can check that the value is compatible with the property's data type. If you pass an incompatible data type, the compiler will issue an error.

If you set a breakpoint at the beginning of CMainFrame::UpdateProperty(), you'll see that it gets called a lot. There are many optional properties in the Ribbon XML that we haven't set. The Ribbon calls UpdateProperty() to query for all of those properties, and falls back to reasonable defaults if the function returns E_NOTIMPL. That is why we have to check the key parameter so we know when the Ribbon is querying for the button's toggle state, and not some other property whose default value is sufficient.

Additional Properties of Commands

Adding tooltips and keytips

The Ribbon has built-in support for keyboard navigation. If you press the Alt key, you'll see tooltips that tell you how to navigate through the Ribbon's elements:

Image 7

These navigation tooltips are called keytips, and can be customized with the Keytip attribute of the <Command> tag. For example, to change the Main tab's navigation key to "M", make this change to the tab's <Command> tag:

 
<Command Name="tabMain" Symbol="RIDT_MAIN" LabelTitle="Main" Keytip="M" />

This string, along with other string attributes, all show up as resources in the ribbon.rc file that uicc creates. That way, the strings can be localized. The string resource IDs begin at 60001, but the IDs can be customized just like the command IDs. To customize a string attribute, you create a sub-tag called <Command.Keytip> to hold the keytip properties. Within that tag, create a <String> tag that contains the C identifier and resource ID:

 
<Command Name="tabMain" Symbol="RIDT_MAIN" LabelTitle="Main">
  <Command.Keytip>
    <String Symbol="RIDS_MAIN_KEYTIP" Id="54321">M</String>
  </Command.Keytip>
</Command>

Each command can also have a tooltip that's shown when the mouse hovers over the button. The tooltip can have a title, which is shown in bold, and a description. These strings are set through the TooltipTitle and TooltipDescription attributes of a <Command> tag. Here's how to add a tooltip to the toggle button:

 
<Command Name="cmdToggleStatusBar" Symbol="RIDC_TOGGLE_STATUS_BAR"
         Keytip="T" LabelTitle="Show status bar"
         TooltipTitle="Toggle the status bar"
         TooltipDescription="Show or hide the status bar" />

As with the Keytip, you can change the IDs of the tooltip strings by using a <String> tag. Here's what the customized tooltip looks like:

Image 8

Assigning images to buttons

Our buttons look pretty boring without any graphics, so let's see how to add some. The Ribbon supports only two graphics formats: 32bpp BMP files for normal images, and 4bpp BMP files for high-contrast images. uicc will issue an error if you use any other type of graphic. (I must apologize up-front for my lame-looking graphics. I am galactically incapable of drawing anything good myself, so I've borrowed graphics from the sample Ribbon apps in the Windows SDK.)

Each command can have four sets of images: large, small, large high-contrast, and small high-contrast. The Ribbon chooses whether to use large or small images based on where the command appears. The two buttons we've added so far are large buttons, so they will use large images. If you add those commands to the QAT, those buttons will use small images. The two sets of high-contrast images are used when the system is using a high-contrast visual theme; the choice about which image size to use remains the same.

The images are set via properties of the <Command> tag. Here is the XML that sets the large and small images to use for the Show status bar button:

 
<Command Name="cmdToggleStatusBar" ... >
  <Command.LargeImages>
    <Image Source="res/StatusBar_lg.bmp" />
  </Command.LargeImages>
  <Command.SmallImages>
    <Image Source="res/StatusBar_sm.bmp" />
  </Command.SmallImages>
</Command>

The Source attributes are file paths relative to where the XML file is, so in this case the files are in a res subdirectory. uicc will include these images in the ribbon.rc file and generate IDs for them. As with strings, you can customize the resource ID and the C identifier for an image by adding attributes to the <Image> tag:

 
<Image Source="res/StatusBar_lg.bmp" Id="12345" Symbol="RIDI_STATUS_BAR_LG" />

Within each of the four sets of images, there can be up to four <Image> tags for use at various DPI settings. In this example program, we have just one image for the default DPI of 96. This is fine, because the Ribbon will scale the images as necessary if the system is using a higher DPI setting.

Here's how the Ribbon looks with images on the two buttons:

Image 9

Setting up the Application Menu

The Applications menu is the Ribbon element that's analogous to the File menu in apps with a regular menu. The Applications menu is accessed through the dropdown button to the left of the Main tab. Its contents are listed in a <Ribbon.ApplicationMenu> tag that is a child of the <Ribbon> tag.

The Applications menu contains two sub-elements, a most-recently-used file list and a menu of commands. The MRU list is set with a <ApplicationMenu.RecentItems> child tag, and the menu's contents are controlled by one or more <MenuGroup> child tags.

Here's the XML that shows the contents of the Applications menu in our sample app:

 
<Application.Commands>
  <!-- New commands: -->
  <Command Name="cmdAbout" Symbol="RIDC_ABOUT" LabelTitle="&amp;About" />
  <Command Name="cmdExit" Symbol="RIDC_EXIT" LabelTitle="E&amp;xit" />
  <Command Name="cmdMRUList" LabelTitle="(MRU goes here)" />
</Application.Commands>
<Application.Views>
  <Ribbon>
    <Ribbon.ApplicationMenu>
      <ApplicationMenu CommandName="cmdApplicationMenu">
        <ApplicationMenu.RecentItems>
          <RecentItems CommandName="cmdMRUList" />
        </ApplicationMenu.RecentItems>
        <MenuGroup>
          <Button CommandName="cmdAbout" />
          <Button CommandName="cmdExit" />
        </MenuGroup>
      </ApplicationMenu>
    </Ribbon.ApplicationMenu>
  </Ribbon>
</Application.Views>

And here's what the menu looks like:

Image 10

There are a few things to note about the menu:

  • The MRU list is just an empty placeholder, to show what it looks like.
  • The menu contains two commands in one group. You can create separators in the menu by closing one <MenuGroup> tag and starting another.
  • The mnemonic for a command that appears in the menu is set by putting an ampersand in the menu item text, as with traditional menus. The ampersand must be escaped in XML and written as "&amp;". If the command is used elsewhere in the Ribbon, the ampersand is ignored and the Keytip attribute is used instead.

Saving Ribbon Settings

The last thing we'll add to the sample app is the ability to save Ribbon settings when the app closes. If you change the contents of the QAT or minimize the Ribbon, those changes are lost when you close the app. To fix that, we'll use two IUIRibbon methods for loading and saving settings. First, add a string member to CMainFrame that holds the data file path:

 
// In MainFrm.h:
CString m_sSettingsFilePath;

Since this path won't change while the app is running, we can initialize it in the CMainFrame constructor:

 
CMainFrame::CMainFrame()
{
TCHAR szTempDir[MAX_PATH] = {0};
 
  // Build the path to the file where we'll store the ribbon settings.
  GetTempPath ( _countof(szTempDir), szTempDir );
  PathAddBackslash ( szTempDir );
 
  m_sSettingsFilePath.Format ( _T("%sHelloRibbonSettings.dat"), szTempDir );
}

We then load the settings when the Ribbon is created, and save them when the Ribbon is destroyed. This is done by adding a couple of lines to CMainFrame::OnViewChanged():

 
STDMETHODIMP CMainFrame::OnViewChanged(...)
{
  switch ( nVerb )
    {
    case UI_VIEWVERB_CREATE:
      m_pRibbon = pView;
      LoadRibbonSettings();
    break;
 
    case UI_VIEWVERB_DESTROY:
      m_pRibbon.Release();
      SaveRibbonSettings();
    break;
  }
 
  return S_OK;
}

LoadRibbonSettings() calls IUIRibbon::LoadSettingsFromStream() to load the settings. Since that method takes an IStream interface on the data, we call SHCreateStreamOnFileEx() to get an IStream interface that can be used to read the file.

 
void CMainFrame::LoadRibbonSetings()
{
HRESULT hr;
CComPtr<IStream> pStrm;
 
  hr = SHCreateStreamOnFileEx ( m_sSettingsFilePath, STGM_READ,
                                0, FALSE, NULL, &pStrm );
 
  if ( SUCCEEDED(hr) )
    m_pRibbon->LoadSettingsFromStream ( pStrm );
}

If the file does not exist or can't be opened, we don't call LoadSettingsFromStream(), and the Ribbon will start out in the default state.

SaveRibbonSettings() is similar. We get an IStream interface that can be used to write to the file and reset the file size to zero so that any existing contents are erased. Then we call IUIRibbon::SaveSettingsToStream() to save the settings to the file.

 
void CMainFrame::SaveRibbonSetings()
{
HRESULT hr;
CComPtr<IStream> pStrm;
 
  hr = SHCreateStreamOnFileEx ( m_sSettingsFilePath, STGM_WRITE|STGM_CREATE,
                                FILE_ATTRIBUTE_NORMAL, TRUE, NULL, &pStrm );
 
  if ( SUCCEEDED(hr) )
    {
    LARGE_INTEGER liPos;
    ULARGE_INTEGER uliSize;
 
    liPos.QuadPart = 0;
    uliSize.QuadPart = 0;
 
    pStrm->Seek ( liPos, STREAM_SEEK_SET, NULL );
    pStrm->SetSize ( uliSize );
 
    m_pRibbon->SaveSettingsToStream ( pStrm );
    }
}

Now, when you close the app, any changes you make to the Ribbon size and QAT will be preserved the next time you run the app.

Conclusion

In this article, we've seen the basic steps necessary to include the Ribbon in an application. Next time, we'll get an in-depth look at how commands are organized in groups, and how to customize a group's layout.

Revision History

posted @ 2022-12-13 14:43  小风风的博客  阅读(52)  评论(0编辑  收藏  举报