JButton does not gain focus immediately after setEnabled(true)

不想你离开。 提交于 2020-01-05 03:44:27

问题


I am trying to make an intuitive user interface where the user would enter the numeric values into the JTextFields, advance with the TAB key and finally activate the button to start processing the input.

At the beginning the button is disabled and it should be enabled only when all of the data is entered into the text fields.

I am using javax.swing.InputVerifier to restrict entering only positive numbers up to 4 decimal places and that works fine.

There are 3 focusable objects, two text fields and the button. Pressing the TAB key after typing the (valid) number into the text field, and if all the text fields contain valid inputs, enables the button. That works fine too.

The problem is:
After typing the valid data into the second text field when the first text field already contains valid data and pressing the TAB, the button does not gain the focus as it should. Instead, the focus is transfered to the next focusable object in a row which is (again) the first text field.

I tried to use two different approaches:

  1. The button's enabled property is changed via FocusListener inside overriden focusLost() method
  2. The button's enabled property is changed inside overriden shouldYieldFocus() method

In both cases the focus skips the button immediately after enabling the button. However, if we then continue to change the focus using TAB and SHIFT+TAB keys, the button gains focus as it should - right after the second text field.

It seems to me as the opposite component has been predetermined before enabling the button so the button does not gain the focus even after it gets enabled.

I even tried to force the button to gain the focus using requestFocusInWindow() after enabling the button but that didnt'n work either so the question is how to force the LayoutFocusTraversalPolicy to re-evaluate the Layout so it can immediately take into account the newly introduced button which was before disabled?

Here is the code for both the approaches I tried:

  1. The button's enabled property is changed via FocusListener inside focusLost() method:
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. The button's enabled property is changed inside overriden shouldYieldFocus() method:
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);
        }
    }
}

Here is the screenshot of the test application:

Note:
The code is related to the code contained in the question I asked before, which was another topic.

EDIT:
Upon trying out the suggestion (using invokeLater() to run the requestFocusInWindow()) proposed by the author of the accepted answer, here is the code that can serve as a proof of concept:

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

This is just the changed focusLost() method pertaining to the approach #01. I am not aware if there is similar solution to be used with the approach #02 - since I don't know if it is possible to reference the opposite from inside the shouldYieldFocus() when there isn't a FocusListener.

Note:
When using this solution it can be clearly observed that after entering the 2nd number and pressing the TAB button, focus first (for the moment of time) jumps to the first text field and only then moves to the button.


回答1:


I would suggest you don't use an InputVerifier, but instead use a DocumentListener.

The benefit of using the DocumentListener is that the text field can be edited as each character is entered, so the user has immediate feedback. Then as soon as you enter the first digit the button can be enabled (if it passes your editing criteria).

Since the button will now be enabled before the user attempts to enter the Tab key you will not have any focus issues.

Here is a basic example to get your started:

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

The basic code enables the button whenver any text is entered. You would need to modify the dataEntered() method to apply your editing criteria.

Edit:

I don't know any way using the API to do what you want. Following is a possible hack.

As I understand it you will have the problem is two situations:

  1. When focus is on the last field of the form and you use Tab
  2. When focus is on the first field of the form and you use Shift-Tab

So maybe what you can do is create you InputVerifier with two parameter, the first and last components. Then when you use the FocusListener and

  1. current focus is on the first component and the opposite component is the last
  2. current focus is on the last component and the opposite component is on the first

You know you are wrapping around the form. In these two situations you want focus to be placed on the "Save" button so you need to manually request focus on the Save button. So you could do this by just using:

saveButton.requestFocusInWindow();

Note focus would still go the to opposite component first and then to the button. You might also need to wrap that code in a SwingUtilities.invokeLater().



来源:https://stackoverflow.com/questions/38779297/jbutton-does-not-gain-focus-immediately-after-setenabledtrue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!