Ribbon with C++, Part 5: Using Galleries with Windows Ribbon Framework

16 ביוני 2011

תגיות: , , , ,
אין תגובות

This is the 5th post about Windows Ribbon Framework features.
On previous posts we have introduced the ribbon framework, seen a complete example, and reviewed how to use buttons-based UI controls and how to control their layout.

In this post we continue our exploration of the Windows Ribbon Framework, this time focusing on Gallery controls.

We will learn what galleries are and what is the difference between an item gallery and a command gallery. We will learn about the different gallery controls: ComboBox, DropDownGallery, SplitButtonGallery and InRibbonGallery.
Of course we will see how to set gallery items and how to respond to a user choice.

What is a gallery?

A gallery is a ribbon UI control which contains a list of elements. The elements in the list are represented by a text caption or an image, and can be organized by categories.

image

Figure 1: An example of a gallery

The galleries that we will soon review comes in two flavors: item gallery and command gallery.

Item Gallery vs. Command Gallery

In this section we will learn about the differences between an item gallery and a command gallery. There are three differences between the two types of galleries:

The first difference relates to what the gallery elements are.
In the item gallery, every element is a "simple" element and is characterized by a text and an image.
On the other hand, an element in a command gallery is literally a command. This means it has a command ID, which we can handle (as seen in previous posts), and has all the attached resources that a command can have.
You may think of it as a list of images vs. a list of buttons.

The second difference is that an item gallery supports the concept of a "selected item".
Every element in an item gallery has an index and there is a property on the gallery which we can check for getting the selected item index.
On the other hand, the commands in the command gallery are not indexed and thus the gallery doesn't support the concept of a selected item.

Finally, item galleries support "live preview". This means you get a notification when the user is hovering over an item (before actual selection); the notification is received via the IUICommandHandler::Execute method, more on this later. This allows application developers to implement a preview feature where the user can see the result of selecting an item before he actually selects it.
The Styles control in Microsoft Word 2007 / 2010 is a great example for an item gallery with live preview. Hovering over different styles shows immediately what the effect will be.

image

Figure 2: Styles item gallery in Word 2010

Command galleries doesn't support preview. Every element in a command gallery can only be clicked.

Gallery Controls

ComboBox

A ribbon ComboBox control is basically the normal ComboBox control that we all know and love, but with the added feature of dividing the items into categories. A category is not an item and cannot be selected from the ComboBox. It is only used to organize the items.
Categories will be discussed further in this post.

image

Figure 3: A ComboBox with categories

A ribbon ComboBox is considered a gallery, although it has one limitation that the other gallery types don't have: It can only be used as an item gallery. This means that the ComboBox items are always "text / image" items and cannot be, for example, buttons.

Defining ComboBox in markup

Following is an example of defining a ComboBox in ribbon markup:

<Application xmlns="http://schemas.microsoft.com/windows/2009/Ribbon">
    <Application.Commands>
        
        <Command Name="cmdLayoutGroup"
                 LabelTitle="Border" />
        <Command Name="cmdLayouts"
                 Symbol="IDR_CMD_LAYOUTS"
                 LabelTitle="Grid Size" />
        
    </Application.Commands>

    <Application.Views>
        <Ribbon>
            <Ribbon.Tabs>
                <Tab CommandName="cmdTabHome">
                    
                    <Group CommandName="cmdLayoutGroup">
                        <ComboBox CommandName="cmdLayouts"
                                  IsEditable="false"
                                  IsAutoCompleteEnabled="false"
                                  ResizeType="VerticalResize" />
                    </Group>
                </Tab>
            </Ribbon.Tabs>
        </Ribbon>
    </Application.Views>
</Application>

As you can see, all it takes is a simple ComboBox markup element, attached to a command.

The ComboBox XML attributes are as follows:

  • CommandName – The name of the command attached to this ComboBox.
  • IsAutoCompleteEnabled – A flag that indicated whether the ComboBox control should automatically complete the word as you write.
  • IsEditable – A flag that indicates whether to allow free text in the ComboBox.
  • ResizeType – Indicates the allowed type of resizing for the ComboBox. Can be either NoResize or VerticalResize.
    Setting ComboBox items

    As you may recall from previous posts, every command has an implementation of IUICommandHandler attached to it.
    Setting the ComboBox items is done by implementing the IUICommandHandler::UpdateProperty function, specifically, handling the case when the property key is UI_PKEY_ItemsSource. You can find more information on how to implement IUICommandHandler::UpdateProperty in part 3 of this series.

    The type of the property ItemsSource is a collection (IUICollection) of IUISimplePropertySet.
    The interface IUICollection represents a simple collection and has all the common collection functions, such as: Add, Insert, RemoveAt, GetCount, etc.
    The interface IUISimplePropertySet represents, as his name suggests, a set of properties. It has only a single function, GetValue, which should return a property value, given the property identifier.
    Both IUICollection and IUISimplePropertySet are defined in the UIRibbon.h, which comes with the Windows 7 SDK.

    Every element in the ComboBox or more generally, in an item gallery, has the following properties:

    • UI_PKEY_Label – Represents the label of the item.
    • UI_PKEY_ItemImage – Represents the item image.
    • UI_PKEY_CategoryId – Represents the category id of which this item belongs.

    Note that an element in a command gallery has different properties. We will review those properties later in this post.

    Following is an example of how to fill the ItemsSource property inside the command handler of a ComboBox. Error handling has been omitted for brevity.

    STDMETHODIMP CLayoutHandler::UpdateProperty(UINT nCmdID,
                                                REFPROPERTYKEY key,
                                                const PROPVARIANT* ppropvarCurrentValue,
                                                PROPVARIANT* ppropvarNewValue)
    {
        HRESULT hr = S_FALSE;

        if (key == UI_PKEY_ItemsSource)
        {
            IUICollection* pCollection;
            hr = ppropvarCurrentValue->punkVal->QueryInterface(
                                                          IID_PPV_ARGS(&pCollection));
            
            int labelIds[] = {IDS_LAYOUT_1, IDS_LAYOUT_2, IDS_LAYOUT_3};

            // Populate the combobox with the three layout options.
            for (int i=0; i<_countof(labelIds); i++)
            {
                // Create a new property set for each item.
                CPropertySet* pItem;
                hr = CPropertySet::CreateInstance(&pItem);
                  
                // Load the label from the resource file.
                WCHAR wszLabel[MAX_RESOURCE_LENGTH];
                LoadString(GetModuleHandle(NULL), labelIds[i], wszLabel,
                                                                 MAX_RESOURCE_LENGTH);

                // Initialize the property set with no image, the label that was just
                // loaded, and no category.
                pItem->InitializeItemProperties(
                                          NULL, wszLabel, UI_COLLECTION_INVALIDINDEX);

                pCollection->Add(pItem);
            }
            pCollection->Release();
            hr = S_OK;
        }
        
        return hr;
    }

    In this example we add three items to the ItemsSource property of the ComboBox. This is done by creating an object that implements IUISimplePropertySet, setting its properties (we set only the label, which is loaded from the resource file), and adding it to the collection.

    The CPropertySet class is a helper class that implements IUISimplePropertySet and exposes convenient initialization functions. Its full source is available with the sample application attached to this post.

    The end result for this ComboBox looks like this:

    image

    Figure 4: The ComboBox created by the previous markup and code

    Handling User Selection

    In this section we will see how to respond to a selection made by the interactive user.
    We will also handle the case where we have an editable ComboBox (IsEditable = "true"), in which case the user might enter a value which is not part of the ComboBox values.

    Handling the "Selected Change" event is done by implementing IUICommandHandler::Execute, when the verb parameter has the value UI_EXECUTIONVERB_EXECUTE and the key parameter has the value UI_PKEY_SelectedItem.

    In the following example we see code that handles the user selection.

    STDMETHODIMP CBorderSizeHandler::Execute(UINT nCmdID,
                       UI_EXECUTIONVERB verb,
                       const PROPERTYKEY* key,
                       const PROPVARIANT* ppropvarValue,
                       IUISimplePropertySet* pCommandExecutionProperties)
    {
        HRESULT hr = E_FAIL;
        if (verb == UI_EXECUTIONVERB_EXECUTE)
        {
            if (key && *key == UI_PKEY_SelectedItem)
            {
                ULONG newSize;
                UINT selected = ppropvarValue->uintVal;
                switch (selected)
                {
                case 0:
                    newSize = 1;
                    break;
                case 1:
                    newSize = 3;
                    break;
                case 2:
                    newSize = 5;
                    break;
                // The new selection is a value that the user typed.
                case UI_COLLECTION_INVALIDINDEX:

                    if (pCommandExecutionProperties != NULL)
                    {
                        PROPVARIANT var;
                        // The text entered by the user is contained in the property set
                        // with the pkey UI_PKEY_Label.
                        pCommandExecutionProperties->GetValue(UI_PKEY_Label, &var);
                        
                        // Convert string to int
                        BSTR bstr = var.bstrVal;
                        hr = VarUI4FromStr(bstr,GetUserDefaultLCID(),0,&newSize);
                        
                        // Validate new size
                        if (FAILED(hr) || newSize < 1 || newSize > 15)
                        {
                            // report invalid size
                            …
                        }
                        PropVariantClear(&var);
                    }
                    break;
                }
                // Do something with newSize
                …
            }
        }
        return hr;
    }

    First we check that the correct verb (Execute) and property key (SelectedItem) is handled. Then we use ppropvarValue->uintVal to extract the index of the selected item. According to the index we can know what the selected value is. In case the index is UI_COLLECTION_INVALIDINDEX, which is just a fancy way of saying -1, we can safely assume that the user entered the text manually, in which case we get the label property and parse it.

    image

    Figure 5: An editable ComboBox with custom user text

    DropDownGallery and SplitButtonGallery

    A DropDownGallery is just a button that a click on it displays a list of items. The button itself has no action.

    On the other hand, a SplitButtonGallery has two buttons, one that is used as a default action and another one that opens a list of items.

    This difference between these two galleries is similar to the difference between the SplitButton control and DropDownButton control, seen on part 3 (Add link to post 3) of this post series.

    In the following image, on the right, you can see a SplitButtonGallery which is composed of a split button, on the top part (the image) there is a button and on the lower part (the "Style" text) there is an arrow that opens the list of items.

    image

    Figure 6: On the left: a DropDownGallery with categories, On the right: a SplitButtonGallery

    Defining SplitButtonGallery in markup

    In the following markup code we see how to define a SplitButtonGallery. The code should be placed in the ribbon markup "views" section.

    <SplitButtonGallery CommandName="cmdBorderStyles"
                        Type="Items"
                        TextPosition="Hide"
                        HasLargeItems="false">
        <SplitButtonGallery.MenuLayout>
            <VerticalMenuLayout />
        </SplitButtonGallery.MenuLayout>
    </SplitButtonGallery>

    The SplitButtonGallery XML attributes are as follows:

    • CommandName – The name of the command attached to this SplitButtonGallery.
    • Type – The type of the gallery. It can be either Items or Commands.
    • TextPosition – The position where to show the attached text. This can be one the following values: Top, Left, Right, Bottom, Overlap and Hide.
    • HasLargeItems – Indicates whether the large or small image resource of the Command is displayed.

    The MenuLayout attribute defines how the list of elements will present itself.
    VerticalMenuLayout is used to show a vertical layout.
    Another option is FlowMenuLayout, which we will review on the next code example.

    Setting SplitButtonGallery items with images

    Since in this example the SplitButtonGallery is used as an item gallery, adding items is done similarly to how it was done in the previous ComboBox sample. However, we will also see how we can provide image resources to the items. As always, error handling has been omitted.

    STDMETHODIMP CBorderStyleHandler::UpdateProperty(UINT nCmdID,
                                                REFPROPERTYKEY key,
                                                const PROPVARIANT* ppropvarCurrentValue,
                                                PROPVARIANT* ppropvarNewValue)
    {
        HRESULT hr = S_FALSE;

        if (key == UI_PKEY_ItemsSource)
        {
            IUICollection* pCollection;
            hr = ppropvarCurrentValue->punkVal->QueryInterface(
                                                              IID_PPV_ARGS(&pCollection));

            int imageIds[] = {IDB_NONE, IDB_SOLID, IDB_DASH};
            int labelIds[] = {IDS_BORDER_NONE, IDS_BORDER_SOLID, IDS_BORDER_DASH};

            // Populate the dropdown with the three border styles.
            for (int i=0; i<_countof(labelIds); i++)
            {
                // Create a new property set for each item.
                CPropertySet* pItem;
                hr = CPropertySet::CreateInstance(&pItem);

                // Create an IUIImage from a resource id.
                IUIImage* pImg;
                CreateUIImageFromBitmapResource(MAKEINTRESOURCE(imageIds[i]), &pImg);

                // Load the label from the resource file.
                WCHAR wszLabel[MAX_RESOURCE_LENGTH];
                LoadString(GetModuleHandle(NULL), labelIds[i], wszLabel,
                                                                     MAX_RESOURCE_LENGTH);

                // Initialize the property set with the image and label that were just
                // loaded and no category.
                pItem->InitializeItemProperties(pImg, wszLabel,
                                                              UI_COLLECTION_INVALIDINDEX);

                // Add the newly-created property set to the collection
                // supplied by the framework.
                pCollection->Add(pItem);

                pItem->Release();
                pImg->Release();
            }
            pCollection->Release();
            hr = S_OK;
        }
        
        return hr;
    }
    // Factory method to create IUIImages from resource identifiers.
    void CBorderStyleHandler::CreateUIImageFromBitmapResource(LPCTSTR pszResource,
                                                                 IUIImage **ppimg)
    {
        IUIImageFromBitmap* m_pifbFactory;
        
        CoCreateInstance(CLSID_UIRibbonImageFromBitmapFactory,
                         NULL, CLSCTX_ALL, IID_PPV_ARGS(&m_pifbFactory));
        
        // Load the bitmap from the resource file.
        HBITMAP hbm = (HBITMAP) LoadImage(GetModuleHandle(NULL), pszResource,
                                          IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
        
        // Use the factory implemented by the framework to produce an IUIImage.
        m_pifbFactory->CreateImage(hbm, UI_OWNERSHIP_TRANSFER, ppimg);
    }

    The important thing to note is the helper function CreateUIImageFromBitmapResource. This function loads a bitmap resource and converts it to an instance of IUIImage, which is the ribbon wrapper for a bitmap. The conversion is done using a COM object provided by the Windows Ribbon Framework, named UIRibbonImageFromBitmapFactory. This object implements the interface IUIImageFromBitmap which has only one function, CreateImage.

    The CreateImage function takes an HBITMAP handle as its first parameter and returns a pointer to IUIImage. The second parameter controls the ownership of the created image, and take the value UI_OWNERSHIP_TRANSFER in case you want to transfer the responsibility of releasing the handle to the ribbon framework, or UI_OWNERSHIP_COPY in case you plan to reuse the same HBITMAP from other places in your application, and so need to release it only when you are done.

    Defining DropDownGallery in markup

    In the following markup code we see how to define a DropDownGallery. The code should be placed in the ribbon markup "views" section.

    <DropDownGallery CommandName="cmdSizeAndColor"
                     TextPosition="Hide"
                     Type="Commands">
        <DropDownGallery.MenuLayout>
            <FlowMenuLayout Rows="2"
                            Columns="3"
                            Gripper="None" />
        </DropDownGallery.MenuLayout>
    </DropDownGallery>

    Note that this time we used "Commands" as the type of the gallery. This means that this particular instance of DropDownGallery will be used as a command gallery.
    Most of the DropDownGallery XML attributes are similar to the attributes of SplitButtonGallery, so I'll only explain the FlowMenuLayout.

    The FlowMenuLayout XML element is used to define a rich layout for the gallery items. We use the Rows and Columns attributes to control the number of rows and columns in the gallery rectangle. On figure 6, the left control is the result of this definition. Note that the categories seen in the image will be defined later in the code.

    Before we continue, I owe you from a previous section the available properties for an element in a command gallery.
    Every element in a command gallery has the following properties:

    • UI_PKEY_CommandId – Represents the command id of the element.
    • UI_PKEY_CommandType – Represents the type the command.
    • UI_PKEY_CategoryId – Represents the category id of which this item belongs.
    Setting DropDownGallery commands items with categories

    In the following code we see how to fill the previously defined DropDownGallery. This time we fill it with commands elements. In addition we will divide the commands into two categories.

    STDMETHODIMP CSizeAndColorHandler::UpdateProperty(UINT nCmdID,
                                  REFPROPERTYKEY key,
                                  const PROPVARIANT* ppropvarCurrentValue,
                                  PROPVARIANT* ppropvarNewValue)
    {
        HRESULT hr = E_FAIL;

        if (key == UI_PKEY_Categories)
        {
            IUICollection* pCollection;
            hr = ppropvarCurrentValue->punkVal->QueryInterface(
                                                              IID_PPV_ARGS(&pCollection));

            // Create a property set for the Size category.
            CPropertySet *pSize;
            hr = CPropertySet::CreateInstance(&pSize);

            // Load the label for the Size category from the resource file.
            WCHAR wszSizeLabel[MAX_RESOURCE_LENGTH];
            LoadString(GetModuleHandle(NULL), IDS_SIZE_CATEGORY,
                                                       wszSizeLabel, MAX_RESOURCE_LENGTH);

            // Initialize the property set with the label that was just loaded and
            // a category id of 0.
            pSize->InitializeCategoryProperties(wszSizeLabel, 0);

            // Add the newly-created property set to the collection supplied
            // by the framework.
            pCollection->Add(pSize);

            pSize->Release();

            // Create a property set for the Color category.
            CPropertySet *pColor;
            hr = CPropertySet::CreateInstance(&pColor);

            // Load the label for the Color category from the resource file.
            WCHAR wszColorLabel[MAX_RESOURCE_LENGTH];
            LoadString(GetModuleHandle(NULL), IDS_COLOR_CATEGORY,
                                                      wszColorLabel, MAX_RESOURCE_LENGTH);

            // Initialize the property set with the label that was just loaded and
            // a category id of 1.
            pColor->InitializeCategoryProperties(wszColorLabel, 1);
            
            // Add the newly-created property set to the collection supplied
            // by the framework.
            pCollection->Add(pColor);

            pColor->Release();
            pCollection->Release();

            hr = S_OK;
        }
        else if (key == UI_PKEY_ItemsSource)
        {
            IUICollection* pCollection;
            hr = ppropvarCurrentValue->punkVal->QueryInterface(
                                                              IID_PPV_ARGS(&pCollection));

            int commandIds[] = {IDR_CMD_SMALL, IDR_CMD_MEDIUM, IDR_CMD_LARGE,
                                IDR_CMD_RED, IDR_CMD_GREEN, IDR_CMD_BLUE};
            int categoryIds[] = {0, 0, 0, 1, 1, 1};

            // Populate the gallery with the three size and three colors in
            // two separate categories.
            for (int i=0; i<_countof(commandIds); i++)
            {
                // Create a new property set for each item.
                CPropertySet* pCommand;
                hr = CPropertySet::CreateInstance(&pCommand);

                // Initialize the property set with the appropriate command id and
                // category id and type Boolean (which makes these appear as
                // ToggleButtons).
                pCommand->InitializeCommandProperties(categoryIds[i], commandIds[i],
                                                                  UI_COMMANDTYPE_BOOLEAN);

                // Add the newly-created property set to the collection supplied
                // by the framework.
                pCollection->Add(pCommand);

                pCommand->Release();
            }
            pCollection->Release();
            hr = S_OK;
        }
        return hr;
    }

    In this code we first define Categories property, which contains the list of available categories (in our case, just two items), and then the ItemsSource property.

    The items elements are now commands which have a command id and command type. In our case we used UI_COMMANDTYPE_BOOLEAN which makes the commands appear as ToggleButton controls. Note that we didn't define any image resource for the elements. The image comes from the previously define command resources.

    Also, check the implementation of the gallery's Execute function:

    STDMETHODIMP CSizeAndColorHandler::Execute(UINT nCmdID,
                       UI_EXECUTIONVERB verb,
                       const PROPERTYKEY* key,
                       const PROPVARIANT* ppropvarValue,
                       IUISimplePropertySet* pCommandExecutionProperties)
    {
        return E_FAIL;
    }

    The implementation is rather empty since the handling of the commands is done by the command handler attached to the elements command id. The gallery's Execute function is never actually called.

    InRibbonGallery

    An InRibbonGallery is a gallery in which the items are displayed inside the ribbon. Other than its visual representation, it's functionally equivalent to the DropDownGallery.

     

    image

    Figure 7: An example of InRibbonGallery control

    Defining InRibbonGallery in markup

    In the following markup code we see how to define an InRibbonGallery. As always, view related code should be placed in the ribbon markup "views" section.

     

    <InRibbonGallery CommandName="cmdShapes"
                     Type="Items"
                     TextPosition="Bottom"
                     MaxColumns="3"
                     MaxRows="1" />

    All of these XML attributes are already known or self-explanatory.

    Handling live preview in an item gallery

    One of the benefits of an item gallery is its support for the live preview feature, i.e. it allows the user to see the result of a selection before just by hovering over an item. If the user doesn't select an item and move the mouse cursor away, the selection effect is removed.

    In the following code we see how to implement IUICommandHandler::Execute in order to handle the live preview feature:

    STDMETHODIMP CShapeHandler::Execute(UINT nCmdID,
                       UI_EXECUTIONVERB verb,
                       const PROPERTYKEY* key,
                       const PROPVARIANT* ppropvarValue,
                       IUISimplePropertySet* pCommandExecutionProperties)
    {
        HRESULT hr = E_FAIL;

        UINT selected;
        hr = UIPropertyToUInt32(*key, *ppropvarValue, &selected);
        switch (verb)
        {
        case UI_EXECUTIONVERB_PREVIEW:
            // Show a preview of a new shape.    
            ShowItem(selected);
            hr = S_OK;
            break;

        case UI_EXECUTIONVERB_CANCELPREVIEW:
            // Show the shape that was selected before the preview –
            // ppropvarValue contains the previous selected item.
            // Note that the developer did not have to store the value from before
            // preview was called.
            ShowItem(selected);
            hr = S_OK;
            break;

        case UI_EXECUTIONVERB_EXECUTE:
            if ( key && *key == UI_PKEY_SelectedItem)
            {      
                // Update the renderer with the newly-selected shape.
                ShowItem(selected);
                hr = S_OK;
            }
        }
        return hr;
    }

    We handle three different verbs: Preview, CancelPreview and Execute.
    The Preview verb is passed when the user hovers over an item, in which case we also get the hovered item index. The CancelPreview verb is passed when a user moves the mouse cursor away from the item gallery. Note that in the CancelPreview case we get the previously selected item so the developer doesn't need to save the previous item.
    The last case is when the passed verb is Execute, which means the user has selected an item.

    The ShowItem function is in charge of doing the selection effect, for example, drawing a new shape according to the selected one.

    Summary

    The sample application attached to this post is the taken from the Windows 7 SDK "Gallery" ribbon sample. You can find it either here or in your local folder
    "\Program Files\Microsoft SDKs\Windows\v7.0\Samples\winui\WindowsRibbon\Gallery\"

     

    image

    Figure 8: Sample application for this post

    In this post we have learned how to use the ribbon gallery controls. We learned about the difference between an item gallery and a command gallery. We saw the different gallery controls: ComboBox, SplitButtonGallery, DropDownGallery and InRibbonGallery. And we saw how to add items, categories and images, and how to provide support for live preview.

    That's it for now,
    Arik Poznanski.

    הוסף תגובה
    facebook linkedin twitter email

    כתיבת תגובה

    האימייל לא יוצג באתר. (*) שדות חובה מסומנים