JavaFX program runs, but still getting exception

时间:2018-04-20 00:42:35

标签: java javafx bigdecimal

I'm writing a JavaFX program that calculates a tip using a slider. It's works fairly well, but when I change the value of the slider, I get a slew of errors such as Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: Label.text : A bound value cannot be set.

It seems that my error has something to do with binding to the slider, but not sure what is the problem since it runs how I was expecting. Any pointers would be greatly appreciated. Thanks

Here is the program:

TipCalculator.java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TipCalculator extends Application
{

    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load(getClass().getResource("TipCalculator.fxml"));

        Scene scene = new Scene(root);
        stage.setTitle("Tip Calculator");
        stage.setScene(scene);
        stage.show();
    }

   public static void main(String[] args)
    {
        launch(args);
    }
}

TipCalculatorController.java

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;

public class TipCalculatorController
{
    private static final NumberFormat currency =
        NumberFormat.getCurrencyInstance();
    private static final NumberFormat percent =
        NumberFormat.getPercentInstance();

    private BigDecimal tipPercentage = new BigDecimal(0.15);

    @FXML
    private TextField amountTextField;

    @FXML
    private Label tipPercentageLabel;

    @FXML 
    private Slider tipPercentageSlider;

    @FXML
    private TextField tipTextField;

    @FXML
    private TextField totalTextField;

    @FXML
    private void initialize()
    {
        try
        {
            BigDecimal amount = new BigDecimal(amountTextField.getText());
            BigDecimal tip = amount.multiply(tipPercentage);
            BigDecimal total = amount.add(tip);

            tipTextField.setText(currency.format(tip));
            totalTextField.setText(currency.format(total));
        }
        catch (NumberFormatException ex)
        {
            amountTextField.setText("Enter amount");
            amountTextField.selectAll();
            amountTextField.requestFocus();
        }

        tipPercentageLabel.textProperty().bind           
            (tipPercentageSlider.valueProperty().asString("%.0f"));

        currency.setRoundingMode(RoundingMode.HALF_UP);

        tipPercentageSlider.valueProperty().addListener(
            new ChangeListener<Number>()
            {
                @Override
                public void changed(ObservableValue<? extends Number> ov,
                        Number oldValue, Number newValue)
                {
                    tipPercentage =
                            BigDecimal.valueOf(newValue.intValue() / 100.0);
                    tipPercentageLabel.setText(percent.format(tipPercentage));
                }
            }
        );
    }
}

TipCalculator.fxml

?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<GridPane hgap="8.0" vgap="8.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="TipCalculatorController">
  <columnConstraints>
    <ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" minWidth="10.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Label text="Amount" />
      <Label fx:id="tipPercentageLabel" text="15%" GridPane.rowIndex="1" />
      <Label text="Tip" GridPane.rowIndex="2" />
      <Label text="Total" GridPane.rowIndex="3" />
      <TextField fx:id="amountTextField" onAction="#initialize" GridPane.columnIndex="1" />
      <TextField fx:id="tipTextField" editable="false" focusTraversable="false" GridPane.columnIndex="1" GridPane.rowIndex="2" />
      <TextField fx:id="totalTextField" editable="false" focusTraversable="false" GridPane.columnIndex="1" GridPane.rowIndex="3" />
      <Slider fx:id="tipPercentageSlider" blockIncrement="5.0" max="30.0" onDragDetected="#initialize" value="15.0" GridPane.columnIndex="1" GridPane.rowIndex="1" />
   </children>
   <padding>
      <Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
   </padding>
</GridPane>

1 个答案:

答案 0 :(得分:2)

As the exception says, a bound value cannot be set. The point of a binding is that you declare that one property should always have a value that depends on another property. If you could then set the bound property directly, you would be able to violate the contract you specify by the binding.

So when you do

tipPercentageLabel.textProperty().bind           
        (tipPercentageSlider.valueProperty().asString("%.0f"));

you ensure that the text property of tipPercentageLabel will always have the string value of tipPercentageSlider.getValue() (formatted with one decimal, etc). If you were then allowed to set the text of tipPercentageLabel, you could violate this binding, so this is now prohibited.

Consequently,

    tipPercentageSlider.valueProperty().addListener(
        new ChangeListener<Number>()
        {
            @Override
            public void changed(ObservableValue<? extends Number> ov,
                    Number oldValue, Number newValue)
            {
                tipPercentage =
                        BigDecimal.valueOf(newValue.intValue() / 100.0);
                tipPercentageLabel.setText(percent.format(tipPercentage));
            }
        }
    );

throws an exception when the slider value changes, because it tries to explicitly set the text of the label.

The binding and the listener are actually just two ways of achieving (more or less) the same thing (give or take some formatting). So the solution to the issue is that you should have only one of these: either remove the binding, or remove the listener. Personally, I find the binding approach more elegant (I prefer the explicit declaration of the dependency, whereas the listener is more mechanistic). Your mileage may vary.

If you want to use the percent format, you can do that with a binding with

tipPercentageLabel.textProperty().bind(Bindings.createStringBinding(() -> {
    tipPercentage = BigDecimal.valueOf(newValue.intValue() / 100.0);
    return percent.format(tipPercentage);
}, tipPercentageSlider.valueProperty());

or you can just do

tipPercentageLabel.textProperty().bind(tipPercentageSlider.asString("%.0f%%"));

(%% in a format string gives a "%" sign).