接受 JSpinner 中的测量单位作为输入 [英] Accept measuring units in JSpinner as input

查看:35
本文介绍了接受 JSpinner 中的测量单位作为输入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个数字 JSpinner,它接受特定测量单位的值.现在,我想要一个特殊的 JSpinner 行为:如果用户输入一个数值并附加一个特定的测量单位字符串(例如inch"、pica"),那么输入的数值必须转换为另一个值(取决于在单位字符串上).当用户离开微调框(失去焦点)或以任何方式发生commitEdit"时,必须发生这种转换.

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.

我尝试了多种变体:自定义文档过滤器、自定义格式实例和用于微调器 JFormattedTextField 的自定义文本字段文档.但我没有发现任何可能挂钩"JFormattedTextField 的commitEdit"方法调用.

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?

推荐答案

还有其他的东西使您能够在提交之前修改用户输入:它是 commitEdit 方法本身(属于 JSpinnerDefaultEditorJFormattedTextField).在commitEdit 中,您可以看到JFormattedTextFieldAbstractFormatter 的方法stringToValue 被调用.这意味着如果您将自己的自定义 AbstractFormatter 提供给文本字段,它可以将任何字符串转换为值,并将值转换为任何字符串.这里是发生异常以指示提交是否失败的地方.

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.

因此,按照您的要求,遵循处理不同单位的自定义 AbstractFormatter:

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 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);
    }
}

我使用了长度单位(毫米、英寸、英尺和码).但您可以根据自己的需要进行调整.

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

注意在上面的实现中 SpinnerNumberModel 只知道毫米.AbstractFormatter 处理毫米和其他单位的转换(根据用户的输入).这意味着当您将单位设置为 YD(即码)时,模型仍将以毫米为单位旋转,但 JFormattedTextField 将以几分之一码的速度旋转.试试看,看看你自己是什么意思.这意味着 JSpinner/SpinnerNumberModelgetValue() 将始终返回毫米量,无论文本字段中的单位是什么(AbstractFormatter 将始终进行转换).

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 amount of millimeters no matter what the units are in the text field (the AbstractFormatter will always do the conversions).

作为第二种情况,如果需要,您可以将转换移到 AbstractFormatter 之外.例如,您可以让用户在微调器中输入一个始终独立于测量单位的值.通过这种方式,用户总是看到值旋转的步长等于 1(在本例中),同时 AbstractFormatter 将保存用户设置为微调器的最后一个单位的属性.因此,现在当您从 JSpinner/SpinnerNumberModel 获取值时,您将获得一个独立于单位的数字,然后使用设置为 AbstractFormatter 确定用户所指的单位.这是使用微调器的方式有点不同,也许更方便.

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 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 解决.

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.

这篇关于接受 JSpinner 中的测量单位作为输入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆