Java无障碍案例

wen java案例 1

本文目录导读:

Java无障碍案例

  1. 核心无障碍原则
  2. 第一步:基础项目结构 (Maven/Gradle)
  3. 第二步:无障碍核心实现 (AccessibilityNotepad.java)
  4. 第三步:测试与验证
  5. 关键无障碍细节总结
  6. 进阶无障碍考虑
  7. 完整项目下载 (概念性)

这是一个关于Java无障碍(Accessibility,通常指 a11y)的综合性案例,该案例旨在展示如何构建一个对残障人士(如视障、听障、行动不便者)友好的Java Swing桌面应用程序。

我们将创建一个简单的记事本应用,并逐步应用无障碍最佳实践。

核心无障碍原则

  1. 可感知:所有UI元素必须有对应的文本描述(标签、提示)。
  2. 可操作:所有功能均可通过键盘完成(Tab导航、快捷键)。
  3. 可理解:界面语言清晰,状态变化有提示。
  4. 鲁棒性:与屏幕阅读器(如JAWS、NVDA, VoiceOver)兼容。

第一步:基础项目结构 (Maven/Gradle)

// Main.java - 入口
import javax.swing.*;
import java.awt.*;
public class Main {
    public static void main(String[] args) {
        // 为Mac系统启用无障碍支持
        System.setProperty("apple.awt.application.name", "无障碍记事本");
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        SwingUtilities.invokeLater(() -> {
            // 设置跨平台外观,避免OS自带外观可能的无障碍bug
            try {
                UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
            } catch (Exception e) {
                e.printStackTrace();
            }
            new AccessibilityNotepad().setVisible(true);
        });
    }
}

第二步:无障碍核心实现 (AccessibilityNotepad.java)

这是核心案例,包含详细注释。

import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
public class AccessibilityNotepad extends JFrame {
    private JTextArea textArea;
    private JLabel statusLabel;
    private JFileChooser fileChooser;
    private int wordCount = 0, charCount = 0;
    public AccessibilityNotepad() {
        initUI();
        setupAccessibility();
        setupKeyboardShortcuts();
    }
    private void initUI() {
        setTitle("Java 无障碍记事本");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(800, 600);
        setLocationRelativeTo(null);
        // 1. 核心文本区域
        textArea = new JTextArea();
        textArea.setFont(new Font("Serif", Font.PLAIN, 18));
        // ★★★ 无障碍核心1: 设置AccessibleName和AccessibleDescription ★★★
        textArea.getAccessibleContext().setAccessibleName("文本编辑区域");
        textArea.getAccessibleContext().setAccessibleDescription("用于输入和编辑纯文本的主要区域");
        // 监听文本变化,更新状态栏
        textArea.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent e) { updateStatus(); }
            @Override
            public void removeUpdate(DocumentEvent e) { updateStatus(); }
            @Override
            public void changedUpdate(DocumentEvent e) { updateStatus(); }
        });
        JScrollPane scrollPane = new JScrollPane(textArea);
        // ★★★ 无障碍核心2: 为滚动面板设置标签,方便屏幕阅读器导航 ★★★
        scrollPane.getAccessibleContext().setAccessibleName("文本滚动面板");
        scrollPane.getAccessibleContext().setAccessibleDescription("包含文本编辑区域的滚动面板");
        // 2. 状态栏 - 用于显示实时信息
        statusLabel = new JLabel("字符: 0 | 单词: 0 | 就绪");
        statusLabel.setFont(new Font("SansSerif", Font.BOLD, 14));
        statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
        // ★★★ 无障碍核心3: 状态标签也设置accessible name ★★★
        statusLabel.getAccessibleContext().setAccessibleName("状态信息");
        // 布局
        setLayout(new BorderLayout());
        add(scrollPane, BorderLayout.CENTER);
        add(statusLabel, BorderLayout.SOUTH);
        // 3. 菜单栏
        setupMenuBar();
        // 4. 文件选择器 (无障碍友好)
        fileChooser = new JFileChooser();
        fileChooser.getAccessibleContext().setAccessibleName("文件选择对话框");
    }
    // ★★★ 无障碍核心4: 为所有菜单项设置AccessibleName和快捷键 ★★★
    private void setupMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        menuBar.getAccessibleContext().setAccessibleName("主菜单栏");
        // ---- 文件菜单 ----
        JMenu fileMenu = new JMenu("文件(F)");
        fileMenu.setMnemonic(KeyEvent.VK_F); // Alt+F
        fileMenu.getAccessibleContext().setAccessibleName("文件菜单");
        // 新建
        JMenuItem newItem = createAccessibleMenuItem("新建", KeyEvent.VK_N, KeyEvent.VK_N,
                "创建一个新的空白文档", e -> newFile());
        fileMenu.add(newItem);
        // 打开
        JMenuItem openItem = createAccessibleMenuItem("打开...", KeyEvent.VK_O, KeyEvent.VK_O,
                "打开一个现有的文本文件", e -> openFile());
        fileMenu.add(openItem);
        // 保存
        JMenuItem saveItem = createAccessibleMenuItem("保存", KeyEvent.VK_S, KeyEvent.VK_S,
                "保存当前文档", e -> saveFile());
        fileMenu.add(saveItem);
        fileMenu.addSeparator();
        JMenuItem exitItem = createAccessibleMenuItem("退出", KeyEvent.VK_Q, KeyEvent.VK_X,
                "退出应用程序", e -> System.exit(0));
        fileMenu.add(exitItem);
        menuBar.add(fileMenu);
        // ---- 编辑菜单 ----
        JMenu editMenu = new JMenu("编辑(E)");
        editMenu.setMnemonic(KeyEvent.VK_E);
        editMenu.getAccessibleContext().setAccessibleName("编辑菜单");
        JMenuItem copyItem = createAccessibleMenuItem("复制", KeyEvent.VK_C, KeyEvent.VK_C,
                "将选中文本复制到剪贴板", e -> textArea.copy());
        editMenu.add(copyItem);
        JMenuItem pasteItem = createAccessibleMenuItem("粘贴", KeyEvent.VK_V, KeyEvent.VK_V,
                "从剪贴板粘贴文本", e -> textArea.paste());
        editMenu.add(pasteItem);
        menuBar.add(editMenu);
        // ---- 帮助菜单 ----
        JMenu helpMenu = new JMenu("帮助(H)");
        helpMenu.setMnemonic(KeyEvent.VK_H);
        helpMenu.getAccessibleContext().setAccessibleName("帮助菜单");
        JMenuItem aboutItem = createAccessibleMenuItem("quot;, KeyEvent.VK_A, KeyEvent.VK_F1,
                "显示关于本软件的信息", e -> showAboutDialog());
        helpMenu.add(aboutItem);
        menuBar.add(helpMenu);
        setJMenuBar(menuBar);
    }
    // ★★★ 辅助方法: 创建一个完全无障碍的菜单项 ★★★
    private JMenuItem createAccessibleMenuItem(String text, int mnemonic, int acceleratorKey,
                                               String description, ActionListener action) {
        JMenuItem item = new JMenuItem(text);
        item.setMnemonic(mnemonic);
        item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey, InputEvent.CTRL_DOWN_MASK));
        item.getAccessibleContext().setAccessibleName(text);
        item.getAccessibleContext().setAccessibleDescription(description);
        item.addActionListener(action);
        return item;
    }
    // ★★★ 无障碍核心5: 确保焦点在启动时落在文本区域 ★★★
    private void setupAccessibility() {
        // 请求焦点
        SwingUtilities.invokeLater(() -> textArea.requestFocusInWindow());
        // ★★★ 设置整个窗体的描述,屏幕阅读器会先读出这段描述 ★★★
        getAccessibleContext().setAccessibleDescription("这是一个支持无障碍访问的简易文本编辑器,支持键盘快捷键操作。");
    }
    // ★★★ 无障碍核心6: 所有操作均可通过键盘完成,并定义额外的快捷键 ★★★
    private void setupKeyboardShortcuts() {
        // Ctrl+Shift+S: 另存为 (扩展功能)
        textArea.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), "saveAs");
        textArea.getActionMap().put("saveAs", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                saveFileAs();
            }
        });
        // Ctrl+L: 显示行号
        textArea.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK), "showLineNumber");
        textArea.getActionMap().put("showLineNumber", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    int line = textArea.getLineOfOffset(textArea.getCaretPosition()) + 1;
                    statusLabel.setText("当前行: " + line + " | " + statusLabel.getText());
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
    }
    // --- 功能实现 (仅做演示,实际应完善) ---
    private void newFile() {
        textArea.setText("");
        statusLabel.setText("字符: 0 | 单词: 0 | 新建文档");
        // ★★★ 无障碍提示: 状态更新后,建议通过focus移动到文本区域让屏幕阅读器读出 ★★★
        textArea.requestFocusInWindow();
    }
    private void openFile() {
        int result = fileChooser.showOpenDialog(this);
        if (result == JFileChooser.APPROVE_OPTION) {
            // 模拟打开文件
            statusLabel.setText("打开了: " + fileChooser.getSelectedFile().getName());
            textArea.requestFocusInWindow();
        }
    }
    private void saveFile() {
        // 模拟保存
        statusLabel.setText("文档已保存 (模拟)");
        textArea.requestFocusInWindow();
    }
    private void saveFileAs() {
        // 模拟另存为
        statusLabel.setText("已执行: 另存为");
        textArea.requestFocusInWindow();
    }
    private void showAboutDialog() {
        // ★★★ 使用JOptionPane,它自带无障碍支持 ★★★
        JOptionPane.showMessageDialog(this,
                "Java 无障碍记事本 v1.0\n演示Java Swing无障碍特性",
                "quot;,
                JOptionPane.INFORMATION_MESSAGE);
    }
    // ★★★ 更新状态并发出无障碍事件 ★★★
    private void updateStatus() {
        String text = textArea.getText();
        charCount = text.length();
        if (text.isEmpty()) {
            wordCount = 0;
        } else {
            wordCount = text.trim().split("\\s+").length;
        }
        String oldStatus = statusLabel.getText();
        String newStatus = String.format("字符: %d | 单词: %d | 就绪", charCount, wordCount);
        if (!oldStatus.equals(newStatus)) {
            statusLabel.setText(newStatus);
            // ★★★ 无障碍核心7: 向屏幕阅读器发送属性变更事件 ★★★
            // 这会告诉屏幕阅读器状态发生了变化,通常屏幕阅读器会自动处理JLabel的文本变化
            // 但更精确的方式是firePropertyChange,不过大多数情况下Swing会自动处理
            statusLabel.firePropertyChange("text", oldStatus, newStatus);
        }
    }
    public static void main(String[] args) {
        // 通过Main类启动,展示模块化
    }
}

第三步:测试与验证

你可以使用以下方法验证无障碍效果:

  1. 屏幕阅读器测试

    • Windows: 打开 讲述人 (Win+Ctrl+Enter) 或 NVDA (免费开源)。
    • macOS: 打开 VoiceOver (Cmd+F5)。
    • 启动程序后,用Tab键导航,听屏幕阅读器是否正确读出“文本编辑区域”、“文件菜单”、“状态信息”等。
  2. 键盘全操作测试

    • 使用 Alt+F 打开文件菜单, Ctrl+N 新建, Ctrl+O 打开。
    • 使用 Ctrl+L 显示当前行号。
    • 确保不需要鼠标可以完成所有操作。
  3. 无障碍检查工具

    • 使用 Accessibility Insights for Windows (免费微软工具) 扫描UI。
    • 使用 Swing Inspector (Java自带) 检查组件树中的 AccessibleNameAccessibleDescription

关键无障碍细节总结

组件 无障碍设置 作用
JTextArea setAccessibleName("文本编辑区域") 屏幕阅读器聚焦时读出“文本编辑区域,编辑文本”
JLabel (状态栏) setAccessibleName("状态信息") 当状态更新时,屏幕阅读器可以读出新的字符/单词数
JMenuItem setMnemonic, setAccelerator, setAccessibleName 保证键盘可导航、可激活,同时有清晰标签
JFrame setAccessibleDescription("这是一个...") 程序启动时,屏幕阅读器会读出这个描述
JScrollPane setAccessibleName("文本滚动面板") 辅助用户在复杂的布局中定位当前区域
Focus requestFocusInWindow() 确保操作后焦点自动回到正确的组件,而不是停留在菜单
状态变更 firePropertyChange("text", old, new) 通知屏幕阅读器文本内容发生变化(自动或被触发)

进阶无障碍考虑

  1. 高对比度模式

    // 检测系统高对比度模式
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    Boolean highContrast = (Boolean) toolkit.getDesktopProperty("win.highContrast.on");
    if (Boolean.TRUE.equals(highContrast)) {
        // 切换为高对比度UI
    }
  2. 自定义焦点指示器:确保焦点边框可见且对比度足够高。

  3. 支持屏幕放大镜:布局使用响应式设计,字体使用相对大小。

完整项目下载 (概念性)

在实际项目中,你还需要添加:

  • pom.xmlbuild.gradle 文件。
  • 实际的文件读写逻辑。
  • 异常处理(如文件不存在)。
  • 国际化支持 (i18n) 与无障碍协同 (a11y + i18n)。

这个案例演示了如何在Java Swing中从头开始构建一个完全无障碍的桌面应用,良好的无障碍设计最终会使所有用户受益——包括你在内。

抱歉,评论功能暂时关闭!