程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 為可訪問性編碼: 用JFC/Swing將可訪問性構建到您的Java應用

為可訪問性編碼: 用JFC/Swing將可訪問性構建到您的Java應用

編輯:關於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("<enter street>", 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 okButton = 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 focus
boolean 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<,
                String borderText>>>>) {...}
}

並非所有組件都需要這個類中所有的值,因此它提供了實現可選參數的多個構造器。 當使用 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) throws
IOException;

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/end
void begin(PrintWriter pw);
void end(PrintWriter pw);
// signal level begin/end
void beginLevel(int level, PrintWriter pw);
void endLevel(int level, PrintWriter pw);
// signal object begin/end
void beginObject(int level, Object c, PrintWriter pw);
void endObject(int level, Object c, PrintWriter pw);
// report object identity
void identifyObject(int level, Object c, PrintWriter pw);
// report object
void 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 report
String indent(int level);
String indent(int level, String pad);
// optionally end report line
void recommendEol(int level, PrintWriter pw);
// signal group (i.e, container) processing
void 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-button5
Component(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 樣式表處理。子集示例如下:

<container level="6"
        desc="javax.swing.JButton:1588FF1D-button6"
        pdesc="javax.swing.JToolBar:17FAFF1D- toolBar0">
  <fields>
   <field context="self" type="java.lang.String">
    <name>name</name>
    <value status="ok">button6</value>
   </field>
   <field context="self" type="java.lang.String">
    <name>text</name>
    <value status="ok"></value>
   </field>
   <field context="self" type="java.lang.String">
    <name>toolTipText</name>
    <value status="ok">Open an existing document</value>
   </field>
   <field context="self" type="java.lang.String">
    <name>value</name>
    <value status="warning">--</value>
   </field>
   <field context="self" type="java.lang.Integer">
    <name>mnemonic</name>
    <value status="ok">79</value>
   </field>
   <field context="accessible" type="java.lang.String">
    <name>name</name>
    <value status="ok">Open</value>
   </field>
   <field context="accessible"
     type="javax.accessibility.AccessibleRole">
    <name>role</name>
    <value status="ok">push button</value>
   </field>
   <field context="accessible" type="java.lang.String">
    <name>description</name>
    <value status="ok">Open an existing document</value>
   </field>
   <field context="accessible"
     type="javax.swing.JButton$AccessibleJButton">
    <name>action</name>
    <value status="ok">
     javax.swing.JButton$AccessibleJButton@1a05bf1d
    </value>
   </field>
   <field context="accessible"
     type="javax.swing.JButton$AccessibleJButton">
    <name>value</name>
    <value status="ok">
     javax.swing.JButton$AccessibleJButton@1a05bf1d
    </value>
   </field>
   <field context="accessible">
    <name>text</name>
    <value status="warning">null</value>
   </field>
   <field context="accessible">
    <name>table</name>
    <value status="warning">null</value>
   </field>
   <field context="accessible"
     type="javax.accessibility.AccessibleRelationSet">
    <name>relationSet</name>
    <value status="ok"></value>
   </field>
  </fields>
  <!-- empty container -->
</container>

該 XML 輸出(未縮排)是由以下代碼生成的:

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

該 XML 的 DTD 是:

<!-- DTD for gui xml -->
<!ELEMENT gui (component|container)>
<!ATTLIST gui  xmlns CDATA #IMPLIED>
<!ELEMENT component (fields?)>
<!ATTLIST component desc  CDATA #REQUIRED
           pdesc CDATA #IMPLIED
           level CDATA #IMPLIED
>
<!ATTLIST container desc  CDATA #REQUIRED
           pdesc CDATA #IMPLIED
           level CDATA #IMPLIED
>
<!ELEMENT container (fields?,children?)>
<!ELEMENT children (component|container)*>
<!ELEMENT fields (field)*>
<!ELEMENT field (name,value)>
<!ATTLIST field context (self|accessible) "self"
         type CDATA #IMPLIED
>
<!ELEMENT name (#PCDATA)>
<!ELEMENT value (#PCDATA)>
<!ATTLIST value status (ok|warning|error) "ok">

請注意 DTD 中的 status 屬性。它包含關於字段值性質的信息。已定義了下列 status 值:

ok:該值是滿足要求的。

warning:該值丟失了或可疑,並有可能導致 AT 不正確地處理組件。

error:該值丟失或錯誤,並很可能導致 AT 不正確地處理組件。

本文中顯示的輸出器都是簡單的示例。通常,人們將構建格式化更多信息並且可能擴 展復雜類型(如 ImageIcon )輸出的顯示輸出器(如 圖 3中所示)。

報告框架的另一種方案是將驗證代碼添加到 AccessibleUtils.output() 方法。如果 遺漏了任何組件上必需的可訪問信息,這個代碼將拋出異常而不是以特定的報告格式報告 遺漏的信息(盡管,實際上這種技術在非容器組件上工作得最好)。

添加驗證代碼可以幫助您更迅速地捕獲可訪問性錯誤,而不必檢查整個報告。此外, 適當地使用驗證代碼,報告生成過程將充當一種驗證測試用例,從而使提供 GUI 實現時 仍未設置所有必需的可訪問性信息的可能性大大降低。有關此類異常代碼的示例,請參閱 清單 7 中的 setAccessibleValues() 方法。請注意 setAccessibleValues 要求每個組 件都有一個名稱。

結束語

在本文中,您已經了解了如何將可訪問性值添加到組成 GUI 的 Swing 組件。在此過 程中,您逐漸熟悉了可訪問性標准,該標准是根據 1998 年美國康復法案 508 條款的修 正案建立的。本文還為您介紹了實用程序方法的示例集,它消除了設置必需的可訪問性值 所涉及的大量重復勞動。

可訪問性工具箱中的實用程序處理可訪問 GUI 開發過程中的以下方面:

強制必需的值

驗證值

掩蓋 GUI 組件設置可訪問值的不同方法之間的差異

提供易於編碼的幫助,如缺省值和國際化支持選項

幫助確保可訪問信息的一致格式(如措辭風格)

可訪問性工具箱還提供了可擴展的報告框架,它有助於您驗證 GUI 的可訪問組件。您 已經了解了如何使用框架來生成關於組件層次結構的可訪問狀態的報告。通過構建定制報 告生成器(或 XML 報告處理器),您可以構造不同類型的報告以驗證應用程序中的組件 。

仔細地使用您在本文中學到的技術,您可以著手為視力和運動有殘疾的人士構建更多 可訪問的應用程序了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved