1

friends. New to stackoverflow, here, and also to JavaFX. Apologies if I mess up any nomenclature and if this is a little long-winded, but I did whittle my code down to a minimal "working" example. (the code below hangs up unless you comment out the while loop, as mentioned in the code itself)

I'm trying to figure out how I can have a Slider change its value continuously after clicking a "wake" button and stop changing value after clicking a "sleep" button. For the sake of argument, say it changes value randomly to an integer between 0 and 99.

From what I've read so far (and I've read a lot, but these are the two most recent examples), it seems I should be able to use Platform.runLater() or updateMessenger() to avoid running an infinite-till-my-sleep-button-breaks-it loop on the UI thread. I so far have failed in implementing the former, and I'm still not sure how to implement the latter.

I also tried learning about property bindings so that I could have the Slider.valueProperty change in the UI thread (changing a UI element in the UI thread makes sense... right?) while changing an intermediate variable, which I called state and created from a class stateObject, on a background thread. After the background thread calculates the value I want to give to the slider, it changes the state's value, which is bound to the Slider's value. I thought separating the value calculation into a background thread and Slider value-changing into the UI thread would do the trick, but I keep getting the hang-up or infamous IllegalStateException exception, Not on FX application thread

I'm not wholly sure how the Platform.runLater() magic is supposed to work, but I still get a hang-up whether I run my runnable interface inside that or a Thread().

Any help or explanation as to why these attempts have failed/how to make things work would be greatly appreciated.

Cheers, Mike

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.geometry.*;

//Not sure which of these, if any, I actually need
import javafx.event.*;
import javafx.concurrent.*;
import javafx.beans.property.*;

public class MWE extends Application {

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

    //Class fields
    Button sleepBtn, wakeBtn;
    Slider displaySldr;
    boolean wakeful = true;

    //state property class
    class stateObject {
        //Store the property
        private DoubleProperty state = new SimpleDoubleProperty();
        //Define getter for property value
        public final double getState() {
            return state.get();
        }
        //Define setter for property value
        public final void setState(double val) {
            state.setValue(val);
        }
        //Define getter for property itself
        public DoubleProperty stateProperty() {
            return state;
        }
    }

    //Create state
    stateObject state = new stateObject();

    //Start method
    @Override
    public void start(Stage primaryStage) {

        //Create top-level pane to store nodes into for the scene
        VBox mainPane = new VBox(20);
        mainPane.setAlignment(Pos.CENTER);

        //Create buttons
        sleepBtn = new Button("Sleep");
        sleepBtn.setOnAction(e -> NPSleep());
        wakeBtn = new Button("Wake");
        wakeBtn.setOnAction(e -> NPWake());

        //Lay out buttons
        VBox buttonsBox = new VBox(10);
        buttonsBox.getChildren().addAll(wakeBtn,sleepBtn);
        buttonsBox.setAlignment(Pos.CENTER);

        //Create slider to be change continuously by clicking wakeBtn once
        //Slider should stop changing when sleepBtn is clicked
        displaySldr = new Slider(0,100,50);
        displaySldr.setMajorTickUnit(25);
        displaySldr.setMinorTickCount(24);
        displaySldr.setSnapToTicks(true);
        displaySldr.setShowTickMarks(false);
        displaySldr.setShowTickLabels(true);

        //Make property event. When 'state' changes, change displaySldr value
        state.stateProperty().addListener(
            (observable,oldvalue,newvalue) ->
                    displaySldr.setValue(newvalue.intValue())
        );


        //Organize labels and sliders
        VBox LSPane = new VBox(10);
        LSPane.getChildren().addAll(displaySldr);
        LSPane.setAlignment(Pos.CENTER);

        //Organize sub-panes into top-level pane, mainPane
        mainPane.getChildren().addAll(LSPane,buttonsBox);

        //Set the scene
        Scene mainScene = new Scene(mainPane);

        //Set the stage
        primaryStage.setTitle("oh god please work");
        primaryStage.setScene(mainScene);
        primaryStage.show();

    }

    private void NPWake() {

        Runnable wakeyWakey = () -> {
            while (wakeful == true) { //Commenting out the while loop and 
                                        //leaving the command allows program to run fine
                                        //but slider changes only once, not continuously
                state.setState( Math.floor(Math.random()*100) );

                try{
                    Thread.sleep(300);
                } catch (InterruptedException ie) {
                }
            }
        };

        Platform.runLater(wakeyWakey);
//        Thread wakeThread = new Thread(wakeyWakey); //
//        wakeThread.start();                         // This doesn't work either
    }


    private void NPSleep() {
        if (wakeful == true) {
            wakeful = false;
        }
    }

}
Community
  • 1
  • 1
Mike Rex
  • 13
  • 4

1 Answers1

1

You do not need to have State Object, since you still must update the value on JavaFX Application Thread, so it defeats the idea of creating such object. You can remove all State Object related code. The code below is the fix for your solution:

private void NPWake() {
    Thread t = new Thread(() -> {
        while (wakeful) {
            try {
                Platform.runLater(() -> {
                    displaySldr.setValue(Math.floor(Math.random() * 100));
                });

                Thread.sleep(300);
            }
            catch (Exception e) {}
        }
    });
    t.start();
}

onAction of a Button is handled on JavaFX Application Thread and under no circumstances do we want to have an infinite loop run on that thread unless it is its own internal, because the loop will simply hang that thread. Hence, we create a new Thread t which will have that loop and in its body will make a call to JavaFX Application Thread to commit the changes. As you have gathered, changes to UI / JavaFX properties must occur on the JavaFX Application Thread. Platform.runLater( "some code" ) means that "some code" will be executed on JavaFX Application Thread at some point in the future (almost instantly, if the thread isn't busy).

As for the design of such applications, I'd recommend you have a look at AnimationTimer - https://docs.oracle.com/javase/8/javafx/api/javafx/animation/AnimationTimer.html or Timeline - https://docs.oracle.com/javase/8/javafx/api/javafx/animation/Timeline.html. They both run on JavaFX Application Thread.

AlmasB
  • 3,377
  • 2
  • 15
  • 17
  • Fantastic, this works - thanks a million for the fix and explanation! – Mike Rex May 30 '15 at 17:15
  • But it's much easier to use a `Timeline` for this kind of thing. See, e.g. http://stackoverflow.com/questions/9966136/javafx-periodic-background-task – James_D May 31 '15 at 03:36