共用方式為


Chapter 10: Using the Windows Ribbon

The Windows Ribbon control is designed to help users find, use, and understand available commands for a particular application in a way that’s more natural and intuitive than menu bars or toolbars. Microsoft Office applications have used the Ribbon control since Office 2007 and Windows 7 applications, such as Paint, use the Ribbon control. The Ribbon control also provides benefits to the developer by separating presentation and logic. The Hilo Annotator application provides access to all of its tools through the Ribbon control. This article describes how the Annotator Ribbon is implemented.

Introducing the Ribbon Framework

The Windows Ribbon control is a COM control and since it has a user interface you must initialize an STA (single threaded) apartment. The Windows Ribbon control is not an ActiveX control. This means that you do not have to provide an ActiveX control site, which simplifies considerably the code that you have to write in your application.

The Ribbon control uses adaptive layout. This means that the developer provides information about the controls that will be used and how they will be grouped, and at run time the Ribbon control determines the actual position of the controls.

To see the effect of adaptive layout you can run Windows 7 Paint and resize the window. This is shown in Figure 1. The top left image shows the Ribbon control with the View tab selected. At this width the items on the Ribbon control are shown full size. When the window width is reduced, the Ribbon control width is reduced and adaptive layout resizes the controls to enable all of them to be shown.

The bottom left image in Figure 1 shows the first change, that the Zoom group has compacted from a row of three buttons to a column of buttons. When the width is reduced further (bottom right, Figure 1) the Display group collapses to a column of buttons. At this size, there is no space to show the Customize Quick Access Toolbar button on the title bar, so instead there is a single button labeled .. and when you click on this button the toolbar pops up. The most compact arrangement (top right, Figure 1) collapses the Zoom group to a drop-down menu. If the window width is reduced further, the items on the Ribbon control cannot be shown and it disappears completely.

Figure 1 Adaptive layout, counter-clockwise, occurring as the window width is reduced

Ff963530.9aa30b0b-9439-48c0-9ff3-9f37b69bad76-thumb(en-us,MSDN.10).png

Adaptive layout is a consequence of the separation of presentation and logic. You design the user interface (UI) with a design tool that generates XAML, compile this to a BML file, and bind it to the application as a UIFILE resource. You do not have to write layout code, resizing code, child control creation, or initialization code. All of this is provided by the Ribbon control based on the markup information in the BML file, which is passed to the control when it is first initialized. The commands on the Ribbon control generate command messages, so you have to write code to handle these command messages. To do this you create a COM object called a command handler object.

The XAML for the presentation is made up of two sections. One section defines the command names including a name for use in the XAML and a unique ID used to identify the command in the code. The command section also allows you to define command specific properties like a label, a tooltip, or image. The other section provides information about the controls that generate the command messages. More than one control can generate a command, and the practice of defining the commands separate from the controls means that all controls that generate a command will have the same label, tooltip, image, and so on. This is illustrated in Figure 2 where you can see that the Save menu item displays a tooltip, a label, and a large icon. The Save item on the Quick Access Toolbar displays a small icon and no label on the toolbar, however, the toolbar shows the same tooltip as the menu item and the same label is used on the quick access toolbar customize menu, indicating that the two items are associated with the same command.

Figure 2 Illustrating command properties shown by Ribbon controls

Ff963530.c26f2a0a-265c-4d5d-9966-94d07755930d-thumb(en-us,MSDN.10).png

Controls are not created on their own, instead they are hosted in a container called a view and they may be grouped together. The Ribbon control API supports two types of view: Ribbon View and ContextPopup View. The Ribbon View contains the application menu, tabs, and the Quick Access Toolbar; the ContextPopup View supports context menus and mini toolbars. These containers can have individual controls or have groups of controls. Grouping together controls helps the user by categorizing controls that perform similar tasks, and it helps the Ribbon control adaptive layout as shown in Figure 1.

Each control will have properties that can be accessed at runtime and define the behavior of the control. Some of these properties can be set in the markup XAML code. For example the Button control has properties for the label, tooltip, and icon that are provided by the command associated with the button. The Ribbon control framework provides access to property values through code.

Adding A Ribbon Resource

A Windows Ribbon control must be initialized with presentation information provided by a BML resource bound to the executable. The BML resource is compiled from markup code provided as XAML.

Writing the Markup File

The first step in creating the Ribbon control presentation is to write the XAML code. Listing 1 shows the basic format of the source XAML file. The <Application> element has two child elements, <Application.Commands> and <Application.Views>, the names of these elements indicate that they are XAML property elements which means that these child elements are actually treated as properties of the <Application> element, rather than child elements. Any attribute of a XAML element can be provided by using property elements, but they are usually used to provide complex objects to a property, and in this case <Application.Commands> provides a collection of **<Command>**elements and <Application.Views> provides a collection of one or more of the view elements.

Listing 1 Basic XAML for a Ribbon control

<Application xmlns="https://schemas.microsoft.com/windows/2009/Ribbon">
   <Application.Commands>
   </Application.Commands>
   <Application.Views>
   </Application.Views>
</Application>

To be useful each command element must have a name and symbol. The name is a string used by other XAML code to refer to the command, and the symbol is an identifier that code will use to identify the command. For example, Listing 2 is an extract from the markup code for Hilo Annotator. This markup describes the command that is used by the Open application menu item. The Name attribute gives the string, openFileMenu, that is used to associate the command with the menu item control. The Symbol attribute gives the name of an integer symbol that the XAML compiler will create in a header file and your code will use this to identify the command. If you do not provide a Symbol item then the XAML compiler will generate a symbol for you. Listing 2 shows several property elements, and these provide string values. Property elements are used rather than XML attributes because the element is used to provide an Id value so that the string will be placed in a string table resource as part of the executable.

Listing 2 Defining a command

<Command Name="openFileMenu" Symbol="ID_FILE_OPEN" Comment="Open">
   <Command.LabelTitle>
      <String Id ="520">Open</String>
   </Command.LabelTitle>
   <Command.LargeImages>
      <Image Id ="521">res/open_32.bmp</Image>
   </Command.LargeImages>
   <Command.SmallImages>
      <Image Id="522">res/open_16.bmp</Image>
   </Command.SmallImages>
</Command>

The <Application.Views> element is mandatory and you use this to provide one or more of the view elements. Listing 3 shows an extract from the markup for Hilo Annotator. Annotator only provides the <Ribbon> element for the <Application.Views> property. The Ribbon control provides definitions for the main application menu through the <Ribbon.ApplicationMenu> property element, a Quick Access Toolbar provided through the <Ribbon.QuickAccessToolbar> element, and information about the Ribbon control tabs through the <Ribbon.Tabs> element.

Listing 3 Illustrating the Views property

<Ribbon>
   <Ribbon.ApplicationMenu>
      <ApplicationMenu CommandName="fileMenu">
         <MenuGroup>
            <Button CommandName="openFileMenu" />
            <Button CommandName="saveFileMenu" />
            <Button CommandName="saveAsFileMenu" />
         </MenuGroup>
         <MenuGroup>
            <Button CommandName="exitMenu" />
         </MenuGroup>
      </ApplicationMenu>
   </Ribbon.ApplicationMenu>
   <Ribbon.Tabs>
      <!-- code omitted -->
   </Ribbon.Tabs>
   <Ribbon.QuickAccessToolbar >
      <!-- code omitted -->
   </Ribbon.QuickAccessToolbar >
</Ribbon>

As you can see from Listing 3, the openFileMenu command defined in Listing 2 is used as one menu item on the application menu. The markup shows two unnamed <MenuGroup> elements and in practice this will mean that the child items in these groups will be shown in the menu as if they are a single group. However, if you provide a CommandName property then the Ribbon control will display the string provided through the command object’s LabelTitle property as an annotation to the menu items

Describing the Ribbon Tabs with XAML

The most noticeable part of the Windows Ribbon control are the tabs showing controls and these are described by using the <Ribbon.Tabs> property element. This element contains one or more <Tab> elements. Hilo Annotator defines two tabs, Home and View, Listing 4 shows an excerpt from the markup that defines the View tab. This tab has a single group called Zoom, which has three buttons and the tab uses a scaling policy to indicate how the buttons will be arranged when the Ribbon control is resized.

Listing 4 Defining the Ribbon tabs

<Ribbon.Tabs>
   <Tab CommandName="homeTab">
      <!-- code omitted -->
   </Tab>
   <Tab CommandName="viewTab">
      <Tab.ScalingPolicy>
         <ScalingPolicy>
            <!-- The following list the maximum sizes of the groups -->
            <ScalingPolicy.IdealSizes>
               <Scale Group="zoomGroup" Size="Large" />
            </ScalingPolicy.IdealSizes>
            <!-- The following items give the shrink order of the groups -->
            <Scale Group="zoomGroup" Size="Medium" />
         </ScalingPolicy>
      </Tab.ScalingPolicy>
      <Group CommandName="zoomGroup" SizeDefinition="ThreeButtons">
         <Button CommandName="zoomInButton" />
         <Button CommandName="zoomOutButton" />
         <Button CommandName="previewButton" />
      </Group>
   </Tab>
</Ribbon.Tabs>

The <ScalingPolicy> element has two purposes: specifying the ideal size of each group and specifying what order the groups collapse during adaptive layout as the Ribbon control size decreases. The <ScalingPolicy.IdealSizes> element has a <Scale> element for each group on the tab and the Size attribute gives the ideal size of the group. There are four possible values for the Size attribute: Small, Medium, Large and Popup. However, the value that you can use depends on the size definition template used by the group, provided by the value of the SizeDefinition attribute. For example, the zoomGroup has a SizeDefinition value of ThreeButtons and this defines two layouts: three buttons in a row or three buttons in a column. These two layouts correspond to the Size attribute values of Large and Medium respectively. The <Scale> elements outside of the <ScalingPolicy.IdealSize> element determine the order that groups collapse to a smaller size. Since the Zoom tab only has one group there is just one element. If there were more groups then the order of the <Scale> items would determine the order of the group shrinkage.

The Ribbon control supports many different controls. Some are simple like Button and Check Box, but the Ribbon control also provides more complex controls through elements called galleries. Hilo Annotator uses a DropDownGallery control to provide the drop-down list of the pencil widths. The markup code for this control is very simple, as shown in Listing 5. This markup simply indicates that the control is shown to the user as a button and the sizeButton command gives the images that will be shown on the button.

Listing 5 Declaring the markup code for Pencil size control

<DropDownGallery CommandName="sizeButton" TextPosition="Hide" 
   Type="Items" ItemHeight="32" ItemWidth="128" HasLargeItems="true">
   <DropDownGallery.MenuLayout>
      <VerticalMenuLayout Gripper="None" />
   </DropDownGallery.MenuLayout>
</DropDownGallery>

When you click on a drop-down gallery button the gallery list is shown. The Gripper property has a value of None which indicates that the user cannot resize the gallery list since there is no gripper resizing handle. Figure 3 shows the drop-down gallery displayed when the user clicks the Size button. There is no information about the drop-down list in the markup in Listing 5 because this information is obtained at run time. The items in a DropDownGallery control are accessed through the IUICollection interface implemented by the control, during the initialization of the Ribbon control the application’s command handler object accesses the IUICollection interface and uses it to add the images.

Figure 3 Displaying the size gallery

Ff963530.e33cea61-b866-4dd9-bdf8-aa42ba59469f(en-us,MSDN.10).png

Binding the BML Resource to the Executable

The only way to initialize a Ribbon control with the markup information is to call the IUIFramework::LoadUI method and pass the name of a UIFILE resource bound to the executable. To do this you must compile the XAML to produce a BML file and bind this to the executable. The tool to compile the XAML to BML is called uicc.exe and is provided as part of the Windows 7 Software Development Kit (SDK). This tool produces three outputs: the BML file that contains the markup information, a header file that contains the symbols that identify the commands, and a resource script that describes the string tables, bitmaps and the UIFILE resource for the BML file. The following steps describe how to add the XAML markup file to a project and add a build step to use the uicc.exe tool. These steps are for Visual C++ Express 2010 and all versions of Visual Studio 2010.

To add the XAML markup file and build step to a project:

  1. Add the XML file to the project using Solution Explorer.

  2. In Solution Explorer, right-click the XML file, and then click the Properties item to show the property page. On the General page, change the Item Type item to Custom Build Tool, and click the Accept button. This will ensure that the Custom Build Tool page is shown on the property page.

  3. In the Configuration drop-down list, click All Configurations.

  4. Click the Custom Build Tool page, and in the Command Line item type the following

    uicc.exe “%(FullPath)” %(Filename).bml /header:%(Filename).h /res: %(Filename).rc

    This indicates that the uicc.exe tool is used to compile the XML file and provides names for the BML file, the header file, and resource script.

  5. Click OK, to save the values and close the property pages.

  6. Now add a line to insert the generated resource script to the project’s resource script. In Solution Explorer right-click the project’s resource script file (for example in Hilo Annotator this is called Annotator.rc) and click View Code.

  7. Scroll to the bottom of the file and add a line like the following where RibbonRes.rc is the name of the resource script created by uicc.exe

    #include "RibbonRes.rc"

  8. Press Ctrl+S to save the resource script.

After these steps when you build the project the XML file will be compiled and the resource bound to the executable.

When you build the solution, if you get an error that uicc.exe is not a command (this will happen if you run Visual C++ 2010, a 32-bit application, on an Windows 7 x64-based system) then it means that you need to add the path to the SDK tools to the project’s properties.

To add the path to the SDK tools to the project properties:

  1. In Solution Explorer right-click the project, and then click Properties.
  2. In the Configuration category, click VC++ Directories.
  3. Edit the Executable Directories item to add the path to the Bin folder of the Windows 7 SDK.

Using the Ribbon Control

To use the Ribbon control in your application you must create it through COM, and initialize the user interface with the BML file and provide a command handler object to handle the command messages generated when the user interacts with the controls on the Ribbon control. The header for the Ribbon control is uiribbon.h. This file defines the interfaces, control property keys, and the Class Id (CLSID) needed to create and use the Ribbon control.

Initializing the Ribbon

The Ribbon control has to be activated with a call to CoCreateInstance and in Hilo Annotator this occurs in the AnnotatorApplication::InitializeRibbonFramework method, which is called when the Annotator window is first created. The relevant lines are shown in Listing 6 which returns a pointer to the IUIFramework interface. This interface gives access to the core functionality of the Ribbon control.

Listing 6 initializing the Ribbon control

HRESULT hr = CoCreateInstance(
   CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER,
   IID_PPV_ARGS(&m_ribbonFramework));

HWND hWnd = nullptr;
if (SUCCEEDED(hr))
{ 
   // window is the main application window
   hr = window->GetWindowHandle(&hWnd);
}

if (SUCCEEDED(hr))
{
   // Initilize the ribbon framework
   hr = m_ribbonFramework->Initialize(hWnd, this);
}

if (SUCCEEDED(hr))
{
   static const int MaxResStringLength = 100;
   wchar_t ribbonMarkup[MaxResStringLength] = {0};
   // Obtain the name of the BML resource
   ::LoadString(
      HINST_THISCOMPONENT, IDS_RIBBON_MARKUP, 
      ribbonMarkup, MaxResStringLength);

   hr = m_ribbonFramework->LoadUI(GetModuleHandle(NULL), ribbonMarkup);
}

After creating the Ribbon object the code in Listing 6 obtains the handle for the main application window and passes this to the Ribbon control by calling the IUIFramework::Initialize method. The Ribbon control is not an ActiveX control so it does not need the application to implement control site interfaces. Instead the control is created as a child window of the window whose handle you pass as the first parameter. The second parameter, Initialize is a pointer to an IUIApplication interface implemented by the application the this pointer is passed because the AnnotatorApplication class implements this interface. Finally the InitializeRibbonFramework method calls IUIFramework::LoadUI to provide information about the presentation of the Ribbon control. This method will only load a BML file bound as a resource to an executable (an EXE or DLL), so you have to provide the instance handle of the executable and the name of the BML file that has been bound as a UIFILE resource. The default name of this resource is APPLICATION_RIBBON and since Hilo Annotator stores this in the application’s string table the InitializeRibbonFramework method calls the LoadString Windows 7 API to load this string.

The IUIApplication interface is essentially the site interface of the application which the Ribbon control uses to initialize the handshake mechanism between the application and the control. There are three methods on this interface, the IUIApplication::OnCreateUICommand method is called for every command when the Ribbon control is first created, the IUIApplication::OnDestroyUICommand method is called for every command when the Ribbon control is destroyed, and the IUIApplication::OnViewChanged method is called when the state of one of the view objects changes.

In Hilo Annotator the implementation of OnViewChanged is used to obtain the height of the Ribbon control by calling a method called GetRibbonHeight, as shown in Listing 7. This code illustrates how you can obtain the various views on the Ribbon control (the Ribbon or a ContextPopup object).

Listing 7 Obtaining the Ribbon control height when the view changes

unsigned int AnnotatorApplication::GetRibbonHeight()
{
   unsigned int ribbonHeight = 0;

   if (m_ribbonFramework)
   {
      ComPtr<IUIRibbon> ribbon;
      HRESULT hr = m_ribbonFramework->GetView(0, IID_PPV_ARGS(&ribbon));

      if (SUCCEEDED(hr))
      {
         hr = ribbon->GetHeight(&ribbonHeight);

         if (FAILED(hr))
         {
            ribbonHeight = 0;
         }
      }
   }

   return ribbonHeight;
}

When the Ribbon control is created it calls the application’s OnCreateUICommand method for each command mentioned in the BML file, requesting a command handler object to handle command messages from the command. Through this method, the Ribbon control passes the ID of the command as specified in the markup, and an ID that indicates the type of the command. The final parameter of this method is an out parameter which the application uses to return an interface pointer to the command handler object. Hilo Annotator has a class called UICommandHandler that implements the IUICommandHandler interface. Annotator creates just one instance of this class when it first starts and returns this as the command handler object for every command, as shown in Listing 8.

Listing 8 Returning the command handler object

HRESULT AnnotatorApplication::OnCreateUICommand(
   unsigned int, UI_COMMANDTYPE, IUICommandHandler** commandHandler)
{
   // The ribbon uses only one command handler
   return m_commandHandler->QueryInterface(IID_PPV_ARGS(commandHandler));
}

The majority of the communication between the application and the Ribbon control object occurs through the command object.

Providing the Command Object

The command object implements the IUICommandHandler interface, which has just two methods, Execute and UpdateProperty. When a user interacts with any control on the Ribbon control a command message is generated and the Ribbon control informs the application by calling the IUICommandHandler::Execute method shown in Listing 9.

Listing 9 The IUICommandHandler::Execute method

HRESULT Execute(
    UINT32 commandId,
    UI_EXECUTIONVERB verb,
    const PROPERTYKEY *key,
    const PROPVARIANT *currentValue,
    IUISimplePropertySet *commandExecutionProperties
);

The first two parameters of this method are identifiers indicating the command and the action that has occurred. The commandId parameter will be one of the symbols in the header file created by the uuic.exe tool and it will be either a symbol that you specified in the markup, or if you did not specify then the uicc.exe will create a symbol for you. The verb parameter indicates the action of the user and can indicate that the command was executed, or that the control was previewed (the mouse is hovered over a control), or a preview was cancelled. Clearly a value of UI_EXECUTIONVERB_EXECUTE, indicating that the command has executed, is the most important.

Commands can be quite complex since a control can be composed of several controls, each of which will have values. These values are the control properties. The Execute method indicates the property that is the subject of the command through the key parameter and the value of the property through currentValue. The property identifier is a pointer to a PROPERTYKEY structure, and the values in the structure identify the type of the property and a unique identifier. You do not need to know the values used, all you need to know is the symbol. The uiribbon.h header file defines the properties that are used by the Ribbon control, so for example, the UI_PKEY_ENABLED symbol is used to identify the enabled property and indicates that this property is a Boolean.

The actual value of the property is accessed through the currentValue parameter which is a pointer to a VARIANT. The final parameter of the Execute method is a pointer to a IUISimplePropertySet interface that you can use to access other properties of the command.

Hilo Annotator provides an implementation of IUICommandHandler interface through the class UICommandHandler. The implementation in Annotator, UICommandHandler::Execute, has a large switch statement so that it can provide different code for each command and much of the code delegates the code to other objects in the application. For example, Listing 10 shows part of this switch statement, where the m_imageEditor variable is a pointer to the object that provides the code for the image editor child window in the lower half of the Annotator window. You will recognise the ID_FILE_OPEN symbol from Listing 2 where it is declared as the symbol for the openFileMenu command.

Listing 10 Handling command messages

HRESULT hr = S_OK;

switch (commandId)
{
case ID_FILE_OPEN:
   {
      hr = m_imageEditor->OpenFile();
      break;
   }
case ID_FILE_SAVE:
   {
      m_imageEditor->SaveFiles();
      break;
   }
case ID_FILE_SAVE_AS:
   {
      m_imageEditor->SaveFileAs();
      break;
   }
case ID_FILE_EXIT:
   {
      m_imageEditor->SaveFiles();
      ::PostQuitMessage(0);
      break;
   }
// other code omitted
}

Accessing Command Properties

The controls that you can use on a Ribbon control are documented in the MSDN Library Each page lists the property keys that the control supports. The documentation describes how some of the properties are accessed through calls to the Ribbon control’s IUIFramework interface (the IUIFramework::GetUICommandProperty and IUIFramework::SetUICommandProperty methods). However, most of the properties are accessible through a process that the documentation calls invalidation.

Invalidation means that the application tells the Ribbon control that a control property is invalid and then the Ribbon control calls the application to request the new value. In Hilo Annotator invalidation occurs in the image editor code whenever the Image Editor window is redrawn or if a command is executed. Listing 11 shows this code where the m_framework variable is the IUIFramework interface on the Ribbon control.

Listing 11 Invalidating control properties

HRESULT ImageEditorHandler::UpdateUIFramework()
{
   // After we're done drawing make sure to update framework buttons as necessary
   if (m_framework)
   {
      m_framework->InvalidateUICommand(
         ID_BUTTON_UNDO, UI_INVALIDATIONS_STATE, nullptr);
      m_framework->InvalidateUICommand(
         ID_BUTTON_REDO, UI_INVALIDATIONS_STATE, nullptr);
      m_framework->InvalidateUICommand(
         ID_FILE_SAVE, UI_INVALIDATIONS_STATE, nullptr);

      m_framework->InvalidateUICommand(
         pencilButton, UI_INVALIDATIONS_VALUE, nullptr);
      m_framework->InvalidateUICommand(
         cropButton, UI_INVALIDATIONS_VALUE, nullptr);
    }

    return S_OK;
}

The IUIFramework::InvalidateUICommand method can be used to invalidate just one property of a command, or all properties of a particular type. The first parameter of this method is an identifier indicating the command to update, the second parameter indicates the type of data to update (a UI_INVALIDATIONS value), and the final parameter is the ID of a property if you wish to update a specific property. In Listing 11 the state is invalidated for the Undo, Redo, and Save buttons so that the Ribbon control requests that the application returns the enabled state (UI_PKEY_Enabled) of the Undo and Save buttons.

The pencilButton and cropButton controls are Toggle Buttoncontrols and in Hilo Annotator only one of them can be pushed in. This pushed-in state is a Boolean value so rather than requesting the change in property state, the call to the InvalidateUICommand method for these two controls requests that the values of the control properties are invalidated, and the application will check for, and return a value for the UI_PKEY_BooleanValue of these controls.

Once you have invalidated a command, the Ribbon control cannot display the associated control’s state, so it must ask the application for the property values by calling the IUICommandHandler::UpdateProperty method. This method is called with four parameters, the first three indicate the command, the property, and the current value of the property.The fourth is an out parameter for the new value of the property. In Hilo Annotator this method is implemented in the UICommandHandler class and has two purposes. The first is to return the state and value properties invalidated by the UpdateUIFramework method (Listing 11)1 The second purpose is to populate the DropDownGallery control for the pencil widths with appropriate images.

Listing 5 shows that the markup for the Gallery control does not provide the values to be shown by the control. When the control is initialized, the Ribbon control calls the UpdateProperty method to update the UI_PKEY_ItemsSource property. The UICommandHandler class implementation of this method accesses the IUICollection interface on this property and adds the images to be shown by the Gallery control.

Conclusion

In this chapter you learned how Hilo Annotator uses a Windows Ribbon control to give easy access to the image editor tools. In the next article you will learn how to use the Windows 7 Imaging Component to load and convert images.

Next | Previous | Home