为什么我的 JTable 在 OS X 上使用 VoiceOver 时总是报告为空?

Why is my JTable always reported as empty using VoiceOver on OS X?

VoiceOver OSX 10.10.4 (Yosemite),使用 JRE 1.7.0_75 和 JRE 1.8.0_45,报告以下 table 作为 "empty".

package Whosebug.examples.jtable;

import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.SwingUtilities;

public class TableDemo extends JFrame
{
    private static final long serialVersionUID = 1L;

    public TableDemo()
    {
        super("Accessible JTable?");

        final String[] columnNames = 
            {
                "First Name",
                "Last Name",
                "Sport",
                "# of Years",
                "Vegetarian"
            };
        final Object[][] data = 
            {
                {"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)},
                {"John", "Doe", "Rowing", new Integer(3), new Boolean(true)},
            };

        final JTable jTable = new JTable(data, columnNames);
        jTable.getAccessibleContext().setAccessibleName("data table");
        System.out.println("rows: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleRowCount());
        System.out.println("cols: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleColumnCount());
        System.out.println("java: " + System.getProperty("java.version"));
        jTable.setOpaque(true);
        setContentPane(jTable);
    }

    private static void createAndShowGUI() 
    {
        final TableDemo frame = new TableDemo();
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}

除了 VoiceOver 说 table 是空的,其他一切似乎都正常:

我错过了什么?

更多信息:

实际上 Java 辅助功能随 Java 7 和 Java 8 软件包一起提供,对于更早的 Java 版本,您需要手动安装它。

而如果您查看 java 文档,您会发现他们推荐的软件很少,例如 JAWS、NonVisual Desktop Access (NVDA)、SuperNova、Window-Eyes 等 OS 默认解说软件。但这仅限于 Windows。 http://docs.oracle.com/javase/7/docs/technotes/guides/access/enable_and_test.html

Since you are running onto OSX 10.10.4 (Yosemite) OS, you are not required to alter anything. But you may still try one of the softwares listed.

VoiceOver 说 "empty" 的原因是因为 accessibility hierarchy is not being exposed correctly. You can use the Accessibility Inspector tool (one of the Xcode developer tools) to examine the accessibility hierarchy. In this case, with the Accessibility Inspector tool running, hovering the mouse pointer over the JTable shows that there is an AXTable element with 10 AXStaticText children (one for each of the cells). Tables should be exposed as AXTable > AXRow > AXCell > … . Also, according to the Roles reference,一个 AXTable 元素在其他必需属性中应该有一个 Rows 属性,但是 JRE 没有向可访问性层次结构公开这些属性.

我已经在 Windows 8.1 Pro 上使用 Java 1.8.0_51 试用了您的程序,我看到了类似的问题。与辅助功能检查器工具类似,Windows SDK 附带一个 Inspect tool,可用于检查辅助功能数据。当 运行 您的测试用例时, JTable 似乎根本没有暴露。启用 Windows 讲述人,我无法导航到 table 或其单元格。

因此,JRE 似乎不完全支持 table 可访问性。

source of javax.accessibility.AccessibleRole 中,您可以看到定义 ROW 常量的代码与记录为 "under consideration for potential future use".

的其他常量一起被注释掉了

source of JTable中,可以看到定义了AccessibleJTableAccessibleTableHeaderAccessibleJTableCellAccessibleJTableHeaderCell helper classes ,但是 table.

的行没有 Accessible 实现

理论上,您可以编写一个自定义 AccessibleContext 实现,该实现将公开 JTable 到 OS 的更完整的可访问性层次结构。但是,我不确定是否可以完全解决 Java 明显缺乏对 table 可访问性的支持。

这是否可能取决于平台。例如,检查 source code of src/macosx/native/sun/awt/JavaAccessibilityUtilities.m, you can see how Java's accessibility roles are mapped to the NSAccessibility*Role constants. You can see that the ROW_HEADER accessible role 是否映射到 AXRow。因此,您可以使用 ROW_HEADER 可访问角色公开 AXTable 的 AXRow 子级。这是一些成功执行此操作的代码:

public static class MyJTable extends JTable {
    public MyJTable(TableModel tm) {
        super(tm);
    }

    @Override
    public MyAccessibleJTable getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new MyAccessibleJTable();
        }
        return (MyAccessibleJTable)accessibleContext;
    }

    protected class MyAccessibleJTable extends AccessibleJTable {

        @Override
        public int getAccessibleChildrenCount() {
            if (MyJTable.this.getColumnCount() <= 0) {
                return 0;
            }
            return MyJTable.this.getRowCount();
        }

        @Override
        public Accessible getAccessibleChild(int i) {
            if (i < 0 || getAccessibleChildrenCount() <= i) {
                return null;
            }
            TableColumn firstColumn = getColumnModel().getColumn(0);
            TableCellRenderer renderer = firstColumn.getCellRenderer();
            if (renderer == null) {
                Class<?> columnClass = getColumnClass(0);
                renderer = getDefaultRenderer(columnClass);
            }
            Component component = renderer.getTableCellRendererComponent(MyJTable.this, null, false, false, i, 0);
            return new MyAccessibleRow(MyJTable.this, i, component);
        }
    }

    protected static class MyAccessibleRow extends AccessibleContext implements Accessible {
        private MyJTable table;
        private int row;
        private Component rendererComponent;

        protected MyAccessibleRow(MyJTable table, int row, Component renderComponent) {
            this.table = table;
            this.row = row;
            this.rendererComponent = rendererComponent;
        }

        @Override
        public AccessibleRole getAccessibleRole() {
            // ROW_HEADER is used because it maps to NSAccessibilityRowRole
            // on Mac.
            return AccessibleRole.ROW_HEADER;
        }

        @Override
        public Locale getLocale() {
            AccessibleContext ac = rendererComponent.getAccessibleContext();
            if (ac != null) {
                return ac.getLocale();
            } else {
                return null;
            }
        }

        @Override
        public int getAccessibleChildrenCount() {
            return 0; // TODO return the number of columns in this row
        }
        @Override
        public Accessible getAccessibleChild(int i) {
            return null; // TODO return a MyAccessibleJTableCell
        }
        @Override
        public int getAccessibleIndexInParent() {
            return row;
        }
        @Override
        public AccessibleStateSet getAccessibleStateSet() {
            return null; // TODO
        }
        @Override
        public AccessibleContext getAccessibleContext() {
            return this; // TODO
        }
        @Override
        public AccessibleComponent getAccessibleComponent() {
            return table.getAccessibleContext(); // TODO
        }
    }
}

从这张截图可以看出:

.. AXTable 上现在有两个 AXRow 子项。但是,VoiceOver 仍将 table 宣布为 "empty"。我不确定这是因为行没有 AXCell 子行,还是因为 AXTable 缺少其必需的属性,还是其他原因。

如果您要走自定义 AccessibleContext 路线,最好完全避免尝试公开 table 层次结构。相反,您可以将 table 建模为列表,其中每一行对应一个列表项,并且每个列表项包含每个单元格的一个组。这类似于 Firefox(已测试版本 39.0)使用的方法。目前在 Mac 上,Firefox 在公开 HTML table 时不使用 table 角色。不过,这应该在 Firefox 41 中得到修复。参见 Bug 744790 - [Mac] HTML table semantics are not communicated to VoiceOver at all

我也在使用 Mac OS 10.10.4 和 Java 1.8.0_51.

编辑: "empty" table 问题已被报告为 OpenJDK Bug JDK-7124284 [macosx] Nothing heard from VoiceOver when navigating in a table。根据评论,Mac Swing 可访问性存在几个已知问题,目前已推迟到 JDK 9.

另一种可能的解决方法是使用 JavaFX TableView class。尝试在 Java 8u40.

中实现的 http://docs.oracle.com/javafx/2/ui_controls/table-view.htm I am seeing that VoiceOver is properly announcing the table. JavaFX accessibility was implemented as part of JEP 204 中的 TableView 示例