确保正确命名 UI 元素

本主题介绍在 Microsoft Win32 应用程序中指定 UI 元素名称的正确方法,使 Microsoft Active Accessibility 可以通过 IAccessible Name 属性准确向客户端应用程序公开名称。

本节中的信息仅适用于 Microsoft Active Accessibility。 不适用于使用 Microsoft UI 自动化或基于标记语言(如 HTML、动态 HTML [DHTML] 或 XML)的应用程序。

概述

在 Microsoft Active Accessibility 中,应用程序中的每个 UI 元素由公开 IAccessible 接口的对象表示。 客户端应用程序使用 IAccessible 接口的属性和方法与 UI 元素交互并检索有关它的信息。 IAccessible 接口公开的最重要属性之一是 Name 属性。 客户端应用程序依赖于 Name 属性以查找、标识或向用户通告 UI 元素。 如果 Microsoft Active Accessibility 无法正确公开特定 UI 元素的 Name 属性,客户端应用程序将无法向用户显示该 UI 元素,并且残疾用户将无法访问 UI 元素。

命名不正确导致问题的方式

为了说明 UI 元素命名不正确导致的问题,请考虑下图中显示的名称条目窗体。

用于输入名字和姓氏的简单表格的图示

尽管窗体中的 UI 元素看起来正常,但编程实现是不正确的。 对于 Microsoft Active Accessibility 客户端(如屏幕阅读器),顶部编辑控件的 Name 属性为“姓氏:”,而底部编辑控件的 Name 属性是空字符串 ("")。 屏幕阅读器会将顶部编辑控件读取为“姓氏”(尽管用户应键入名字)。 屏幕阅读器将第二个编辑控件读取为“无名称”,因此用户不知道要在第二个编辑控件中键入的内容。 屏幕阅读器无法帮助用户将数据输入到此简单窗体中。

该窗体的另一个问题是没有给任何一个编辑控件分配快捷键。 用户必须按 Tab 切换至控件或使用鼠标。

以下各节介绍了这些问题的来源,并提供了更正这些问题的指南。

MSAA 如何获取 Name 属性

Microsoft Active Accessibility 根据 UI 元素的类型从不同的位置获取 Name 属性字符串。 对于大多数具有关联窗口文本的 UI 元素,Microsoft Active Accessibility 使用窗口文本作为 Name 属性字符串。 此类 UI 元素的示例包括按钮、菜单项和工具提示等控件。

对于以下控件,Microsoft Active Accessibility 忽略窗口文本,而是按照 Tab 键顺序查找紧邻控件之前的静态文本标签(或分组框标签)。

  • 组合框
  • 日期和时间选取器
  • 编辑和富编辑控件
  • IP 地址控件
  • 列表框
  • 列表视图
  • 进度栏
  • 滚动条
  • 具有 SS_ICON 或 SS_BITMAP 样式的静态控件
  • 跟踪栏
  • 树视图

如果前述控件没有附带静态文本标签,或者标签未正确实现,Microsoft Active Accessibility 无法向客户端应用程序提供正确的 Name 属性

前述大多数控件实际上都具有关联的窗口文本。 资源编辑器自动生成窗口文本,该文本由泛型字符串(如“edit1”或“listbox3”)组成。 尽管开发人员可以将生成的窗口文本替换为更有意义的文本,大多数开发人员从不这样做。 由于生成的窗口文本对用户没有意义,因此 Microsoft Active Accessibility 忽略它,并改用随附的静态文本标签。

如何查找和更正命名问题

在“命名不正确导致问题的方式”中所示的名称条目窗体中,问题的原因是控件的 Tab 键顺序不正确。 使用测试工具(如 Inspect)检查 UI 将揭示对象层次结构的问题。 以下屏幕截图显示 Inspect 中出现的名称条目窗体的损坏对象层次结构。

检查工具屏幕截图,其中显示了名称输入表的对象层次结构不正确

在上一个屏幕截图中,请注意,对象层次结构与控件在名称输入窗体的用户界面中显示的结构不匹配。 另请注意,Inspect 将不正确的名称分配给了倒数第二个项(它是用于输入名字的编辑控件,应命名为“名字:”)。 最后,请注意,Inspect 找不到最后一项的名称(它是用于输入姓氏的编辑控件,其名称应为“姓氏:”)。

以下示例显示了名称条目窗体的资源文件的内容。 请注意,Tab 键顺序与控件在用户界面中显示的逻辑结构不一致。 另请注意,没有为这两个编辑控件指定快捷键。

IDD_INPUTNAME DIALOGEX 22, 17, 312, 118
STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
CAPTION "Enter your name"
FONT 8, "System", 0, 0, 0x0
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,179,35,30,11,WS_GROUP
    LTEXT           "First Name:",IDC_STATIC,8,16,43,8
    LTEXT           "Last Name:",IDC_STATIC,8,33,43,8
    EDITTEXT        IDC_EDIT1,53,15,120,12,ES_AUTOHSCROLL
    EDITTEXT        IDC_EDIT2,53,34,120,12,ES_AUTOHSCROLL
END

若要更正名称输入窗体的问题,应编辑资源 (.rc) 文件以指定键盘快捷方式,并且控件应按以下顺序放置:

  1. “&名字:”静态文本标签。
  2. 用于输入名字 (IDC_EDIT1) 的编辑控件。
  3. “&姓氏:”静态文本标签。
  4. 用于输入姓氏 (IDC_EDIT2) 的编辑控件。
  5. “确定”默认推送按钮。

以下示例显示了名称条目窗体的更正资源文件:

IDD_INPUTNAME DIALOGEX 22, 17, 312, 118
STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
CAPTION "Enter your name"
FONT 8, "System", 0, 0, 0x0
BEGIN
    LTEXT           "&First Name:",IDC_STATIC,8,16,43,8
    EDITTEXT        IDC_EDIT1,53,15,120,12,ES_AUTOHSCROLL
    LTEXT           "&Last Name:",IDC_STATIC,8,33,43,8
    EDITTEXT        IDC_EDIT2,53,34,120,12,ES_AUTOHSCROLL
    DEFPUSHBUTTON   "OK",IDOK,179,35,30,11,WS_GROUP
END

若要对资源文件进行更正,可以直接编辑文件,也可以在 Microsoft Visual Studio 中使用 Tab Order 工具。 可以通过按 Ctrl+D 或在“格式”菜单中选择“Tab Order”来访问 Visual Studio 中的 Tab Order 工具

更正并重新生成应用程序后,名称输入窗体的 UI 将看起来与以前一样。 但是,Microsoft Active Accessibility 现在将为客户端应用程序提供正确的 Name 属性,并在用户按 Alt+F 或 Alt+L 键盘快捷方式时正确设置焦点。 此外,Inspect 将显示正确的对象层次结构,如以下屏幕截图所示。

可访问资源管理器工具的屏幕截图,其中显示了名称条目表的正确对象层次结构

如何正确命名跟踪栏

定义跟踪栏(或滑块)时,请确保跟踪栏的主静态文本标签显示在跟踪栏之前,并且最小和最大范围的静态文本标签显示在跟踪栏之后。 请记住,Microsoft Active Accessibility 使用紧邻控件之前的静态文本标签作为控件的 Name 属性。 紧邻跟踪栏之前放置主静态文本标签,并在跟踪栏之后放置其他标签,这样可确保 Microsoft Active Accessibility 向客户端提供正确的 Name 属性。

下图显示了一个典型的跟踪栏,其中包含名为“Speed”的主静态文本标签,以及最小(“min”)和最大(“max”)范围的静态文本标签。

轨迹栏控件的图示,具有主标签以及最小范围和最大范围标签

以下示例显示了在资源文件中定义跟踪栏及其静态文本标签的正确方法:

BEGIN
    ...

    LTEXT           "&Speed",IDC_STATIC,47,20,43,8
    CONTROL         "",IDC_SLIDER1,"msctls_trackbar32",
                    TBS_AUTOTICKS | TBS_BOTH | WS_TABSTOP,
                    32,32,62,23
    LTEXT           "min",IDC_STATIC,16,37,15,8
    LTEXT           "max",IDC_STATIC,94,38,43,8

    ...
END

如何使用不可见标签命名控件

每个控件都有一个可见的标签并不总是可能或可取的。 例如,有时添加标签可能会导致 UI 外观发生不良更改。 在这种情况下,可以使用不可见标签。 Microsoft Active Accessibility 仍将选取与不可见标签关联的文本,但该标签不会出现在可视 UI 中,也不会干扰可视 UI。

与可见标签一样,不可见标签必须按 Tab 键顺序紧邻于控件之前。 若要使标签在资源文件 (.rc) 中不可见,请将 NOT WS_VISIBLE|~WS_VISIBLE 添加到静态文本控件的样式部分。 如果在 Visual Studio 中使用资源编辑器,可以将 Visible 属性设置为 False。

如何使用直接注释指定 Name 属性

Microsoft Active Accessibility 运行时组件 Oleacc.dll 中包含的默认代理自动为所有标准 Windows 控件提供一个 IAccessible 对象。 如果自定义标准 Windows 控件,则默认代理将尽力准确地为自定义控件提供所有 IAccessible 属性。 应全面测试自定义控件,以确保默认代理提供准确且完整的属性值。 如果测试显示不准确或不完整的属性值,则可以使用称为直接注释的动态注释技术来提供正确的属性值并添加缺少的属性值。

请注意,动态注释不仅适用于 Microsoft Active Accessibility 代理支持的控件。 还可以使用它来修改或提供任何提供自己的 IAccessible 实现的控件的属性。

本节重点介绍如何使用直接注释为控件的 IAccessible 对象的 Name 属性提供正确的值。 也可以使用直接注释来提供其他属性值。 此外,除了直接注释之外,其他动态注释技术也可用,动态注释 API 的特性和功能远远超出了本节所述。 有关动态注释的详细信息,请参阅动态注释 API

批注 Name 属性的步骤

使用直接注释更改控件的 Name 属性涉及以下步骤。

  1. 包括以下头文件:

    • Initguid.h
    • Oleacc.h

    注意

    若要定义 GUID,必须在同一文件中将 Initguid.h 包含在 Oleacc.h 之前。

     

  2. 通常在应用程序初始化过程中,通过调用 CoInitializeEx 函数初始化组件对象模型 (COM) 库。

  3. 创建目标控件后不久(通常在 WM_INITDIALOG 消息期间),创建注释管理器的实例并获取指向其 IAccPropServices 指针的指针。

  4. 使用 IAccPropServices::SetHwndPropStr 方法批注目标控件的 Name 属性

  5. 释放 IAccPropServices 指针。

  6. 在销毁目标控件之前(通常在处理 WM_DESTROY 消息时),创建注释管理器的实例并获取指向其 IAccPropServices 接口的指针。

  7. 使用 IAccPropServices::ClearHwndProps 方法从目标控件中清除 Name 属性注释。

  8. 释放 IAccPropServices 指针。

  9. 在应用程序退出之前(通常在处理 WM_DESTROY 消息时),通过调用 CoUninitialize 函数释放 COM 库。

IAccPropServices::SetHwndPropStr 函数采用五个参数。 前三个参数(hwnd、idObject 和 idChild)合并以标识控件。 第四个参数 idProp 指定要更改的属性的标识符。 若要更改 Name 属性,请将 idProp 设置为“PROPID_ACC_NAME”。 (有关可以通过直接注释设置的其他属性的列表,请参阅使用直接注释。)SetHwndPropStr 的最后一个参数 str 是用作 Name 属性的新字符串

批注 Name 属性的示例

以下示例代码显示如何使用直接注释更改控件的 IAccessible 对象的 Name 属性。 为了简单起见,该示例使用硬编码字符串(“新建控件名称”)来设置 Name 属性。 硬编码字符串不应在应用程序的最终版本中使用,因为它们无法本地化。 而始终从资源文件加载字符串。 此外,该示例不显示对 CoInitializeExCoUninitialize 函数的调用。

#include <initguid.h>
#include <oleacc.h>

// AnnotateControlName - Uses direct annotation to change the Name property 
// of the IAccessible object for a control.
//
// hDlg - Handle of the dialog box that contains the control.
// hwndCtl - Handle of the control whose Name property is to be changed.
HRESULT AnnotateControlName(HWND hDlg, HWND hwndCtl)
{
    HRESULT hr;        

    IAccPropServices *pAccPropSvc = NULL;  

    // Create an instance of the annotation manager and retrieve the 
    // IAccPropServices pointer.
    hr = CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, 
        IID_IAccPropServices, (void **) &pAccPropSvc);

    if (hr != S_OK || pAccPropSvc == NULL)
        return hr;

    // Set the Name property for the control.
    // Note: A hard-coded string is used here to keep the example simple.
    // Always use localizable string resources in your applications. 
    hr = pAccPropSvc->SetHwndPropStr(hwndCtl, OBJID_CLIENT, CHILDID_SELF, 
        PROPID_ACC_NAME, L"New Control Name");

    pAccPropSvc->Release();
    
    return hr;
}

// RemoveAnnotatedNameFromControl - Removes the annotated name from the 
// Name property of the IAccessible object for a control.
//
// hDlg - Handle of the dialog box that contains the control.
// hwndCtl - Handle of the control whose annotated name is to be removed.
HRESULT RemoveAnnotatedNameFromControl(HWND hDlg, HWND hwndCtl)
{
    HRESULT hr;

    IAccPropServices *pAccPropSvc = NULL;

    // Create an instance of the annotation manager and retrieve the 
    // IAccPropServices pointer.
    hr = CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, 
        IID_IAccPropServices, (void **) &pAccPropSvc);

    if (hr != S_OK || pAccPropSvc == NULL)
        return hr;

    // Remove the annotated name from the Name property for the control.
    MSAAPROPID propid = PROPID_ACC_NAME;
    hr = pAccPropSvc->ClearHwndProps(hwndCtl, OBJID_CLIENT, CHILDID_SELF, 
        &propid, 1);

    // Release the annotation manager.
    pAccPropSvc->Release();

    return hr;
}