【问题标题】:JSpinner: Notification whether input is valid or notJSpinner:通知输入是否有效
【发布时间】:2014-03-01 13:51:28
【问题描述】:

我有一个带有 NumberModel 的 JSpinner。 Spinner 的文本字段允许任意输入,但只接受 commitEdit 上的数字输入。这意味着,如果我输入一个数字后跟任何字母字符并按 Enter,那么格式化程序会尝试解析输入并最终切断垃圾输入:"2adsklfja" --> "2"

如果有垃圾输入,我希望在它尝试解析整个输入之前得到通知。是否有任何事件提供此信息?

更新:我接受一些数字和字母输入的组合。微调器存储测量值。如果用户输入 "23 inch" 然后我检测子字符串英寸并相应地转换数值。我的目标是在无法检测到或未知测量单位字符串时显示错误警报。

更新(不完整的解决方案): 此解决方案有助于用户输入以下形式:number text

我在微调器的文本字段中添加了动作监听器,并为微调器添加了更改监听器。两个侦听器都调用尝试解析文本字段中潜在测量单位字符串的代码。如果这不成功,则当前输入可能只是数字输入,或者具有未知的尾随字母字符串。我们需要通过使用正则表达式检查当前输入字符串来检测第二种情况:如果它完全是数字,那么它不是无效输入。我们还必须考虑十进制分隔符和千位分隔符:

final DefaultFormatterFactory formatFact = (DefaultFormatterFactory)spinnerTextField.getFormatterFactory();
final NumberFormatter formatter = (NumberFormatter)formatFact.getDefaultFormatter();
final String currentValue = spinnerTextField.getText().trim();

// detecting known measuring unit strings
...

// catch invalid strings
if (!unitFound) {
  final char decSeparator = ((DecimalFormat)formatter.getFormat()).getDecimalFormatSymbols().getDecimalSeparator();
  final char thousandsSeparator = ((DecimalFormat)formatter.getFormat()).getDecimalFormatSymbols()
    .getGroupingSeparator();
  final boolean numberOnly = currentValue.matches("[\\d\\Q" + decSeparator + thousandsSeparator + "\\E]+");
  System.err.println("Invalid=" + !numberOnly + (!numberOnly ? ": " + currentValue : ""));
}

【问题讨论】:

  • 你可以使用InputVerifier,看看Validating Input
  • @MadProgrammer 我一直在玩InputVerifier(一个微调器默认有一个)。它比看起来更棘手!
  • outlined in a recent QA 收听文本字段的 editValid 属性
  • @MadProgrammer - 同意 Andrew:InputVerifier 和 JFormattedTextField 不能很好地相互配合
  • 嗯……听起来很可疑。无论如何,出路是一个自定义格式化程序,它可以检测有效/无效单元(或其中的一部分)

标签: java swing validation jspinner


【解决方案1】:

你说:

Spinner 的文本字段允许任意输入,但只接受 commitEdit 上的数字输入。

如果您只是希望您的commitEdit() 电话接受“125 英寸”之类的内容,请尝试以下方法:

  1. JSpinner 提供DefaultEditor
  2. 获取DefaultEditorJFormattedTextField
  3. 使其可编辑。
  4. 为其提供自定义(简单)AbstractFormatterFactory
  5. 您的自定义 AbstractFormatterFactory 使自定义(简单)AbstractFormatters。
  6. 这些AbstractFormatters 将用户的输入字符串解析为一个值并返回,因此您可以检查任何解析错误并在需要时抛出ParseException

遵循示例代码:

import java.awt.GridLayout;  
import java.text.ParseException;  
import java.util.HashMap;  
import java.util.Objects;  
import javax.swing.JButton;  
import javax.swing.JFormattedTextField;  
import javax.swing.JFormattedTextField.AbstractFormatter;  
import javax.swing.JFormattedTextField.AbstractFormatterFactory;  
import javax.swing.JFrame;  
import javax.swing.JOptionPane;  
import javax.swing.JPanel;  
import javax.swing.JSpinner;  
import javax.swing.JSpinner.DefaultEditor;  
import javax.swing.SpinnerNumberModel;  

public final class CommitSpinner extends JPanel {  
    private String currentUnit;

    /**
     * Your formatter converts the text to integer by ignoring any measuring units,
     * and then converts the integer back to string by appending the last measuring unit:
     */
    private final class MyFormatter extends AbstractFormatter {
        @Override
        public Object stringToValue(final String text) throws ParseException {
            currentUnit = parseUnitPart(text);
            return parseIntegerPart(text);
        }

        @Override
        public String valueToString(final Object value) throws ParseException {
            return Objects.toString(value) + ' '  + currentUnit;
        }
    }

    private final class MyFormatterFactory extends AbstractFormatterFactory {
        private final HashMap<JFormattedTextField, AbstractFormatter> formatters;

        private MyFormatterFactory() {
            formatters = new HashMap<>();
        }

        /**
         * Because this method is a 'getter', I implemented the factory with a HashMap.
         * If it was a 'createFormatter' method for example, you could simply return a new
         * instance of MyFormatter.
         * @param tf the formatted text field to obtain its formatter.
         * @return the formatter for the given formatted text field.
         */
        @Override
        public AbstractFormatter getFormatter(final JFormattedTextField tf) {
            if (!formatters.containsKey(tf))
                formatters.put(tf, new MyFormatter());
            return formatters.get(tf);
        }
    }

    private CommitSpinner() {
        super(new GridLayout(0, 1));

        currentUnit = "inch"; //Let's say the first value of the spinner is measured in inches.

        final JSpinner spin = new JSpinner(new SpinnerNumberModel(0, 0, 10000, 1)); //Your values here. Let's say for now this is a spinner for integers.

        final DefaultEditor editor = new DefaultEditor(spin); //We need a DefaultEditor (to be able to obtain the JFormattedTextField and customize it).

        final JFormattedTextField field = editor.getTextField();
        field.setFocusLostBehavior(JFormattedTextField.COMMIT); //Could be "PERSIST" also, but it shall not be "COMMIT_OR_REVERT" (which is the default).
        field.setFormatterFactory(new MyFormatterFactory());
        field.setEditable(true); //Allow user input (obvious reasons).

        spin.setEditor(editor);

        //The commitButton button will "commitEdit()", and the ParseException exception part works as expected!
        final JButton commitButton = new JButton("Check input");
        commitButton.addActionListener(e -> {
            try {
                spin.commitEdit();
                JOptionPane.showMessageDialog(null, currentUnit + " = " + spin.getValue());
            }
            catch (final ParseException pe) {
                JOptionPane.showMessageDialog(null, pe.toString(), "Illegal input!", JOptionPane.ERROR_MESSAGE);
            }
        });

        add(spin);
        add(commitButton);
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("CommitSpinner demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new CommitSpinner());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    //Let's parse user input... What could go wrong?... xD
    private static int parseIntegerPart(final String text) throws ParseException {
        if (text == null)
            throw new ParseException("Text is null.", 0);
        if (text.trim().isEmpty())
            throw new ParseException("Text is empty.", 0);
        final String[] args = text.split(" ");
        if (args.length != 2)
            throw new ParseException("Text has invalid number of arguments (required 2, found " + args.length + ").", 0);
        final String unit = args[1].trim();
        if (!unit.equalsIgnoreCase("inch")
            && !unit.equalsIgnoreCase("cm"))
            throw new ParseException("Nor 'inch', nor 'cm' detected.", 0);
        try {
            return Integer.valueOf(args[0].trim());
        }
        catch (final NumberFormatException nfe) {
            throw new ParseException(args[0] + " is not a valid integer value.", 0);
        }
    }

    //Let's parse user input... What could go wrong?... xD
    private static String parseUnitPart(final String text) throws ParseException {
        if (text == null)
            throw new ParseException("Text is null.", 0);
        if (text.trim().isEmpty())
            throw new ParseException("Text is empty.", 0);
        final String[] args = text.split(" ");
        if (args.length != 2)
            throw new ParseException("Text has invalid number of arguments (required 2, found " + args.length + ").", 0);
        final String unit = args[1].trim().toLowerCase();
        if (!unit.equals("inch") && !unit.equals("cm"))
            throw new ParseException("Nor 'inch', nor 'cm' detected.", 0);
        return unit;
    }
}  

你也说过:

如果有垃圾输入,我希望在它尝试解析整个输入之前得到通知。

JFormattedTextField 中的自定义 DocumentFilter 可以解决问题。

AbstractFormatter 中有一个名为getDocumentFilter() 的方法。如果您看到代码,则调用此方法将DocumentFilter 添加到JFormattedTextField

DocumentFilters' 方法在用户输入时被调用,也就是说,您可以在用户“提交”之前解析用户的输入。

默认实现返回null,这意味着不会添加DocumentFilter

所以我只是重写了这个方法来实现我的自定义(简单)DocumentFilter,它反过来在用户输入时检查用户输入......

如果用户的输入解析成功,那么我只需将标签的前景色设置为绿色。如果没有,那么红色。但这只是为了演示您可以在哪里处理此类事件。

以下示例代码:

import java.awt.Color;
import java.awt.GridLayout;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Objects;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.SpinnerNumberModel;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;

public final class RealTimeSpinner extends JPanel {
    private String currentUnit;
    private final JLabel stateLabel;

    private final class MyFormatter extends AbstractFormatter {
        @Override
        public Object stringToValue(final String text) throws ParseException {
            currentUnit = parseUnitPart(text);
            return parseIntegerPart(text);
        }

        @Override
        public String valueToString(final Object value) throws ParseException {
            return Objects.toString(value) + ' '  + currentUnit;
        }

        @Override
        protected DocumentFilter getDocumentFilter() {
            return new DocumentFilter() {
                private void after(final FilterBypass fb) throws BadLocationException {
                    try {
                        //Obtain the user's input text so far:
                        final String theWholeNewText = fb.getDocument().getText(0, fb.getDocument().getLength());

                        //Try parse the user's input so far:
                        parseUnitPart(theWholeNewText);
                        parseIntegerPart(theWholeNewText);

                        //If the parsing succeeds, then set the foreground color to GREEN:
                        stateLabel.setForeground(Color.GREEN.darker());
                    }
                    catch (final ParseException pe) {

                        //If the parsing fails, then set the foreground color to RED:
                        stateLabel.setForeground(Color.RED.darker());
                    }
                }

                @Override
                public void remove(final FilterBypass fb, final int offset, final int length) throws BadLocationException {
                    super.remove(fb, offset, length);
                    after(fb);
                }

                @Override
                public void insertString(final FilterBypass fb, final int offset, final String string, final AttributeSet attr) throws BadLocationException {
                    super.insertString(fb, offset, string, attr);
                    after(fb);
                }

                @Override
                public void replace(final FilterBypass fb, final int offset, final int length, final String text, final AttributeSet attrs) throws BadLocationException {
                    super.replace(fb, offset, length, text, attrs);
                    after(fb);
                }
            };
        }
    }

    private final class MyFormatterFactory extends AbstractFormatterFactory {
        private final HashMap<JFormattedTextField, AbstractFormatter> formatters;

        private MyFormatterFactory() {
            formatters = new HashMap<>();
        }

        /**
         * Because this method is a 'getter', I implemented the factory with a HashMap.
         * If it was a 'createFormatter' method for example, you could simply return a new
         * instance of MyFormatter.
         * @param tf the formatted text field to obtain its formatter.
         * @return the formatter for the given formatted text field.
         */
        @Override
        public AbstractFormatter getFormatter(final JFormattedTextField tf) {
            if (!formatters.containsKey(tf))
                formatters.put(tf, new MyFormatter());
            return formatters.get(tf);
        }
    }

    private RealTimeSpinner() {
        super(new GridLayout(0, 1));

        currentUnit = "inch"; //Let's say the first value of the spinner is measured in inches.

        final JSpinner spin = new JSpinner(new SpinnerNumberModel(0, 0, 10000, 1)); //Your values here. Let's say for now this is a spinner for integers.

        final DefaultEditor editor = new DefaultEditor(spin); //We need a DefaultEditor (to be able to obtain the JFormattedTextField and customize it).

        final JFormattedTextField field = editor.getTextField();
        field.setFocusLostBehavior(JFormattedTextField.COMMIT); //Could be "PERSIST" also, but it shall not be "COMMIT_OR_REVERT" (which is the default).
        field.setFormatterFactory(new MyFormatterFactory());
        field.setEditable(true); //Allow user input (obvious reasons).

        spin.setEditor(editor);

        final JButton commitButton = new JButton("Check input");
        commitButton.addActionListener(e -> {
            try {
                spin.commitEdit();
                JOptionPane.showMessageDialog(null, currentUnit + " = " + spin.getValue());
            }
            catch (final ParseException pe) {
                JOptionPane.showMessageDialog(null, pe.toString(), "Illegal input!", JOptionPane.ERROR_MESSAGE);
            }
        });

        stateLabel = new JLabel("This is the color of the state of the input.", JLabel.CENTER);
        stateLabel.setForeground(Color.GREEN.darker());

        add(spin);
        add(commitButton);
        add(stateLabel);
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("CommitSpinner demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new RealTimeSpinner());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    //Let's parse user input... What could go wrong?... xD
    private static int parseIntegerPart(final String text) throws ParseException {
        if (text == null)
            throw new ParseException("Text is null.", 0);
        if (text.trim().isEmpty())
            throw new ParseException("Text is empty.", 0);
        final String[] args = text.split(" ");
        if (args.length != 2)
            throw new ParseException("Text has invalid number of arguments (required 2, found " + args.length + ").", 0);
        final String unit = args[1].trim();
        if (!unit.equalsIgnoreCase("inch")
            && !unit.equalsIgnoreCase("cm"))
            throw new ParseException("Nor 'inch', nor 'cm' detected.", 0);
        try {
            return Integer.valueOf(args[0].trim());
        }
        catch (final NumberFormatException nfe) {
            throw new ParseException(args[0] + " is not a valid integer value.", 0);
        }
    }

    //Let's parse user input... What could go wrong?... xD
    private static String parseUnitPart(final String text) throws ParseException {
        if (text == null)
            throw new ParseException("Text is null.", 0);
        if (text.trim().isEmpty())
            throw new ParseException("Text is empty.", 0);
        final String[] args = text.split(" ");
        if (args.length != 2)
            throw new ParseException("Text has invalid number of arguments (required 2, found " + args.length + ").", 0);
        final String unit = args[1].trim().toLowerCase();
        if (!unit.equals("inch") && !unit.equals("cm"))
            throw new ParseException("Nor 'inch', nor 'cm' detected.", 0);
        return unit;
    }
}

【讨论】:

    猜你喜欢
    • 2016-03-13
    • 2013-03-06
    • 1970-01-01
    • 2020-05-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    相关资源
    最近更新 更多