Making Custom Controls Accessible, Part 2: Implementing MSAA for a Simple Control

This is the second 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.

In the first article, we created a simple custom label control and overrode some of its accessible properties by using Dynamic Annotation. The project in this article creates a more complex control that requires a full implementation of IAccessible.

Description of the Initial Project

This project contains a custom checkbox control hosted in a regular Windows dialog box.

After building and running the original project, we can use the Inspect utility to confirm that the following errors are present:

  1. The control does not have a default action.
  2. The control does not have values or displays incorrect values for Name, Role and State properties.
  3. The control does not have a keyboard shortcut.

How Accessibility Issues Are Fixed

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

  • Create a helper object that implements IAccessible.
  • Use CreateStdAccessibleObject to create the default IAccessible implementation for our control’s window, and then override the appropriate methods as follows.
  • Override the IAccessible::get_accDefaultAction method to return the right value.
  • Override the IAccessible::accDoDefaultAction method to update the control as appropriate.
  • Override methods to retrieve properties.
  • Add support to the original control for appropriate focus tracking and override the IAccessible::get_accKeyboardShortcut method.

Details

Before we detail each of these solutions, let’s discuss what is needed to create an object that implements IAccessible.

Create a COM Object That Implements IAccessible

This is relatively straightforward but a little tedious. The standard implementations for IUnknown methods will work. Because IAccessible is derived from IDispatch, we need to implement the IDispatch interface as well. However, MSAA saves us the trouble of doing a real implementation of IDispatch, because it wraps the objects that we use and provides its own implementation if we code the IDispatch methods to null the out parameters and then return E_NOTIMPL, as shown in the following code:

HRESULT CCheckbox4AccessibleObject::GetTypeInfo(UINT iTInfo,   LCID lcid,    ITypeInfo **ppTinfo){    *ppTinfo = NULL;    return E_NOTIMPL;}

After we have solved the COM infrastructure problems, we can deal with each of the accessibility issues previously described.

Because we are using a COM object, it is possible that the control will be destroyed while there are still references to the object. To account for this situation, many of the IAccessible methods will check whether the control still exists by calling CheckAlive and returning the appropriate error if the control has been destroyed. There is also a check to verify that we are not requesting information about a nonexistent child control. These checks are shown in the following code:

HRESULT CCheckbox4AccessibleObject::CheckAlive(){    return _alive ? S_OK : RPC_E_DISCONNECTED;}HRESULT CCheckbox4AccessibleObject::ValidateChildId(VARIANT &varChildId){    HRESULT hr = CheckAlive();    if (SUCCEEDED(hr))    {        // This control does not support children.       if ((varChildId.vt != VT_I4) || (varChildId.lVal != CHILDID_SELF))        {            hr = E_INVALIDARG;        }    }    return hr;}

These methods should be called as appropriate from the corresponding IAccessible method to ensure that the correct error is returned, as shown in the following code:

HRESULT CCheckbox4AccessibleObject::get_accChildCount(long *pCountChildren){   HRESULT hr = CheckAlive();    if (SUCCEEDED(hr))    {        *pCountChildren = 0;    }    return hr;}

To save time and code, we also use a default MSAA proxy for methods that ascertain the location of the window. We first create the proxy in the constructor for the accessible object:

CCheckbox4AccessibleObject::CCheckbox4AccessibleObject(CCheckbox4Control* pControl) :    _pControl(pControl),    _alive(TRUE),    _refCount(0){    CreateStdAccessibleObject(_pControl->get_hwnd(),        OBJID_CLIENT,        IID_IAccessible,        (void**)&_pBase);}

We can use this new object (_pBase) to respond to methods for which the default IAccessible implementation works. An example is shown in the following code:

HRESULT CCheckbox4AccessibleObject::get_accParent(IDispatch **ppDispParent){    return _pBase->get_accParent(ppDispParent);}

Problem 1: Setting the Default Action

Setting the default action is fairly straightforward and is solved with the following implementation of get_accDefaultAction. (In production code, the string should come from a resource file to enable localization.)

HRESULT CCheckbox4AccessibleObject::get_accDefaultAction(VARIANT varId, BSTR *pszDefaultAction){    *pszDefaultAction = NULL;    HRESULT hr = ValidateChildId(varId);    if (SUCCEEDED(hr))    {        // NOTE: This string should come from a resource and be localizable.        *pszDefaultAction = SysAllocString(L"Toggle");        if (*pszDefaultAction == NULL)        {            hr = E_OUTOFMEMORY;        }    }    return hr;}

Problem 2: Performing the Default Action

The solution is very simple, but this is one of the most important methods because it enables accessibility programs to perform actions on the control:

HRESULT CCheckbox4AccessibleObject::accDoDefaultAction(VARIANT varId){    _pControl->MoveToNextState();    return S_OK;}

When we update the value of a control, we should call NotifyWinEvent to provide accessibility tools information about the change, as shown in the following code:

void CCheckbox4Control::MoveToNextState(){    if ((!_fTopChecked) && (!_fBottomChecked))    {        _fTopChecked = FALSE;        _fBottomChecked = TRUE;    }    else if ((!_fTopChecked) && (_fBottomChecked))    {        _fTopChecked = TRUE;        _fBottomChecked = FALSE;    }    else if ((_fTopChecked) && (!_fBottomChecked))    {        _fTopChecked = TRUE;        _fBottomChecked = TRUE;    }    else if ((_fTopChecked) && (_fBottomChecked))    {        _fTopChecked = FALSE;        _fBottomChecked = FALSE;    }   InvalidateRect(_hwnd, NULL, TRUE);    NotifyWinEvent(EVENT_OBJECT_STATECHANGE,        _hwnd,        OBJID_CLIENT,        CHILDID_SELF);}

Problem 3: Returning Appropriate Values for Name, Role, and State

The solution is easy; all these methods follow the pattern shown in the following example:

HRESULT CCheckbox4AccessibleObject::get_accName(VARIANT varId, BSTR *pszName){    *pszName = NULL;    HRESULT hr = ValidateChildId(varId);    if (SUCCEEDED(hr))    {        // Remove & characters from the string.        CString str(_pControl->get_label());        str.Remove(L'&');        *pszName = SysAllocString(str);>        if (*pszName == NULL)>        {            hr = E_OUTOFMEMORY;        }>    }   return hr;}

Be sure to perform the appropriate checks for an active control and the correct child ID, as described previously.

Problem 4: Returning Keyboard Accelerators

The solution for this problem requires multiple steps, because it requires cooperation from the control and the accessible object.

First, we need to make sure our control has the WS_TABSTOP style. This is set in the resource.rc file of our project. We must also add an ampersand to the control name to indicate the correct keyboard accelerator, as shown in the following code:

IDD_MAINDIALOG DIALOGEX 0, 0, 150, 70CAPTION "Sample Application"STYLE WS_SYSMENU | WS_MINIMIZEBOXBEGIN    CONTROL "&One checkbox", CB4_1, "My:Checkbox4Control",        WS_VISIBLE | WS_TABSTOP, 10, 10, 130, 20    CONTROL "&The other", CB4_2, "My:Checkbox4Control",        WS_VISIBLE | WS_TABSTOP, 10, 40, 130, 20END

Next we need to respond to the WM_GETTEXT message so that the dialog box can determine the name and shortcut of the control, as in the following code:

case WM_GETTEXT:    {        // NOTE: We are copying strings here.        // This is safe usage because wParam gives us        //       the length of the buffer and we check        //       return values for success.        //       Be extremely careful when doing your own        //       string manipulation!        if (wcscpy_s(reinterpret_cast<WCHAR*>(lParam), wParam, _label) == 0)        {            return wcsnlen(reinterpret_cast<WCHAR*>(lParam), wParam);        }        else        {            return 0;        }    }    break;

We must also respond to the WM_GETDLGCODE message with the return value of DLGC_BUTTON so that we get BM_CLICK notifications when the accelerator is pressed, as shown in the following code:

    case WM_GETDLGCODE:        {            return DLGC_BUTTON;        }        break;

And finally, whenever we receive the WM_LBUTTONDOWN messsage, we must perform the same action that we do when we receive a click, as shown in the following code:

    case WM_LBUTTONDOWN:    case BM_CLICK:        {            SetFocus(_hwnd);            MoveToNextState();            return 0;        }        break;

Now that we’ve got the accelerator working, it must be exposed through IAccessible by an implementation of get_accKeyboardShortcut. This process works by looking for an ampersand and then concatenating the next character to the “Alt+” string, as in the following code:

RESULT CCheckbox4AccessibleObject::get_accKeyboardShortcut(VARIANT varId, BSTR *pszKeyboardShortcut){    *pszKeyboardShortcut = NULL;    HRESULT hr = ValidateChildId(varId);    if (SUCCEEDED(hr))    {        BSTR label = _pControl->get_label();        long len = SysStringLen(label);        long i;        for (i=0; i<len; i++)        {            if (label[i] == L'&')            {>                break;            }        }        if (i == len)       // Did not find the "&".        {            hr = DISP_E_MEMBERNOTFOUND;        }        else        {            BSTR shortcut = SysAllocString(L"Alt+?");            shortcut[4] = label[i+1];            *pszKeyboardShortcut = shortcut;        }    }    return hr;}

Description of the Final Project

Now that of the problems are solved, you can use the Inspect utility to verify that the correct properties are exposed. You can also use Inspect to perform the default action, and you should see the value of the checkbox changing. Finally, verify that the dialog can be navigated with the TAB key and that the keyboard accelerators will set focus and perform the appropriate action.

Next Article