Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX: How to bind two values?

I'm new guy here :)

I have a small problem which concerns binding in JavaFX. I have created Task which is working as a clock and returns value which has to be set in a special label (label_Time). This label presents how many seconds left for player's answer in quiz.

The problem is how to automatically change value in label using the timer task? I tried to link value from timer Task (seconds) to label_Time value in such a way...

label_Time.textProperty().bind(timer.getSeconds());

...but it doesn't work. Is it any way to do this thing?

Thanks in advance for your answer! :)


Initialize method in Controller class:

public void initialize(URL url, ResourceBundle rb) {

        Timer2 timer = new Timer2();
        label_Time.textProperty().bind(timer.getSeconds());
        new Thread(timer).start();  
}

Task class "Timer2":

public class Timer2 extends Task{

    private static final int SLEEP_TIME = 1000;
    private static int sec;
    private StringProperty seconds;


    public Timer2(){
        Timer2.sec = 180;
        this.seconds = new SimpleStringProperty("180");
    }

    @Override protected StringProperty call() throws Exception {


        int iterations;

        for (iterations = 0; iterations < 1000; iterations++) {
            if (isCancelled()) {
                updateMessage("Cancelled");
                break;
            }

            System.out.println("TIK! " + sec);
            seconds.setValue(String.valueOf(sec));
            System.out.println("TAK! " + seconds.getValue());

            // From the counter we subtract one second
            sec--;

            //Block the thread for a short time, but be sure
            //to check the InterruptedException for cancellation
            try {
                Thread.sleep(10);
            } catch (InterruptedException interrupted) {
                if (isCancelled()) {
                    updateMessage("Cancelled");
                    break;
                }
            }
        }
        return seconds;
    }

    public StringProperty getSeconds(){
        return this.seconds;
    }

}
like image 699
Wicia Avatar asked Apr 01 '13 15:04

Wicia


1 Answers

Why your app does not work

What is happening is that you run the task on it's own thread, set the seconds property in the task, then the binding triggers an immediate update of the label text while still on the task thread.

This violates a rule for JavaFX thread processing:

An application must attach nodes to a Scene, and modify nodes that are already attached to a Scene, on the JavaFX Application Thread.

This is the reason that your originally posted program does not work.


How to fix it

To modify your original program so that it will work, wrap the modification of the property in the task inside a Platform.runLater construct:

  Platform.runLater(new Runnable() {
    @Override public void run() {
      System.out.println("TIK! " + sec);
      seconds.setValue(String.valueOf(sec));
      System.out.println("TAK! " + seconds.getValue());
    }
  });

This ensures that when you write out to the property, you are already on the JavaFX application thread, so that when the subsequent change fires for the bound label text, that change will also occur on the JavaFX application thread.


On Property Naming Conventions

It is true that the program does not correspond to JavaFX bean conventions as Matthew points out. Conforming to those conventions is both useful in making the program more readily understandable and also for making use of things like the PropertyValueFactory which reflect on property method names to allow table and list cells to automatically update their values as the underlying property is updated. However, for your example, not following JavaFX bean conventions does not explain why the program does not work.


Alternate Solution

Here is an alternate solution to your countdown binding problem which uses the JavaFX animation framework rather than the concurrency framework. I prefer this because it keeps everything on the JavaFX application thread and you don't need to worry about concurrency issues which are difficult to understand and debug.

countdown

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class CountdownTimer extends Application {
  @Override public void start(final Stage stage) throws Exception {
    final CountDown      countdown       = new CountDown(10);
    final CountDownLabel countdownLabel  = new CountDownLabel(countdown);

    final Button         countdownButton = new Button("  Start  ");
    countdownButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override public void handle(ActionEvent t) {
        countdownButton.setText("Restart");
        countdown.start();
      }
    });

    VBox layout = new VBox(10);
    layout.getChildren().addAll(countdownLabel, countdownButton);
    layout.setAlignment(Pos.BASELINE_RIGHT);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 20; -fx-font-size: 20;");

    stage.setScene(new Scene(layout));
    stage.show();
  }

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

class CountDownLabel extends Label {
  public CountDownLabel(final CountDown countdown) {
    textProperty().bind(Bindings.format("%3d", countdown.timeLeftProperty()));
  }
}

class CountDown {
  private final ReadOnlyIntegerWrapper timeLeft;
  private final ReadOnlyDoubleWrapper  timeLeftDouble;
  private final Timeline               timeline;

  public ReadOnlyIntegerProperty timeLeftProperty() {
    return timeLeft.getReadOnlyProperty();
  }

  public CountDown(final int time) {
    timeLeft       = new ReadOnlyIntegerWrapper(time);
    timeLeftDouble = new ReadOnlyDoubleWrapper(time);

    timeline = new Timeline(
      new KeyFrame(
        Duration.ZERO,          
        new KeyValue(timeLeftDouble, time)
      ),
      new KeyFrame(
        Duration.seconds(time), 
        new KeyValue(timeLeftDouble, 0)
      )
    );

    timeLeftDouble.addListener(new InvalidationListener() {
      @Override public void invalidated(Observable o) {
        timeLeft.set((int) Math.ceil(timeLeftDouble.get()));
      }
    });
  }

  public void start() {
    timeline.playFromStart();
  }
}

Update for additional questions on Task execution strategy

Is it possible to run more than one Task which includes a Platform.runLater(new Runnable()) method ?

Yes, you can use multiple tasks. Each task can be of the same type or a different type.

You can create a single thread and run each task on the thread sequentially, or you can create multiple threads and run the tasks in parallel.

For managing multiple tasks, you can create an overseer Task. Sometimes it is appropriate to use a Service for managing the multiple tasks and the Executors framework for managing multiple threads.

There is an example of a Task, Service, Executors co-ordination approach: Creating multiple parallel tasks by a single service In each task.

In each task you can place no runlater call, a single runlater call or multiple runlater calls.

So there is a great deal of flexibility available.

Or maybe I should create one general task which will be only take data from other Tasks and updating a UI?

Yes you can use a co-ordinating task approach like this if complexity warrants it. There is an example of such an approach in in Render 300 charts off screen and save them to files.

like image 169
jewelsea Avatar answered Nov 09 '22 15:11

jewelsea