I have a simpleIntegerProperty representing a quantity in seconds which I want to represent in hh:mm:ss format.
I'd like to render this in a Label via binding the Label textProperty to the simpleIntegerProperty. I understand I can do something similar to this with format strings, e.g.
activeTimeText.textProperty().bind(model.activeTimeSeconds.asString("Seconds: %04d"));
renders:
Seconds: 0000
So the question, how to implement a more complex asString conversion? For example my current desired output output (where the digits are functions of the seconds simpleIntegerProperty.):
00:00:00
I've searched for an a similar question already as I feel this should be quite common. However have not found the answer. Apologies if this is a duplicate.
You can extend SimpleIntegerProperty to override asString:
class MySimpleIntegerProperty extends SimpleIntegerProperty{
#Override
public StringBinding asString(){
return Bindings.createStringBinding(() -> " hello " + get() , this);
}
}
To test use:
MySimpleIntegerProperty activeTimeSeconds = new MySimpleIntegerProperty();
activeTimeSeconds.set(7);
SimpleStringProperty activeTimeText = new SimpleStringProperty();
activeTimeText.bind(activeTimeSeconds.asString());
System.out.println(activeTimeText.get());
You can of course delegate the value processing to a method:
#Override
public StringBinding asString(){
return Bindings.createStringBinding(() -> processValue(get()), this);
}
private String processValue(int value){
return " hello " + get() ;
}
The NumberExpression.asString(String) formats the number according to the rules of Formatter, same as if using String.format or Print[Stream|Writer].printf. Unfortunately, unless I'm missing something, the Formatter class expects date/time objects to represent a moment in time, not a duration of time. To format your property as a duration with a HH:MM:SS format you'll need to create your own binding.
To get the String you want you can still use String.format, but by formatting as integral numbers rather than time. This requires you to calculate the hours, minutes, and seconds.
String str = String.format("%02d:%02d:%02d", hours, minutes, seconds);
If you're using Java 9+, calculating the hours, minutes, and seconds is made extremely easy with java.time.Duration; the class had the toHoursPart, toMinutesPart, toSecondsPart, and other similar methods added in Java 9. If using Java 8 you'll need to do the calculations manually or pull in a library, see this question for some help in that regard.
Here's an example assuming Java 9+ and using Bindings.createStringBinding to create the binding:
import java.time.Duration;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
private final IntegerProperty seconds = new SimpleIntegerProperty(this, "seconds");
#Override
public void start(Stage primaryStage) {
Label label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() -> {
// java.time.Duration
Duration duration = Duration.ofSeconds(seconds.get());
return String.format("%02d:%02d:%02d", duration.toHoursPart(),
duration.toMinutesPart(), duration.toSecondsPart());
}, seconds));
primaryStage.setScene(new Scene(new StackPane(label), 500, 300));
primaryStage.show();
Timeline timeline = new Timeline(
new KeyFrame(javafx.util.Duration.seconds(1.0), e -> seconds.set(seconds.get() + 1))
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
}
Related
The test program below reproduces the problem. I understand why the exception is thrown but I would like to know how can I work around it or use a different construct in JavaFX to get what I want.
The full application is a robot simulator with multiple robots that move autonomously, independently, and simultaneously around a field. Each robot has its own SequentialTransition for its particular set of movements. The program adds the SequentialTransitions to a ParallelTransition, which it then plays. Everything was fine until I put in a listener that notices if a robot runs into an obstacle. I've simplified the collision detection in the test program to apply to only one robot and one wall. The point of the error is marked with //** BROKEN!! IllegalStateException on next line.
I really do want to stop the SequentialTransition for a robot that runs into an obstacle but let the other robot(s) continue. How can I do this?
The error comes up in Java 8 but also in Java 11 and JavaFX 15.
package sample;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private static final double FIELD_WIDTH = 600;
private static final double FIELD_HEIGHT = 600;
private Pane field = new Pane();
ParallelTransition parallel = new ParallelTransition();
SequentialTransition sequentialRobot1 = new SequentialTransition();
SequentialTransition sequentialRobot2 = new SequentialTransition();
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
// Place one field boundary for testing.
Line northBoundary = new Line(0, 0, FIELD_WIDTH, 0);
northBoundary.setStrokeWidth(5.0);
field.getChildren().add(northBoundary);
// Place the robots on the field.
// The first robot.
Rectangle robotBody1 = new Rectangle(100, 300, 60, 60);
robotBody1.setArcHeight(15);
robotBody1.setArcWidth(15);
robotBody1.setStroke(Color.BLACK);
robotBody1.setFill(Color.CRIMSON);
field.getChildren().add(robotBody1);
robotBody1.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
if (northBoundary.getBoundsInParent().intersects(robotBody1.getBoundsInParent())) {
//** BROKEN!! IllegalStateException on next line
sequentialRobot1.stop();
System.out.println("Collision detected");
parallel.play();
}
});
TranslateTransition translateTransition1 = new TranslateTransition();
translateTransition1.setNode(robotBody1);
translateTransition1.setByX(0);
translateTransition1.setByY(-300);
translateTransition1.setDuration(Duration.seconds(1));
translateTransition1.setOnFinished(event -> {
robotBody1.setLayoutX(robotBody1.getLayoutX() + robotBody1.getTranslateX());
robotBody1.setLayoutY(robotBody1.getLayoutY() + robotBody1.getTranslateY());
robotBody1.setTranslateX(0);
robotBody1.setTranslateY(0);
});
sequentialRobot1.getChildren().add(translateTransition1);
// The second robot.
Rectangle robotBody2 = new Rectangle(300, 300, 60, 60);
robotBody2.setArcHeight(15);
robotBody2.setArcWidth(15);
robotBody2.setStroke(Color.BLACK);
robotBody2.setFill(Color.CYAN);
field.getChildren().add(robotBody2);
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(robotBody2);
translateTransition2.setByX(0);
translateTransition2.setByY(-100);
translateTransition2.setDuration(Duration.seconds(1));
translateTransition2.setOnFinished(event -> {
robotBody2.setLayoutX(robotBody2.getLayoutX() + robotBody2.getTranslateX());
robotBody2.setLayoutY(robotBody2.getLayoutY() + robotBody2.getTranslateY());
robotBody2.setTranslateX(0);
robotBody2.setTranslateY(0);
});
sequentialRobot2.getChildren().add(translateTransition2);
parallel.getChildren().addAll(sequentialRobot1, sequentialRobot2);
parallel.play();
primaryStage.setTitle("Field");
primaryStage.setScene(new Scene(field, FIELD_WIDTH, FIELD_HEIGHT, Color.GRAY));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
#Slaw's idea of using an AnimationTimer is probably the best direction to go, but the obvious answer is to not use the ParallelTransition at all. Since the robot animations are going to be independent, just use the SequentialTransitions and start them all at the same time by calling play() on each.
I am new to binding especially bidirectional binding. I have three text fields of quantity, price, and total. the following code from online which converted these text field into Integer and Double format for their values.
StringConverter<Number> converter = new NumberStringConverter();
StringConverter<Double> toDouble = new DoubleStringConverter();
TextFormatter<Number> quantity = new TextFormatter<>(converter);
TextFormatter<Double> price = new TextFormatter<>(toDouble);
TextFormatter<Double> total = new TextFormatter<>(toDouble);
Quantity.setTextFormatter(quantity);
Price.setTextFormatter(price);
Total.setTextFormatter(total);
total.valueProperty().bindBidirectional(Bindings.createObjectBinding(() -> {
// ?????????????????????????????????????????
}, quantity.valueProperty(), price.valueProperty()));
Please help fill out the calculation or any other way to do trigger the change whenever i enter differnt number of the text field?
Since the text field representing the total is not editable, you don't need a bidirectional binding here, just a regular binding that updates one value (the total's value) when the computed value changes.
You can do something like
total.valueProperty().bind(Bindings.createObjectBinding(
() -> quantity.getValue() * price.getValue(),
quantity.valueProperty(), price.valueProperty()));
The Bindings.createXXXBinding(...) methods create observable values whose values are computed (here essentially by the formula quantity * price). The binding expresses the fact that when the computed value changes, the value of total should change. A bidirectional binding expresses the idea that when either one of two values changes, the other should be set to match it. This simply won't work in this scenario, because you can't set a computed value.
From a programming perspective, this works because the bindBidirectional() method expects a WritableValue (of the appropriate type); the Bindings.createXXXBinding() methods return an ObservableValue, which is not a WritableValue. (By contrast, the bind() method only expects an ObservableValue.) This makes sense from a semantic perspective too: if you know the total, there's no unique way to determine the price and quantity; however if you know the price and quantity, you can determine the total. So the relationship is not symmetric.
Here's a complete working example. This can probably be improved substantially, e.g. by using TextFormatters that restrict to valid entry and custom StringConverter implementations that use localized number formats, etc.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.converter.DoubleStringConverter;
import javafx.util.converter.IntegerStringConverter;
public class App extends Application {
#Override
public void start(Stage stage) {
TextFormatter<Integer> quantity = new TextFormatter<>(new IntegerStringConverter(), 0);
TextFormatter<Double> price = new TextFormatter<>(new DoubleStringConverter(), 0.0);
TextFormatter<Double> total = new TextFormatter<>(new DoubleStringConverter(), 0.0);
total.valueProperty().bind(Bindings.createObjectBinding(
() -> quantity.getValue() * price.getValue(),
quantity.valueProperty(), price.valueProperty()));
TextField quantityTF = new TextField("0");
quantityTF.setTextFormatter(quantity);
TextField priceTF = new TextField("0.0");
priceTF.setTextFormatter(price);
TextField totalTF = new TextField();
totalTF.setEditable(false);
totalTF.setTextFormatter(total);
GridPane root = new GridPane();
ColumnConstraints leftCol = new ColumnConstraints();
leftCol.setHalignment(HPos.RIGHT);
ColumnConstraints rightCol = new ColumnConstraints();
rightCol.setHalignment(HPos.LEFT);
root.getColumnConstraints().setAll(
leftCol, rightCol
);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(5));
root.setHgap(5);
root.setVgap(5);
root.addRow(0, new Label("Price:"), priceTF);
root.addRow(1, new Label("Quantity:"), quantityTF);
root.addRow(2, new Label("Total:"), totalTF);
stage.setScene(new Scene(root, 800, 500));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
I have a StringBuffer that is occasionally appended with new information.
In a separate module, I have a JavaFX TextArea that displays that StringBuffer.
Right now, I have to manually update the TextArea every time the underlying data is modified.
Is there something like an ObservableList (which I use for TableViews) that I can use as the back-end data for the TextArea instead, so I don't have to manually manage pushing the changes to the display?
I am not attached to using a StringBuffer. I'm glad to use any appendable data structure to hold text.
You can consider something simple like this:
import javafx.beans.binding.StringBinding;
public class ObservableStringBuffer extends StringBinding {
private final StringBuffer buffer = new StringBuffer() ;
#Override
protected String computeValue() {
return buffer.toString();
}
public void set(String content) {
buffer.replace(0, buffer.length(), content);
invalidate();
}
public void append(String text) {
buffer.append(text);
invalidate();
}
// wrap other StringBuffer methods as needed...
}
This enables easy coding for binding to a text area. You can simply do
TextArea textArea = new TextArea();
ObservableStringBuffer buffer = new ObservableStringBuffer();
textArea.textProperty().bind(buffer);
// ...
buffer.append("Hello world");
However, it's important to note here that you don't transfer the efficiency of the buffer API to the text area: the text area simply has a textProperty() representing its text, which can still only really be modified by set(...) and setValue(...). In other words, when you append to the buffer, you essentially end up with textArea.setText(textArea.getText() + "Hello world") (not textArea.appendText("Hello world"). If you're just looking for a clean API, then this should work for you; if you're looking for something efficient, you would have to "wire" the calls to appendText yourself, since that is simply not supported by the text area's textProperty().
Here's a SSCCE using the above class:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ObservableStringBufferTest extends Application {
private int counter ;
#Override
public void start(Stage primaryStage) {
ObservableStringBuffer buffer = new ObservableStringBuffer();
TextArea textArea = new TextArea();
textArea.setEditable(false);
textArea.textProperty().bind(buffer);
buffer.set("Item 0");
Timeline timeline = new Timeline(new KeyFrame(
Duration.seconds(1),
e -> buffer.append("\nItem "+(++counter))));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
primaryStage.setScene(new Scene(new StackPane(textArea)));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Hi I am trying to read a the numbers from a text field that shows a price e.g. £3.00, and convert the value of the price to a double. Is there a way to do
Double value;
value = Double.parseDouble(textField.getText());
But it won't let me do that because of the £ sign. Is there a way to strip the pound sign then read the digits.
Thanks
There is some TextFormatter and change filter handling logic built into the JavaFX TextField API, you could make use of that.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.text.ParseException;
class CurrencyFormatter extends TextFormatter<Double> {
private static final double DEFAULT_VALUE = 5.00d;
private static final String CURRENCY_SYMBOL = "\u00A3"; // british pound
private static final DecimalFormat strictZeroDecimalFormat
= new DecimalFormat(CURRENCY_SYMBOL + "###,##0.00");
CurrencyFormatter() {
super(
// string converter converts between a string and a value property.
new StringConverter<Double>() {
#Override
public String toString(Double value) {
return strictZeroDecimalFormat.format(value);
}
#Override
public Double fromString(String string) {
try {
return strictZeroDecimalFormat.parse(string).doubleValue();
} catch (ParseException e) {
return Double.NaN;
}
}
},
DEFAULT_VALUE,
// change filter rejects text input if it cannot be parsed.
change -> {
try {
strictZeroDecimalFormat.parse(change.getControlNewText());
return change;
} catch (ParseException e) {
return null;
}
}
);
}
}
public class FormattedTextField extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(final Stage stage) {
TextField textField = new TextField();
textField.setTextFormatter(new CurrencyFormatter());
Label text = new Label();
text.textProperty().bind(
Bindings.concat(
"Text: ",
textField.textProperty()
)
);
Label value = new Label();
value.textProperty().bind(
Bindings.concat(
"Value: ",
textField.getTextFormatter().valueProperty().asString()
)
);
VBox layout = new VBox(
10,
textField,
text,
value,
new Button("Apply")
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
}
The exact rules for DecimalFormat and the filter could get a little tricky if you are very particular about user experience (e.g. can the user enter the currency symbol? what happens if the user does not enter a currency symbol? are empty values permitted? etc.) The above example offers a compromise between a reasonable user experience and a (relatively) easy to program solution. For an actual production level application, you might wish to tweak the logic and behavior a bit more to fit your particular application.
Note, the apply button doesn't actually need to do anything to apply the change. Changes are applied when the user changes focus away from the text field (as long as they pass the change filter). So if the user clicks on the apply button, it gains, focus, the text field loses focus and the change is applied if applicable.
The above example treats the currency values as doubles (to match with the question), but those serious about currency may wish to look to BigDecimal.
For a simpler solution using similar concepts, see also:
Java 8 U40 TextFormatter (JavaFX) to restrict user input only for decimal number
Is there a way to determine the first and last visible row of a listview? In other words I'm looking for two indexes into an array that populates a listview which represent the top and the bottom row of the 'display window'.
You could get the VirtualFlow of the ListView which has methods for getting the first and last rows.
Example:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow flow = (VirtualFlow) list.lookup( ".virtual-flow");
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
You see the fully visible first and last items in the console.
ps: I leave the no data check and event handling to you
Alternate version without css lookup:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import com.sun.javafx.scene.control.skin.VirtualFlow;
public class ListViewSample extends Application {
ListView<String> list = new ListView<String>();
#Override
public void start(Stage stage) {
VBox box = new VBox();
ListView<Integer> list = new ListView<>();
ObservableList<Integer> items = FXCollections.observableArrayList();
for( int i=0; i < 100; i++) {
items.add(i);
}
list.setItems(items);
box.getChildren().add(list);
VBox.setVgrow(list, Priority.ALWAYS);
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.show();
VirtualFlow virtualFlow = null;
for( Node node: list.getChildrenUnmodifiable()) {
if( node instanceof VirtualFlow) {
virtualFlow = (VirtualFlow) node;
}
}
final VirtualFlow flow = virtualFlow;
flow.addEventFilter(Event.ANY, event -> {
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println( list.getItems().get( first.getIndex()) + " - " + list.getItems().get( last.getIndex()) );
});
}
public static void main(String[] args) {
launch(args);
}
}
UPDATE
VirtualFlow is available only after the ListView has been rendered, because it uses Layout parameters which are not available until after the ListView is visible on the stage. So I had to make sure that I got the VirtualFlow when it was certain that the ListView had been rendered. Since I was manipulating the list with various methods I call this method at the end of each method:
private VirtualFlow flow;
private void updateListView(int centreIndex) {
if (flow == null)
flow = (VirtualFlow) myListView.lookup(".virtual-flow");
if (flow != null){
IndexedCell first = flow.getFirstVisibleCellWithinViewPort();
IndexedCell last = flow.getLastVisibleCellWithinViewPort();
System.out.println(first.getIndex() + " - " + last.getIndex());
}
// Now the list can be selectively 'redrawn' using the scollTo() method,
// and using the .getSelectionModel().select(centreIndex) to set the
// desired cell
}
It's bit of a hack, but it works. Using layout parameters does have a drawback though that needs to be considered. If the height of the ListView is only 1 pixel less than the total height of all rows, n number of rows will be visible, but the flow will report n-1 rows which will appear to be a discrepancy at first. Hence keeping a fixed layout height is imperative. At least now by using scrollTo(..) I have control over the position of the selected item in the list (I want to keep it centred in the list display when an item is dragged through the list). This solution leaves me feeling uneasy, but it seems to be the only 'simple' way.
Just a note on the odd-looking logic. It seems that getting the flow takes time, while the program keeps executing. The second (flow != null) is necessary to avoid a NullPointerException.
UPDATE 2
My hack turns out not to work. The whole hack is dependent on timing. Rendering is done on a different thread and as soon as I changed the order of instantiation of classes in my app, I got a NullPointerException again. I turned to the Java doc:
"JavaFX is not thread safe and all JavaFX manipulation should be run on the JavaFX processing thread. If you allow a JavaFX application to interact with a thread other than the main processing thread, unpredictable errors will occur"
And they do! So forget the above - it does not work and will make you scratch your head (and more!) trying to debug it ;-)