Best practise for handling VK_TAB in Java Swing to move between components as well as to move between table cells using the keyboard only

huangapple go评论88阅读模式
英文:

Best practise for handling VK_TAB in Java Swing to move between components as well as to move between table cells using the keyboard only

问题

我试图在与其他组件相关的情况下找到表格的无障碍最佳实践。在一个主要由 JTables 和 JTextFields 组成的应用中,我尝试通过键盘和鼠标使其具有可访问性。我思考的重点是通过 VK_TAB 键帮助用户在组件之间进行导航。

我的第一个目标是阻止 JTables 在用户试图使用来自 Coderanch 的解决方案导航到相邻的 JTextField 时“吞噬” VK_TAB 键。我尝试了在下面提供的最小编译和可运行示例中进行了整合。

// 代码部分已省略

但对于只想使用键盘进行导航以编辑表格单元格的用户来说,这可能过于激进且令人沮丧。因此,我的第二个目标是为表格单元格提供键盘访问。

在这里,有什么最佳做法?我考虑到将焦点放在有焦点的 JTable 上,通过 VK_ENTER 响应它,然后通过将焦点移动到下一个单元格来响应 VK_TAB,直到按下 ESC 或其他操作。

谢谢!

英文:

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

得分: 0

> 这里有什么最佳做法?

默认实现如下:

  1. Tab 键 - 移动到下一个组件

  2. Ctrl+Tab 键 - 移动到下一个组件

  3. Shift+Tab 键 - 移动到上一个组件

  4. Ctrl+Shift+Tab 键 - 移动到上一个组件

一些组件会处理 Tab 键。例如:

  1. JTable - 使用 Tab 键可移动到下一个单元格
  2. 文本组件 (JTextArea、JTextPane) - 会在文本中插入一个制表符(Tab 字符)

因此,对于处理 Tab 键的组件,用户在使用键盘进行导航时会使用 Ctrl+Tab 键来移动到下一个组件。

编辑:

> 我考虑让获得焦点的 JTable 对 VK_ENTER 做出反应

您已经知道如何将不同的操作分配给 Tab 键。

所以现在您需要做的就是将默认的 Tab 操作分配给 Enter 键。您可以通过更改表格的 InputMap 中的绑定来实现这一点:

InputMap im = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke addedKeyStroke = KeyStroke.getKeyStroke("ENTER");
im.put(addedKeyStroke, "selectNextColumnCell");

请查看 Key Bindings,了解一个简单的应用程序,它会显示每个 Swing 组件的默认操作。

英文:

> What is a best practise here?

The default implementation is:

  1. Tab - moves to the next component

  2. Ctrl+Tab - moves to the next component

  3. Shift+Tab - moves to the previous component

  4. Ctrl+Shift+Tab - moves to the previoius component

Some components handle the Tab key. For example:

  1. JTable - tab is used to move to the next cell
  2. 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

得分: 0

感谢你,camickr!

根据你的建议,我修改了代码,现在它能够正常工作。

以下是完整的示例代码:

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());
		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" }));

		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);

		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);

		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);

		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) {
			}

			@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();
	}

	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");
	}
}

代码中我从JTableActionMapInputMap获取了按键绑定到动作的信息。我还添加了一个小的方法来打印每个KeyStroke的字符串表示。

    // 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());


<details>
<summary>英文:</summary>

Thank you, camickr!

So when I change the code following your advice, it works. 

That&#39;s the complete example:

``` java
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 }, //
				{ &quot;#&quot;, 0, &quot;*&quot; }, }, //
				new String[] { &quot;First&quot;, &quot;Second&quot;, &quot;Third&quot; }));

		// 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, &quot;tabNext&quot;);
		final AbstractAction tabNext = new AbstractAction() {
			private static final long serialVersionUID = 1L;

			public void actionPerformed(ActionEvent ae) {
				KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent();
			}
		};
		table.getActionMap().put(&quot;tabNext&quot;, 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, &quot;tabBefore&quot;);
		final AbstractAction tabBefore = new AbstractAction() {
			private static final long serialVersionUID = 1L;

			public void actionPerformed(ActionEvent event) {
				KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent();
			}
		};
		table.getActionMap().put(&quot;tabBefore&quot;, tabBefore);

		// on VK_ENTER, navigate in JTable only (&quot;edit mode&quot;)
		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, &quot;editModeAction&quot;);
		table.getActionMap().put(&quot;editModeAction&quot;, editModeAction);

		// On VK_ESCAPE or when JTable loses focus, quit the &quot;edit mode&quot;
		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, &quot;quitEditModeAction&quot;);
		table.getActionMap().put(&quot;quitEditModeAction&quot;, 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(&quot;Text here&quot;);

		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(&quot;editing activated&quot;);
		table.setCellSelectionEnabled(true);
		InputMap input = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
		input.remove(shiftTabKey);
		input.remove(tabKey);
		input.put(shiftTabKey, &quot;selectPreviousColumnCell&quot;);
		input.put(tabKey, &quot;selectNextColumnCell&quot;);
	}

	private void quitEditMode(JTable table, final KeyStroke tabKey, final KeyStroke shiftTabKey) {
		System.out.println(&quot;editing de-activated&quot;);
		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, &quot;tabBefore&quot;);
		input.put(tabKey, &quot;tabNext&quot;);
	}
}


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 &amp;&amp; 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(&quot;[&quot;);
		int modifiers = key.getModifiers();
		if ((modifiers &amp; InputEvent.SHIFT_DOWN_MASK) != 0)
			tk.append(&quot;shift+&quot;);
		if ((modifiers &amp; InputEvent.CTRL_DOWN_MASK) != 0)
			tk.append(&quot;ctrl+&quot;);
		if ((modifiers &amp; InputEvent.META_DOWN_MASK) != 0)
			tk.append(&quot;cmd+&quot;);
		if ((modifiers &amp; InputEvent.ALT_DOWN_MASK) != 0)
			tk.append(&quot;alt+&quot;);
		tk.append(&quot;&#39;&quot;);
		tk.append(KeyEvent.getKeyText(key.getKeyCode()));
		tk.append(&quot;&#39;=&quot;);
		tk.append(&quot;keycode=&quot;);
		tk.append(key.getKeyCode());
		tk.append(&quot;]&quot;);
		System.out.print(tk.toString());
	}

	private void printActionName(InputMap input, KeyStroke key) {
		System.out.print(&quot;: &quot;);
		Object string = input.get(key);
		if (string != null &amp;&amp; string instanceof String)
			System.out.println(string.toString());
	} 

huangapple
  • 本文由 发表于 2020年5月2日 15:50:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/61556057.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定