Using Dynamic Annotation

Before reading this article, please ensure that you have read and completed the steps in the Getting Started article.

Dynamic Annotation is a technology introduced with MSAA version 2.0. Its purpose is to enable a custom control to expose accessibility information without the need to fully implement the IAccessibleinterface. Dynamic Annotation works by overriding the properties of the default proxy that MSAA creates for standard Windows controls. If your custom control is a slight modification of an existing control, or requires support for only a few accessibility properties, Dynamic Annotation is a good candidate for implementing your accessibility solution.

Dynamic Annotation provides three different mechanisms for handling annotations: Direct Annotation, Value Map Annotation, and Server Annotation. In this article we will discuss only Direct Annotation and Server Annotation.

Description of the Initial Project

The project creates a custom label control that simulates some complex text rendering. A form hosts four of these controls. For the full effect, imagine that the control responds to the WM_PAINT message by a more interesting transformation (like 3-D rotation or full-color drawing) instead of only drawing the text three times.

This very simple control already has several accessibility problems:

  • The control has no Name property. The Name should match the text of the label.
  • The MSAA role of the control is Client. Instead, it should be Text.
  • The state of the control is STATE_SYSTEM_FOCUSABLE. Instead, it should be STATE_SYSTEM_READONLY.

The following screen shot shows these problems in the Inspect utility.

Accessibility errors in the Inspect utility

Notice the following properties that may look like errors but are in fact correct:

  • The ChildCount property is correctly set to zero.
  • There is no keyboard shortcut, but because we are using this label with a control that cannot receive the focus, it is okay to not have a shortcut.
  • The Parent is set to the appropriate Window.

A complete list of the properties that should be supported by a text label can be found on the Static Text reference page. Reference information for most controls is on the User Interface Element Reference page. If the custom control you create does not match exactly one in the list, choose the most similar one and then extend it by filling in the other properties as appropriate.

How Accessibility Issues Are Fixed

The preceding problems can be solved by using Dynamic Annotation to change the properties. Dynamic Annotation allows us to modify some of the MSAA properties of a control without requiring us to fully implement IAccessible.

We will discuss both Direct Annotation and Server Annotation techniques. Direct Annotation lets us set a property once (usually on creation) while Server Annotation lets us register a callback that returns the appropriate value when called.

Before we delve into the solutions to these problems, let’s take a look at what Dynamic Annotation offers.

Why Use Dynamic Annotation?

Dynamic Annotation supports overriding the default value for the following properties using either Direct Annotation or Server Annotation:

  • PROPID_ACC_NAME
  • PROPID_ACC_DESCRIPTION
  • PROPID_ACC_ROLE
  • PROPID_ACC_STATE
  • PROPID_ACC_HELP
  • PROPID_ACC_KEYBOARDSHORTCUT
  • PROPID_ACC_DEFAULTACTION
  • PROPID_ACC_VALUEMAP
  • PROPID_ACC_ROLEMAP
  • PROPID_ACC_STATEMAP.

By using Server Annotation, a control can also override the following properties:

  • PROPID_ACC_FOCUS
  • PROPID_ACC_SELECTION
  • PROPID_ACC_PARENT
  • PROPID_ACC_NAV_UP
  • PROPID_ACC_NAV_DOWN
  • PROPID_ACC_NAV_LEFT
  • PROPID_ACC_NAV_RIGHT
  • PROPID_ACC_NAV_PREV
  • PROPID_ACC_NAV_NEXT
  • PROPID_ACC_NAV_FIRSTCHILD
  • PROPID_ACC_NAV_LASTCHILD

To illustrate both approaches, we will use Direct Annotation for the Role and State properties, and then we will use Server Annotation for the Accessible Name.

Solution

Fixing the problems described previously will require the following 3 steps:

  1. Set up the infrastructure to enable properties on creation.
  2. Set up a Property Server.
  3. Implement a Callback Server.

Setting Properties on Control Creation

First we need to set up the infrastructure. Because MSAA is based on COM, we must initialize properly. For this, we will add the following line of code at the beginning of WinMain:

CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

We are requesting to do initialization in an Apartment thread to avoid dealing with synchronization issues. And remember to callCoUninitialize at the end of the thread:

CoUninitialize();

Now let’s work on the control itself. The steps needed to use the Direct Annotation API are as follows:

  1. Create an AccPropServices COM object.
  2. Call the appropriate SetHwndProp methods to annotate the control. If appropriate, you can use theSetProp... functions with the correct Identity String).
  3. Release the AccPropServices object. (You can cache it if you plan to use it repeatedly, but don't forget to release it.)

The properties are stored in VARIANT variables, so you must initialize them properly (sett the vt member) and use the correct member of the union.

This is a set of operations that can easily be abstracted in the following function:

HRESULT CFancyTextControl::SetAccessibleProperties(){    // We will assume COM has been initialized.    IAccPropServices* pAccPropServices = NULL;    HRESULT hr = CoCreateInstance(CLSID_AccPropServices,        NULL,        CLSCTX_SERVER,        IID_IAccPropServices,       (void**)&pAccPropServices);    if (SUCCEEDED(hr))    {        VARIANT var;        var.vt = VT_I4;        var.lVal = ROLE_SYSTEM_STATICTEXT;        hr = pAccPropServices->SetHwndProp(_hwnd,            OBJID_CLIENT,            CHILDID_SELF,            PROPID_ACC_ROLE,            var);        if (SUCCEEDED(hr))        {            var.lVal = STATE_SYSTEM_READONLY;            hr = pAccPropServices->SetHwndProp(_hwnd,                OBJID_CLIENT,                CHILDID_SELF,                PROPID_ACC_STATE,                var);        }        pAccPropServices->Release();    }    return hr;}

We call this function when the window is created to ensure that it receives the right properties from the beginning.

A short discussion of the parameters for SetHwndProp is in order:

  • The first parameter is the window handle of the control whose properties we want to override.
  • Next we need an object identifier. Most of the time it will be OBJID_CLIENT.
  • If the object has children and you are setting the property for one of them, you should use its index here. If the value you want to set refers to the object itself instead of one of its children, use the value CHILDID_SELF.
  • The fourth parameter is the property identifier, which can be any of the values that Direct Annotation can override.
  • The final parameter is a VARIANT that contains the new value assigned to the property.

Add the following line to the handler for the WM_NCCREATE message, just before the return statement.

SetAccessibleProperties();

Finally, you will need to include initguid.h and oleacc.h at the beginning of the CPP file for the custom control.

If you run the program as it is now, you should see that the Role and State properties now show the correct values. The Name property can be fixed in the same way, but we will use instead a Property Server object to explain this approach.

Creating a Property Server

A Property Server gives us more flexibility when setting the accessibility properties of a control. The Property Server is called every time there is a request for the specified property, so we can recalculate it based on the current value of the control. This is the correct solution when we have a control whose properties can change at run time, as long as it is one of the properties specified in the documentation under Using Server Annotation.

The steps to set up a property server are very similar to the ones used for overriding a property. An AccPropServices object is created, and then SetPropServer is called with the window and Property Server object, as shown in the following method.

HRESULT CFancyTextControl::SetAccessibleCallback(){    // We will assume COM has been initialized.    MSAAPROPID propIds[] = {PROPID_ACC_NAME};    IAccPropServices* pAccPropServices = NULL;    HRESULT hr = CoCreateInstance(CLSID_AccPropServices,        NULL,       CLSCTX_SERVER,        IID_IAccPropServices,        (void**)&pAccPropServices);    if (SUCCEEDED(hr))    {        IAccPropServer* pPropServer = new CFancyTextPropertyServer(_hwnd);        if (pPropServer == NULL)        {            pAccPropServices->Release();    // Do not forget to release before returning.            return E_OUTOFMEMORY;        }        // We can skip the local calls to AddRef and Release, because        //    we are not keeping a reference to the object.        hr = pAccPropServices->SetHwndPropServer(_hwnd,            OBJID_CLIENT,            CHILDID_SELF,            propIds,            1,            pPropServer,            ANNO_THIS);        pAccPropServices->Release();    }    return hr;}

The sequence of calls is very similar to the one for setting Direct Annotation. The differences when using SetHwndPropServer are:

  • Multiple properties can be indicated at the same time. For this you create an array of MSAAPROPIDs and pass it as the fourth parameter.  he fifth parameter is the number of elements in this array.
  • Instead of annotating with a property, we pass a pointer toIAccPropServices. This should point to a COM object that will respond appropriately to requests for the property value.
  • The final parameter is used to set the context. Normally ANNO_THIS is used; if the object contains children and the server will respond to property requests on them, you should use ANNO_CONTAINER.

The real work occurs in the AccPropServer object. This is a COM object, but because it cannot be created except by our own code when setting the Property Server, we will not need to do any registration. The following code shows the class definition:

class CPropertyServer : public IAccPropServer{private:    ULONG _refCount;    HWND _hwnd;public:  CPropertyServer(HWND hwnd);    // IUnknown    STDMETHOD(QueryInterface)(const IID& iid, void** ppv);    STDMETHOD_(ULONG, AddRef)();    STDMETHOD_(ULONG, Release)();    // IAccPropServer    STDMETHOD(GetPropValue)(        VARIANT* pvarValue,       BOOL* pfHasProp);};

TheIUnknown methods have the usual implementations. GetPropValue (the only method in the IAccPropServer interface) is what we're mostly interested in.

HRESULT CPropertyServer::GetPropValue(const BYTE *pIDString,   DWORD dwIDStringLen,   MSAAPROPID idProp,    VARIANT *pvarValue,    BOOL *pfHasProp){    // By default we indicate that the object does not have this property.    *pfHasProp = FALSE;    pvarValue->vt = VT_EMPTY;    // We only respond to accessible name requests    if (idProp == PROPID_ACC_NAME)    {        // Get the information.        FancyTextControlInfo* pControl =            reinterpret_cast(GetWindowLongPtr(_hwnd, GWLP_USERDATA));        pvarValue->bstrVal = SysAllocString(pControl->text);        pvarValue->vt = VT_BSTR;        *pfHasProp = TRUE;    }    return S_OK;    // This method should ALWAYS return S_OK.}

All this method does is check that the appropriate property is being requested and store the values based on the control data in the pvarValueparameter. If the property is not applicable given the current state of the control, pvarValue->vt should be set to VT_EMPTY and pfHasProp should be set to FALSE. Compare idProp to other property id values if your object registered for multiple properties. The method should always return S_OK, regardless of whether the property is set.

For simplicity, we've kept a reference to the associated window. If we don't keep it, we're able to get the right one by calling IAccPropServices::DecomposeHwndIdentityString. Using this technique we can have one object service multiple controls.

Finally, don't forget to clear the properties (remember to do this even if you do not create a server object), so that all the associated resources are freed by the system. We will call this function when the window is destroyed, just before releasing the other resources needed by the control.

void ClearAccessibleProperties(HWND hwnd){    // We will assume COM has been initialized.    MSAAPROPID propIds[] = {PROPID_ACC_NAME, PROPID_ACC_ROLE, PROPID_ACC_STATE};    IAccPropServices* pAccPropServices = NULL;    HRESULT hr = CoCreateInstance(        CLSID_AccPropServices,        NULL,        CLSCTX_SERVER,        IID_IAccPropServices,         (void**)&pAccPropServices);    if (SUCCEEDED(hr))    {        pAccPropServices->ClearHwndProps(hwnd, OBJID_CLIENT, CHILDID_SELF, propIds, 3);        pAccPropServices->Release();    }}

If you run the program now, the Inspect tool will report all the properties with the values we defined for them.

Description of Final Project

To verify that the problems were fixed, launch Inspect, and following the steps from the beginning of this article, verify that the properties Name, Role, and State have the values “First” (or the appropriate label), “text” and “read only”. The following screen shot shows these values in the Inspect tool's UI.

Values in the Inspect tool's UI

Other Considerations

Accessibility Events

Windows events occur to notify accessibility applications of changes in the user interface. Whenever one of the following happens, NotifyWinEvent should be called:

  • An object is created, destroyed, shown, hidden, reordered, or invoked.
  • Selection is changed in a custom lit object.
  • Name, Value, State, Description, Location, Parent, Help, Default Action, or Accelerator accessible properties change in an object.

These calls are needed only for objects for which you implement accessibility. The system provides accessibility for common objects and will call the appropriate events when needed. In the example we did not have to worry about notifying clients because we set the accessible properties when the object was being created. After it is created and shown, calls to NotifyWinEvent should be made as appropriate. To see how the system generates some events for us, let's look at the accessibility events for one of our controls by using AccEvent.

The first time we launch AccEvent, we will see a large number of events being generated by changes in the UI. To make our life easier, let's filter out events based on the window handle of our custom control. To find out what the HWND is, we can use Inspect and look at the HWND property, as shown in the following screen shot.

HWND property shown in the Inspect too

Then we use the HWND value to filter events based only on this window. To do so in AccEvent, open the Options Menu and clickSettings. At the bottom of this dialog box, set up the filter to include only the specified window and enter the HWND we found using Inspect. The following screen shot shows how to do it.

Using the HWND value to filter events in AccEvent

Finally, clear all the events shown in AccEvent (Edit >Clear). Go back to the application and resize the window. Notice that AccEvent logs all the location changes in the custom control, as shown in the following screen shot.

This happens automatically for HWND-based custom controls. We will see later how to deal with changes in other properties and how to deal with a child control that does not have its own HWND.

Keyboard Focus

In this example we used static text boxes, which do not get keyboard focus. This has made life simpler for us, but in the next project we will see how to deal with focus issues.

Next Article