Making Custom Controls Accessible, Part 3: Implementing MSAA for a Complex Control

This is the third of a series of 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.

This article follows Making Custom Controls Accessible, Part 2: Implementing MSAA for a Simple Control, which you should read first. Here we will skip over most of the IAccessible implementation details and focus on the issue of dealing with a containing control (that is, one that has child controls inside it).

To illustrate the concepts, we are using a list whose elements contain a custom element similar to a progress bar.

Description of the Initial Project

The initial project contains a custom list-box control. The elements of the list contain a progress bar in addition to their regular label. The final project contains a complete accessibility implementation, but for this article we will focus on the following problems:

  1. The control returns the wrong number of child controls.
  2. Child controls are not exposed.
  3. Navigation of child controls does not work.
  4. The progress bar’s value is not exposed.

How Accessibility Issues Are Fixed

For each of the previous problems we will implement the following solution:

  • Override IAccessible::get_accChildCount to return the right number of child objects.
  • Override IAccessible::get_accChild to return the appropriate value and make sure all accessibility methods respond correctly when given a child ID to request information about the items instead of using CHILDID_SELF to refer to the list.
  • Override the corresponding IAccessible::accNavigate to set focus appropriately.
  • Use IAccessible::get_accValue to expose the value of the progress bar for each of the child objects

Details

We will assume that the COM infrastructure has been set up as discussed previously. Our method for checking the validity of a child ID has been modified to what is shown in the following code:

HRESULT CListboxAccessibleObject::ValidateChildId(VARIANT& varChild){    HRESULT hr = CheckAlive();    if(SUCCEEDED(hr))    {        if(varChild.vt != VT_I4)        {            hr = E_INVALIDARG;        }        else if(varChild.lVal != CHILDID_SELF)        {            if((UINT)(varChild.lVal - 1) >= _pControl->GetItemCount() )            {                hr = E_INVALIDARG;            }        }    }    return hr;}

The child ID is the number of items in the control plus one. This is done because child IDs must be positive integers.

Problem 1: Returning the Appropriate Number of Child Controls

The following code shows how to return the appropriate number of child controls:

HRESULT CListboxAccessibleObject::get_accChildCount(long *pCountChildren){    HRESULT hr = CheckAlive();    if (SUCCEEDED(hr))    {        *pCountChildren = _pControl->GetItemCount();    }    return hr;}

Problem 2: Exposing the Child Controls

First, we need to implement get_accChild to return the right pointer. In the sample project, child objects are simple enough that the parent can deal with their accessibility implementations, but in some situations it is necessary to implement the child’s IAccessible interface separately (for example, when child objects contain other controls themselves). Because the child objects for this control are simple enough, we should set the IAccessible pointer for children to null when requested, as shown in the following code:

HRESULT CListboxAccessibleObject::get_accChild(VARIANT varChildId,    IDispatch **ppDispChild){    // This IAccessible doesn't use Child IDs, so this just does param    // validation, and returns S_OK with *ppdispChild as NULL.    *ppDispChild = NULL;    HRESULT hr = ValidateChildId(varChildId);    if(SUCCEEDED(hr))    {        hr = S_FALSE;    }    return hr;}

Because the parent is taking care of the accessibility methods for the child objects, in each of them we should verify whether the call is for the list itself (when the ID is CHILDID_SELF) or for one of the elements. The following code illustrates the pattern followed in this project:

HRESULT CListboxAccessibleObject::get_accName(VARIANT varId, BSTR *pszName){    // Let the base IAccessible handle the name of the overall control -
    // default support in OLEACC will look for a label if the control    // is in a dialog. Otherwise, return the string for the appropriate    // child item.    *pszName = NULL;    HRESULT hr = ValidateChildId(varId);    if(SUCCEEDED(hr))    {        if(varId.lVal == CHILDID_SELF)        {            hr = _pBase->get_accName(varId, pszName);        }        else        {            *pszName = SysAllocString(_pControl->                GetItemString(varId.lVal - 1));            if(*pszName == NULL)            {                hr = E_OUTOFMEMORY;            }        }    }    return hr;}

Problem 3: Enabling Navigation for Child Controls

The following implementation of accNavigate sets the focus appropriately:

HRESULT CListboxAccessibleObject::accNavigate(long navDir,    VARIANT varStart, VARIANT *pvarEndUpAt){    pvarEndUpAt->vt = VT_EMPTY;    HRESULT hr = ValidateChildId(varStart);    if(SUCCEEDED(hr))    {        if(varStart.lVal == CHILDID_SELF)        {            if(navDir == NAVDIR_FIRSTCHILD)            {                if(_pControl->GetItemCount() == 0)                {                    // No child objects to navigate to.                    hr = S_FALSE;                }                else                {                    pvarEndUpAt->vt = VT_I4;                    pvarEndUpAt->lVal = 1;                }            }            else if(navDir == NAVDIR_LASTCHILD)            {                if(_pControl->GetItemCount() == 0)               {                   // No child objects to navigate to.                    hr = S_FALSE;                }                else                {                    pvarEndUpAt->vt = VT_I4;                    pvarEndUpAt->lVal = _pControl->GetItemCount();                }            }            else            {                hr = _pBase->accNavigate(navDir, varStart, pvarEndUpAt);            }        }        else        {            if(navDir == NAVDIR_DOWN || navDir == NAVDIR_NEXT)            {                UINT cItems = _pControl->GetItemCount();                if(cItems > 1 && (UINT)varStart.lVal < cItems)                {                    pvarEndUpAt->vt = VT_I4;                    pvarEndUpAt->lVal = varStart.lVal + 1;                }                else                {                    hr = S_FALSE;                }            }            else if(navDir == NAVDIR_PREVIOUS || navDir == NAVDIR_UP )            {                if(varStart.lVal > 1)                {                    pvarEndUpAt->vt = VT_I4;                    pvarEndUpAt->lVal = varStart.lVal - 1;                }                else                {                    hr = S_FALSE;                }            }            else if(navDir == NAVDIR_LEFT || navDir == NAVDIR_RIGHT)            {                // Leave out param as VT_EMPTY from above.                hr = S_FALSE;            }            else            {                hr = E_INVALIDARG;            }        }    }    return hr;}

We also need to return the appropriate control boundaries and perform hit testing to determine which child object will be selected, as in the following code:

HRESULT CListboxAccessibleObject::accLocation(long *pxLeft, long *pyTop,    long *pcxWidth, long *pcyHeight, VARIANT varId){    *pxLeft = *pyTop = *pcxWidth = *pcyHeight = 0;    HRESULT hr = ValidateChildId(varId);    if(SUCCEEDED(hr))    {        if(varId.lVal == CHILDID_SELF)        {            hr = _pBase->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight, varId);        }        else        {            RECT rc;            _pControl->GetLocation(varId.lVal - 1, &rc);            MapWindowPoints(_pControl->GetControlWindow(),                NULL, (POINT *)&rc, 2);            *pxLeft = rc.left;            *pyTop = rc.top;            *pcxWidth = rc.right - rc.left;            *pcyHeight = rc.bottom - rc.top;       }    }    return hr;}HRESULT CListboxAccessibleObject::accHitTest(long xLeft, long yTop,    VARIANT *pvarChild){    pvarChild->vt = VT_EMPTY;    HRESULT hr = CheckAlive();    if(SUCCEEDED(hr))    {        hr = _pBase->accHitTest(xLeft, yTop, pvarChild);        if(SUCCEEDED(hr) && pvarChild->vt == VT_I4 &&            pvarChild->lVal == CHILDID_SELF)        {            POINT pt = { xLeft, yTop };            MapWindowPoints(NULL, _pControl->GetControlWindow(), &pt, 1);            UINT iItem = _pControl->ItemFromPoint(pt);            if(iItem != (UINT)-1)            {                pvarChild->lVal = iItem + 1;            }        }    }    return hr;}

Problem 4: Exposing the Progress Bar Value

This method is just like all the other ones we’ve used, where we check for the parent and then for the children. Note that it is possible for children to return a value while the parent does not. The following code exposes the progress bar value:

HRESULT CListboxAccessibleObject::get_accValue(VARIANT varId, BSTR *pszValue){    *pszValue = NULL;    HRESULT hr = ValidateChildId(varId);    if(SUCCEEDED(hr))    {        if(varId.lVal == CHILDID_SELF)        {            // Value is only supported on the items, not            //    on the overall control itself.            hr = DISP_E_MEMBERNOTFOUND;        }        else        {            UINT val = _pControl->GetItemValue(varId.lVal - 1);            WCHAR valAsString[30];  // Should be a named constant.            hr = StringCchPrintf(valAsString,                ARRAYSIZE(valAsString),                L"%d", val);            if(SUCCEEDED(hr))            {                *pszValue = SysAllocString(valAsString);                if(*pszValue == NULL)               {                    hr = E_OUTOFMEMORY;                }            }        }    }    return hr;}

Description of the Final Project

When you run the final project with the Inspect utility, notice that the children are now correctly highlighted, navigation to each of the elements works correctly, and the appropriate values are displayed for the progress bar.

Next