I am trying to get a group of JRadioButtons to be navigable using the arrow keys. I was going to implement this manually with KeyListeners, but apparently this behavior is already supposed to work for at least the last 8 years (http://bugs.sun.com/view_bug.do?bug_id=4104452). However, it's not working for me: pressing the arrow keys does nothing. Java version is 7u45 on Windows.

A standalone test case to see what I'm talking about:

import java.awt.*;
import javax.swing.*;

public class Test {
    public static void main(final String[] args) {
        if (!EventQueue.isDispatchThread()) {
            try {
                EventQueue.invokeAndWait(new Runnable() {
                    public void run() {
                        main(args);
                    }
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            return;
        }

        try {
            //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            //UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        ButtonGroup group = new ButtonGroup();
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        JRadioButton rb;

        rb = new JRadioButton("Option A");
        panel.add(rb);
        group.add(rb);

        rb = new JRadioButton("Option B");
        panel.add(rb);
        group.add(rb);

        rb = new JRadioButton("Option C");
        panel.add(rb);
        group.add(rb);

        frame.add(panel);
        frame.pack();
        frame.setVisible(true);
    }
}

I have tried using different look & feels, different containers, and different layout managers, but it still does not work.

有帮助吗?

解决方案 3

Thank you everyone for the answers.

I discovered the reason for my confusion. Apparently, when the Sun bug report system says that a bug's status is "Closed" and its "Resolved Date" is "2005-07-19", that doesn't mean the bug is fixed at all. Apparently, it's just logged as a duplicate of some other (newer?) bug. Nearly 16 years since it was first reported it still isn't fixed. Whatever.

The needed behavior is much more subtle than I realized. I experimented in native Windows dialogs in various programs:

  • Most button-like components: buttons, checkboxes, and radio buttons, implement the arrow keys for focus navigation. In Java this corresponds to the AbstractButton class. (JMenuItem is also a subclass of that, but that has its own distinct arrow key behavior.)
  • Only radio buttons get selected/checked during this navigation.
  • Unfocusable (including disabled or invisible) components must be skipped.
  • Attempting to navigate before the first button in a group or after the last one is inconsistent: on some dialogs it loops from end to end; on others it moves irreversibly onto non-button components; and on yet others it does nothing. I experimented with all these different behaviors and none of them was particularly better than the others.

I implemented a looping behavior below as it felt slightly more fluent. The navigation silently skips past non-AbstractButton components, forming a sort-of separate focus cycle private to buttons. This is dubious but sometimes needed when a set of related checkboxes or radio buttons are mixed with other components. Testing for a common parent component to identify groups would also be a reasonable behavior, but that didn't work in one dialog where I'd used separate components purely for layout reasons (to implement a line break in a FlowLayout).

As suggested I studied up on InputMaps and ActionMaps instead of using a KeyListener. I've always avoided the maps as they seem overcomplicated but I guess I see the advantage of being able to easily override the binding.

This code uses an auxialiary look and feel to install the desired behavior for all AbstractButton components application-wide (which is a nice technique I found out about here). I've tested it with several different dialog boxes and windows and it seems to be okay. If it causes issues I'll update this post.

Call:

ButtonArrowKeyNavigation.install();

once at application startup to install it.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ButtonArrowKeyNavigation {
    private ButtonArrowKeyNavigation() {}

    public static void install() {
        UIManager.addAuxiliaryLookAndFeel(lookAndFeel);
    }

    private static final LookAndFeel lookAndFeel = new LookAndFeel() {
        private final UIDefaults defaults = new UIDefaults() {
            @Override
            public javax.swing.plaf.ComponentUI getUI(JComponent c) {
                if (c instanceof AbstractButton && !(c instanceof JMenuItem)) {
                    if (c.getClientProperty(this) == null) {
                        c.putClientProperty(this, Boolean.TRUE);
                        configure(c);
                    }
                }
                return null;
            }
        };
        @Override public UIDefaults getDefaults() { return defaults; };
        @Override public String getID() { return "ButtonArrowKeyNavigation"; }
        @Override public String getName() { return getID(); }
        @Override public String getDescription() { return getID(); }
        @Override public boolean isNativeLookAndFeel() { return false; }
        @Override public boolean isSupportedLookAndFeel() { return true; }
    };

    private static void configure(JComponent c) {
        InputMap im = c.getInputMap(JComponent.WHEN_FOCUSED);
        ActionMap am = c.getActionMap();
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,  0), "focusPreviousButton");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,    0), "focusPreviousButton");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "focusNextButton");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,  0), "focusNextButton");
        am.put("focusPreviousButton", focusPreviousButton);
        am.put("focusNextButton",     focusNextButton);
    }

    private static final Action focusPreviousButton = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
            move((AbstractButton)e.getSource(), -1);
        }
    };

    private static final Action focusNextButton = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
            move((AbstractButton)e.getSource(), +1);
        }
    };

    private static void move(AbstractButton ab, int direction) {
        Container focusRoot = ab.getFocusCycleRootAncestor();
        FocusTraversalPolicy focusPolicy = focusRoot.getFocusTraversalPolicy();
        Component toFocus = ab, loop = null;
        for (;;) {
            toFocus = direction > 0
                ? focusPolicy.getComponentAfter(focusRoot, toFocus)
                : focusPolicy.getComponentBefore(focusRoot, toFocus);
            if (toFocus instanceof AbstractButton) break;
            if (toFocus == null) return;
            // infinite loop protection; should not be necessary, but just in
            // case all buttons are somehow unfocusable at the moment this
            // method is called:
            if (loop == null) loop = toFocus; else if (loop == toFocus) return;
        }
        if (toFocus.requestFocusInWindow()) {
            if (toFocus instanceof JRadioButton) {
                ((JRadioButton)toFocus).setSelected(true);
            }
        }
    }
}

其他提示

You need to add the right/left (up/down?) keys to the focus traversal policy of each radio button. For example to add the right/left arrow keys:

    Set set = new HashSet( rb.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS ) );
    set.add( KeyStroke.getKeyStroke( "RIGHT" ) );
    rb.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, set );

    set = new HashSet( rb.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS ) );
    set.add( KeyStroke.getKeyStroke( "LEFT" ) );
    rb.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, set );

Read the section from the Swing tutorial on How to Use the Focus Subsystem for more information.

I believe you can achieve your goal using KeyBindings instead of KeyListeners. In many cases bindings are actually recommended over KeyListeners, as the second ones can generate many problems (frame catching the key activity must be active one etc.)

Here is my example of JRadioButtons can be navigable using the arrow keys(UP and Down) and modified few codes for you.

public class JRadioButton extends JPanel {
    private JRadioButton[] buttons;

    public JRadioButtonTest(int row) {

       ButtonGroup group = new ButtonGroup();
       buttons = new JRadioButton[row];

       for (int i = 0; i < buttons.length; i++) {

            final int curRow = i;

            buttons[i] = new JRadioButton("Option " + i);
            buttons[i].addKeyListener(enter);
            buttons[i].addKeyListener(new KeyAdapter() {
               @Override
               public void keyPressed(KeyEvent e) {
                  switch (e.getKeyCode()) {
                  case KeyEvent.VK_UP:
                     if (curRow > 0)
                        buttons[curRow - 1].requestFocus();
                     break;
                  case KeyEvent.VK_DOWN:
                     if (curRow < buttons.length - 1)
                        buttons[curRow + 1].requestFocus();
                     break;

                  default:
                     break;
                  }
               }
            });
            group.add(buttons[i]);
            add(buttons[i]);

      }
   }

   private KeyListener enter = new KeyAdapter() {
      @Override
      public void keyTyped(KeyEvent e) {
         if (e.getKeyChar() == KeyEvent.VK_ENTER) {
            ((JButton) e.getComponent()).doClick();
         }
      }
   };

   public static void main(String[] args) {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.add(new JRadioButton(3));
      frame.pack();
      frame.setVisible(true);
   }
}

The core implement method is calling requestFocus() on the correct JRadioButton when an arrow key is called. Extra KeyListener for when the Enter key is pressed.

You can use this KeyListener to your program and add more key.

Good luck!

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top