大家好,我是张赐荣。
作为一名专注于无障碍优化的工程师,在日常工作中经常会遇到需要为应用添加读屏软件可识别的控件标签的需求。本文将分享我在C/C++中实现这一需求的经验。
在c/c++开发的Windows原生应用程序中,通过实现IAccessible
接口并处理WM_GETOBJECT
消息,我们可以为控件提供可访问性标签,从而让读屏软件用户理解控件的作用,提升读屏使用体验。以下是实现这一过程的具体步骤。
背景知识
在Windows无障碍API中,IAccessible
接口是Microsoft Active Accessibility(MSAA)的一部分,专为读屏软件等辅助技术服务。通过此接口,开发者可以向辅助技术提供控件的详细信息,例如名称、角色和状态等。本文将介绍如何在C/C++中实现IAccessible
接口,特别是如何实现get_accName
方法以便为控件提供标签。
实现步骤
定义并继承IAccessible接口
首先,我们需要定义一个类并使其继承自IAccessible
接口。此接口包含许多方法,例如get_accName
、get_accRole
等,用于描述控件的各种属性。在本实现中,我们仅需关注get_accName
方法,以返回控件的自定义标签。对于暂时不需要的其他方法,可以简单地返回E_NOTIMPL
。
以下是定义IAccessible
接口的代码:
#include <windows.h>
#include <oleacc.h>
class MyAccessibleControl : public IAccessible {
public:
// 实现IAccessible中的方法
IFACEMETHODIMP get_accName(VARIANT varChild, BSTR* pszName) override;
// 其他方法可以直接返回E_NOTIMPL
};
在这里,定义了一个名为MyAccessibleControl
的类,它继承自IAccessible
并且重点实现了get_accName
方法。此方法将为控件提供读屏可以朗读的名称。
实现get_accName方法,定义控件的标签
get_accName
方法用于返回控件的标签。在读屏软件读取控件时,此标签将被朗读。这个方法包含两个参数:
varChild
:用于确定请求的对象。如果varChild.vt
是VT_I4
且lVal
等于CHILDID_SELF
,则表示请求控件本身的标签。pszName
:标签名的缓冲区。我们可以通过此参数为读屏软件设置标签内容。
以下代码演示了一个标签为“测试标签”的实现:
IFACEMETHODIMP MyAccessibleControl::get_accName(VARIANT varChild, BSTR* pszName) {
if (varChild.vt == VT_I4 && varChild.lVal == CHILDID_SELF) {
*pszName = SysAllocString(L"测试标签"); // 设置标签内容
return S_OK; // 返回S_OK表示调用成功
}
return E_NOTIMPL; // 其他情况返回未实现
}
处理WM_GETOBJECT消息
为了让读屏软件调用我们自定义的IAccessible
对象,我们需要在控件的窗口过程中处理WM_GETOBJECT
消息。当读屏软件聚焦到控件时,它会发送WM_GETOBJECT
消息,要求获取该控件的无障碍对象。我们通过拦截此消息并返回自定义的IAccessible
对象来响应请求。
- 在窗口过程函数中拦截
WM_GETOBJECT
消息。 - 使用
LresultFromObject
函数将IAccessible
对象返回给读屏软件。
以下是具体代码:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
static MyAccessibleControl* iAccessible = nullptr;
switch (uMsg) {
case WM_CREATE:
iAccessible = new MyAccessibleControl(); // 初始化IAccessible对象
break;
case WM_GETOBJECT:
if (lParam == OBJID_CLIENT) {
// 返回IAccessible对象
return LresultFromObject(IID_IAccessible, wParam, static_cast<IAccessible*>(iAccessible));
}
break;
case WM_DESTROY:
delete iAccessible; // 释放资源
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
在WM_CREATE
消息处理中,我们初始化了自定义的IAccessible
对象;在WM_GETOBJECT
消息处理中,我们通过LresultFromObject
将其返回;在WM_DESTROY
消息处理中释放资源,确保内存的正确管理。
测试验证
- 编译并运行程序。
- 使用读屏软件(例如NVDA)测试,通过读屏软件聚焦控件,听到朗读为“测试标签”。
完整代码示例
以下是完整的示例代码,包含了窗口的创建、IAccessible接口的实现以及消息处理。
#include <windows.h>
#include <oleacc.h>
class MyAccessibleControl : public IAccessible {
public:
IFACEMETHODIMP get_accName(VARIANT varChild, BSTR* pszName) override {
if (varChild.vt == VT_I4 && varChild.lVal == CHILDID_SELF) {
*pszName = SysAllocString(L"测试标签");
return S_OK;
}
return E_NOTIMPL;
}
// 其他方法省略
};
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
static MyAccessibleControl* iAccessible = nullptr;
switch (uMsg) {
case WM_CREATE:
iAccessible = new MyAccessibleControl();
break;
case WM_GETOBJECT:
if (lParam == OBJID_CLIENT) {
return LresultFromObject(IID_IAccessible, wParam, static_cast<IAccessible*>(iAccessible));
}
break;
case WM_DESTROY:
delete iAccessible;
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"AccessibleWindowClass";
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0,
L"AccessibleWindowClass",
L"Accessible Window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
nullptr,
nullptr,
hInstance,
nullptr
);
ShowWindow(hwnd, nCmdShow);
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
小结
通过实现IAccessible
接口并处理WM_GETOBJECT
消息,我们为控件添加了可访问性标签,使其能够被读屏软件识别并朗读。这一过程显著提升了应用的可访问能力,帮助视障用户更好地理解和使用控件。
在无障碍优化中,清晰明了的标签至关重要。我们应始终遵循以下最佳实践:
- 保持标签简洁和明确:让标签直观且符合控件功能。
- 充分测试和验证:使用不同的读屏软件验证标签的准确性。
- 考虑多语言支持:对不同语言的用户提供适配。