Accept measuring units in JSpinner as input

佐手、 提交于 2020-06-28 02:42:32

问题


I have a numeric JSpinner which accepts values in a specific measuring unit. Now, I'd like to have a special JSpinner behavior: If a user enters a numeric value and appends a specific measuring unit string (e.g. "inch", "pica") then the entered numeric value must be converted into another value (depending on the unit string). This conversion must occur when user leaves the spinner field (focus lost) or if a "commitEdit" occurs in any way.

I've tried several variants: Custom document filter, custom format instance and custom text field document for the spinner's JFormattedTextField. But I didn't find any possibility to "hook" the "commitEdit" method invocation of JFormattedTextField.

What's the best approach to implement my requirements? Is there an easy way doing that?


回答1:


There is also something else that enables you to modify the user input before it becomes commited: It is the commitEdit method itself (of the JFormattedTextField of the DefaultEditor of the JSpinner). Inside the commitEdit you can see that the method stringToValue of the JFormattedTextField's AbstractFormatter is called. Which means that if you give your own custom AbstractFormatter to the text field it can convert any string to a value and a value to any string. Here is where the exceptions occur to indicate if the commit failed or not.

So, follows a custom AbstractFormatter handling different units, as you requested:

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 ammount) {
            return ammount * getFactorToMilliMeters();
        }

        public double fromMilliMeters(final double ammount) {
            return ammount / 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 ammountStr = matcher.group(2),
                             unitStr = matcher.group(6);
                final double ammount = Double.parseDouble(ammountStr);
                lastUnit = unitStr.trim().isEmpty()? null: Unit.valueOf(unitStr);
                return lastUnit == null? ammount: lastUnit.toMilliMeters(ammount);
            }
            catch (final IllegalArgumentException iax) {
                throw new ParseException("Failed to parse input \"" + text + "\".", 0);
            }
        }

        @Override
        public String valueToString(final Object value) throws ParseException {
            final double ammount = lastUnit == null? (Double) value: lastUnit.fromMilliMeters((Double) value);
            return String.format("%.4f", ammount).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);
    }
}

I used units measuring length (millimeters, inches, feet and yards). But you can adapt this to your own needs.

Note in the above implementation the SpinnerNumberModel only knows millimeters. The AbstractFormatter handles converting millimeters to ther units and back (as per the user's input). That means that when you set the units to YD (ie yards) the model will still spin at millimeters but the JFormattedTextField is going to spin at fractions of yards. Try it out to see yourself what I mean. That means that getValue() of the JSpinner/SpinnerNumberModel will always return the ammount of millimeters no matter what the units are in the text field (the AbstractFormatter will always do the conversions).

As a second scenario, if you want, you can move the conversion outside the AbstractFormatter. You can, for example, let the user input a value in the spinner which will always be independent from the measuring unit. This way the user always sees value spinning with step equal to 1 (in this example) and meanwhile the AbstractFormatter will hold a property of the last unit set to the spinner by the user. So now when you get the value from the JSpinner/SpinnerNumberModel you will get a number independent from units and then use the last unit set to the AbstractFormatter to determine what units the user means. This is a bit different and maybe more convenient way to use the spinner.

Here is the code for the second case:

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 ammountStr = matcher.group(2),
                             unitStr = matcher.group(6);
                final double ammount = Double.parseDouble(ammountStr);
                lastUnit = Unit.valueOf(unitStr);
                return ammount;
            }
            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);
    }
}

As for the locale thing you said, if I understood correctly, you want commas and dots to both operate in the same spinner? If so, you can check the answer here which is about exactly that. In that case again the problem is solved by using a custom AbstractFormatter.



来源:https://stackoverflow.com/questions/21410092/accept-measuring-units-in-jspinner-as-input

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!