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>
答案 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).