问题
I'm trying to find a best practise in terms of accessibility on Tables in relation to other components. In an app that's mainly a set of JTables and JTextFields, I tried to make it accessibility with keyboard as well as mouse. My thoughts are on the best way to help the user to navigate around between components using the VK_TAB key.
My first goal was to stop JTables "swallow" the VK_TAB key when the user tries to navigate to a neighbor JTextField using a solution from Coderanch. I tried to put together a minimal compilable and runnable example below.
package TableTest;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;
public class MyFrame extends JFrame {
private static final long serialVersionUID = 1L;
public MyFrame() {
super();
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
MyFrame frame = new MyFrame();
frame.init();
frame.setVisible(true);
}
});
}
private void init() {
JPanel contentPane = new JPanel(new BorderLayout());// new GridBagLayout()
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
JTable table = new JTable(new DefaultTableModel(new Object[][] { { 1, 2, 3 }, //
{ 4, 5, 6 }, //
{ 7, 8, 9 }, //
{ "#", 0, "*" }, }, //
new String[] { "First", "Second", "Third" }));
// When TAB is hit, go to next Component instead of next cell
table.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "tabNext");
table.getActionMap().put("tabNext", new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent ae) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent();
}
});
// When Shift+TAB is hit, go to previous Component instead of previous cell
table.getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK), "tabBefore");
table.getActionMap().put("tabBefore", new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent ae) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
}
});
JTextField jtf = new JTextField("Text here");
contentPane.add(jtf, BorderLayout.NORTH);
contentPane.add(table, BorderLayout.CENTER);
pack();
}
}
But that's rather radical and frustrating for a user who wants to navigate to a Table cell, e.g. for editing, using only the keyboard. So, my second goal is to give keyboard access to Table Cells as well.
What is a best practise here? I thought of the focussed JTable reacting to VK_ENTER: after that, it would react to VK_TAB by giving focus to the next cell until ... ESC is pressed or whatever.
Thank you!
回答1:
What is a best practise here?
The default implementation is:
- Tab - moves to the next component
Ctrl+Tab - moves to the next component
Shift+Tab - moves to the previous component
- Ctrl+Shift+Tab - moves to the previoius component
Some components handle the Tab key. For example:
- JTable - tab is used to move to the next cell
- Text components (JTextArea, JTextPane) - will insert a Tab character into the text
So for components that handle the Tab key the user would use Ctrl+Tab to navigate to the next component when using the keyboard.
Edit:
I thought of the focussed JTable reacting to VK_ENTER
You already know how to assign a different Action to the Tab key.
So now all you need to do is assign the default Tab Action to the Enter key. You can do this by changing the binding in the InputMap of the Table:
InputMap im = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke addedKeyStroke = KeyStroke.getKeyStroke("ENTER");
im.put(addedKeyStroke, "selectNextColumnCell");
Check out Key Bindings for a simple app that display all the default Actions for each Swing component.
回答2:
Thank you, camickr!
So when I change the code following your advice, it works.
That's the complete example:
package TableTest;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;
public class MyFrame extends JFrame {
private static final long serialVersionUID = 1L;
public MyFrame() {
super();
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
MyFrame frame = new MyFrame();
frame.init();
frame.setVisible(true);
}
});
}
private void init() {
JPanel contentPane = new JPanel(new BorderLayout());// new GridBagLayout()
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
JTable table = new JTable(new DefaultTableModel(new Object[][] { { 1, 2, 3 }, //
{ 4, 5, 6 }, //
{ 7, 8, 9 }, //
{ "#", 0, "*" }, }, //
new String[] { "First", "Second", "Third" }));
// When TAB is hit, go to next Component instead of next cell
final KeyStroke tabKey = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(tabKey, "tabNext");
final AbstractAction tabNext = new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent ae) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent();
}
};
table.getActionMap().put("tabNext", tabNext);
// When Shift+TAB is hit, go to previous Component instead of previous cell
final KeyStroke shiftTabKey = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK);
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shiftTabKey, "tabBefore");
final AbstractAction tabBefore = new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent event) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
}
};
table.getActionMap().put("tabBefore", tabBefore);
// on VK_ENTER, navigate in JTable only ("edit mode")
final KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
final AbstractAction editModeAction = new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent event) {
editMode(table, tabKey, shiftTabKey);
}
};
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(enterKey, "editModeAction");
table.getActionMap().put("editModeAction", editModeAction);
// On VK_ESCAPE or when JTable loses focus, quit the "edit mode"
final KeyStroke escKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
final AbstractAction quitEditModeAction = new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent event) {
quitEditMode(table, tabKey, shiftTabKey);
}
};
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(escKey, "quitEditModeAction");
table.getActionMap().put("quitEditModeAction", quitEditModeAction);
final FocusListener listener = new FocusListener() {
@Override
public void focusGained(FocusEvent event) {
//do nothing
}
@Override
public void focusLost(FocusEvent event) {
quitEditMode(table, tabKey, shiftTabKey);
}
};
table.addFocusListener(listener);
JTextField jtf = new JTextField("Text here");
contentPane.add(jtf, BorderLayout.NORTH);
contentPane.add(table, BorderLayout.CENTER);
pack();
//printActions(table);
}
private void editMode(JTable table, final KeyStroke tabKey, final KeyStroke shiftTabKey) {
System.out.println("editing activated");
table.setCellSelectionEnabled(true);
InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
input.remove(shiftTabKey);
input.remove(tabKey);
input.put(shiftTabKey, "selectPreviousColumnCell");
input.put(tabKey, "selectNextColumnCell");
}
private void quitEditMode(JTable table, final KeyStroke tabKey, final KeyStroke shiftTabKey) {
System.out.println("editing de-activated");
table.setCellSelectionEnabled(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
input.remove(shiftTabKey);
input.remove(tabKey);
input.put(shiftTabKey, "tabBefore");
input.put(tabKey, "tabNext");
}
}
I got the Key bindings to actions from the JTable's ActionMap and InputMap. I tried adding a small method
// print a String representation of each KeyStroke from the InputMap
private void printActions(JTable table) {
InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
if (input != null && input.allKeys() != null) {
for (KeyStroke key : input.allKeys()) {
if (key != null) {
printKeyStroke(key);
printActionName(input, key);
}
}
}
}
// build the String represantation
private void printKeyStroke(KeyStroke key) {
StringBuilder tk = new StringBuilder("[");
int modifiers = key.getModifiers();
if ((modifiers & InputEvent.SHIFT_DOWN_MASK) != 0)
tk.append("shift+");
if ((modifiers & InputEvent.CTRL_DOWN_MASK) != 0)
tk.append("ctrl+");
if ((modifiers & InputEvent.META_DOWN_MASK) != 0)
tk.append("cmd+");
if ((modifiers & InputEvent.ALT_DOWN_MASK) != 0)
tk.append("alt+");
tk.append("'");
tk.append(KeyEvent.getKeyText(key.getKeyCode()));
tk.append("'=");
tk.append("keycode=");
tk.append(key.getKeyCode());
tk.append("]");
System.out.print(tk.toString());
}
private void printActionName(InputMap input, KeyStroke key) {
System.out.print(": ");
Object string = input.get(key);
if (string != null && string instanceof String)
System.out.println(string.toString());
}
来源:https://stackoverflow.com/questions/61556057/best-practise-for-handling-vk-tab-in-java-swing-to-move-between-components-as-we