JavaFX TextField validation for integer input and also allow either K or k(for Thousand) or M or m(for Million) in last

后端 未结 3 1788
鱼传尺愫
鱼传尺愫 2021-01-21 11:40

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 B

相关标签:
3条回答
  • 2021-01-21 12:01

    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);
        }
    });
    
    0 讨论(0)
  • 2021-01-21 12:11

    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)

    0 讨论(0)
  • 2021-01-21 12:20

    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.

    0 讨论(0)
提交回复
热议问题