【问题标题】:JButton does not gain focus immediately after setEnabled(true)JButton 在 setEnabled(true) 之后没有立即获得焦点
【发布时间】:2023-03-10 05:40:02
【问题描述】:

我正在尝试制作一个直观的用户界面,用户可以在其中将数值输入JTextFields,使用 TAB 键前进,最后激活按钮以开始处理输入。

按钮一开始是禁用的,只有在所有数据输入到文本字段时才应该启用。

我正在使用 javax.swing.InputVerifier 来限制只输入 最多 4 位小数的正数,这很好用。

有 3 个可聚焦对象、两个文本字段和按钮。在文本字段中输入(有效)数字后按 TAB 键,如果 all 文本字段包含有效输入,则启用该按钮。这也很好用。

问题是:
当第一个文本字段已包含有效数据并按TAB 时,在第二个文本字段中键入有效数据后,按钮不会获​​得应有的焦点。相反,焦点会转移到一行中的下一个可聚焦对象,该对象(再次)是第一个文本字段。

我尝试使用两种不同的方法:

  1. 按钮的 enabled 属性通过 FocusListener 内部覆盖 focusLost() 方法进行更改
  2. 按钮的 enabled 属性在覆盖的 shouldYieldFocus() 方法内更改

在这两种情况下,焦点都会在启用按钮后立即跳过按钮。但是,如果我们随后 继续 使用 TABSHIFT+TAB 键更改焦点,按钮将获得焦点应该 - 在第二个文本字段之后。

在我看来,opposite 组件在启用按钮之前已预先确定,因此即使在启用按钮之后,该按钮也不会获得焦点。

我什至尝试在启用按钮后使用requestFocusInWindow() 强制按钮获得焦点,但这也没有用,所以问题是如何强制 LayoutFocusTraversalPolicy 重新评估布局,以便立即考虑新引入的按钮,该按钮之前被禁用?

这是我尝试过的两种方法的代码:

  1. 按钮的enabled 属性通过FocusListener 内部focusLost() 方法更改:
package verifiertest;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;

import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.text.JTextComponent;
import javax.swing.UIManager;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.math.BigDecimal;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingConstants;
import javax.swing.JTextField;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;

import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ActionEvent;

public class TestVerifier implements FocusListener {

    private JFrame frmInputverifierTest;
    private JTextField tfFirstNum;
    private JTextField tfSecondNum;
    private JLabel lblStatus;
    private JButton btnStart;
    private String statusText = "Input the numbers and press the \"Start!\" button...";

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    TestVerifier window = new TestVerifier();
                    window.frmInputverifierTest.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TestVerifier() {
        initialize();
    }

    private void initialize() {
        frmInputverifierTest = new JFrame();
        frmInputverifierTest.setTitle("InputVerifier Test");
        frmInputverifierTest.setBounds(100, 100, 500, 450);
        frmInputverifierTest.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // center the window
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
        frmInputverifierTest.setLocation(dim.width/2 - frmInputverifierTest.getWidth()/2, dim.height/2 - frmInputverifierTest.getHeight()/2);

        JPanel panelContainer = new JPanel();
        panelContainer.setBorder(new EmptyBorder(5, 5, 5, 5));
        frmInputverifierTest.getContentPane().add(panelContainer, BorderLayout.CENTER);
        panelContainer.setLayout(new BorderLayout(0, 0));

        JPanel panelInput = new JPanel();
        panelInput.setBorder(new TitledBorder(null, "Input", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelContainer.add(panelInput, BorderLayout.NORTH);
        panelInput.setLayout(new GridLayout(2, 2, 10, 4));

        JLabel lblFirstNum = new JLabel("Number #1:");
        lblFirstNum.setHorizontalAlignment(SwingConstants.TRAILING);
        panelInput.add(lblFirstNum);

        tfFirstNum = new JTextField();
        panelInput.add(tfFirstNum);
        tfFirstNum.setColumns(10);
        // setup the verifier
        MyTxtVerifier txtVerifier = new MyTxtVerifier();
        tfFirstNum.setInputVerifier(txtVerifier);
        // add focus listener
        tfFirstNum.addFocusListener(this);

        JLabel lblSecondNum = new JLabel("Number #2:");
        lblSecondNum.setHorizontalAlignment(SwingConstants.TRAILING);
        panelInput.add(lblSecondNum);

        tfSecondNum = new JTextField();
        panelInput.add(tfSecondNum);
        tfSecondNum.setColumns(10);
        // setup the verifier
        tfSecondNum.setInputVerifier(txtVerifier);
        // add focus listener
        tfSecondNum.addFocusListener(this);

        JPanel panelOutput = new JPanel();
        panelOutput.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Output (not used at the moment)", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelContainer.add(panelOutput, BorderLayout.CENTER);

        JPanel panelSouth = new JPanel();
        panelSouth.setBorder(null);
        panelContainer.add(panelSouth, BorderLayout.SOUTH);
        panelSouth.setLayout(new GridLayout(0, 1, 0, 0));

        JPanel panelStatus = new JPanel();
        FlowLayout flowLayout_1 = (FlowLayout) panelStatus.getLayout();
        flowLayout_1.setAlignment(FlowLayout.LEFT);
        panelStatus.setBorder(new TitledBorder(null, "Status", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelSouth.add(panelStatus);

        lblStatus = new JLabel(statusText);
        panelStatus.add(lblStatus);

        JPanel panelActions = new JPanel();
        panelActions.setBorder(new TitledBorder(null, "Actions", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        FlowLayout flowLayout = (FlowLayout) panelActions.getLayout();
        flowLayout.setAlignment(FlowLayout.RIGHT);
        panelSouth.add(panelActions);

        btnStart = new JButton("Start!");
        btnStart.setEnabled(false);
        btnStart.setVerifyInputWhenFocusTarget(true);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(frmInputverifierTest, "Start button pressed...", "Start", JOptionPane.PLAIN_MESSAGE);
            }
        });
        panelActions.add(btnStart);
    }

    // an inner class so it can access parent fields
    public class MyTxtVerifier extends InputVerifier {
        // This method should have no side effects
        @Override
        public boolean verify(JComponent input) {
            String text = ((JTextField)input).getText();
            // to allow changing focus when nothing is entered
            if(text.isEmpty())
                return true;
            try {
                BigDecimal value = new BigDecimal(text);
                if(value.floatValue() <= 0.0)
                    return false;
                return (value.scale() <= 4);
            } catch (Exception e) {
                return false;
            }
        }

        // This method can have side effects
        @Override
        public boolean shouldYieldFocus(JComponent input) {
            String statusOld, status;

            statusOld = statusText;         // remember the original text
            boolean isOK = verify(input);   // call overridden method
            if(isOK)
                status = statusOld;
            else {
                btnStart.setEnabled(false);
                status = "Error: The parameter should be a positive number up to 4 decimal places";
            }
            lblStatus.setText(status);
            // return super.shouldYieldFocus(input);
            return isOK;
        }
    }

    @Override
    public void focusGained(FocusEvent e) {
        // nothing to do on focus gained
    }

    @Override
    public void focusLost(FocusEvent e) {
        // in case we want to show a message box inside focusLost() - not to be fired twice
        if(e.isTemporary())
            return;
        final JTextComponent c = (JTextComponent)e.getSource();
        // in case there are more text fields but
        // we are validating only some of them
        if(c.equals(tfFirstNum) || c.equals(tfSecondNum)) {
            // are all text fields valid?
            if(c.getInputVerifier().verify(tfFirstNum) && c.getInputVerifier().verify(tfSecondNum) &&
                    !tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
                btnStart.setEnabled(true);
            else
                btnStart.setEnabled(false);
        }
    }
}
  1. 按钮的enabled 属性在覆盖的shouldYieldFocus() 方法中被更改:
package verifiertest;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;

import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.UIManager;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.math.BigDecimal;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingConstants;
import javax.swing.JTextField;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;

import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class TestVerifier {

    private JFrame frmInputverifierTest;
    private JTextField tfFirstNum;
    private JTextField tfSecondNum;
    private JLabel lblStatus;
    private JButton btnStart;
    private String statusText = "Input the numbers and press the \"Start!\" button...";

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    TestVerifier window = new TestVerifier();
                    window.frmInputverifierTest.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TestVerifier() {
        initialize();
    }

    private void initialize() {
        frmInputverifierTest = new JFrame();
        frmInputverifierTest.setTitle("InputVerifier Test");
        frmInputverifierTest.setBounds(100, 100, 500, 450);
        frmInputverifierTest.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // center the window
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
        frmInputverifierTest.setLocation(dim.width/2 - frmInputverifierTest.getWidth()/2, dim.height/2 - frmInputverifierTest.getHeight()/2);

        JPanel panelContainer = new JPanel();
        panelContainer.setBorder(new EmptyBorder(5, 5, 5, 5));
        frmInputverifierTest.getContentPane().add(panelContainer, BorderLayout.CENTER);
        panelContainer.setLayout(new BorderLayout(0, 0));

        JPanel panelInput = new JPanel();
        panelInput.setBorder(new TitledBorder(null, "Input", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelContainer.add(panelInput, BorderLayout.NORTH);
        panelInput.setLayout(new GridLayout(2, 2, 10, 4));

        JLabel lblFirstNum = new JLabel("Number #1:");
        lblFirstNum.setHorizontalAlignment(SwingConstants.TRAILING);
        panelInput.add(lblFirstNum);

        tfFirstNum = new JTextField();
        panelInput.add(tfFirstNum);
        tfFirstNum.setColumns(10);
        // setup the verifier
        MyTxtVerifier txtVerifier = new MyTxtVerifier();
        tfFirstNum.setInputVerifier(txtVerifier);

        JLabel lblSecondNum = new JLabel("Number #2:");
        lblSecondNum.setHorizontalAlignment(SwingConstants.TRAILING);
        panelInput.add(lblSecondNum);

        tfSecondNum = new JTextField();
        panelInput.add(tfSecondNum);
        tfSecondNum.setColumns(10);
        // setup the verifier
        tfSecondNum.setInputVerifier(txtVerifier);

        JPanel panelOutput = new JPanel();
        panelOutput.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Output (not used at the moment)", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelContainer.add(panelOutput, BorderLayout.CENTER);

        JPanel panelSouth = new JPanel();
        panelSouth.setBorder(null);
        panelContainer.add(panelSouth, BorderLayout.SOUTH);
        panelSouth.setLayout(new GridLayout(0, 1, 0, 0));

        JPanel panelStatus = new JPanel();
        FlowLayout flowLayout_1 = (FlowLayout) panelStatus.getLayout();
        flowLayout_1.setAlignment(FlowLayout.LEFT);
        panelStatus.setBorder(new TitledBorder(null, "Status", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        panelSouth.add(panelStatus);

        lblStatus = new JLabel(statusText);
        panelStatus.add(lblStatus);

        JPanel panelActions = new JPanel();
        panelActions.setBorder(new TitledBorder(null, "Actions", TitledBorder.LEADING, TitledBorder.TOP, null, null));
        FlowLayout flowLayout = (FlowLayout) panelActions.getLayout();
        flowLayout.setAlignment(FlowLayout.RIGHT);
        panelSouth.add(panelActions);

        btnStart = new JButton("Start!");
        btnStart.setEnabled(false);
        btnStart.setVerifyInputWhenFocusTarget(true);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(frmInputverifierTest, "Start button pressed...", "Start", JOptionPane.PLAIN_MESSAGE);
            }
        });
        panelActions.add(btnStart);
    }

    // an inner class so it can access parent fields
    public class MyTxtVerifier extends InputVerifier {
        // This method should have no side effects
        @Override
        public boolean verify(JComponent input) {
            String text = ((JTextField)input).getText();
            // to allow changing focus when nothing is entered
            if(text.isEmpty())
                return true;
            try {
                BigDecimal value = new BigDecimal(text);
                if(value.floatValue() <= 0.0)
                    return false;
                return (value.scale() <= 4);
            } catch (Exception e) {
                return false;
            }
        }

        // This method can have side effects
        @Override
        public boolean shouldYieldFocus(JComponent input) {
            String statusOld, status;

            statusOld = statusText;         // remember the original text
            boolean isOK = verify(input);   // call overridden method
            if(isOK)
                status = statusOld;
            else {
                status = "Error: The parameter should be a positive number up to 4 decimal places";
            }
            lblStatus.setText(status);
            setBtnState(input);             // enable or disable the button
            //btnStart.requestFocusInWindow();  //  <-- does not help
            // return super.shouldYieldFocus(input);
            return isOK;
        }
    }

    private void setBtnState(JComponent input) {
        if (input.equals(tfFirstNum) || input.equals(tfSecondNum)) {
            // are all text fields valid?
            if (input.getInputVerifier().verify(tfFirstNum) && input.getInputVerifier().verify(tfSecondNum)
                    && !tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
                btnStart.setEnabled(true);
            else
                btnStart.setEnabled(false);
        }
    }
}

这是测试应用的截图:

注意:
该代码与the question I asked before中包含的代码相关,这是另一个主题。

编辑:
在尝试接受答案的作者提出的建议(使用invokeLater() 运行requestFocusInWindow())后,这里是可以作为概念证明的代码:

@Override
public void focusLost(FocusEvent e) {
    // in case we want to show a message box inside focusLost() - not to be fired twice
    if(e.isTemporary())
        return;
    final JTextComponent c = (JTextComponent)e.getSource();
    // in case there are more text fields but
    // we are validating only some of them
    if(c.equals(tfFirstNum) || c.equals(tfSecondNum)) {
        // are all text fields valid?
        if(c.getInputVerifier().verify(tfFirstNum) && c.getInputVerifier().verify(tfSecondNum) &&
                !tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
            btnStart.setEnabled(true);
        else
            btnStart.setEnabled(false);
    }
    if (btnStart.isEnabled() && e.getOppositeComponent()==tfFirstNum) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                btnStart.requestFocusInWindow();
            }
        });
    }
}

这只是与approach #01 相关的更改后的focusLost() 方法。我不知道是否有类似的解决方案可用于approach #02 - 因为我不知道当没有FocusListener 时是否可以从shouldYieldFocus() 内部引用opposite

注意:
使用此解决方案时可以清楚地观察到,在输入第二个数字并按下 TAB 按钮后,焦点首先(暂时)跳转到第一个文本字段,然后才移动到按钮.

【问题讨论】:

    标签: java swing validation focus inputverifier


    【解决方案1】:

    我建议你不要使用InputVerifier,而是使用DocumentListener

    使用DocumentListener 的好处是可以在输入每个字符时编辑文本字段,因此用户可以立即获得反馈。然后,只要您输入第一个数字,就可以启用按钮(如果它通过了您的编辑条件)。

    由于该按钮现在将在用户尝试输入 Tab 键之前​​启用,因此您不会遇到任何焦点问题。

    这是一个入门的基本示例:

    import java.awt.*;
    import java.awt.event.*;
    import java.util.List;
    import java.util.ArrayList;
    import javax.swing.*;
    import javax.swing.event.*;
    
    public class DataEntered implements DocumentListener
    {
        private JButton button;
        private List<JTextField> textFields = new ArrayList<JTextField>();
    
        public DataEntered(JButton button)
        {
            this.button = button;
        }
    
        public void addTextField(JTextField textField)
        {
            textFields.add( textField );
            textField.getDocument().addDocumentListener( this );
        }
    
        public boolean isDataEntered()
        {
            for (JTextField textField : textFields)
            {
                if (textField.getText().trim().length() == 0)
                    return false;
            }
    
            return true;
        }
    
        @Override
        public void insertUpdate(DocumentEvent e)
        {
            checkData();
        }
    
        @Override
        public void removeUpdate(DocumentEvent e)
        {
            checkData();
        }
    
        @Override
        public void changedUpdate(DocumentEvent e) {}
    
        private void checkData()
        {
            button.setEnabled( isDataEntered() );
        }
    
        private static void createAndShowUI()
        {
            JButton submit = new JButton( "Submit" );
            submit.setEnabled( false );
    
            JTextField textField1 = new JTextField(10);
            JTextField textField2 = new JTextField(10);
    
            DataEntered de = new DataEntered( submit );
            de.addTextField( textField1 );
            de.addTextField( textField2 );
    
            JFrame frame = new JFrame("SSCCE");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(textField1, BorderLayout.WEST);
            frame.add(textField2, BorderLayout.EAST);
            frame.add(submit, BorderLayout.SOUTH);
            frame.pack();
            frame.setLocationByPlatform( true );
            frame.setVisible( true );
        }
    
        public static void main(String[] args)
        {
            EventQueue.invokeLater(new Runnable()
            {
                public void run()
                {
                    createAndShowUI();
                }
            });
        }
    }
    

    只要输入任何文本,基本代码就会启用按钮。您需要修改 dataEntered() 方法以应用您的编辑条件。

    编辑:

    我不知道有什么方法可以使用 API 来做你想做的事。以下是可能的黑客攻击。

    据我了解,您会遇到两种情况:

    1. 当焦点位于表单的最后一个字段并且您使用 Tab 时
    2. 当焦点位于表单的第一个字段并且您使用 Shift-Tab 时

    所以也许你可以做的是用两个参数创建你的 InputVerifier,第一个和最后一个组件。然后当你使用 FocusListener 和

    1. 当前焦点在第一个组件,相反的组件是最后一个
    2. 当前焦点在最后一个组件上,相反的组件在第一个组件上

    您知道您正在环绕表单。在这两种情况下,您希望将焦点放在“保存”按钮上,因此您需要手动请求将焦点放在“保存”按钮上。所以你可以通过使用来做到这一点:

    saveButton.requestFocusInWindow();
    

    注意焦点仍然会先转到相反的组件,然后再转到按钮。您可能还需要将该代码包装在 SwingUtilities.invokeLater() 中。

    【讨论】:

    • 非常感谢您提供的代码!您是否建议不可能使用我的任何一种方法来实现所需的行为?我发布的代码只是为了演示这个问题,真正的应用程序将包含许多具有不同且更复杂的验证算法的输入字段,我觉得在每个输入字符后重新评估所有内容将是一个很大的开销。我也是出于好奇而写了这个问题,而不仅仅是为了找到解决方法。可能有一些事情可以做 - 也许以某种方式或类似的方式即时更改opposite
    • @Chupo_cro,也许我的 hack 会给你一些想法?见编辑。
    • 我已经尝试用requestFocusInWindow()强制聚焦目标,你可以在我的代码中看到注释行//btnStart.requestFocusInWindow(); // &lt;-- does not help,我也在问题中提到它并说它也不起作用。我会尝试使用invokeLater() 看看是否有帮助。我很惊讶这样的基本行为是如此难以实现。我希望 API 的设计理念是“允许以有效和直接的方式创建 UI”。
    • @Chupo_cro, I am surprised such a basic behavior is so difficult to achieve. - 不是。 invokeLater 对我来说很好。我没有注意到您的代码,因为您应该在启用按钮时设置焦点,而不是从方法返回时。也就是说,如果按钮仍处于禁用状态,您不想尝试请求焦点。
    • 是的,invokeLater() 确实成功了。我刚刚编辑了问题并根据您的建议添加了代码作为概念证明。我确实在启用按钮之后请求了焦点,请查看setBtnState(input); // enable or disable the button 行,仅在//btnStart.requestFocusInWindow(); // &lt;-- does not help 上方一行。第一行启用按钮,但没有invokeLater() 将无法工作。现在,如果有办法使用我的approach 02 实现相同的效果,那么唯一的“谜团”仍然存在(请参阅我的编辑)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多