为可访问性编码:用JFC/Swing将可访问性构建到您的Java应用

所有 Java 应用程序对于残疾人士都应该是可访问的。用 GUI 应用程序实现这一点时 需要格外注意。本文向您展示了如何使用基于 JFC/Swing 的可访问性工具箱事半功倍地 实现最高级别的可访问性。

大多数基于 GUI 的软件设计都基于这一假设:即用户可以清楚地看到屏幕并且可以有 效地使用鼠标选择图形用户界面(GUI)上的选项。对于许多残疾人士,尤其是那些视力 和运动控制受损的人,这个假设是有问题的。1998 年,美国康复法案(Rehabilitation Act)经过修改(请参阅侧栏的 508 条款),以确保残疾人可以访问政府使用的电子和信 息技术产品。因此,许多 IT 企业已经开始采用可访问性指导原则作为其整个 GUI 设计 标准的一部分。自从美国康复法案修正案通过以后,可访问性也成为商业软件设计中日益 重要的问题,从而导致了 Java 平台的一些更改和添加。

本文有助于您快速了解一些联邦政府的可访问性要求,并且也有助于您使用 JFC/Swing 构建满足那些要求的 GUI。我开发了两个基于 Swing 的工具箱来辅助实现可 访问性功能;这两个工具箱如下:

com.ibm.wac.AccessibilityUtils:一组可重用于任何 Swing GUI 的通用实用程序

com.ibm.wac.demos.AccessibilityDemo1:演示应用程序,它包括一组更特定于应用 程序的实用程序,这些实用程序可重用于特定 GUI 中的类似结构

尽管您将在 AccessibilityDemo1 中看到的许多方法都是为单个应用程序创建的,但 很容易使它们一般化以支持多个 GUI。实际上,本文使用的所有代码(请参阅 参考资料 )都是开放源码,欢迎修改这些工具箱以便为您所用。

因为 JFC/Swing 是本文中讨论的所有 GUI 开发的基础,所以假定您熟悉使用 Swing 编程的基本概念。并进一步假定您正在使用 Java 版本 1.3.1 或更新的版本,因为我们 将在这里讨论的部分方法在 Java 平台的较早版本中不可用。

AWT 的可访问性支持

本文中的所有 GUI 构造都是基于 JFC/Swing 的。目前,AWT 对可访问性功能提供有 限的支持。尽管 AWT 组件支持 Accessible 接口,但它们并没有完全实现 AccessibleContext 和其它 Accessible 类。因此,许多辅助性技术不能有效地处理 AWT GUI。

重新审视您的 GUI

因为大多数 GUI 面向视力正常的人,所以对于弱视者或盲人,它们通常是作用有限的 或无用的。同样,大多数 GUI 设计依赖鼠标进行导航,对于运动和视力有残疾的人,这 会成为障碍。在本文中,我们将研究一些将可访问性功能添加到简单 GUI 的方法,侧重 于那些针对视力和运动有残疾人士的功能。

“IBM Guidelines for Writing Accessible Applications Using 100% Pure Java”(请参阅 参考资料)描述了可使残疾人士访问 Java 应用程序的应用程序设 计和编码指南。在这些指南中,我们将侧重于下列内容:

为所有操作提供等价的键盘操作

在组件上设置助记符

为常用功能使用加速键

提供逻辑键盘跳格顺序

提供逻辑组件布局(用于多媒体访问)

标记组件

命名逻辑组

提供语义关系

描述图标与图形

盲人用户界面

盲人必须能够以不依赖于视觉反馈的方式访问应用程序的功能。针对盲人的最常用的 辅助技术是“文本到语音”屏幕阅读器、可刷新的布莱叶盲文显示系统或相关 的 I/O 设备。要使您的 Java 应用程序可访问,您需要以标准代码格式描述每个应用程 序组件,这种格式可由辅助技术(AT)设备转换。例如,对于 GUI 组件(如按钮),您 需要命名和描述其功能,然后将这些组件作为诸如“发送”、“进入 ”或“退出”之类的消息传递给用户。

一些 GUI 应用程序组件传达信息时比其它组件需要考虑更多的东西。例如,盲人如何 才能访问表中可视化格式的信息,或图标、树或者滚动列表呢?要使这类组件可访问,要 求您提供大量的文本形式的描述性信息。尽管这个任务可能很乏味,但它是迈向创建支持 可访问性应用程序必不可少的步骤。

添加描述性信息

使您的应用程序对视力受损者可访问的第一步,是提供对将要接收焦点的组件的描述 。当用户或 AT 阅读器选择组件时(通常是通过键盘控制装置),则组件接收焦点。接收 焦点的组件对应用程序功能(而非其设计或布局)是不可或缺的。因此,打个比方说,包 含其它组件的 JPanel 本身不接收焦点,尽管其内部的个别组件可能将接收焦点。另一方 面,如果面板对信息进行了分组,有时则需要使该分组可访问。类似地,通过使用 setLabelFor(Component) 方法可将标签与其它组件相关联。

在 Swing 中,我们使用 javax.Accessibility.Accessible 接口来提供关于应用程序 组件的描述性信息。所有 Swing 组件都实现 Accessible 接口,该接口只有一个方法 javax.Accessibility.AccessibleContext getAccessibleContext() 。使用 getAccessibleContext() 方法,AT 阅读器可以访问所有它需要的信息来将组件的描述呈 现给用户,并且与该组件交互和使用该组件。

AccessibleContext()

setAccessibleName(String name) 设置与给定 Accessible 对象相关联的 name。通 常,只要组件接收到焦点,辅助阅读器就会提供这个名称。

setAccessibleDescription(String description) 设置与给定 Accessible 对象相关 联的 description。通常,当用户要求关于组件的更多详细信息时,辅助阅读器将提供这 个描述。

标准 Swing 组件通常都为 AccessibleName 和 AccessibleDescription 提供了缺省 值。例如, JLabel 或 JTextField 文本将被用作其缺省的可访问名称。同样,任何组件 的 ToolTip 将被用作其缺省的可访问描述。但是,我的经验表明,缺省值不能为给定组 件提供最佳名称或描述,因此我建议您显式地设置您的组件值。

要设置文本域值,您需要输入一些类似于清单 1 所示的代码:

清单 1. 设置文本域值

import javax.swing.*;   :JTextField streetField = new JTextField("", 20);streetField.setName("streetField");streetField.getAccessibleContext().    setAccessibleName("Street Entry Field");streetField.getAccessibleContext().    setAccessibleDescription("Enter a street address");streetField.setToolTip("Street Address");   :-- set any other desired characteristics --

类似地,要设置按钮的值,您可以输入清单 2 中所示的代码:

清单 2. 设置按钮的值

import javax.swing.*;   :JButton kButton = new JButton("OK");okButton.setName("okButton");okButton.getAccessibleContext().setAccessibleName("OK Button");okButton.getAccessibleContext().setAccessibleDescription(    "Activate to commit changes and continue");okButton.setToolTip("Commit changes");okButton.setMnemonic((int)'O');   :-- set any other desired characteristics --

可访问键盘导航

通常 Swing 允许用跳格(Tab)、反向跳格和箭头键进行键盘导航。遗憾的是,这个 系统难以实现并且很费时,因为它要求用户导航所有中间组件才能到达它需要的那个组件 。对于更有效的键盘导航,用户应该能够迅速地在重要组件之间切换,而无须考虑它们在 GUI 布局中的顺序。我们可以将助记符键盘设置用于 javax.swing.AbstractButton 和 javax.swing.JLabel 的子类以及应用程序菜单中的项。助记符通常称为 加速键,因为它 们根据 GUI 内容直接进行工作。

在为您的界面建立了助记符系统之后,用户就可以通过使用 Alt 键和键盘上表示该组 件的助记符键(Alt+键)导航到任何想用的组件上。但是这种设置有一个问题,就是它对 于顶级组件(通常是 JFrame. 或 JDialog )而言是全局的。这意味着基本上只有 26 个 唯一值,却要分配给所有菜单和菜单项以及基本的 GUI 内容。在频繁使用的 GUI 上并非 所有组件都可以链接到助记符键,因此您必须确定哪些组件对于用户是最重要的,然后相 应的设置它们。我建议您为菜单项、重要的操作按钮(如 OK 或 Cancel)和 GUI 中每个 逻辑组中的初始组件创建助记符链接,然后让用户跳格到其它每个组件上。

设置跳格顺序和初始焦点

对于大多数基于跳格的逻辑导航,我建议您将组件按您希望跳格选择的顺序添加到容 器中。您可能希望以相同的方式组织嵌套的容器(即 JPanel )。尽管从上到下、从左向 右(T2B、L2R)的顺序是标准的,但您可能希望建立不同的系统,如基于列排列的系统。 您可以使用方法 JComponent.setNextFocusableComponent(Component c) (或 Java 1.4 中的类 java.awt.FocusTraversalPolicy )来强制规定定制的跳格顺序。 AccessibilityDemo1 GUI说明了一个跳格系统,该系统基于将组件以 T2B、L2R 顺序添加 到容器中。

在定义了跳格顺序之后,您需要确保每个初始组件都在选中其容器时接收到焦点。当 容器接收到焦点时(请参阅 参考资料以获取关于 FocusListener 的更多信息),它应该 向期望的初始组件发出 java.awt.Component.requestFocus() (在 Java 1.4 中是 java.awt.Component.requestFocusInWindow() )。

另一种方法是在窗口激活时设置初始焦点。例如,下列代码将 WindowListener 添加 到 JFrame. ,后者在窗口被激活时为 JTextField 请求焦点。

清单 3. 在激活时设置初始焦点

import java.awt.event.*;import javax.swing.*;   :JFrame. frame. = new JFrame();JTextField field = new JTextField(); /// field to get initial focusboolean focusSet;   :frame.addWindowListener(new WindowAdapter() {   public void windowActivate() {     if ( !focusSet ) {       field.requestFocus();       focusSet = true;     }   }}

如果您希望将初始焦点设置到按钮而不是 JTextField ,则可以设置 DefaultButton 字段,如下所示:

清单 4. 设置 DefaultButton 字段

import java.awt.event.*;import javax.swing.*;   :JFrame. frame. = new JFrame();JButton button = new JButton();   :panel.add(button);   :frame.getRootPane().setDefaultButton(button);

您只需设置初始焦点一次,因为 Swing 始终会将焦点恢复到初始的设置。

可访问性演示

图 1 中显示的可访问性演示 GUI 并不打算完成任何实际工作(即,在该演示背后并 没有功能代码);相反,它的目的是演示大多数 Swing GUI 组件,并向您展示如何将可 访问性信息添加到每个组件。图 1 显示了演示应用程序的一个面板,其中包括几种常用 组件类型,如输入域、单选按钮、复选框和按钮。

图 1. AccessibilityDemo1 的第一幅抓屏

注:在这个 GUI 中,所有按钮都使用了助记符(加下划线的字母),因此,它过度使 用了助记符。正如我先前提到的,助记符最好仅用于经过精心挑选的组件,以辅助组件组 之间的总体导航。此外,尽管有可能通过标准键盘支持选择跳格,但这样做会需要多次击 键。要改进跳格窗格的可用性,您可以通过在 javax.swing.JTabbedPane 或其容器上注 册 java.awt.event.KeyListener 来添加直接选择跳格的键。

图 2 是演示应用程序的另一个面板,它包含更复杂的组件类型,如分割窗格、树和显 示 HTML 内容的编辑器窗格。图 2 还显示了已定义的组件 ToolTip。

图 2. AccessibilityDemo1 的第 2 幅抓屏

在不访问 AT 阅读器的情况下,很难看到添加可访问性信息的结果。我将在本文的稍 后部分中向您展示如何显示该信息。目前,图 3 展示了将可访问性信息添加到应用程序 的结果。它是图 1 中 New Document 工具栏按钮( )的显示所 产生的输出的一部分。

图 3. AccessibilityDemo1 的 HTML 转储的子集输出

级别 索引 对象 6 0

javax.swing.JToolBar:17FAFF1D-toolBar0 中的 *javax.swing.JButton:16E7BF1D-button5 组件字段 名称 值 name button5 text toolTipText Create a new document value — 无 — mnemonic 78 = ‘N’ AccessibleContext 字段 名称 值 name New role push button stateSet enabled,focusable,visible,showing,opaque indexInParent 0 description Create a new document action javax.swing.JButton$AccessibleJButton@1bd8bf1d value javax.swing.JButton$AccessibleJButton@1bd8bf1d text null editableText — 无 — table null icon [Ljavax.Accessibility.AccessibleIcon;@2ae9ff1e relationSet — empty AccessibleRelationSet — childrenCount 0

文本的颜色表明该项的状态。蓝色文本表明无须担心。黄色文本表明该项可能会引起 可访问性问题。红色斜体文本(示例中未显示)表明该项很可能引起可访问性问题。

不应该将使用颜色和其它格式化增强(如使用 斜体)作为特殊文本的唯一表示。通常 AT 设备都不表示这些增强,因此它们可能不会受到注意。尽管这个示例中未作显示,但 我建议您除了使用颜色之外还要使用其它指示符,或使用其它指示符而不使用颜色。例如 ,您可以用括号或星号括住文本。

可访问性工具箱

为复杂 GUI 中的每个组件设置值很乏味冗长,这项工作经常会导致错误或完全地遗漏 某些重要步骤。为了纠正这一点,我创建了一个可访问性工具箱,这是一组实用程序方法 ,它们可以显著减少在您的 GUI 中提供可访问信息时需要的“set”方法的数 量。

下列实用程序方法是 com.ibm.wac.AccessibleUtils 类的 public static 成员:

setAccessibleValues(ResourceBundle rb, Accessible a, AccessibleValues av) 设置最常用的可访问组件值。

Accessible setMemberRelationship(Accessible group, Collection members) 创建 组组件和由集合定义的可访问对象集之间的成员关系。

Accessible setMemberRelationship(Accessible group, Accessible[] members) 创 建组组件和由数组定义的可访问对象之间的成员关系。

Accessible setLabelRelationship(Accessible label, Accessible target) 创建可 访问目标和标签之间的关系。 setLabelRelationship 通常用来为自身没有适当可访问信 息的组件提供可访问信息。它还允许通过键盘助记符访问那些不支持键盘助记符的组件( 例如, JTextField )。

在后面几节中,我们将仔细研究该工具箱的 setAccessibleValues() 方法,以了解它 是如何辅助创建和定义大量 GUI 组件的。在详细描述对 关系和 助记符支持章节中,您 还会大致了解到其它实用程序方法(以及它们的助手方法)是如何工作的。

使用 setAccessibleValues

setAccessibleValues() 方法有三个参数。 ResourceBundle 参数(出自 java.util )允许对国际化的自动支持;如果不需要进行文本转换,它将为 null。 Accessible 参 数由 setAccessibleValues() 方法更新。 AccessibleValues 参数(出自 com.ibm.wac )提供了最常用的可访问属性。您可以自由地将更常用的组件属性添加到这个集合中。

清单 5 显示了 AccessibleValues 类的精简版本:

清单 5. AccessibleValues 的精简版本

public static class AccessibleValues {    public String name;       // component's name/id    public String shortDescription; // == Accessible name    public String longDescription;  // == Accessible description    public String toolTip;      // component's tool tip    public String text;       // component's text    public String borderText;    // component border's text    public int  mnemonic;     // component's mnemonic    public AccessibleValues(String name,                String text,                String shortDescription<,                String longDescription<,                String toolTip<,                int mnemonic>>>) {...}}

并非所有组件都需要这个类中所有的值,因此它提供了实现可选参数的多个构造器。 当使用 setAccessibleValues 方法时,最好使用这个方法而不是组件的普通方法来设置 组件的文本(如果有的话)。清单 6 说明了如何使用 setAccessibleValues 方法设置按 钮组件的值:

清单 6. 按钮组件的 setAccessibleValues

JButton b = new JButton();AccessibleUtils.setAccessibleValues(null, (Accessible)b,   new AccessibleUtils.AccessibleValues(     "button1",     "OK",     "OK Button",     "Activate to commit changes and continue",     "Commit changes",     (int)'O');

尽管清单 6 中的代码与 清单 2中的按钮序列所做的事情相同,但它有下列优点:

如果遗漏了必需的参数,则 setAccessibleValues() 语法会强制产生一个错误。

setAccessibleValues() 方法比清单 2 中的按钮序列更简洁(如果将所有参数都放到 一行中,该方法甚至只需较少几行就可以了)。

因为调用了一个方法,该方法可以执行额外的处理和验证。

通过转换由 java.util.ResourceBundle 提供的文本,可以自动地支持国际化。

实际使用的实用程序方法

清单 7 显示了 setAccessibleValues() 方法是如何工作的。首先研究代码,然后查 看后面的注释。

清单 7. 实际使用的 setAccessibleValues()

protected static final Class[] _sType = {String.class};protected static final Class[] _iType = {Integer.TYPE};   :Accessible setAccessibleValues(     ResourceBundle rb, Accessible a, AccessibleValues av) {    if ( av.name != null ) {      throw new NullPointerException(        "Accessible components require a name");    }    if ( a instanceof Component ) {   // nearly always true      ((Component)a).setName(av.name);    }    if ( av.text != null ) {      Method m = resolveMethod(a, "setText", _sType);    try {       invokeMethod(a, m, new String[] {resolveString(rb, av.text)});      }      catch ( Exception e ) {        throw new AccessibleException(          "cannot invoke method setText(String text) - " + a, e);      }    }    if ( av.borderText != null ) {      JComponent c = (JComponent)a;      Border b = c.getBorder();      Border tb = new TitledBorder(resolveString(rb, av.borderText));      c.setBorder(b != null ? new CompoundBorder(b, tb) : tb);    }    if ( av.toolTip != null ) {     String text = resolveString(rb, av.toolTip.equalsIgnoreCase ("=ld")        ? av.longDescription : av.toolTip);     if ( a instanceof JComponent ) {        ((JComponent)a).setToolTipText(text);     }     else if ( a instanceof ImageIcon ) {        ((ImageIcon)a).setDescription(text);     }    }    if ( av.mnemonic >= 0 ) {      Method m = resolveMethod(a, "setMnemonic", _iType);      if ( m == null ) {        m = resolveMethod(a, "setDisplayedMnemonic", _iType);      }    try {        invokeMethod(a, m, new Integer[] {new Integer (av.mnemonic)});      }      catch ( Exception e ) {        throw new AccessibleException(         "cannot invoke method set{Displayed}Mnemonic(int key) - "         + a, e);      }    }    if ( av.shortDescription == null ) {      throw new NullPointerException(        "Accessible components require a shortDescription");    }    if ( av.shortDescription.equalsIgnoreCase("=tt") ) {      av.shortDescription = av.toolTip;    }    if ( av.shortDescription.equalsIgnoreCase("=ld") ) {      av.shortDescription = av.longDescription;    }    if ( av.shortDescription.length() == 0 ) {      av.shortDescription = null;    }    if ( av.shortDescription != null ) {      if ( a instanceof ImageIcon ) {        ((ImageIcon)a).setDescription(          resolveString(rb, av.shortDescription));      }    }    AccessibleContext ac = a.getAccessibleContext();    if ( ac == null ) {      throw new NullPointerException(        "AccessibleContext cannot be null on an Accessible object "        + formatClassToken(a));    }    if ( av.shortDescription != null ) {      ac.setAccessibleName(resolveString(rb, av.shortDescription));    }    if ( av.longDescription != null ) {      ac.setAccessibleDescription(        resolveString(rb, av.longDescription.equalsIgnoreCase ("=tt")          ? av.toolTip : av.longDescription));    }    return a;}

代码注释:

在 Swing(以及 AWT)中,每个组件都可以任意地由标识字符串标识;但是, setAccessibleValues() 方法需要一个名称。在本文中您将更进一步地深入理解名称的使 用。

如果提供了文本参数,则设置组件的文本。这允许在组件的构造器中省略该文本,以 便进行国际化转换。为了不要求组件必须是某种特定类型,用反射(而不是向下类型转换 (downcasting))来发现和调用 setText() 方法。

如果提供了边框文本参数,则为组件创建并设置有标题的边框。有标题的边框提供了 关于组件组的信息(我们将在本文中进一步讨论组件组)。

如果提供了 ToolTip 文本参数,则设置组件的 ToolTip 或图标的描述。

如果提供了助记符参数,则设置组件的助记符。为了不要求组件必须是某种特定类型 ,用反射来发现和调用可用的方法。

如果提供了短描述参数,则设置组件的短描述。短描述是 AccessibleName 的另一种 说法。可以将其缺省地设置为 ToolTip(通过输入 =tt )或长描述(通过输入 =ld )。 短描述是必需的(即它们不能为 null )。允许但不推荐空白短描述。

如果提供了长描述参数,则设置长描述。长描述是 AccessibleDescription 的另一种 说法。可以通过输入“ =tt ”将长描述缺省地设置为 ToolTip。

来自朋友的一点帮助

清单 8 显示了一个更实际的 setAccessibleValues() 用法示例,其中 resourceBundle 是实例字段。请注意助手方法的使用。

清单 8. setAccessibleValues 用法示例

JButton setupButton(String name, String action, int vKey) {    return (JButton)AccessibleUtils.setAccessibleValues(resourceBundle,      (Accessible)new JButton(),      new AccessibleUtils.AccessibleValues(        idGen.nextId("button"),        name,        AccessibleUtils.formatText(resourceBundle,          "{0} button", name),        "=tt",        AccessibleUtils.formatText(resourceBundle,          "Press to {0}", action),        vKey));}

setupButton 方法通过封装 setAccessibleValues() 调用,对按钮创建进行了进一步 简化和标准化。请注意它使用“ =tt ”将可访问描述设置成了 ToolTip。 IdGeneraTor.nextId(String base) 是生成唯一名称的实用程序方法。 idGen 是 IdGeneraTor 的实例。

各种 public static AccessibilityUtils.formatText() 方法通过插入值对字符串进 行格式化。 formatText 方法使用了 java.text.MessageFormat 类。 formatText 有以 下形式:

String formatText(String pattern, String args, String delims);String formatText(ResourceBundle rb, String pattern, Object[] args);String formatText(ResourceBundle rb, String pattern, String args);String formatText(ResourceBundle rb, String pattern, String args,    String delims);

类似的助手方法用于大多数组件类型。

对关系的支持

GUI 中组件组之间的关系通常很杂乱。让 AT 阅读器明了这些关系,可以使它增强组 件组的表示,从而将比较复杂的信息传递给用户。在 Swing 中,我们使用 AccessibleContext() 方法的 AccessibleRelationSet getAccessibleRelationSet() 方 法来定义关系。

AccessibleRelationSet 包含一组 AccessibleRelation 。每个 AccessibleRelation 描述两个 Accessible 对象(源和目标)之间的关系。目前,这些关系是如下所示定义的 :

CONTROLLED_BY 将给定目标标识为给定组件的控制器。

CONTROLLER_FOR 表明给定组件控制给定目标。

LABELLED_BY 表明给定组件是由给定目标标记的。

LABEL_FOR 表明给定组件是给定目标的标签。

MEMBER_OF 表明给定组件是给定目标组的成员。

AccessibilityDemo1 中的可访问性工具箱提供了几种实用程序方法,它们可以帮助您 定义可访问关系。单选按钮之间的“只有一个被选中”关系是一种很常见的关 系。在 Swing 中,我们使用 javax.swing.ButtonGroup 来实现这种关系。清单 9 显示 了可访问性工具箱用于在按钮组中定义单选按钮的实用程序方法。您会注意到,既有定义 单个单选按钮的方法,也有定义单选按钮集(或组)的方法。

清单 9. 定义组中按钮的方法

JRadioButton setupRadioButton(     String name, String action, int vKey, boolean selected) {    JRadioButton b = new JRadioButton();    b.setSelected(selected);    return (JRadioButton)AccessibleUtils.setAccessibleValues(      resourceBundle, (Accessible)b,      new AccessibleUtils.AccessibleValues(       idGen.nextId("radioButton"),       name,       AccessibleUtils.formatText(resourceBundle, "{0} Button", name),       "=tt",       AccessibleUtils.formatText(resourceBundle,         "Press to {0}", action), vKey));}JPanel setupRadioButtonSet(String title, JRadioButton[] bs) {    return setupRadioButtonSet(title, Arrays.asList(bs));}JPanel setupRadioButtonSet(String title, Collection bs) {   JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 20, 10));   AccessibleUtils.setAccessibleValues(resourceBundle, (Accessible)p,     new AccessibleUtils.AccessibleValues(       idGen.nextId("panel"),       null,       "languages",       "=tt",       "Select the desired language"));   AccessibleUtils.setMemberRelationship(p, bs);   p.setBorder(new TitledBorder(title));   addAll(p, bs);   ButtonGroup bg = new ButtonGroup();   addAll(bg, bs);   return p;}void addAll(ButtonGroup g, Collection l) {    for ( IteraTor i = l.iteraTor(); i.hasNext(); ) {      g.add((AbstractButton)i.next());    }}

您可以使用上述助手方法定义如 图 1所示的语言选择单选按钮集,如清单 10 所示:

清单 10. 定义语言选择单选按钮(来自图 1)

protected JComponent createTextFieldDemoUI() {   :    ArrayList buttons = new ArrayList();    StringTokenizer st2 = new StringTokenizer(      "English!French!Spanish!German!Italian!Japanese!Chinese!" +      "Korean!Arabic!Hebrew!Russian", "!");    for ( int i = 0; st2.hasMoreTokens(); i++ ) {      String name = nextToken(st2);      buttons.add(name, setupRadioButton(name,        AccessibleUtils.formatText(rb, "select {0}", name),        (int)name.charAt(0), i == 0));    }    JPanel formBox2 = setupRadioButtonSet("Languages", buttons);   :}

使用与设置按钮相同的方法将单选按钮设置成可访问的。但是,与按钮不同的是,经 常将单选按钮添加到 ButtonGroup 和 AccessibleRelationSet 中。清单 11 中详细说明 了用来辅助完成这项任务的 AccessibileUtils() 方法:

清单 11. AccessibileUtils 方法

Accessible setMemberRelationship(Accessible group, Accessible member) {    return setMemberRelationship(group, new Accessible[] {member});}Accessible setMemberRelationship(Accessible group, Accessible[] members) {    return setMemberRelationship(group, Arrays.asList(members));}Accessible setMemberRelationship(Accessible group, Collection members) {    for ( IteraTor i = members.iteraTor(); i.hasNext(); ) {      Accessible a = (Accessible)i.next();      AccessibleContext ac = a.getAccessibleContext();      if ( ac == null ) {        throw new NullPointerException(         "AccessibleContext cannot be null on an Accessible object"         + formatClassToken(a));      }      AccessibleRelationSet ars = ac.getAccessibleRelationSet();      AccessibleRelation ar =        new AccessibleRelation(AccessibleRelation.MEMBER_OF, group);      ars.add(ar);    }    return group;}

对助记符的支持

一些 Swing 组件不直接支持助记符。正如我先前讨论的,这些 Swing 通过允许 JLabel 来标记组件以弥补这一不足。可访问性工具箱提供了助手方法来辅助对组件作标 记。清单 12 展示了作用于 JTextField 组件的助手方法:

清单 12. 对组件作标记的助手方法

JTextField setupLabelledField(     JPanel lp, JPanel fp, String name, int vKey) {    JLabel l = new JLabel("", JLabel.RIGHT);    AccessibleUtils.setAccessibleValues(resourceBundle, (Accessible)l,      new AccessibleUtils.AccessibleValues(        idGen.nextId("label"),        name,        name + " label",        "=tt",        AccessibleUtils.formatText(resourceBundle,          "Identifies the {0} field", name),        vKey));    lp.add(l);    JTextField tf = new JTextField("", 40);    AccessibleUtils.setAccessibleValues(resourceBundle, (Accessible)tf,      new AccessibleUtils.AccessibleValues(        idGen.nextId("textField"),        null,        name + " entry field",        "=tt",        AccessibleUtils.formatText(resourceBundle,          "Enter the value for {0}", name)));    fp.add(tf);    AccessibleUtils.setLabelRelationship(l, tf);    return tf;}

尽管并未显示,但是请注意也应该用 AccessibleUtils.formatText 处理 name + “label” 和 name + “entry field” 子句,以充分地支持国际化转 换。

现在就在清单 13 中查阅方法的细节:

清单 13. setLabelRelationship 助手方法的细节

Accessible setLabelRelationship(Accessible label, Accessible target) {    if ( label instanceof JLabel ) {      ((JLabel)label).setLabelFor((Component)target);/* *** done by setLabelFor ***      AccessibleContext ac1 = label.getAccessibleContext();      if ( ac1 == null ) {        throw new NullPointerException(         "AccessibleContext cannot be null on an Accessible object"         + formatClassToken(label));      }      AccessibleRelationSet ars1 = ac1.getAccessibleRelationSet();      AccessibleRelation  ar1 = new AccessibleRelation(        AccessibleRelation.LABEL_FOR, target);      ars1.add(ar1);*/      AccessibleContext ac2 = target.getAccessibleContext();      if ( ac2 == null ) {        throw new NullPointerException(         "AccessibleContext cannot be null on an Accessible object"         + formatClassToken(target));      }      AccessibleRelationSet ars2 = ac2.getAccessibleRelationSet();      AccessibleRelation  ar2 = new AccessibleRelation(        AccessibleRelation.LABELLED_BY, label);      ars2.add(ar2);    }    return label;}

呈现复杂组件

在 Swing 中,某些复杂组件(如 JTree 、 JTable 、 JList 和 JComboBox )不直 接呈现其内容。而是将此任务委托给由 呈现程序创建的组件。呈现程序是一个工厂对象 ,它创建一个组件,用来创建显示复杂组件的行/单元值的组件。该组件仅在绘制行/单 元的短暂时间内使用。通过提供定制的组件,您可以控制如何将行/单元呈现给用户,包 括提供由 AT 阅读器使用的可访问信息。

由呈现程序生成的组件需要和我们迄今为止所讨论过的比较简单的组件一样,对于用 户是可访问的,这意味着我们必须能够设置其可访问性值。在 Swing 中,我们通常通过 创建 xxxCellRenderer 子类来做到这一点,其中 xxx 是基本组件类型。清单 14 显示了 JList 的单元呈现程序。请注意实际使用的工具箱的助手类。

清单 14. JList 的单元呈现程序

class DemoListCellRenderer implements ListCellRenderer {    protected ListCellRenderer _lcr = new DefaultListCellRenderer();    public Component getListCellRendererComponent(       JList list,       Object value,       int index,       boolean isSelected,       boolean cellHasFocus) {s      String name = value.toString();      String shortDesc =       AccessibleUtils.formatText(resourceBundle, "months {0} ", name);      String longDesc =       AccessibleUtils.formatText(resourceBundle, "Selects month {0}",         (String)_monthsMap.get(name.substring(0,3)));      JComponent c = (JComponent)_lcr.getListCellRendererComponent(        list,        name,        index,        isSelected,        cellHasFocus);      return (Component)AccessibleUtils.setAccessibleValues(        resourceBundle, (Accessible)c,        new AccessibleUtils.AccessibleValues(          idGen.nextId("label"),          name,          shortDesc, longDesc, "=ld"));    }}

如您所见,从可访问性的观点看,使用呈现程序提供的组件非常类似于使用普通的组 件。尽管本讨论只展示了如何向呈现程序添加可访问性支持,但用于可编辑的行或单元的 编辑器也需要类似的考虑事项。请参阅 源代码以了解更多关于工具箱对呈现复杂组件的 支持。

验证您的 GUI

在本文的大部分篇幅中,我们讨论了如何使 Swing 应用程序可访问,但您如何验证 GUI 的可访问性呢?测试复杂 GUI 上的每个组件是费时的,并需要您手头有 AT 阅读器 。Sun Microsystems 提供了帮助您不用 AT 阅读器即可测试 GUI 的工具(请参阅 参考 资料),但这些工具需要大量人工交互才能有效实施。

为了解决这一问题, AccessibilityUtils 提供了指出可能的或实际遗漏的可访问性 信息的报告框架。 public static AccessibilityUtils.output() 方法使用 com.ibm.wac.Outputter 实现来生成报告,如下所示:

void output(Component c, PrintWriter pw, Outputter out);void output(Component c, OutputStream os, Outputter out);void output(Component c, String fileName, Outputter out) throwsIOException;

output 方法的实现如清单 15 所示:

清单 15. output() 方法实现

public static void output(Component c, PrintWriter pw, Outputter out) {   if ( out.isEnabled() ) {     out.begin(pw);    try {       outputWorker(0, new HashSet(), c, pw, out);     }     finally {       out.end(pw);     }   }}protected static void outputWorker(    int level, Set seen, Component c, PrintWriter pw, Outputter out) {   out.beginLevel(level, pw);    try {     if ( seen.add(c) ) {       // only do the first time seen       // output self       out.beginObject(level, c, pw);    try {         out.identifyObject(level, c, pw);         out.recommendEol(level, pw);         out.outputComponent(level, c, pw);         if ( c instanceof Accessible ) {           out.recommendEol(level, pw);           out.outputAccessible(level, (Accessible)c, pw);         }         out.recommendEol(level, pw);         // output children (if any)         if ( c instanceof Container ) {           Component[] components =             ((Container)c).getComponents();           if ( components.length == 0 ) {             out.emptyGroup(level, pw);           }           else {             out.beginGroup(level, pw);             for ( int i = 0;                i <                components.length; i++ ) {               out.separateGroupMembers(level, i, pw);               out.identifyGroupMember(level, i, pw);               Component xc = components[i];               if ( xc instanceof JComponent ) {                 outputWorker(level + 1, seen,                        (JComponent)xc, pw, out);               }               else {                 out.outputObject(level, xc, pw);               }             }             out.endGroup(level, pw);             out.recommendEol(level, pw);           }         }       }       finally {         out.endObject(level, c, pw);       }     }     else {       out.outputObject(level, c, pw);       out.recommendEol(level, pw);     }   }   finally {     out.endLevel(level, pw);   }}

代码注释:

output 方法仅报告指定的组件(通常是 JFrame. 、 JDialog 或 JPanel )。如果您 的应用程序由多个框架或对话框组成,则您需要为每个框架或对话框调用 output() 方法 ,以全面地了解应用程序的情况。

任何组件都可以被报告,但它应该是 JComponent 。通常使用顶级的组件(如 JFrame. ),但较低级别的或动态的组件(如弹出式 JDialog )也可以被报告。

输出器定义了下列方法:

清单 16. 由输出器定义的方法

boolean isEnabled();// signal report begin/endvoid begin(PrintWriter pw);void end(PrintWriter pw);// signal level begin/endvoid beginLevel(int level, PrintWriter pw);void endLevel(int level, PrintWriter pw);// signal object begin/endvoid beginObject(int level, Object c, PrintWriter pw);void endObject(int level, Object c, PrintWriter pw);// report object identityvoid identifyObject(int level, Object c, PrintWriter pw);// report objectvoid outputObject(int level, Object c, PrintWriter pw);void outputComponent(int level, Component c, PrintWriter pw);void outputAccessible(int level, Accessible a, PrintWriter pw);// optional indent reportString indent(int level);String indent(int level, String pad);// optionally end report linevoid recommendEol(int level, PrintWriter pw);// signal group (i.e, container) processingvoid beginGroup(int level, PrintWriter pw);void separateGroupMembers(int level, int index, PrintWriter pw);void identifyGroupMember(int level, int index, PrintWriter pw);void endGroup(int level, PrintWriter pw);void emptyGroup(int level, PrintWriter pw);

与 SAX XML 解析器的工作方式类似,当由方法名表示的事件出现时,由 AccessibilityUtils.output() 方法调用这些方法。 AccessibleUtils 的这个示例中包 括下列输出器:

TextOutputter

生成简单的文本格式报告。子集示例是:

*javax.swing.JButton:16E7BF1D-button5Component(id=button5,      text=,      toolTipText=Create a new document,      value=?,      mnemonic=78,      ...)  ** others omitted for brevity **Accessible(name=New,       role=push button,       description=Create a new document,       action=javax.swing.JButton$AccessibleJButton@1bd8bf1d,       value=javax.swing.JButton$AccessibleJButton@1bd8bf1d,       text=null,       table=null,       relationSet=,       ...)  ** others omitted for brevity **

该文本(不包括所有封装)是由以下代码生成的:

AccessibleUtils.output(frame, "demo.txt", new TextOutputter());

HtmlOutputter

生成浏览器中显示的 HTML 报告。 图 3中显示了子集示例。该 HTML 是由下列代码生 成的:

AccessibleUtils.output(frame, "demo.html", new HtmlOutputter(HtmlOutputter.defaultHeader("Accessibility Demo 1")));

XmlOutputter

生成允许进一步处理的 XML 报告,譬如由 XSLT 样式表处理。子集示例如下:

         name    button6          text              toolTipText    Open an existing document          value    --          mnemonic    79          name    Open          role    push button          description    Open an existing document          action         javax.swing.JButton$AccessibleJButton@1a05bf1d              value         javax.swing.JButton$AccessibleJButton@1a05bf1d              text    null          table    null          relationSet           

该 XML 输出(未缩排)是由以下代码生成的:

AccessibleUtils.output(frame, "demo.xml", new XmlOutputter(XmlOutputter.defaultHeader("Accessibility Demo 1")));

该 XML 的 DTD 是:

请注意 DTD 中的 status 属性。它包含关于字段值性质的信息。已定义了下列 status 值:

ok:该值是满足要求的。

warning:该值丢失了或可疑,并有可能导致 AT 不正确地处理组件。

error:该值丢失或错误,并很可能导致 AT 不正确地处理组件。

本文中显示的输出器都是简单的示例。通常,人们将构建格式化更多信息并且可能扩 展复杂类型(如 ImageIcon )输出的显示输出器(如 图 3中所示)。

报告框架的另一种方案是将验证代码添加到 AccessibleUtils.output() 方法。如果 遗漏了任何组件上必需的可访问信息,这个代码将抛出异常而不是以特定的报告格式报告 遗漏的信息(尽管,实际上这种技术在非容器组件上工作得最好)。

添加验证代码可以帮助您更迅速地捕获可访问性错误,而不必检查整个报告。此外, 适当地使用验证代码,报告生成过程将充当一种验证测试用例,从而使提供 GUI 实现时 仍未设置所有必需的可访问性信息的可能性大大降低。有关此类异常代码的示例,请参阅 清单 7 中的 setAccessibleValues() 方法。请注意 setAccessibleValues 要求每个组 件都有一个名称。

结束语

在本文中,您已经了解了如何将可访问性值添加到组成 GUI 的 Swing 组件。在此过 程中,您逐渐熟悉了可访问性标准,该标准是根据 1998 年美国康复法案 508 条款的修 正案建立的。本文还为您介绍了实用程序方法的示例集,它消除了设置必需的可访问性值 所涉及的大量重复劳动。

可访问性工具箱中的实用程序处理可访问 GUI 开发过程中的以下方面:

强制必需的值

验证值

掩盖 GUI 组件设置可访问值的不同方法之间的差异

提供易于编码的帮助,如缺省值和国际化支持选项

帮助确保可访问信息的一致格式(如措辞风格)

可访问性工具箱还提供了可扩展的报告框架,它有助于您验证 GUI 的可访问组件。您 已经了解了如何使用框架来生成关于组件层次结构的可访问状态的报告。通过构建定制报 告生成器(或 XML 报告处理器),您可以构造不同类型的报告以验证应用程序中的组件 。

仔细地使用您在本文中学到的技术,您可以着手为视力和运动有残疾的人士构建更多 可访问的应用程序了。

切忌贪婪,恨不得一次玩遍所有传说中的好景点,

为可访问性编码:用JFC/Swing将可访问性构建到您的Java应用

相关文章:

你感兴趣的文章:

标签云: