Sam Hoffmann
Sam Hoffmann

Reputation: 318

Javafx live thread updates

I'm working with Javafx and threads simultaneously and I constanly run into this problem where I make a button and then when the button is clicked (using event handlers) I make a for loop that changes the button to 1,2,3,4,5 and then delays for a second in the middle of each. Like a count down!

But what happens is it delays for 5 seconds and changes the text of button to 5.

The problem is I want to see it change between 1 and 5 but all I see is 5 at the end of a 5 second delay. I would assume that it changing the button text but I don't see it. I might have to to do with the .show() method in the Javafx class.

public class HewoWorld extends Application implements EventHandler<ActionEvent>
{
    Thread t = new Thread();
    Button butt;
    boolean buttWasClicked = false;
    Circle circ1 = new Circle(40, 40, 30, Color.RED);
    Circle circ2 = new Circle(100, 100, 30, Color.BLUE);
    Group root;
    Scene scene;
    Stage disStage = new Stage();
    int i = 1;
    public static void main(String[] args)
    {
        launch(args);
    }
    public void start(Stage stage) throws Exception 
    {

        disStage.setTitle("tests stuffs");
        Screen screen = Screen.getPrimary();
        Rectangle2D bounds = screen.getVisualBounds();
        double windh = bounds.getHeight()/2+150;//sets height of screen 
        double windw = bounds.getWidth()/3;//sets width of screen 
        Pane layout = new Pane();
        butt = new Button();
        butt.setText("Hello world");


        root = new Group(circ1, circ2, butt);
        scene = new Scene(root, 800, 400);
        disStage.setWidth(windw);
        disStage.setHeight(windh);


        butt.setLayoutX(200);
        butt.setLayoutY(200);
        butt.setOnAction(this);
        disStage.setScene(scene);
        disStage.show();
    }
     public void handle(ActionEvent event) 
    {
        if (event.getSource() == butt && buttWasClicked == false) 
        {
            try
            {
                butt.setText(i+"");
                t.sleep(1000);
                i++;
            }
            catch(Exception q)
            {

            } 
            circ1 = new Circle(40, 40, 30, Color.BLACK);
            circ2 = new Circle(100, 100, 30, Color.RED);
        }
    }
}

Upvotes: 0

Views: 1665

Answers (3)

James_D
James_D

Reputation: 209358

Why your code doesn't work

The reason your code doesn't work is that you are blocking the FX Application Thread.

Like (almost?) all UI toolkits, JavaFX is a single-threaded UI toolkit. This means that all event handlers, and all the rendering of the UI, are performed on a single thread (called the FX Application Thread).

In your code, you have an event handler that takes more than a second to run, because it pauses for a second via a call to Thread.sleep(...). While that event handler is running, the UI cannot be redrawn (because a single thread cannot do two things at once). So while the value of the button's text has changed immediately, the new value won't actually be rendered on the screen until the handle(...) method has finished running. If you had a for loop in the handle method, nothing would be rendered until the entire loop (and anything else in the method) had completed.

How to fix it

The simplest way to do what you want in JavaFX is to use a Timeline to handle the pause. The Timeline manages the threading appropriately for you:

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

public class CountingButton extends Application {


    @Override
    public void start(Stage primaryStage) {

        Button button = new Button("Count");

        Timeline timeline = new Timeline();
        for (int count = 0; count <= 5 ; count++) {
            final String text = Integer.toString(count);
            KeyFrame frame = new KeyFrame(Duration.seconds(count), event -> 
                button.setText(text));
            timeline.getKeyFrames().add(frame);
        }

        button.setOnAction(e -> timeline.play());

        primaryStage.setScene(new Scene(new StackPane(button), 120, 75));
        primaryStage.show();
    }


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

In general, for changing the appearance of the user interface at specific time points, the JavaFX Animation API (see also the tutorial) can be useful, especially Timeline and PauseTransition.

A "lower-level" way to do this would be to create a Thread yourself and pause in that thread. This is much more advanced: you need to be careful to update the UI on the FX Application Thread, not on the thread you created. You can do this with a call to Platform.runLater(...):

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class CountingButton extends Application {


    @Override
    public void start(Stage primaryStage) {

        Button button = new Button("Start");

        button.setOnAction(e -> {
            Thread thread = new Thread(() -> {
                for (int i = 0; i <= 5 ; i++) {
                    final String text = "Count: "+i ;
                    Platform.runLater(() -> button.setText(text));
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException exc) {
                        exc.printStackTrace();
                    }
                }
            });
            thread.start();
        });

        primaryStage.setScene(new Scene(new StackPane(button), 120, 75));
        primaryStage.show();
    }


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

For more general information on threading in JavaFX, have a look at this post: Using threads to make database requests

Upvotes: 6

J Atkin
J Atkin

Reputation: 3140

In this case you need a timer to run every second and increment a counter on every hit. To my knowledge, the best way to make a timer in javafx is to use a timeline. https://stackoverflow.com/a/9966213/4683264.

int i = 0;// class field
// ....
Timeline fiveSecondsWonder = new Timeline(new KeyFrame(Duration.seconds(1), event -> 
                                             button.setText(++i)));
fiveSecondsWonder.setCycleCount(5);// repeat five times
fiveSecondsWonder.play();

Upvotes: 0

Normal design
Normal design

Reputation: 148

What you have to do is to replace the thread use by the following method :

scheduler = Executors.newScheduledThreadPool(1);

scheduler.scheduleAtFixedRate(

        new Runnable(){


            @Override
            public void run() {
               Platform.runLater(new Runnable() {

                   @Override
                   public void run() {

                           //Here your code to change the number by for example incrementig the value of the button
                   }
               });

            }
        }, 
        1000, 
        80, 
        TimeUnit.MILLISECONDS);  

+1 if it helps :D

Upvotes: 0

Related Questions