在 Swing 應用程式中處理鍵盤事件是常見需求,例如實作熱鍵、遊戲控制或表單快捷鍵。早期常用 registerKeyboardAction,但現已棄用。本文介紹兩種實作方式,並說明為何應改用 InputMap/ActionMap 架構。
此方法直接在元件上註冊鍵盤動作,語法簡單但靈活度有限。
public class KeystrokeTest extends JApplet {
private JButton button = new JButton("button");
public void init() {
Container contentPane = getContentPane();
JPanel panel = new JPanel();
JCheckBox checkbox = new JCheckBox("checkbox");
JButton southButton = new JButton("south button");
Listener listener = new Listener();
panel.setBorder(BorderFactory.createTitledBorder("Ancestor of button and checkbox"));
// WHEN_FOCUSED:元件擁有焦點時觸發
checkbox.registerKeyboardAction(listener,
KeyStroke.getKeyStroke(KeyEvent.VK_F, 0, false),
JComponent.WHEN_FOCUSED);
// WHEN_ANCESTOR_OF_FOCUSED_COMPONENT:祖先元件擁有焦點時觸發
panel.registerKeyboardAction(listener,
KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.ALT_MASK, false),
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
// WHEN_IN_FOCUSED_WINDOW:視窗內任一元件擁有焦點時觸發
southButton.registerKeyboardAction(listener,
KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), // true 表示 key released
JComponent.WHEN_IN_FOCUSED_WINDOW);
panel.add(button);
panel.add(checkbox);
contentPane.add(panel, "Center");
contentPane.add(southButton, "South");
}
}
class Listener implements ActionListener {
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
if (src instanceof JCheckBox) {
System.out.println("'f' key PRESSED when checkbox had focus");
} else if (src instanceof JPanel) {
System.out.println("'ALT-a' key PRESSED when ancestor of titled panel had focus");
} else if (src instanceof JButton) {
System.out.println("'w' key RELEASED when any component in window had focus");
}
System.out.println("Source: " + src.getClass().getName());
}
}
| 常數 | 觸發時機 |
|---|---|
| WHEN_FOCUSED | 該元件本身擁有鍵盤焦點 |
| WHEN_ANCESTOR_OF_FOCUSED_COMPONENT | 該元件的子孫元件擁有焦點 |
| WHEN_IN_FOCUSED_WINDOW | 視窗內任一元件擁有焦點(全域熱鍵) |
這是 Swing 設計的「動作映射」架構,將「按鍵觸發」與「實際動作」解耦,支援更複雜的綁定邏輯。
KeyStroke 映射到動作名稱(Object)Action 物件getInputMap(WHEN_FOCUSED) 等public class KeyBindingDemo {
public static void main(String[] args) {
try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignore) {}
final JTextField tf = new JTextField();
tf.setEnabled(false); // 僅顯示,不接受直接輸入
JPanel p = new JPanel(new GridLayout(0, 3, 5, 5));
for (int i = 0; i <= 9; i++) {
final JButton btn = new JButton(String.valueOf(i));
p.add(btn);
// 1. 定義按鍵觸發條件:數字鍵 0-9,在視窗焦點時
KeyStroke ks = KeyStroke.getKeyStroke((char)('0' + i));
String actionName = "PressKeyAction" + i; // 每個按鈕獨立動作名稱
// 2. 綁定按鍵 -> 動作名稱
btn.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, actionName);
// 3. 綁定動作名稱 -> 實際行為
btn.getActionMap().put(actionName, new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
btn.doClick(); // 程式化點擊按鈕
}
});
// 4. 按鈕本身的點擊邏輯
btn.addActionListener(e -> tf.setText(tf.getText() + btn.getText()));
}
JFrame f = new JFrame("鍵盤綁定示範");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setLayout(new BorderLayout(0, 5));
f.getContentPane().add(p, BorderLayout.CENTER);
f.getContentPane().add(tf, BorderLayout.NORTH);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
查看 JDK 原始碼可發現,registerKeyboardAction 內部實際上就是封裝了 InputMap/ActionMap 操作:
public void registerKeyboardAction(ActionListener anAction, String aCommand,
KeyStroke aKeyStroke, int aCondition) {
InputMap inputMap = getInputMap(aCondition, true);
if (inputMap != null) {
ActionMap actionMap = getActionMap(true);
ActionStandin action = new ActionStandin(anAction, aCommand);
inputMap.put(aKeyStroke, action);
if (actionMap != null) actionMap.put(action, action);
}
}
直接使用 InputMap/ActionMap 好處:
許多初學者傾向使用 addKeyListener,但 不推薦 用於 Swing 應用:
isFocusable() 為 true最佳實務: 針對特定元件綁定用 InputMap/ActionMap;全域熱鍵則在頂層容器(如 JRootPane)的 WHEN_IN_FOCUSED_WINDOW InputMap 設定。
| 特性 | registerKeyboardAction | InputMap/ActionMap |
|---|---|---|
| API 狀態 | 已棄用 | 標準推薦 |
| 程式碼結構 | 耦合度高 | 關注點分離 |
| 動態修改 | 困難 | 容易 |
| Action 重用 | 不支援 | 支援 |
請直接使用 InputMap/ActionMap 架構,這是 Swing 處理鍵盤事件的正統之道。