Каков рекомендуемый способ сделать числовое текстовое поле в JavaFX?


Мне нужно ограничить ввод в текстовое поле целых чисел. Какие-нибудь советы?

21 68

21 ответ:

очень старый поток, но это кажется более аккуратным и удаляет нечисловые символы, если они вставлены.

// force the field to be numeric only
textField.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, 
        String newValue) {
        if (!newValue.matches("\d*")) {
            textField.setText(newValue.replaceAll("[^\d]", ""));
        }
    }
});

Я знаю, что это довольно старая тема, но для будущих читателей вот еще одно решение, которое я нашел довольно интуитивно:

public class NumberTextField extends TextField
{

    @Override
    public void replaceText(int start, int end, String text)
    {
        if (validate(text))
        {
            super.replaceText(start, end, text);
        }
    }

    @Override
    public void replaceSelection(String text)
    {
        if (validate(text))
        {
            super.replaceSelection(text);
        }
    }

    private boolean validate(String text)
    {
        return text.matches("[0-9]*");
    }
}

Редактировать: Спасибо none_ и SCBoy для предложенных улучшений.

Обновление Апр 2016

этот ответ был создан несколько лет назад, и оригинальный ответ сейчас во многом устарели.

начиная с Java 8u40, Java имеет TextFormatter который обычно лучше всего подходит для принудительного ввода определенных форматов, таких как цифры в текстовых полях JavaFX:

см. также другие ответы на этот вопрос, которые конкретно упоминают TextFormatter.


Оригинальный Ответ

есть несколько примеров в этой суть, я продублировал один из примеров ниже:

// helper text field subclass which restricts text input to a given range of natural int numbers
// and exposes the current numeric int value of the edit box as a value property.
class IntField extends TextField {
  final private IntegerProperty value;
  final private int minValue;
  final private int maxValue;

  // expose an integer value property for the text field.
  public int  getValue()                 { return value.getValue(); }
  public void setValue(int newValue)     { value.setValue(newValue); }
  public IntegerProperty valueProperty() { return value; }

  IntField(int minValue, int maxValue, int initialValue) {
    if (minValue > maxValue) 
      throw new IllegalArgumentException(
        "IntField min value " + minValue + " greater than max value " + maxValue
      );
    if (maxValue < minValue) 
      throw new IllegalArgumentException(
        "IntField max value " + minValue + " less than min value " + maxValue
      );
    if (!((minValue <= initialValue) && (initialValue <= maxValue))) 
      throw new IllegalArgumentException(
        "IntField initialValue " + initialValue + " not between " + minValue + " and " + maxValue
      );

    // initialize the field values.
    this.minValue = minValue;
    this.maxValue = maxValue;
    value = new SimpleIntegerProperty(initialValue);
    setText(initialValue + "");

    final IntField intField = this;

    // make sure the value property is clamped to the required range
    // and update the field's text to be in sync with the value.
    value.addListener(new ChangeListener<Number>() {
      @Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
        if (newValue == null) {
          intField.setText("");
        } else {
          if (newValue.intValue() < intField.minValue) {
            value.setValue(intField.minValue);
            return;
          }

          if (newValue.intValue() > intField.maxValue) {
            value.setValue(intField.maxValue);
            return;
          }

          if (newValue.intValue() == 0 && (textProperty().get() == null || "".equals(textProperty().get()))) {
            // no action required, text property is already blank, we don't need to set it to 0.
          } else {
            intField.setText(newValue.toString());
          }
        }
      }
    });

    // restrict key input to numerals.
    this.addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
      @Override public void handle(KeyEvent keyEvent) {
        if (!"0123456789".contains(keyEvent.getCharacter())) {
          keyEvent.consume();
        }
      }
    });

    // ensure any entered values lie inside the required range.
    this.textProperty().addListener(new ChangeListener<String>() {
      @Override public void changed(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
        if (newValue == null || "".equals(newValue)) {
          value.setValue(0);
          return;
        }

        final int intValue = Integer.parseInt(newValue);

        if (intField.minValue > intValue || intValue > intField.maxValue) {
          textProperty().setValue(oldValue);
        }

        value.set(Integer.parseInt(textProperty().get()));
      }
    });
  }
}

The TextInput есть TextFormatter который может быть использован для форматирования, преобразования и ограничения типов текста, который может быть введен.

The TextFormatter есть фильтр, который может быть использован для отклонения входного сигнала. Нам нужно установить это, чтобы отклонить все, что не является допустимым целым числом. Он также имеет конвертер, который нам нужно установить, чтобы преобразовать строковое значение в целое значение, которое мы можем связать позже.

позволяет создать многоразовый фильтр:

public class IntegerFilter implements UnaryOperator<TextFormatter.Change> {
    private final static Pattern DIGIT_PATTERN = Pattern.compile("\d*");

    @Override
    public Change apply(TextFormatter.Change aT) {
        return DIGIT_PATTERN.matcher(aT.getText()).matches() ? aT : null;
    }
}

фильтр может сделать один из трех вещи, он может вернуть изменение немодифицированным, чтобы принять его как есть, он может изменить изменение каким-то образом он считает нужным или он может вернуть null отклонить изменение все вместе.

мы будем использовать стандартный IntegerStringConverter как конвертер.

собрав все это вместе мы имеем:

TextField textField = ...;

TextFormatter<Integer> formatter = new TextFormatter<>(
    new IntegerStringConverter(), // Standard converter form JavaFX
    defaultValue, 
    new IntegerFilter());
formatter.valueProperty().bindBidirectional(myIntegerProperty);

textField.setTextFormatter(formatter);

если вы хотите, не нужен многоразовый фильтр вы можете сделать это фантазии один-лайнер вместо:

TextFormatter<Integer> formatter = new TextFormatter<>(
    new IntegerStringConverter(), 
    defaultValue,  
    c -> Pattern.matches("\d*", c.getText()) ? c : null );

начиная с JavaFX 8u40, вы можете установить объект TextFormatter в текстовом поле:

UnaryOperator<Change> filter = change -> {
    String text = change.getText();

    if (text.matches("[0-9]*")) {
        return change;
    }

    return null;
};
TextFormatter<String> textFormatter = new TextFormatter<>(filter);
fieldNport = new TextField();
fieldNport.setTextFormatter(textFormatter);

это позволяет избежать как подклассов, так и повторяющихся событий изменения, которые вы получите, когда добавите прослушиватель изменений в свойство text и измените текст в этом прослушивателе.

мне не нравятся исключения, поэтому я использовал matches функция из String-Class

text.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, 
        String newValue) {
        if (newValue.matches("\d*")) {
            int value = Integer.parseInt(newValue);
        } else {
            text.setText(oldValue);
        }
    }
});
TextField text = new TextField();

text.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable,
            String oldValue, String newValue) {
        try {
            Integer.parseInt(newValue);
            if (newValue.endsWith("f") || newValue.endsWith("d")) {
                manualPriceInput.setText(newValue.substring(0, newValue.length()-1));
            }
        } catch (ParseException e) {
            text.setText(oldValue);
        }
    }
});

The if предложение важно для обработки входов, таких как 0.5 d или 0.7 f, которые правильно анализируются Int.parseInt (), но не должен появляться в текстовом поле.

начиная от Java SE 8u40, для такой необходимости вы можете использовать"целое"Spinner позволяет безопасно выбрать допустимое целое число с помощью клавиш со стрелками вверх/вниз клавиатуры или кнопок со стрелками вверх/вниз.

вы также можете определить мин, a макс и нач значение, чтобы ограничить допустимые значения и количество для увеличения или уменьшения, за шаг.

для пример

// Creates an integer spinner with 1 as min, 10 as max and 2 as initial value
Spinner<Integer> spinner1 = new Spinner<>(1, 10, 2);
// Creates an integer spinner with 0 as min, 100 as max and 10 as initial 
// value and 10 as amount to increment or decrement by, per step
Spinner<Integer> spinner2 = new Spinner<>(0, 100, 10, 10);

пример с "целое "spinner and a"двойной " spinner

enter image description here

A spinner - это однострочный элемент управления текстовым полем, который позволяет пользователю выберите число или значение объекта из упорядоченной последовательности таких объектов ценности. Блесны обычно обеспечивают пару крошечных кнопок со стрелками для проходя через элементы последовательности. Клавиатура поднята стрелки/вниз клавиши со стрелками также цикл через элементы. Пользователь может также можно ввести (юридическое)значение непосредственно в счетчик. Хотя поля со списком предоставляют аналогичную функциональность, блесны иногда предпочтительнее, потому что они не требуют выпадающего списка, который могут скрывать важные данные, а также потому, что они позволяют по возможности например, упаковка от максимального значения до минимального значения (например, от самого большого положительного целого числа до 0).

больше подробности о блесны контроля

предпочтительный ответ может быть еще меньше, если вы используете Java 1.8 Lambdas

textfield.textProperty().addListener((observable, oldValue, newValue) -> {
    if (!newValue.matches("\d*")) {
        textfield.setText(newValue.replaceAll("[^\d]", ""));
    }
});

этот работал на меня.

public void RestrictNumbersOnly(TextField tf){
    tf.textProperty().addListener(new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, 
            String newValue) {
            if (!newValue.matches("|[-\+]?|[-\+]?\d+\.?|[-\+]?\d+\.?\d+")){
                tf.setText(oldValue);
            }
        }
    });
}

вот что я использую:

private TextField textField;
textField.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        if(!newValue.matches("[0-9]*")){
            textField.setText(oldValue);
        }

    }
});

то же самое в лямбда-нотации будет:

private TextField textField;
textField.textProperty().addListener((observable, oldValue, newValue) -> {
    if(!newValue.matches("[0-9]*")){
        textField.setText(oldValue);
    }
});

Если вы хотите применить один и тот же прослушиватель к более чем одному текстовому полю, вот самое простое решение:

TextField txtMinPrice, txtMaxPrice = new TextField();

ChangeListener<String> forceNumberListener = (observable, oldValue, newValue) -> {
    if (!newValue.matches("\d*"))
      ((StringProperty) observable).set(oldValue);
};

txtMinPrice.textProperty().addListener(forceNumberListener);
txtMaxPrice.textProperty().addListener(forceNumberListener);

попробуйте этот простой код он будет делать эту работу.

DecimalFormat format = new DecimalFormat( "#.0" );
TextField field = new TextField();
field.setTextFormatter( new TextFormatter<>(c ->
{
    if ( c.getControlNewText().isEmpty() )
    {
        return c;
    }

    ParsePosition parsePosition = new ParsePosition( 0 );
    Object object = format.parse( c.getControlNewText(), parsePosition );

    if ( object == null || parsePosition.getIndex() <          c.getControlNewText().length() )
    {
        return null;
    }
    else
    {
        return c;
    }
}));

этот метод позволяет текстовому полю завершить всю обработку (копировать/вставить/отменить безопасно). Не требует расширения классов и позволяет решать, что делать с новым текстом после каждого изменения (чтобы подтолкнуть его к логике, или вернуться к предыдущему значению, или даже изменить его).

  // fired by every text property change
textField.textProperty().addListener(
  (observable, oldValue, newValue) -> {
    // Your validation rules, anything you like
      // (! note 1 !) make sure that empty string (newValue.equals("")) 
      //   or initial text is always valid
      //   to prevent inifinity cycle
    // do whatever you want with newValue

    // If newValue is not valid for your rules
    ((StringProperty)observable).setValue(oldValue);
      // (! note 2 !) do not bind textProperty (textProperty().bind(someProperty))
      //   to anything in your code.  TextProperty implementation
      //   of StringProperty in TextFieldControl
      //   will throw RuntimeException in this case on setValue(string) call.
      //   Or catch and handle this exception.

    // If you want to change something in text
      // When it is valid for you with some changes that can be automated.
      // For example change it to upper case
    ((StringProperty)observable).setValue(newValue.toUpperCase());
  }
);

для вашего случая просто добавьте эту логику внутрь. Работать отлично.

   if (newValue.equals("")) return; 
   try {
     Integer i = Integer.valueOf(newValue);
     // do what you want with this i
   } catch (Exception e) {
     ((StringProperty)observable).setValue(oldValue);
   }

вот простой класс, который обрабатывает некоторые основные проверки на TextField, используя TextFormatter введено в JavaFX 8u40

EDIT:

(код добавлен относительно комментария Флоерна)

import java.text.DecimalFormatSymbols;
import java.util.regex.Pattern;

import javafx.beans.NamedArg;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;

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("countOf") int countOf) {
        this(modus.createPattern(countOf));
    }

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

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

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

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

    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 countOf) {
        return Pattern.compile("\d*(\" + DECIMAL_SEPARATOR + "\d{0," + countOf + "})?");
    }

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

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

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

    public enum ValidationModus {

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

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

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

        public abstract Pattern createPattern(int countOf);
    }

}

вы можете использовать его как это:

textField.setTextFormatter(new TextFieldValidator(ValidationModus.INTEGERS_ONLY).getFormatter());

или вы можете создать его в файле fxml и применить его к customTextField с помощью соответствующего свойства.

приложение.FXML-файл:

<fx:define>
    <TextFieldValidator fx:id="validator" modus="INTEGERS_ONLY"/>
</fx:define>

CustomTextField.класс:

public class CustomTextField {

private TextField textField;

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

код на github

этот код прекрасно работает для меня, даже если вы пытаетесь копировать/вставить.

myTextField.textProperty().addListener((observable, oldValue, newValue) -> {
    if (!newValue.matches("\d*")) {
        myTextField.setText(oldValue);

    }
});

Мммм. я столкнулся с этой проблемой несколько недель назад. Поскольку API не предоставляет элемент управления для достижения этого,
вы можете использовать свой собственный.
я использовал что-то вроде:

public class IntegerBox extends TextBox {
    public-init var value : Integer = 0;
    protected function apply() {
        try {
            value = Integer.parseInt(text);
        } catch (e : NumberFormatException) {}
        text = "{value}";
    }
    override var focused = false on replace {apply()};
    override var action = function () {apply()}
}

он используется так же, как обычный TextBox,
но имеет и value атрибут, который хранит введенное целое число.
Когда элемент управления теряет фокус, он проверка значение и возвращает его (если не действует).

этот код делает ваше текстовое поле принимать только число

textField.lengthProperty().addListener((observable, oldValue, newValue) -> {
        if(newValue.intValue() > oldValue.intValue()){
            char c = textField.getText().charAt(oldValue.intValue());
            /** Check if the new character is the number or other's */
            if( c > '9' || c < '0'){
                /** if it's not number then just setText to previous one */
                textField.setText(textField.getText().substring(0,textField.getText().length()-1));
            }
        }
    });

в последних обновлениях JavaFX, вы должны установить новый текст в платформе.метод runLater так же, как это:

    private void set_normal_number(TextField textField, String oldValue, String newValue) {
    try {
        int p = textField.getCaretPosition();
        if (!newValue.matches("\d*")) {
            Platform.runLater(() -> {
                textField.setText(newValue.replaceAll("[^\d]", ""));
                textField.positionCaret(p);
            });
        }
    } catch (Exception e) {
    }
}

Это хорошая идея, чтобы установить позицию курсора.

Я хотел бы улучшить Эван Ноулз ответ:https://stackoverflow.com/a/30796829/2628125

в моем случае у меня был класс с обработчиками для компонентной части пользовательского интерфейса. Инициализация:

this.dataText.textProperty().addListener((observable, oldValue, newValue) -> this.numericSanitization(observable, oldValue, newValue));

и метод numbericSanitization:

private synchronized void numericSanitization(ObservableValue<? extends String> observable, String oldValue, String newValue) {
    final String allowedPattern = "\d*";

    if (!newValue.matches(allowedPattern)) {
        this.dataText.setText(oldValue);
    }
}

Ключевое слово синхронизироваться добавляется, чтобы предотвратить возможную проблему блокировки рендеринга в javafx, если setText будет вызван до завершения выполнения старого. Это легко воспроизвести, если вы хотите начните вводить неправильные символы очень быстро.

еще одно преимущество заключается в том, что вы держите только один шаблон, чтобы соответствовать и просто сделать откат. Это лучше, потому что вы можете легко абстрагировать решение для различных моделей дезинфекции.

    rate_text.textProperty().addListener(new ChangeListener<String>() {

        @Override
        public void changed(ObservableValue<? extends String> observable,
                String oldValue, String newValue) {
            String s="";
            for(char c : newValue.toCharArray()){
                if(((int)c >= 48 && (int)c <= 57 || (int)c == 46)){
                    s+=c;
                }
            }
            rate_text.setText(s);
        }
    });

Это прекрасно работает, так как позволяет вводить только целочисленное значение и десятичное значение (с кодом ASCII 46).