Принимать единицы измерения в JSpinner в качестве входных данных

У меня есть числовой JSpinner, который принимает значения в определенной единице измерения. Теперь я хотел бы иметь специальное поведение JSpinner: если пользователь вводит числовое значение и добавляет строку определенной единицы измерения (например, «дюйм», «пика»), тогда введенное числовое значение должно быть преобразовано в другое значение (в зависимости от в строке единиц измерения). Это преобразование должно происходить, когда пользователь покидает поле счетчика (потеря фокуса) или если каким-либо образом происходит "commitEdit".

Я пробовал несколько вариантов: фильтр пользовательского документа, экземпляр пользовательского формата и документ пользовательского текстового поля для JFormattedTextField счетчика. Но я не нашел возможности «перехватить» вызов метода «commitEdit» JFormattedTextField.

Каков наилучший подход для реализации моих требований? Есть ли простой способ сделать это?


person PAX    schedule 28.01.2014    source источник
comment
+1 Я думаю, что InputMask может иметь другой формат, тип данных, никогда не пробовал, есть два более простых варианта JSpinner с ListModel (см. учебник по Oracle ---> Месяц в году) или напрямую использовать JComboBox вместо какого-то дерева с JSpinner   -  person mKorbel    schedule 28.01.2014
comment
Итак, у вас есть разные единицы измерения в JSpinner и значение в поле JFormattedText? Не можете ли вы использовать событие statechanged JSpinner?   -  person Reinstate Monica    schedule 28.01.2014
comment
Я не могу использовать ComboBox. Работодатель запрашивает компонент Spinner со всеми преимуществами (такими как форматирование чисел с плавающей запятой для конкретной локали) NumberModel.   -  person PAX    schedule 29.01.2014
comment
Удивительно, но ChangeListener — это решение! Это позволяет мне изменить пользовательский ввод (если есть строка единиц измерения) до того, как он будет зафиксирован. Мне нужно только разделить часть строки единицы и часть строки числа. Чтобы определить формат числа с плавающей запятой, зависящий от локали, я могу использовать: new DecimalFormat("", DecimalFormatSymbols.getInstance(this.locale)).parse(input);   -  person PAX    schedule 29.01.2014


Ответы (1)


Есть еще кое-что, что позволяет вам изменить пользовательский ввод до того, как он будет зафиксирован: это сам метод commitEdit (из JFormattedTextField из DefaultEditor из JSpinner). Внутри commitEdit вы можете видеть, что вызывается метод stringToValue из JFormattedTextField AbstractFormatter. Это означает, что если вы зададите свой собственный AbstractFormatter для текстового поля, оно может преобразовать любую строку в значение и значение в любую строку. Вот где возникают исключения, чтобы указать, не удалось ли выполнить фиксацию.

Итак, следует пользовательский AbstractFormatter, обрабатывающий разные единицы, как вы просили:

import java.text.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.SpinnerNumberModel;

public class MilliMeterMain {

    public static enum Unit {
        MM(1), //Millimeters.
        IN(25.4), //Inches (1 inch == 25.4 mm).
        FT(25.4 * 12), //Feet (1 foot == 12 inches).
        YD(25.4 * 12 * 3); //Yards (1 yard == 3 feet).

        private final double factorToMilliMeters; //How much of this Unit composes a single millimeter.

        private Unit(final double factorToMilliMeters) {
            this.factorToMilliMeters = factorToMilliMeters;
        }

        public double getFactorToMilliMeters() {
            return factorToMilliMeters;
        }

        public double toMilliMeters(final double amount) {
            return amount * getFactorToMilliMeters();
        }

        public double fromMilliMeters(final double amount) {
            return amount / getFactorToMilliMeters();
        }
    }

    public static class UnitFormatter extends AbstractFormatter {

        private static final Pattern PATTERN;

        static {
            //Building the Pattern is not too tricky. It just needs some attention.

            final String blank = "\\p{Blank}"; //Match any whitespace character.
            final String blankGroupAny = "(" + blank + "*)"; //Match any whitespace character, zero or more times and group them.

            final String digits = "\\d"; //Match any digit.
            final String digitsGroup = "(" + digits + "+)"; //Match any digit, at least once and group them.
            final String digitsSuperGroup = "(\\-?" + digitsGroup + "\\.?" + digitsGroup + "?)"; //Matches for example "-2.4" or "2.4" or "2" or "-2" in the same group!

            //Create the pattern part which matches any of the available units...
            final Unit[] units = Unit.values();
            final StringBuilder unitsBuilder = new StringBuilder(Pattern.quote("")); //Empty unit strings are valid (they default to millimeters).
            for (int i = 0; i < units.length; ++i)
                unitsBuilder.append('|').append(Pattern.quote(units[i].name()));
            final String unitsGroup = "(" + unitsBuilder + ")";

            final String full = "^" + blankGroupAny + digitsSuperGroup + blankGroupAny + unitsGroup + blankGroupAny + "$"; //Compose full pattern.

            PATTERN = Pattern.compile(full);
        }

        private Unit lastUnit = Unit.MM;

        @Override
        public Object stringToValue(final String text) throws ParseException {
            if (text == null || text.trim().isEmpty())
                throw new ParseException("Null or empty text.", 0);
            try {
                final Matcher matcher = PATTERN.matcher(text.toUpperCase());
                if (!matcher.matches())
                    throw new ParseException("Invalid input.", 0);
                final String amountStr = matcher.group(2),
                             unitStr = matcher.group(6);
                final double amount = Double.parseDouble(amountStr);
                lastUnit = unitStr.trim().isEmpty()? null: Unit.valueOf(unitStr);
                return lastUnit == null? amount: lastUnit.toMilliMeters(amount);
            }
            catch (final IllegalArgumentException iax) {
                throw new ParseException("Failed to parse input \"" + text + "\".", 0);
            }
        }

        @Override
        public String valueToString(final Object value) throws ParseException {
            final double amount = lastUnit == null? (Double) value: lastUnit.fromMilliMeters((Double) value);
            return String.format("%.4f", amount).replace(',', '.') + ((lastUnit == null)? "": (" " + lastUnit.name()));
        }
    }

    public static class UnitFormatterFactory extends AbstractFormatterFactory {
        @Override
        public AbstractFormatter getFormatter(final JFormattedTextField tf) {
            if (!(tf.getFormatter() instanceof UnitFormatter))
                return new UnitFormatter();
            return tf.getFormatter();
        }
    }

    public static void main(final String[] args) {
        final JSpinner spin = new JSpinner(new SpinnerNumberModel(0d, -1000000d, 1000000d, 1d)); //Default numbers in millimeters.

        ((DefaultEditor) spin.getEditor()).getTextField().setFormatterFactory(new UnitFormatterFactory());

        final JFrame frame = new JFrame("JSpinner infinite value");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(spin);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Я использовал единицы измерения длины (миллиметры, дюймы, футы и ярды). Но вы можете адаптировать это под свои нужды.

Обратите внимание, что в приведенной выше реализации SpinnerNumberModel знает только миллиметры. AbstractFormatter обрабатывает преобразование миллиметров в другие единицы и обратно (в соответствии с вводом пользователя). Это означает, что когда вы устанавливаете единицы измерения YD (т.е. ярды), модель по-прежнему будет вращаться в миллиметрах, но JFormattedTextField будет вращаться в долях ярдов. Попробуйте сами увидеть, что я имею в виду. Это означает, что getValue() из JSpinner/SpinnerNumberModel всегда будет возвращать количество миллиметров, независимо от единиц измерения в текстовом поле (AbstractFormatter всегда будет выполнять преобразования).

В качестве второго сценария, если вы хотите, вы можете перенести преобразование за пределы AbstractFormatter. Вы можете, например, позволить пользователю вводить значение в счетчике, которое всегда будет независимым от единицы измерения. Таким образом, пользователь всегда видит значение, вращающееся с шагом, равным 1 (в этом примере), а тем временем AbstractFormatter будет содержать свойство последней единицы, установленной для счетчика пользователем. Итак, теперь, когда вы получаете значение из JSpinner/SpinnerNumberModel, вы получите число, независимое от единиц измерения, а затем используете последнюю единицу измерения, установленную в AbstractFormatter, чтобы определить, какие единицы имеет в виду пользователь. Это немного другой и, возможно, более удобный способ использования спиннера.

Вот код для второго случая:

import java.text.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.SpinnerNumberModel;

public class StepMain {

    public static enum Unit {
        MM, //Milimeters.
        IN, //Inches (1 inch == 25.4 mm).
        FT, //Feet (1 foot == 12 inches).
        YD; //Yards (1 yard == 3 feet).
    }

    public static class UnitFormatter extends AbstractFormatter {

        private static final Pattern PATTERN;

        static {
            //Building the Pattern is not too tricky. It just needs some attention.

            final String blank = "\\p{Blank}"; //Match any whitespace character.
            final String blankGroupAny = "(" + blank + "*)"; //Match any whitespace character, zero or more times and group them.

            final String digits = "\\d"; //Match any digit.
            final String digitsGroup = "(" + digits + "+)"; //Match any digit, at least once and group them.
            final String digitsSuperGroup = "(\\-?" + digitsGroup + "\\.?" + digitsGroup + "?)"; //Matches for example "-2.4" or "2.4" or "2" or "-2" in the same group!

            //Create the pattern part which matches any of the available units...
            final Unit[] units = Unit.values();
            final StringBuilder unitsBuilder = new StringBuilder(Pattern.quote("")); //Empty unit strings are valid (they default to milimeters).
            for (int i = 0; i < units.length; ++i)
                unitsBuilder.append('|').append(Pattern.quote(units[i].name()));
            final String unitsGroup = "(" + unitsBuilder + ")";

            final String full = "^" + blankGroupAny + digitsSuperGroup + blankGroupAny + unitsGroup + blankGroupAny + "$"; //Compose full pattern.

            PATTERN = Pattern.compile(full);
        }

        private Unit lastUnit = Unit.MM;

        @Override
        public Object stringToValue(final String text) throws ParseException {
            if (text == null || text.trim().isEmpty())
                throw new ParseException("Null or empty text.", 0);
            try {
                final Matcher matcher = PATTERN.matcher(text.toUpperCase());
                if (!matcher.matches())
                    throw new ParseException("Invalid input.", 0);
                final String amountStr = matcher.group(2),
                             unitStr = matcher.group(6);
                final double amount = Double.parseDouble(amountStr);
                lastUnit = Unit.valueOf(unitStr);
                return amount;
            }
            catch (final IllegalArgumentException iax) {
                throw new ParseException("Failed to parse input \"" + text + "\".", 0);
            }
        }

        @Override
        public String valueToString(final Object value) throws ParseException {
            return String.format("%.3f", value).replace(',', '.') + ' ' + lastUnit.name();
        }
    }

    public static class UnitFormatterFactory extends AbstractFormatterFactory {
        @Override
        public AbstractFormatter getFormatter(final JFormattedTextField tf) {
            if (!(tf.getFormatter() instanceof UnitFormatter))
                return new UnitFormatter();
            return tf.getFormatter();
        }
    }

    public static void main(final String[] args) {
        final JSpinner spin = new JSpinner(new SpinnerNumberModel(0d, -1000000d, 1000000d, 0.001d));

        ((DefaultEditor) spin.getEditor()).getTextField().setFormatterFactory(new UnitFormatterFactory());

        final JFrame frame = new JFrame("JSpinner infinite value");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(spin);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

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

person gthanop    schedule 06.01.2020