2

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.
Jonny
  • 78
  • 1
  • 11
  • one problem per question please - first solve the plain textField, then go ahead and use it in a cell :) – kleopatra Aug 07 '20 at 15:12
  • you want to bidi-bind the data to the textFormatter's value (not the field's textProperty) – kleopatra Aug 07 '20 at 15:22
  • Thanks for the advice, splitted and answered the first one. – Jonny Aug 10 '20 at 12:52
  • you created a mess with your questions ;) Should be the other way round: this here was the starter (base question binding with conversion between property and textField input), the follow-up would be to apply that base solution to cells. Note that the other question is not answerable - not without the details you provided here! – kleopatra Aug 10 '20 at 13:16
  • strictly speaking, this is still not answerable (because all the details are in the answer) - ideally, you would keep the code example here (as it was at the very beginning) and edit the answer to contain only the changed wiring (from textField.textProperty to formatter.valueProperty). Note that the goal of SO is build a knowledge base useful for future readers - which would expect the question context in the .. question :) But then, much improved .. thanks :) – kleopatra Aug 11 '20 at 09:14
  • I tried my best. Thanks for the advice. – Jonny Aug 11 '20 at 11:32

1 Answers1

0

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.

Jonny
  • 78
  • 1
  • 11