本文目录导读:

- 核心无障碍原则
- 第一步:基础项目结构 (Maven/Gradle)
- 第二步:无障碍核心实现 (
AccessibilityNotepad.java) - 第三步:测试与验证
- 关键无障碍细节总结
- 进阶无障碍考虑
- 完整项目下载 (概念性)
这是一个关于Java无障碍(Accessibility,通常指 a11y)的综合性案例,该案例旨在展示如何构建一个对残障人士(如视障、听障、行动不便者)友好的Java Swing桌面应用程序。
我们将创建一个简单的记事本应用,并逐步应用无障碍最佳实践。
核心无障碍原则
- 可感知:所有UI元素必须有对应的文本描述(标签、提示)。
- 可操作:所有功能均可通过键盘完成(Tab导航、快捷键)。
- 可理解:界面语言清晰,状态变化有提示。
- 鲁棒性:与屏幕阅读器(如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类启动,展示模块化
}
}
第三步:测试与验证
你可以使用以下方法验证无障碍效果:
-
屏幕阅读器测试:
- Windows: 打开 讲述人 (Win+Ctrl+Enter) 或 NVDA (免费开源)。
- macOS: 打开 VoiceOver (Cmd+F5)。
- 启动程序后,用Tab键导航,听屏幕阅读器是否正确读出“文本编辑区域”、“文件菜单”、“状态信息”等。
-
键盘全操作测试:
- 使用
Alt+F打开文件菜单,Ctrl+N新建,Ctrl+O打开。 - 使用
Ctrl+L显示当前行号。 - 确保不需要鼠标可以完成所有操作。
- 使用
-
无障碍检查工具:
- 使用 Accessibility Insights for Windows (免费微软工具) 扫描UI。
- 使用 Swing Inspector (Java自带) 检查组件树中的
AccessibleName和AccessibleDescription。
关键无障碍细节总结
| 组件 | 无障碍设置 | 作用 |
|---|---|---|
| JTextArea | setAccessibleName("文本编辑区域") |
屏幕阅读器聚焦时读出“文本编辑区域,编辑文本” |
| JLabel (状态栏) | setAccessibleName("状态信息") |
当状态更新时,屏幕阅读器可以读出新的字符/单词数 |
| JMenuItem | setMnemonic, setAccelerator, setAccessibleName |
保证键盘可导航、可激活,同时有清晰标签 |
| JFrame | setAccessibleDescription("这是一个...") |
程序启动时,屏幕阅读器会读出这个描述 |
| JScrollPane | setAccessibleName("文本滚动面板") |
辅助用户在复杂的布局中定位当前区域 |
| Focus | requestFocusInWindow() |
确保操作后焦点自动回到正确的组件,而不是停留在菜单 |
| 状态变更 | firePropertyChange("text", old, new) |
通知屏幕阅读器文本内容发生变化(自动或被触发) |
进阶无障碍考虑
-
高对比度模式:
// 检测系统高对比度模式 Toolkit toolkit = Toolkit.getDefaultToolkit(); Boolean highContrast = (Boolean) toolkit.getDesktopProperty("win.highContrast.on"); if (Boolean.TRUE.equals(highContrast)) { // 切换为高对比度UI } -
自定义焦点指示器:确保焦点边框可见且对比度足够高。
-
支持屏幕放大镜:布局使用响应式设计,字体使用相对大小。
完整项目下载 (概念性)
在实际项目中,你还需要添加:
pom.xml或build.gradle文件。- 实际的文件读写逻辑。
- 异常处理(如文件不存在)。
- 国际化支持 (i18n) 与无障碍协同 (a11y + i18n)。
这个案例演示了如何在Java Swing中从头开始构建一个完全无障碍的桌面应用,良好的无障碍设计最终会使所有用户受益——包括你在内。