在Java中,如何实现对JButton进行焦点控制?

5
我在Java Swing中发现了一个奇怪的异常。UI上按时间顺序添加的第一个JButton,只要用户在按空格键之前没有点击其他按钮,就会触发。即使调用了getRootPane().setDefaultButton(JButton)JButton.requestFocus()也会出现这种行为。在请求JButton的焦点时,似乎至少有两种不同的“焦点”。其中一种“焦点”或突出显示是在按钮上的文本周围显示虚线矩形,而另一种是在指定按钮周围显示更粗的轮廓线。带虚线轮廓文本的按钮会在按下空格键时触发,而具有粗边框的按钮则会在按下回车键时触发。我准备了一个可编译的最小示例来说明这种行为。其中没有涉及任何键映射/绑定。
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;

public class ButtonFocusAnomalyExample extends JFrame {
    public ButtonFocusAnomalyExample() {
        super();
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        int frameWidth = 300;
        int frameHeight = 300;
        setSize(frameWidth, frameHeight);
        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
        int x = (d.width - getSize().width) / 2;
        int y = (d.height - getSize().height) / 2;
        setLocation(x, y);
        setTitle("Any Frame");
        setResizable(false);
        Container cp = getContentPane();
        cp.setLayout(null);
        setVisible(true);
        new DialogMinimal(this, true); // Runs the Dialog
    }

    public static void main(String[] args) {
        new ButtonFocusAnomalyExample();
    }

    static class DialogMinimal extends JDialog {
        private final JTextField output = new JTextField();

        public DialogMinimal(final JFrame owner, final boolean modal) {
            super(owner, modal);
            setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            int frameWidth = 252;
            int frameHeight = 126;
            setSize(frameWidth, frameHeight);
            Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
            int x = (d.width - getSize().width) / 2;
            int y = (d.height - getSize().height) / 2;
            setLocation(x, y);
            setTitle("Minimal Button Focus Example");
            Container cp = getContentPane();
            cp.setLayout(null);
            JButton bYes = new JButton();
            bYes.setBounds(0, 0, 100, 33);
            bYes.setText("Yes (Space)");
            bYes.addActionListener(this::bYes_ActionPerformed);
            JPanel buttonPanel = new JPanel(null, true);
            buttonPanel.add(bYes);
            JButton bNo = new JButton();
            bNo.setBounds(108, 0, 120, 33);
            bNo.setText("No (Enter/Return)");
            getRootPane().setDefaultButton(bNo); // Set "No" as default button
            bNo.requestFocus(); // Get focus on "No" button
            bNo.addActionListener(this::bNo_ActionPerformed);
            buttonPanel.add(bNo);
            buttonPanel.setBounds(8, 8, 400, 92);
            buttonPanel.setOpaque(false);
            cp.add(buttonPanel);
            output.setBounds(8, 50, 220, 32);
            cp.add(output);
            setResizable(false);
            setVisible(true);
        }

        public void bYes_ActionPerformed(final ActionEvent evt) {
            output.setText("Yes"); // Still fires on every space bar press
        }

        public void bNo_ActionPerformed(final ActionEvent evt) {
            output.setText("No"); // Only fires on every return/enter press
        }
    }
}

这就是它的样子:

Button Focus Example

可执行代码也可以在这里找到。

我的问题是:

  1. 这些不同的焦点是什么?
  2. 如何更改显示为虚线轮廓的按钮文本周围的焦点,以便空格键回车键触发“否”按钮的事件?

3
1)虚线只会在按钮有焦点时显示(即当文本字段有焦点时,您看不到虚线)。通常,如果按钮具有焦点,LAF将通过空格键调用该按钮。较粗的实线显示默认按钮。即使按钮没有焦点,通常情况下,LAF也会通过Enter键调用该按钮。 2)如果您想要能够使用空格键或Enter键,则必须a)使按钮具有焦点,并且它必须是默认按钮。请参见:https://stackoverflow.com/a/23771988/131872,特别是“Enter Key and Button”链接。 - camickr
既然我调用了 getRootPane().setDefaultButton(bNo) 将其设置为默认按钮,又调用了 bNo.requestFocus() 使其获得焦点,那么两者都应该生效才对吧? - Nico Wawrzyniak
3
应该使用requestFocusInWindow()方法而不是requestFocus()方法,该方法仅适用于在可见窗口上显示的可见组件。由于对话框不可见,因此该方法无效。由于对话框是模态的,除非使用类似于Dialog Focus中找到的RequestFocusListener,否则无法对按钮请求焦点。 - camickr
4
关于 [mre],你做得很好!由于 @camickr(Swing焦点方面最好的资源)已经解决了问题的核心,我只想补充一些内容。Java GUI必须在不同的操作系统、屏幕大小、屏幕分辨率等条件下工作,并在不同的区域设置中使用不同的PLAF。因此,它们不适合像素完美的布局。相反,应使用布局管理器或它们的组合以及布局填充和边框来创建白色空间。具体细节可参考这里:https://dev59.com/pG035IYBdhLWcg3wGMHa#5630271 和 https://dev59.com/3GMm5IYBdhLWcg3wG7_A#17874718。 - Andrew Thompson
我将您的评论转化为完整的答案,以供遇到相同困惑的人参考。希望您不介意。如果有任何错误或遗漏,请随时告诉我! - Nico Wawrzyniak
2个回答

1

对话框焦点资源(已在评论和已接受的解决方案中引用)也展示了一种更简单的方法。虽然该文章明确指出了简单解决方法的缺点,但对于上述场景,其中对话框完全是由用户代码构建的(与使用静态JOptionPane.showXXX相反),它将运行良好。

诀窍是在对话框可见前调用pack(),它的模态将阻止任何进一步的代码执行(以及焦点请求)

Component c = myDialog.getContentPane();
...
c.add(myYesButton);  // 1. Add all components to the dialog
myDialog.pack();  // Call pack() on the dialog
myYesButton.requestFocusInWindow();  // Request focus after pack() was called
myDialog.setVisible(true);  // Show the dialog

0
关于问题1:“虚线轮廓表示的按钮和粗实线轮廓表示的按钮有什么区别?”
回答:没有两种“焦点”。每种方法都执行其各自名称所说的操作:
JButton.requestFocus()(最好使用JButton.requestFocusInWindow())请求将焦点放在按钮上,而getRootPane().setDefaultButton(JButton)设置所选按钮,此操作由LAF单独处理。
关于问题2:“为什么我的特定实现会表现出这样的行为,我该如何实现我想要的行为?”
答案:对话框的模态是问题所在。在模态窗口上调用setVisible(true)后,您无法请求焦点。
因此,可能的解决方案是:
  1. 在创建对话框时将模态设置为false,例如使用new DialogMinimal(this, false);,并通过调用bNo.requestFocusInWindow()而不是getRootPane().setDefaultButton(bNo);和/或bNo.requestFocus();来获取焦点,但如果对话框必须是模态的,则这不是一个解决方案。

或者

  1. 按照用户camickr的建议实现Dialog Focus中找到的RequestFocusListener
public DialogMinimal(final JFrame owner, final boolean modal) {
    Button bNo = new JButton();
    [...]
    // bNo.requestFocusInWindow(); // obsolete now
    getRootPane().setDefaultButton(bNo); // To fire on enter key
    bNo.addAncestorListener(new RequestFocusListener()); // To fire on space bar
    [...]
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接