2

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.

John Forbes
  • 1,248
  • 14
  • 19

2 Answers2

3

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() ;
}
c0der
  • 18,467
  • 6
  • 33
  • 65
2

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();
  }

}
Slaw
  • 37,820
  • 8
  • 53
  • 80