This post describes some of the steps you can take to enhance the programmatic accessibility of your Win32 app.
Introduction
By default, use standard Win32 controls in your app, and leverage the work that the Win32 UI framework can do to provide the foundation for an accessible experience.
Way back, when work was done to make Win32 UI accessible, the API powering programmatic accessibility in Windows, was Microsoft Active Accessibility (MSAA). Two key components of MSAA were the IAccessible interface, which enabled an assistive technology (AT) app to learn about some other app’s UI, and WinEvents, which enabled the AT app to react to changes in that UI. Support for MSAA still exists in Windows, but most of us focus solely on MSAA’s successor, UI Automation (UIA). UIA provides a richer feature set, and in some situations significantly improves performance.
For some Win32 UI, support for accessibility is still based on MSAA. So in order to interact with a UIA client app like Narrator, UIA itself performs a conversion from the MSAA data being exposed by the Win32 UI, to UIA data. The UI properties exposed through the IAccessible interface, and the notifications raised via WinEvents, are converted into UIA data by UIA’s internal MSAAProxy component.
An example of when the MSAAProxy is being used, is when a UIA client interacts with the ribbon in the Windows Explorer UI. The Inspect SDK tool is a UIA client app, (just like Narrator is,) and it can report the UIA ProviderDescription property associated with the UI that it’s interacting with. The ProviderDescription string includes details of which component is providing the UIA data associated with the UI element. The screenshot below shows the Inspect SDK tool reporting the UIA properties of the “New folder” button on the Windows Explorer Ribbon.
Figure 1: The Inspect SDK tool reporting UIA properties of the “New folder” button on the Windows Explorer ribbon.
The full text of the UIA ProviderDescription property for the “New folder” button is:
[pid:18236,providerId:0x0 Main(parent link):Microsoft: MSAA Proxy (unmanaged:uiautomationcore.dll)]
It’s usually the last portion of the ProviderDescription string that I’m most interested in, as that often shows which DLL contains the UIA provider exposing the UIA information that Inspect is reporting. Some UI frameworks implement the UIA Provider API themselves. For example, WPF, UWP XAML, and Edge do that. The table below lists the UIA FrameworkId properties when Inspect examines related UI, along with the DLL name included in the UIA ProviderDescription property.
UIA FrameworkId property |
DLL shown in the UIA ProviderDescription property |
Win32 |
MSAA Proxy (unmanaged:uiautomationcore.dll)] |
WinForm |
MSAA Proxy (unmanaged:uiautomationcore.dll)] |
WPF |
PresentationCore |
XAML |
Windows.UI.Xaml.dll |
Edge/HTML |
edgehtml.dll |
So for both Win32 and WinForms UI, UIA’s MSAAProxy is converting MSAA data into UIA data for UIA client apps like Inspect and Narrator. This means that when using standard Win32 controls, the Windows platform can provide the foundation for an accessible experience for your customers using Narrator.
Enhancing the default accessible experience
Below are some of the ways that you can enhance the programmatic accessibility of your Win32 app.
Giving your standard controls helpful accessible names
Wherever practical, the Windows platform will make your Win32 UI accessible by default. For example, if you use a standard Button control that shows the text “Save” on it, then this control will automatically be exposed by UIA as an element whose ControlType property is Button, and whose Name property is “Save”. But what happens when a control has no visual text label associated with it?
For example, say I present an Edit control or ComboBox control, which has no visual label. There’s no visual text describing the purpose of the control that can be repurposed as the accessible name of the control. This means your customer using Narrator will be told that they’ve reached an Edit or ComboBox control, but not the purpose of the control. In some situations this could render the app unusable.
A great way to fix this problem is by adding a visual label which precedes the control in question. By default, Win32 will repurpose the text on a label that lies just before the Edit or ComboBox control, as the accessible name of for the control. By adding a visual label in this way, you’ve unblocked your customers using Narrator, and in many cases you’ll have improved the usability of the app for your sighted customers too.
In some UI designs you may feel that for some reason, you don’t want a visual string to appear before the Edit control or ComboBox control. So assuming this is a valid design, (and somehow it’s still efficient for your sighted customers to determine exactly what the purpose of the control is,) you can add a hidden label before the control. Win32 will still repurpose the label text as the accessible name of the control, despite the label having no visual representation on the screen.
For example, say I add the following to a dialog box in my Win32 file:
LTEXT “First name”,IDC_STATIC,8,100,0,8,NOT WS_VISIBLE
EDITTEXT IDC_FIRSTNAMEEDIT,70,100,100,10
I’ve prevented the label from having any visual representation on the screen by setting its width to zero, and making it not visible. Either of those steps would be sufficient for this demonstration.
When I point the Inspect SDK tool to the Edit control, I find that the control has a UIA Name property set from the text label, as shown in the screenshot below. And if I now tab to the control when Narrator’s running, I hear Narrator announce “First name, editing”.
Figure 2: The Inspect SDK tool reporting that the UIA Name property of the Edit control is “First name”.
Interestingly I also find that the zero-width, hidden label is not exposed through UIA at all.
Definitely use the Inspect SDK tool to determine whether any of your standard Win32 controls have no accessible name by default, and consider whether adding a label before the control can resolve that issue.
Explicitly setting some UIA property on your Win32 control
Sometimes the accessibility of your UI can be significantly enhanced by customizing a specific UIA property associated with a control. A common way of doing this with hwnd-based Win32 UI is to use SetHwndProp() and SetHwndPropStr(), available through the IAccPropServices interface. This interface was originally built to provide a way to set custom MSAA properties on UI, but it can also be used to customize some UIA properties too.
Explicitly setting properties in this way is known as Direct Annotation, and that’s a type of Dynamic Annotation.
The general approach is shown below. (And by the way, sorry about the wide paragraph spacing in all the code snippets. It’s not practical for me set to zero spacing when uploading these posts to MSDN.)
// Near the top of the file…
#include <initguid.h>
#include “objbase.h”
#include “uiautomation.h”
IAccPropServices* _pAccPropServices = NULL;
// When the UI is created…
HRESULT SetCustomUIAProperties (HWND hDlg)
{
HRESULT hr = CoCreateInstance(
CLSID_AccPropServices,
nullptr,
CLSCTX_INPROC,
IID_PPV_ARGS(&_pAccPropServices));
if (SUCCEEDED(hr))
{
VARIANT var;
Initialize var with the type and value required here!
hr = _pAccPropServices->SetHwndProp(
GetDlgItem(hDlg, IDC_MYCONTROL),
OBJID_CLIENT,
CHILDID_SELF,
<UIA property guid of interest>,
var);
}
return hr;
}
// When the UI is destroyed…
void ClearCustomUIAProperties (HWND hDlg)
{
if (_pAccPropServices != nullptr)
{
// Clear all the properties we set on the hwnd.
MSAAPROPID props[] = { <UIA property guid of interest> };
_pAccPropServices->ClearHwndProps(
GetDlgItem(hDlg, IDC_MYCONTROL),
OBJID_CLIENT,
CHILDID_SELF,
props,
ARRAYSIZE(props));
_pAccPropServices->Release();
_pAccPropServices = NULL;
}
}
The above steps assume the control of interest has its own hwnd.
If the property being customized is a string, then I’d use the very handy SetHwndPropStr() rather than SetHwndProp().
Example
I’ve seen SetHwndProp() and SetHwndPropStr() used for a number of different reasons. The example below sets custom Name, HelpText, and ItemStatus properties on a Button. It’s pretty uncommon for all these properties to be set on a single Button, but you might sometimes need to set at least one of them. The code below also turns a text label into a LiveRegion, so that a screen reader is informed when the text on the label changes.
// At the top of the file…
#include <initguid.h>
#include “objbase.h”
#include “uiautomation.h”
IAccPropServices* _pAccPropServices = NULL;
// When the UI is created…
void SetCustomUIAProperties(HWND hDlg)
{
HRESULT hr = CoCreateInstance(
CLSID_AccPropServices,
nullptr,
CLSCTX_INPROC,
IID_PPV_ARGS(&_pAccPropServices));
if (SUCCEEDED(hr))
{
// First customize the UIA properties of the Button.
WCHAR szButtonName[MAX_LOADSTRING];
LoadString(
hInst,
IDS_CONNECTION,
szButtonName,
ARRAYSIZE(szButtonName));
// Set the Name on the button.
hr = _pAccPropServices->SetHwndPropStr(
GetDlgItem(hDlg, IDC_BUTTON_CONNECTION),
OBJID_CLIENT,
CHILDID_SELF,
Name_Property_GUID,
szButtonName);
if (SUCCEEDED(hr))
{
WCHAR szButtonHelp[MAX_LOADSTRING];
LoadString(
hInst,
IDS_CONNECTION_HELP,
szButtonHelp,
ARRAYSIZE(szButtonHelp));
// Set the HelpText on the button.
hr = _pAccPropServices->SetHwndPropStr(
GetDlgItem(hDlg, IDC_BUTTON_CONNECTION),
OBJID_CLIENT,
CHILDID_SELF,
HelpText_Property_GUID,
szButtonHelp);
}
if (SUCCEEDED(hr))
{
WCHAR szButtonStatus[MAX_LOADSTRING];
LoadString(
hInst,
IDS_CONNECTION_STATUS,
szButtonStatus,
ARRAYSIZE(szButtonStatus));
// Set the ItemStatus on the button.
hr = _pAccPropServices->SetHwndPropStr(
GetDlgItem(hDlg, IDC_BUTTON_CONNECTION),
OBJID_CLIENT,
CHILDID_SELF,
ItemStatus_Property_GUID,
szButtonStatus);
}
// Now set the LiveSetting property on the label.
if (SUCCEEDED(hr))
{
VARIANT varLiveSetting;
varLiveSetting.vt = VT_I4;
varLiveSetting.lVal = Assertive; // From UIAutomationCore.h.
hr = _pAccPropServices->SetHwndProp(
GetDlgItem(hDlg, IDC_LABEL_CONNECTIONSTATUS),
OBJID_CLIENT,
CHILDID_SELF,
LiveSetting_Property_GUID,
varLiveSetting);
}
}
}
// When the LiveRegion label data is changing…
WCHAR szFullConnectionStatus[MAX_LOADSTRING];
LoadString(
hInst,
IDS_LABEL_CONNECTIONSTATUS_UNAVAILABLE,
szFullConnectionStatus,
ARRAYSIZE(szFullConnectionStatus));
// Set the new status text on the label.
HWND hWndStatusLabel = GetDlgItem(hDlg, IDC_LABEL_CONNECTIONSTATUS);
SetWindowText(hWndStatusLabel, szFullConnectionStatus);
// Raise an event to let Narrator know that the LiveRegion data has changed.
NotifyWinEvent(EVENT_OBJECT_LIVEREGIONCHANGED, hWndStatusLabel, OBJID_CLIENT, CHILDID_SELF);
// When the UI is destroyed…
void ClearCustomUIAProperties(HWND hDlg)
{
if (_pAccPropServices != nullptr)
{
// Clear all the properties we set on the controls.
MSAAPROPID propsButton[] = {
Name_Property_GUID,
HelpText_Property_GUID,
ItemStatus_Property_GUID };
_pAccPropServices->ClearHwndProps(
GetDlgItem(hDlg, IDC_BUTTON_CONNECTION),
OBJID_CLIENT,
CHILDID_SELF,
propsButton,
ARRAYSIZE(propsButton));
MSAAPROPID propsLabel[] = { LiveSetting_Property_GUID };
_pAccPropServices->ClearHwndProps(
GetDlgItem(hDlg, IDC_LABEL_CONNECTIONSTATUS),
OBJID_CLIENT,
CHILDID_SELF,
propsLabel,
ARRAYSIZE(propsLabel));
_pAccPropServices->Release();
_pAccPropServices = NULL;
}
}
If I then point the Inspect SDK tool to the Button, I find the custom Name, HelpText and ItemStatus properties set as required, as shown in the screenshot below.
Figure 3: The Inspect SDK tool reporting custom UIA Name, HelpText and ItemStatus properties set on a Button control.
I can also set up the AccEvent SDK tool to report UIA LiveRegionChanged events raised by my UI, and show me the UIA LiveSetting property of the element that raised the event. The event reported by AccEvent in response to executing the code snippet above is as follows:
UIA:AutomationEvent [LiveRegionChanged] Sender: ControlType:UIA_TextControlTypeId (0xC364), Name:”No connection is available. Try again later.”, LiveSetting:Assertive (2)
Below are the announcements made by Narrator when I interact with the UI. First Narrator tells me I’ve reached the Connection button, and then after a brief pause, it announces the custom HelpText and ItemStatus of the Button. When I invoke the Button with a press of the Spacebar, the status label changes, and Narrator announces the new value of the status label.
Connection button,
Establish a network connection if possible, Disconnected,
Space
No connection is available. Try again later.,
All in all, this is pretty powerful customization of the accessibility of the Win32 UI.
Other examples of where you might set custom UIA properties on a Win32 control might be:
- FullDescription_Property_GUID
- IsControlElement_Property_GUID, IsContentElement_Property_GUID: By setting both of these to false, the control will only be exposed through the Raw view of the UIA tree, and as such Narrator will ignore the element. So these properties might be set on some custom container control which you didn’t intend to expose to any of your customers.
Note that for any property values that change while the app is running, you’ll want to make sure that a related UIA PropertyChanged event is raised. Sometimes this can be done by calling NotifyWinEvent() with a WinEvent, and UIA will convert the event into a UIA event. In other cases, NotifyWinEvent() might be called with a UIA event directly. For example, with UIA_ItemStatusPropertyId.
Think very carefully before customizing the UIA ControlType of a control, (and by default, don’t do it). Typically a control of a particular type supports UIA patterns associated with that ControlType. For example, a CheckBox supports the UIA Toggle pattern, and a ComboBox supports the UIA ExpandCollapse pattern. Customizing the ControlType property will not add support for the associated patterns, and so if you do change the exposed ControlType, your customers might be left wondering why they can’t perform the expected actions at the control.
And by the way, trying to set one of the Is*PatternAvailable properties on the control in the hope that this will add all the pattern support associated with the custom control type, won’t work. SetHwndProp() will return failure in that case.
So while you can’t completely customize the accessibility of your Win32 controls by using SetHwndProp() and SetHwndPropStr(), you can do a great deal of useful stuff by calling them.
Incrementally adding UIA support to a control that already supports MSAA
If you really want to, you could turn a control into a native UIA provider. When a UIA client like Narrator wants to interact with your hwnd-based control, UIA will ask your control whether it supports UIA natively. It does this by sending WM_GETOBJECT with UiaRootObjectId, and if you’ve implemented all the required UIA provider interfaces, UIA will call all those interfaces directly.
In practice, I don’t tend to get asked questions about that. More commonly for devs with Win32 UI, I find that they already have an existing MSAA implementation, and they need to make some small but important enhancement to the Narrator experience at their UI. They understandably don’t want to have to completely replace the existing MSAA implementation with a new UIA implementation. So what they can consider using here is IAccessibleEx.
The IAccessibleEx interface exists to provide a way to add support for specific UIA functionality to an existing MSAA provider. (Technically the UI implementing IAccessible is known as an MSAA “server”, rather than a “provider”. But I tend to use “provider” for both MSAA and UIA these days, to avoid making the subject even more complicated than it already is.)
While supporting UIA functionality through IAccessibleEx can initially seem like a lot of work, in some cases it can be the most efficient way to add the support that your customer needs.
Example
Say I have a custom control whose visuals can be expanded and collapsed. I’ve already implemented IAccessible, and given it appropriate MSAA properties for such things as name and role, which map to UIA’s Name and ControlType. While I’ve also set MSAA state values as appropriate using STATE_SYSTEM_EXPANDED and STATE_SYSTEM_COLLAPSED, this does not lead to full support of the UIA ExpandCollapse pattern. A Narrator customer expects to be told the current expanded state of the control, and on a touch device, to be able to change the state through touch gestures. This is only possible if the control supports the UIA ExpandCollapse pattern.
With the code below, the following actions are possible:
- UIA determines that an IAccessibleEx object is available. It determines this by querying for that interface through IServiceProvider on the original MSAA object which implements IAccessible. The code below has a single object implementing all interfaces, but the object that implements IAccessible and IServiceProvider does not have to be the same object that implements the other interfaces.
- Having determined that an object is available which supports IAccessibleEx, UIA gets an IRawElementProviderSimple interface from the object supporting IAccessibleEx.
- UIA then calls IRawElementProviderSimple::GetPatternProvider() to get an object that implements IExpandCollapseProvider.
- UIA calls into the IExpandCollapseProvider.
The code below does not include an IAccessible implementation, as we’re interested here in adding the UIA support to an existing MSAA provider. (No implementation of IUnknown is included either, but some QueryInterface will have to handle all the interfaces of interest.)
The code assumes that the custom control does not have any child IAccessible elements.
class MyMSAAProvider : public
IAccessible,
IServiceProvider,
IAccessibleEx,
IRawElementProviderSimple,
IExpandCollapseProvider
{
public:
MyMSAAProvider(HWND hWnd);
// IUnknown.
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void ** ppvObject);
ULONG STDMETHODCALLTYPE AddRef(void);
ULONG STDMETHODCALLTYPE Release(void);
// IAccessible.
HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT * pctinfo);
HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo ** ppTInfo);
HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR * rgszNames, UINT cNames, LCID lcid, DISPID * rgDispId);
HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS * pDispParams,
VARIANT * pVarResult, EXCEPINFO * pExcepInfo, UINT * puArgErr);
HRESULT STDMETHODCALLTYPE get_accParent(IDispatch ** ppdispParent);
HRESULT STDMETHODCALLTYPE get_accChildCount(long * pcountChildren);
HRESULT STDMETHODCALLTYPE get_accChild(VARIANT varChild, IDispatch ** ppdispChild);
HRESULT STDMETHODCALLTYPE get_accName(VARIANT varChild, BSTR * pszName);
HRESULT STDMETHODCALLTYPE get_accValue(VARIANT varChild, BSTR * pbstrValue);
HRESULT STDMETHODCALLTYPE get_accDescription(VARIANT varChild, BSTR * pszDescription);
HRESULT STDMETHODCALLTYPE get_accRole(VARIANT varChild, VARIANT * pvarRole);
HRESULT STDMETHODCALLTYPE get_accState(VARIANT varChild, VARIANT * pvarState);
HRESULT STDMETHODCALLTYPE get_accHelp(VARIANT varChild, BSTR * pszHelp);
HRESULT STDMETHODCALLTYPE get_accHelpTopic(BSTR * pszHelpFile, VARIANT varChild, long * pidTopic);
HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut(VARIANT varChild, BSTR * pszKeyboardShortcut);
HRESULT STDMETHODCALLTYPE get_accFocus(VARIANT * pvarChild);
HRESULT STDMETHODCALLTYPE get_accSelection(VARIANT * pvarChildren);
HRESULT STDMETHODCALLTYPE get_accDefaultAction(VARIANT varChild, BSTR * pszDefaultAction);
HRESULT STDMETHODCALLTYPE accSelect(long flagsSelect, VARIANT varChild);
HRESULT STDMETHODCALLTYPE accLocation(long * pxLeft, long * pyTop, long * pcxWidth, long * pcyHeight, VARIANT varChild);
HRESULT STDMETHODCALLTYPE accNavigate(long navDir, VARIANT varStart, VARIANT * pvarEndUpAt);
HRESULT STDMETHODCALLTYPE accHitTest(long xLeft, long yTop, VARIANT * pvarChild);
HRESULT STDMETHODCALLTYPE accDoDefaultAction(VARIANT varChild);
HRESULT STDMETHODCALLTYPE put_accName(VARIANT varChild, BSTR szName);
HRESULT STDMETHODCALLTYPE put_accValue(VARIANT varChild, BSTR szValue);
// IServiceProvider. Provides access to an IAccessibleEx interface.
HRESULT STDMETHODCALLTYPE QueryService(REFGUID guidService, REFIID riid, LPVOID *ppvObject);
// IAccessibleEx interface.
HRESULT STDMETHODCALLTYPE GetObjectForChild(long idChild, IAccessibleEx ** pRetVal);
HRESULT STDMETHODCALLTYPE GetIAccessiblePair(IAccessible ** ppAcc, long * pidChild);
HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY ** pRetVal);
HRESULT STDMETHODCALLTYPE ConvertReturnedElement(IRawElementProviderSimple * pIn, IAccessibleEx ** ppRetValOut);
// IRawElementProviderSimple interface.
HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions * pRetVal);
HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID patternId, IUnknown ** pRetVal);
HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID propertyId, VARIANT * pRetVal);
HRESULT STDMETHODCALLTYPE get_HostRawElementProvider(IRawElementProviderSimple ** pRetVal);
// IExpandCollapseProvider interface.
HRESULT STDMETHODCALLTYPE Expand();
HRESULT STDMETHODCALLTYPE Collapse();
HRESULT STDMETHODCALLTYPE get_ExpandCollapseState(ExpandCollapseState * pRetVal);
private:
LONG _cRef = 0;
HWND _hWnd = NULL;
BOOL _fExpanded = FALSE;
};
MyMSAAProvider::MyMSAAProvider(HWND hWnd)
{
// Cache the hwnd associated with this custom Win32 Control.
_hWnd = hWnd;
}
// IServiceProvider implementation. Provides access to an IAccessibleEx interface.
HRESULT STDMETHODCALLTYPE MyMSAAProvider::QueryService(REFGUID guidService, REFIID riid, LPVOID *ppvObject)
{
if (ppvObject == NULL)
{
return E_INVALIDARG;
}
*ppvObject = NULL;
if (guidService == __uuidof(IAccessibleEx))
{
// This object implements both IServiceProvider and IAccessibleEx.
return this->QueryInterface(riid, ppvObject);
}
return E_NOINTERFACE;
};
// IAccessibleEx interface.
HRESULT STDMETHODCALLTYPE MyMSAAProvider::GetObjectForChild(long idChild, IAccessibleEx ** pRetVal)
{
// This implementation does not have any children.
return NULL;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::GetIAccessiblePair(IAccessible ** ppAcc, long * pidChild)
{
// This element is not a child element.
*pidChild = CHILDID_SELF;
return this->QueryInterface(IID_IAccessible, (LPVOID*)ppAcc);
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::GetRuntimeId(SAFEARRAY ** pRetVal)
{
// MSDN states that it’s ok to not implement this method, but UIA is less efficient as
// a result.
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::ConvertReturnedElement(
IRawElementProviderSimple * pIn, IAccessibleEx ** ppRetValOut)
{
*ppRetValOut = NULL;
return E_NOTIMPL;
}
// IRawElementProviderSimple interface.
HRESULT STDMETHODCALLTYPE MyMSAAProvider::get_ProviderOptions(ProviderOptions * pRetVal)
{
// This example assumes that we’re always running in the provider process.
// If in some cases the code can run in the UIA client process, this will
// need to be updated.
return ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::GetPatternProvider(PATTERNID patternId, IUnknown ** pRetVal)
{
// This custom control only adds support for the UIA ExpandCollapse pattern.
if (patternId == UIA_ExpandCollapsePatternId)
{
return this->QueryInterface(IID_IUnknown, (LPVOID*)pRetVal);
}
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::GetPropertyValue(PROPERTYID propertyId, VARIANT * pRetVal)
{
// This IRawElementProviderSimple only supplies a custom UIA pattern implementation.
// and does not supply any custom UIA properties. Do NOT return E_NOTIMPL here.
pRetVal->vt = VT_EMPTY;
return S_OK;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::get_HostRawElementProvider(IRawElementProviderSimple ** pRetVal)
{
return UiaHostProviderFromHwnd(_hWnd, pRetVal);
}
// IExpandCollapseProvider interface.
// IMPORTANT: The sample code adds support for the ExpandCollapse pattern. Typically apps
// also have visual UI which must be kept in sync with the programmatic state. This means
// that whenever one of the visual state or programmatic state changes, the other of the
// two must be updated. The related implementation depends on the design of the control.
// For example, while this sample code caches a current state through _fExpanded, you
// could decide to have no cached state. Instead whenever the get_ExpandCollapseState()
// is called, you query the control for its current expanded state. (Also, when your
// Expand() and Collapse() are called, you call into the control to tell it to change
// its visual state.
// IMPORTANT: Whenever the expanded state of the control changes, an ExpandCollapseState
// property state changed event must be raised to let UIA client apps know of the change.
// Care must be taken to make sure the event is only raised once when the state changes,
// despite it needing to be raised in response to action through either UIA or visual UI.
HRESULT STDMETHODCALLTYPE MyMSAAProvider::Expand(void)
{
_fExpanded = TRUE;
// Always raise the event AFTER setting the new state on the element. (Some UIA
// clients ask UIA to cache the current state of the element beneath the call to
// raise the event.)
NotifyWinEvent(UIA_ExpandCollapseExpandCollapseStatePropertyId, _hWnd, OBJID_CLIENT, CHILDID_SELF);
return S_OK;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::Collapse(void)
{
_fExpanded = FALSE;
NotifyWinEvent(UIA_ExpandCollapseExpandCollapseStatePropertyId, _hWnd, OBJID_CLIENT, CHILDID_SELF);
return S_OK;
}
HRESULT STDMETHODCALLTYPE MyMSAAProvider::get_ExpandCollapseState(ExpandCollapseState * pRetVal)
{
*pRetVal = (_fExpanded ? ExpandCollapseState_Expanded : ExpandCollapseState_Collapsed);
return S_OK;
}
Other options
The above approaches for enhancing the accessibility of Win32 UI are the ones I’ve hit most commonly in practice.
Another option I’ve implemented once, a long time ago, is Value Map Annotation, which is another type of Dynamic Annotation. I used it to map the values on a slider to strings which the customer will find more meaningful. So instead of the customer using Narrator hearing “one”, “two”, “three” and “four” as they interact with the slider, they might hear (say) “Tiny”, “Small”, Medium” and “Large”. It worked great, so I’d recommend it if you have need of such a thing,
Summary
Say you have existing Win32 UI, and you want to enhance its accessibility. Often you don’t want to invest in building a full native UIA implementation, given that you only need to address specific issues with the UI. (Note that if you did have a full native UIA implementation, you could call UiaRaiseAutomationEvent() to make UIA client apps like Narrator aware of changes in your UI, rather than calling the NotifyWinEvent() mentioned below.)
So consider the following options first:
- Replace custom UI with a standard Win32 control which is accessible by default. Perhaps the custom UI isn’t really essential.
- If some control has no accessible name, consider whether a visible or hidden label added immediately before the control could be used to provide the control with an accessible name.
- Consider whether use of SetHwndProp() or SetHwndPropStr() could be used to customize specific UIA properties on a control.
- If you already have an IAccessible implementation, consider whether support for targeted UIA patterns could be added through use of IAccessibleEx.
- If you need to raise an event to make a screen reader aware of a change in your UI, call NotifyWinEvent, passing in WinEvent ids or in some cases UIA event ids, as listed at Event Constants.
Thanks for helping everyone benefit from all the great features of your Win32 app!
Guy
Posts in this series: