-->

JavaFX TextField validation for integer input and

2020-04-15 15:46发布

问题:

I want to add validation in javafx TextField such that user should only be able to insert integer values ([0-9] and Dot ). Also user should be able to insert either B or b(for Billion) and K or k (for Thousand) and M or m( for Million). Basically it should be an amountfield. Delete and Backspace should also work.

for example :

10k should become 10,000.00 as soon as user hit k and K should not be displayed on amountfield (textfield) Similarly 10M or 10m should convert into 10,000,000.00

adsadi342fn3 or 31233123werwer or dsad342134k should not be allowed to enter in the textfield.

I have used getKeyChar method while validating TextField in case of Swing. But I need same implementation in case of JavaFx where we don't have getKeyChar method.

I have used the following method but the problem with this is it allows user to enter any value. example : sdafewr23rf

private void amountEntered() {
        if (amountField != null) {
            String value;
            char[] charArray = amountField.getText().toCharArray();
            if (charArray.length > 0)
                switch (charArray[charArray.length - 1]) {
                    case 't':
                    case 'T':
                        value = multiplyValue(amountField.getText(), new BigDecimal(1000000000000.0));
                        updateAmount(value);
                        break;
                    case 'b':
                    case 'B':
                        value = multiplyValue(amountField.getText(), new BigDecimal(1000000000.0));
                        updateAmount(value);
                        break;
                    case 'm':
                    case 'M':
                        value = multiplyValue(amountField.getText(), new BigDecimal(1000000.0));
                        updateAmount(value);
                        break;
                    case 'k':
                    case 'K':
                        value = multiplyValue(amountField.getText(), new BigDecimal(1000.0));
                        updateAmount(value);
                        break;
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                    case '.':
                    case ',':
                        updateAmount(amountField.getText());
                        break;
                    default:
                        break;
                }
        }
    }

private String multiplyValue(String number, BigDecimal multValue) {
        //get rid of "," for double parsing
        BigDecimal value = new BigDecimal(cleanDouble(number.substring(0, number.length() - 1)));
        value = value.multiply(multValue);
        return value.toPlainString();
    }

回答1:

As well as listening to changes in the text property and reverting if they are invalid, you can use a TextFormatter to veto changes to the text. Using this approach will avoid other listeners to the textProperty seeing the invalid value and then seeing it revert to the previous value: i.e. the textProperty will always contain something valid.

The TextFormatter takes a UnaryOperator<TextFormatter.Change> which acts as a filter. The filter can return null to veto the change entirely, or can modify properties of the Change as needed.

Here is a fairly straightforward example, where "k" or "K" is replaced by "000", "m" or "M" by "000000", and other non-digit characters are removed:

import java.util.function.UnaryOperator;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class TextFieldFilteringExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TextField textField = new TextField();

        textField.textProperty().addListener((obs, oldValue, newValue) -> {
           System.out.println("Text change from "+oldValue+" to "+newValue);
        });

        UnaryOperator<Change> filter = change -> {
            if (change.isAdded()) {
                String addedText = change.getText();
                if (addedText.matches("[0-9]*")) {
                    return change ;
                }
                // remove illegal characters:
                int length = addedText.length();
                addedText = addedText.replaceAll("[^0-9kKmM]", "");
                // replace "k" and "K" with "000":
                addedText = addedText.replaceAll("[kK]", "000");
                // replace "m" and "M" with "000000":
                addedText = addedText.replaceAll("[mM]", "000000");
                change.setText(addedText);

                // modify caret position if size of text changed:
                int delta = addedText.length() - length ;
                change.setCaretPosition(change.getCaretPosition() + delta);  
                change.setAnchor(change.getAnchor() + delta);
            }
            return change ;
        };

        textField.setTextFormatter(new TextFormatter<String>(filter));

        StackPane root = new StackPane(textField);
        root.setPadding(new Insets(20));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

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

You could also modify the text to introduce grouping separators (e.g. 1,000,000), though the logic gets quite tricky there. You can additionally specify a StringConverter<BigInteger> for the text formatter, so that the formatter itself has a value of type BigInteger which is the result of passing the text through the supplied converter.



回答2:

I created the following class to filter input on TextField, which also uses the TextFormatter introduced in JavaFX 8.

public class TextFieldValidator {

    private static final String CURRENCY_SYMBOL   = DecimalFormatSymbols.getInstance().getCurrencySymbol();
    private static final char   DECIMAL_SEPARATOR = DecimalFormatSymbols.getInstance().getDecimalSeparator();

    private final Pattern       INPUT_PATTERN;

    public TextFieldValidator(@NamedArg("modus") ValidationModus modus, @NamedArg("maxCountOf") int maxCountOf) {
        this(modus.createPattern(maxCountOf));
    }

    public TextFieldValidator(@NamedArg("regex") String regex){
        this(Pattern.compile(regex));
    }

    public TextFieldValidator(Pattern pattern){ 
        INPUT_PATTERN = pattern;
    }

    public static TextFieldValidator maxFractionDigits(int maxCountOf) {
        return new TextFieldValidator(maxFractionPattern(maxCountOf));
    }

    public static TextFieldValidator maxIntegers(int maxCountOf) {
        return new TextFieldValidator(maxIntegerPattern(maxCountOf));
    }

    public static TextFieldValidator integersOnly() {
        return new TextFieldValidator(integersOnlyPattern());
    }

    public TextFormatter<Object> getFormatter() {
        return new TextFormatter<>(this::validateChange);
    }

    private Change validateChange(Change c) {
        if (validate(c.getControlNewText())) {
            return c;
        }
        return null;
    }

    public boolean validate(String input) {
        return INPUT_PATTERN.matcher(input).matches();
    }

    private static Pattern maxFractionPattern(int maxCountOf) {
        return Pattern.compile("\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + maxCountOf+ "})?");
    }

    private static Pattern maxCurrencyFractionPattern(int maxCountOf) {
        return Pattern.compile("^\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + maxCountOf+ "})?\\s?\\" +
                CURRENCY_SYMBOL + "?");
    }

    private static Pattern maxIntegerPattern(int maxCountOf) {
        return Pattern.compile("\\d{0," + maxCountOf+ "}");
    }

    private static Pattern integersOnlyPattern() {
        return Pattern.compile("\\d*");
    }

    public enum ValidationModus {

        MAX_CURRENCY_FRACTION_DIGITS {
            @Override
            public Pattern createPattern(int maxCountOf) {
                return maxCurrencyFractionPattern(maxCountOf);
            }
        },

        MAX_FRACTION_DIGITS {
            @Override
            public Pattern createPattern(int maxCountOf) {
                return maxFractionPattern(maxCountOf);
            }
        },
        MAX_INTEGERS {
            @Override
            public Pattern createPattern(int maxCountOf) {
                return maxIntegerPattern(maxCountOf);
            }
        },

        INTEGERS_ONLY {
            @Override
            public Pattern createPattern(int maxCountOf) {
                return integersOnlyPattern();
            }
        };

        public abstract Pattern createPattern(int maxCountOf);
    }
}

You can use it like this:

textField.setTextFormatter(new TextFieldValidator(ValidationModus.MAX_INTEGERS, 4).getFormatter());

or you can instantiate it in a fxml file, and apply it to a customTextField with the according properties.

app.fxml:

<fx:define>
    <TextFieldValidator fx:id="validator" modus="MAX_INTEGERS" maxCountOf="4"/>
</fx:define>

<CustomTextField validator="$validator" />

CustomTextField:

public class CustomTextField {

private TextField textField;

public CustomTextField(@NamedArg("validator") TextFieldValidator validator) {
        this();
        textField.setTextFormatter(validator.getFormatter());
    }
}

For your usecase you could call the TextFieldValidor constructor with the appropriate regex pattern and add the filter of James-D's answer to validateChange(Change c)



回答3:

You can listen to changes in the text property to check for valid inputs. Personally I prefer the user being able to input any string and not preventing any edits until the user commits the edit.

The following example is for BigInteger (for simplicity) only and allows any number starting with non-zero and followed either only by digits or by digits that are grouped to 3 digits by seperating them with ,. It adds the CSS class invalid, if the input is not valid and converts it to a string containing only digits if the user presses enter:

// regex for matching the input and extracting the parts
private static final Pattern NUMBER_PATTERN = Pattern.compile("([1-9](?:\\d*|\\d{0,2}(?:\\,\\d{3})*))([tbmk]?)", Pattern.CASE_INSENSITIVE);

// map from suffix to exponent for 10
private static final Map<Character, Byte> SUFFIX_EXPONENTS;

static {
    Map<Character, Byte> prefixes = new HashMap<>();
    prefixes.put('k', (byte) 3);
    prefixes.put('m', (byte) 6);
    prefixes.put('b', (byte) 9);
    prefixes.put('t', (byte) 12);
    SUFFIX_EXPONENTS = Collections.unmodifiableMap(prefixes);
}

private static BigInteger convert(String s) {
    if (s == null) {
        return null;
    }

    Matcher m = NUMBER_PATTERN.matcher(s);

    if (!m.matches()) {
        return null;
    }

    String numberString = m.group(1).replace(",", "");
    String suffix = m.group(2);

    BigInteger factor = suffix.isEmpty() ? BigInteger.ONE : BigInteger.TEN.pow(SUFFIX_EXPONENTS.get(Character.toLowerCase(suffix.charAt(0))));

    return new BigInteger(numberString).multiply(factor);
}

@Override
public void start(Stage primaryStage) throws Exception {
    TextField tf = new TextField();
    tf.getStyleClass().add("invalid");

    // property bound to the current number in the TextField or null, if invalid
    ObjectProperty<BigInteger> numberProperty = new SimpleObjectProperty<>();

    // Binding reevaluated on every change of the text property.
    // A listener could be used instead to change the text to the
    // previous value, if the new input is invalid.
    numberProperty.bind(Bindings.createObjectBinding(() -> convert(tf.getText()), tf.textProperty()));

    // change styleclass, if the string becomes (in)valid input
    numberProperty.addListener((observable, oldValue, newValue) -> {
        if (oldValue == null) {
            tf.getStyleClass().remove("invalid");
        } else if (newValue == null) {
            tf.getStyleClass().add("invalid");
        }
    });

    // handle user pressing enter
    tf.setOnAction(evt -> {
        BigInteger num = numberProperty.get();
        tf.setText(num == null ? null : num.toString());
    });

    Pane root = new StackPane(tf);

    Scene sc = new Scene(root, 300, 300);

    sc.getStylesheets().add(getClass().getResource("style.css").toExternalForm());

    primaryStage.setScene(sc);
    primaryStage.show();
}

In the stylesheet I set the background for invalid textfields to red-tone to give the user visual feedback:

.text-field.invalid {
    -fx-background-color: #f55;
}

If you want to prevent users from inputing anything that cannot be made a valid string by apending chars, you could remove numberProperty and everything related to it and add a listener that reverts to the old value instead:

tf.textProperty().addListener((observable, oldValue, newValue) -> {
    if (isInvalid(newValue)) {
        tf.setText(oldValue);
    }
});