Search code examples
javajavafxbindingcurrency

Editiing Currency with a TextField


How do I get a JavaFX TextField for editing a currency which is stored without factional digits (a long for example)? Using Databinding, TextFormatter and the other javaFX Stuff.

The goal should be:

  • Bo with a LongProperty (currency Value in cents)
  • a editable TextField, in the Users known format (optinal leading minus, thousands separator, decimal separator, (currency symbol), and no other chars possible)
  • BiDirectionalBinding between Bo and the TextField.

Solution

  • Here is a solution (maybe not the best, pls comment if I could improve it)

    The Bo:

    import java.util.Random;
    
    import javafx.beans.property.LongProperty;
    import javafx.beans.property.SimpleLongProperty;
    
    public class SimpleBo {
            //a simple LongProperty to store the currency without fractional digits (56,81 € would be 5681)
            private LongProperty currencyLong = new SimpleLongProperty();
            public SimpleBo() {
                setCurrencyLong(new Random().nextLong());
            }
            public final LongProperty currencyLongProperty() {
                return this.currencyLong;
            }
            public final long getCurrencyLong() {
                return this.currencyLongProperty().get();
            }
            public final void setCurrencyLong(final long currencyLong) {
                this.currencyLongProperty().set(currencyLong);
            }
    }
    

    A Number to String Converter:

    import java.text.NumberFormat;
    import java.util.Locale;
    
    import javafx.util.converter.NumberStringConverter;
    
    public class MyNumberStringConverter extends NumberStringConverter {
        public MyNumberStringConverter() {
            super();
        }
    
        public MyNumberStringConverter(Locale locale, String pattern) {
            super(locale, pattern);
        }
    
        public MyNumberStringConverter(Locale locale) {
            super(locale);
        }
    
        public MyNumberStringConverter(NumberFormat numberFormat) {
            super(numberFormat);
        }
    
        public MyNumberStringConverter(String pattern) {
            super(pattern);
        }
    
        @Override
        public Number fromString(String value) {
            //to transform the double, given by the textfield, just multiply by 100 and round if any left
            Number rValue = Math.round(super.fromString(value).doubleValue() * 100);
            return rValue.longValue();
        }
    
        @Override
        public String toString(Number value) {
            if(value == null) {
                return "";
            }
            //Check for too big long value
            //If the long is too big, it could result in a strange double value.
            if(value.longValue() > 1000000000000l || value.longValue() < -1000000000000l ) {
                return "";
            }
            BigDecimal myBigDecimal = new BigDecimal(value.longValue());
            //to convert the long to a double (currency with fractional digits)
            myBigDecimal = myBigDecimal.movePointLeft(2);
            double asDouble = myBigDecimal.doubleValue();
            if(asDouble == Double.NEGATIVE_INFINITY || asDouble == Double.POSITIVE_INFINITY) {
                return "";
            }
            return super.toString(asDouble);
        }
    

    Util Class:

    import java.util.function.UnaryOperator;
    import javafx.scene.control.TextFormatter;
    
    public class Util {
    
        // This will filter the changes
        public static UnaryOperator<TextFormatter.Change> createFilter() {
            //this is a simple Regex to define the acceptable Chars
            String validEditingStateRegex = "[0123456789,.-]*";
            return change -> {
                String text = change.getText();
                //Check if something changed and just return if not
                if (!change.isContentChange()) {
                    return change;
                }
                //check if the changed text validates against the regex
                if (text.matches(validEditingStateRegex) || text.isEmpty()) {
                    //if valid return the change
                    return change;
                }
                //otherwise return null
                return null;
            };
        }
    }
    

    Test Application:

    import java.text.NumberFormat;
    import javafx.application.Application;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextField;
    import javafx.scene.control.TextFormatter;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    public class BindingExample extends Application {
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) throws Exception {
            Scene scene = new Scene(createBindingExample());
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        //Creates just a sample gui with a Business Objekt 
        public static Parent createBindingExample() {
            VBox vbox = new VBox();
            SimpleBo myBo = new SimpleBo();
            TextField myTextField = new TextField();
            Label fooLabel = new Label();
    
            //Setting up the textField with a Formatter
            NumberFormat nFormat = NumberFormat.getInstance();
            //Define the integer and fractional digits
            nFormat.setMinimumIntegerDigits(1);
            nFormat.setMaximumFractionDigits(2);
            //setting up the TextFormatter with the NumberFormat and a Filter to limit the inputchars
            TextFormatter<Number> textFormatter = new TextFormatter<>(new MyNumberStringConverter(nFormat), 0l,
                    Util.createFilter());
            //Bind (Bidirectional) the BO currency value to the textformatter value
            textFormatter.valueProperty().bindBidirectional(myBo.currencyLongProperty());
            myTextField.setTextFormatter(textFormatter);
    
            //just to show the currency value, bind it to the label
            fooLabel.textProperty().bind(myBo.currencyLongProperty().asString());
    
            vbox.getChildren().add(myTextField);
            //just for spacing
            vbox.getChildren().add(new Label(" "));
            vbox.getChildren().add(fooLabel);
            return vbox;
        }
    }
    

    You could go ahead a put the TextField inside a HBox and a Label for the Currency Symbol. Or with a dropbox of Currency Symbols or what ever. It would be possible to use a NumberFormat with Currency, so the format would add the symbol. But this has some other drawbacks, so I headed this way.