Compartilhar via


Making Custom Controls Accessible, Part 5: Using IAccessibleEx to Add UI Automation Support to a Custom Control

This is the fifth of five articles on making custom controls accessible. Before reading this article, please ensure that you have read and completed the steps in the Getting Started article.

A previous article in this series showed how to create a custom control that implements the IAccessible interface to enable access by Microsoft Active Automation (MSAA) clients. The control is a list box containing items that each include a value indicator similar to a progress bar. The user can manipulate the progress bar by selecting an item and using the arrow keys.

In an ideal world, all controls would fully implement interfaces for both MSAA and UI Automation. In practice, many existing controls could benefit from implementing some UI Automation functionality, but it is not practical to implement UI Automation completely.

In this article, we add accessibility features to the control by implementing the new IAccessibleEx interface, which in turn allows us to add partial support for UI Automation. By implementing UI Automation interfaces, the control can provide properties and functionality that are not available through IAccessible.

Note   The definition of IAccessibleEx in the project headers is preliminary and subject to change. The final interface definition will be published in an update to the SDK header files.

Description of the Initial Project

The initial project is a slightly revised version of the final project from Article 3. It contains a custom control that has been made accessible through an implementation of IAccessible.

Problem

Although the custom control in the initial project is accessible through its implementation of IAccessible, that interface is generic and cannot report custom properties, such as the range or step values of the progress bar.

UI Automation providers, on the other hand, can implement interfaces that are specific to different kinds of controls. In this case, because each list item displays a value within a range, the IRangeValueProvider interface provides a way to expose the range and step values.

Solution

The IAccessibleEx interface serves as a bridge between an IAccessible implementation, such as that in our initial project, and a partial implementation of UI Automation.

The control does not have to implement UI Automation fully, because it is designed for use with MSAA clients that are operating in-process rather than UI Automation clients that are communicating with controls through the mediation of UIAutomationCore.dll. The control’s IAccessibleEx and IRawElementProviderSimple interfaces provide a direct link between the control’s UI Automation functionality and the client.

The support for UI Automation consists of the following:

1.   Implementations of IRawElementProviderSimple for both the list box and the list items. This interface is primarily used to return properties not supported by IAccessible.

2.   Implementations of any control pattern interfaces that can provide functionality not available through IAccessible. In our example, the only control pattern interface implemented is IRangeValueProvider, which is implemented on list box items. It is not necessary to implement ISelectionProvider or ISelectionItemProvider, because those interfaces do not provide much useful functionality beyond what is already available through IAccessible.

Implementation

Updating the custom control requires these main steps:

  1. Implement IServiceProvider on the list box accessible object so that the IAccessibleEx interface can be found on this or a separate object.
  2. Implement IAccessibleEx on the list box accessible object.
  3. Create an accessible object for list items, which in MSAA are not objects but are accessed by their child IDs. Implement IAccessibleEx on this object.
  4. Implement IRawElementProviderSimple for the list box and list items.
  5. Implement IRangeValueProvider on the list item accessible object.

Details

Step 1: Expose IAccessibleEx

Because the implementation of IAccessibleEx for a control may reside in a separate object, client applications cannot rely on QueryInterface to obtain this interface. Instead, clients are expected to call IServiceProvider::QueryService. The implementation of this method for the example list box simply calls through to QueryInterface, because the list box accessible object itself implements IAccessibleEx. The following code shows the implementation of QueryService:

HRESULT CListboxAccessibleObject::QueryService(REFGUID guidService, REFIID riid, LPVOID *ppvObject){    if (!ppvObject)    {        return E_INVALIDARG;    }    *ppvObject = NULL;    if (guidService == __uuidof(IAccessibleEx))    {        return QueryInterface(riid, ppvObject);    }    else    {        return E_NOINTERFACE;    }};

Step 2: Implement IAccessibleEx on the List Box

The method of IAccessibleEx that is of most interest is GetObjectForChild. This method gives us an opportunity to create an accessible object (one that exposes, at a minimum, IAccessibleEx) for a list item. Remember that in our implementation of MSAA, the list items do not have their own IAccessible interface but are treated as children of the list box accessible object. The following code shows the implementation of GetObjectForChild:

HRESULT CListboxAccessibleObject::GetObjectForChild(    long idChild, IAccessibleEx **pRetVal){    int x = sizeof(long);    VARIANT vChild;    vChild.vt = VT_I4;    vChild.lVal = idChild;    HRESULT hr = ValidateChildId(vChild);    if (FAILED(hr))    {        return E_INVALIDARG;    }    // List item accessible objects are stored as an array of pointers;    // for purposes of the example it is assumed that the list contents    // will not change. Accessible objects are created only when needed.    if (itemProviders[idChild - 1] == NULL)        // Create an object that supports UI Automation and        /// IAccessibleEx for the item.    {        itemProviders[idChild - 1] =            new CListItemAccessibleObject(idChild, g_pListboxControl);        if (itemProviders[idChild - 1] == NULL)        {            return E_OUTOFMEMORY;        }    }    IAccessibleEx* pAccEx = static_cast<IAccessibleEx*>        (itemProviders[idChild - 1]);    if (pAccEx != NULL)    {        pAccEx->AddRef();    }    *pRetVal = pAccEx;    return S_OK;}

See the notes in the sample code for descriptions of the other methods of IAccessibleEx.

Step 3: Implement IAccessibleEx on the List Items

For the list item accessible object, the main role of IAccessibleEx is to provide a means of working backward from the object to the parent IAccessible, which is returned by the CListItemAccessibleObject::GetIAccessiblePair method along with the child ID used to identify the list item in an IAccessible implementation. The following code shows the implementation of GetIAccessiblePair:

HRESULT CListItemAccessibleObject::GetIAccessiblePair(    IAccessible **ppAcc, long *pidChild){    if (!ppAcc || !pidChild)    {        return E_INVALIDARG;       }    CListboxAccessibleObject* pParent = m_control->GetAccessibleObject();    HRESULT hr = QueryInterface(__uuidof(IAccessible), (void**)ppAcc);    if (FAILED(hr))    {        *pidChild = 0;        return E_NOINTERFACE;    }    *pidChild = m_childID;    return S_OK;}

Step 4: Implement IRawElementProviderSimple for the List Box and List Items

Only one method of IRawElementProviderSimple is of interest for the list box. This is GetPropertyValue, which enables the control to return properties specific to UI Automation. In the example project, a single property is supported as an illustration: a localized description of the custom control. (For simplicity, the string is hard-coded rather than taken from a resource file.)

The property identifiers used by UI Automation are GUIDs that have to be retrieved by using the UiaLookupId function, found in UIAutomationCore.lib and UIAutomationCore.h. The following code shows the implementation of GetPropertyValue:

HRESULT STDMETHODCALLTYPE CListboxAccessibleObject::GetPropertyValue(PROPERTYID propertyId,    VARIANT* pRetVal){    if (pRetVal == NULL)    {        return E_INVALIDARG;    }    HRESULT hr = CheckAlive();    if (SUCCEEDED(hr))    {        if (propertyId == AutoIds.LocalizedControlTypeProperty)        {            pRetVal->vt = VT_BSTR;            pRetVal->bstrVal = SysAllocString(L"CustomSliderList");            if (pRetVal->bstrVal == NULL)            {                pRetVal->vt = VT_EMPTY;                return E_OUTOFMEMORY;            }        }        // Else pRetVal is empty, and UI Automation will attempt        // to get the property from the HostRawElementProvider,        // which is the default provider for the HWND.        else        {            pRetVal->vt = VT_EMPTY;        }    }    return hr;}

The list item accessible object has a similar implementation of GetPropertyValue. In addition, because we want list items to be able to return pattern-specific properties, we must also implement GetPatternProvider. The implementation simply returns a reference to the IRangeValueProvider interface, or NULL if another interface is requested. The following code shows the implementation of GetPatternProvider:

HRESULT STDMETHODCALLTYPE CListItemAccessibleObject::GetPatternProvider(    PATTERNID patternId, IUnknown** pRetVal){    if (pRetVal == NULL)    {        return E_INVALIDARG;   }    if (patternId == AutoIds.RangeValuePattern)    {        *pRetVal = static_cast<IUnknown*>            (static_cast<IRangeValueProvider*>(this));        AddRef();    }    else    {        *pRetVal = NULL;      }    return S_OK;}

Step 5: Implement IRangeValueProvider on the List Item Accessible Object

Control pattern interfaces need to be implemented only if they add functionality that is not provided by IAccessible. However, all methods of such interfaces should be implemented. For example, IAccessible enables control values to be read, so the implementation of IRangeValueProvider::get_Value is not used by MSAA clients. However, other clients might expect to find this method implemented.

Our implementation of IRangeValueProvider is mostly intended to supply property values not otherwise available, namely the minimum and maximum values of the control, and the amount by which the value is incremented or decremented when the user presses an arrow key. The following code shows a typical method for getting the maximum value of the control:

HRESULT STDMETHODCALLTYPE CListItemAccessibleObject::get_Maximum(    double *pRetVal){    if (!pRetVal)    {        return E_INVALIDARG;    }    *pRetVal = m_control->GetMaxValue();    return S_OK;};

Description of the Final Project

When examining the custom control, you will now see that additional properties are displayed: the localized control type for both the list box and the list items, and the custom properties of the slider.

Appendices

Properties

The following UI Automation AutomationElement properties do not overlap with any MSAA properties, and can be used in an IAccessibleEx implementation:

  • AutomationId
  • ClassName
  • ClickablePoint
  • Culture
  • FrameworkId
  •  IsRequiredForForm
  • ItemStatus
  • ItemType
  • LabeledBy
  • LocalizedControlType
  • Orientation
  • IsContentElement
  • IsControlElement

The following properties have some overlap with MSAA properties, and so can be used in an IAccessibleEx implementation, with the specified caveats:

  • AcceleratorKey, AccessKey: These overlap with accKeyboardShortcut; but can be provided if a control has both an access key and an accelerator.
  • ControlType: This overlaps with accRole, but can be specified to provide a more specific role.

The properties in the following table are already covered by MSAA properties and do not need to be supported by an IAccessibleEx implementation.

Property MSAA equivalent
BoundingRectangle accLocation
HasKeyboardFocus accState, STATE_SYSTEM_FOCUSED
IsEnabled accState, STATE_SYSTEM_UNAVAILABLE
IsKeyboardFocusable accState, STATE_SYSTEM_FOCUSABLE
IsPassword accState, STATE_SYSTEM_PROTECTED
HelpText accHelp
Name accName
NativeWindowHandle WindowFromAccessibleObject
IsOffscreen accState, STATE_SYSTEM_INVISIBLE/OFFSCREEN
ProcessId Provided by core UI Automation
RuntimeId Provided by core UI Automation

Control Patterns

The following UI Automation control patterns do not have to be implemented when the control has one of the roles outlined in the following table. Other control patterns should be supported if relevant.

Control pattern Corresponding MSAA roles
InvokePattern ROLE_PUSHBUTTON, ROLE_MENUITEM, ROLE_BUTTONDROPDOWN, ROLE_SPLITBUTTON, any other role where there is a default action.
SelectionItemPattern ROLE_LISTITEM, ROLE_RADIOBUTTON
SelectionPattern ROLE_LIST
TogglePattern ROLE_CHECKBUTTON
ValuePattern ROLE_TEXT when not read-only; ROLE_PROGRESSBAR, ROLE_COMBOBOX, or any other role where accValue is valid.
WindowPattern Automatically supported on top-level Win32 HWNDs.

List of All Articles