Engineer
Engineer

Reputation: 109

JavaFX ToggleButton get in and out of an infinite loop

I want to create a toggle button in JavaFX that will execute a loop, and will stop only when the user clicks the button again; while active, the loop will continuously change the text in the TextField...

Here is the code for the ToggleButton:

toggleButton.setOnAction(event -> {
    try {

        if(toggleButton.isSelected())
        {
            new Thread(
                    new Runnable() {
                        public void run() {
                            while(toggleButton.isSelected())
                            {
                                try {
                                    Thread.sleep(200);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                                double rand = Math.random();
                                textField.setText(rand);
                            }
                        }
                    }
                    ).start();
        }

        else if(!toggleButton.isSelected())
        {
            toggleButton.setSelected(false);
        }

    } catch (Exception ex) {
        ex.printStackTrace();
    }
});

Well, the above code does work, but... the problem is, that without slowing down the loop enough, (the Thread.sleep(200) thing), tons of errors will be printed in the console... But I need it to be as fast as possible...

Here is an example of the output without using Thread.sleep(200):

(Couldn't paste everything because it's way too long... but here's a link to the full output: http://pastebin.com/SZfugk3d)

[sts] -----------------------------------------------------
[sts] Starting Gradle build for the following tasks: 
[sts]      run
[sts] -----------------------------------------------------
:compileJava
:compileRetrolambdaMain
:processResources UP-TO-DATE
:classes
:compileAndroidJava SKIPPED
:compileRetrolambdaAndroid SKIPPED
:compileTestJava UP-TO-DATE
:compileRetrolambdaTest SKIPPED
:compileRetrolambda
:compileDesktopJava UP-TO-DATE
:processDesktopResources UP-TO-DATE
:desktopClasses UP-TO-DATE
:run
Exception in thread "Thread-6" java.lang.NullPointerException
    at com.sun.javafx.text.PrismTextLayout.layout(PrismTextLayout.java:1209)
    at com.sun.javafx.text.PrismTextLayout.ensureLayout(PrismTextLayout.java:223)
    at com.sun.javafx.text.PrismTextLayout.getBounds(PrismTextLayout.java:246)
    at javafx.scene.text.Text.getLogicalBounds(Text.java:358)
    at javafx.scene.text.Text.getYRendering(Text.java:1069)
    at javafx.scene.text.Text.access$4400(Text.java:95)
    at javafx.scene.text.Text$TextAttribute$11.computeValue(Text.java:1785)
    at javafx.scene.text.Text$TextAttribute$11.computeValue(Text.java:1777)
    at javafx.beans.binding.ObjectBinding.get(ObjectBinding.java:153)
    at javafx.beans.binding.ObjectExpression.getValue(ObjectExpression.java:50)
    at javafx.beans.property.ObjectPropertyBase.get(ObjectPropertyBase.java:132)
    at com.sun.javafx.scene.control.skin.TextFieldSkin.lambda$new$198(TextFieldSkin.java:233)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.beans.property.ObjectPropertyBase.access$000(ObjectPropertyBase.java:51)
    at javafx.beans.property.ObjectPropertyBase$Listener.invalidated(ObjectPropertyBase.java:233)
    at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:172)
    at javafx.scene.text.Text.impl_geomChanged(Text.java:769)
    at javafx.scene.text.Text.needsTextLayout(Text.java:194)
    at javafx.scene.text.Text.needsFullTextLayout(Text.java:189)
    at javafx.scene.text.Text.access$200(Text.java:95)
    at javafx.scene.text.Text$2.invalidated(Text.java:389)
    at java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
Exception in thread "JavaFX Application Thread"
[sts] Cancellation request posted...
[sts] Build cancelled
[sts] Time taken: 0 min, 39 sec
[sts] -----------------------------------------------------

Oh and also, when I click the red square in the eclipse console to stop the program, it takes ages to respond, why is it so? After all it's just a TextField and a Button...

screenshot

Upvotes: 1

Views: 1110

Answers (1)

James_D
James_D

Reputation: 209339

You're accessing and modifying the toggle button and text field from a background thread, which violates the single-threaded rule of JavaFX (see, e.g. the "Threading" section in the Application documentation).

You need to modify the text field on the FX Application Thread, by using Platform.runLater(). You should also use an AtomicBoolean to flag whether or not to stop the thread.

Finally, to allow the application to exit properly, make your background thread a daemon thread:

import java.util.concurrent.atomic.AtomicBoolean;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class UpdateTextFieldOnToggle extends Application {

    @Override
    public void start(Stage primaryStage) {
        ToggleButton toggleButton = new ToggleButton("Generate");
        TextField textField = new TextField();

        AtomicBoolean running = new AtomicBoolean();

        toggleButton.setOnAction(event -> {

            running.set(toggleButton.isSelected());

            try {

                if (running.get()) {
                    Thread t = new Thread(new Runnable() {
                        public void run() {
                            while (running.get()) {
                                try {
                                    Thread.sleep(200);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                                Platform.runLater(() -> {
                                    double rand = Math.random();
                                    textField.setText(String.format("%f", rand));
                                });
                            }
                        }
                    });
                    t.setDaemon(true);
                    t.start();
                }

            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        VBox root = new VBox(10, toggleButton, textField);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(24));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Note that for this kind of functionality, the animation API is usually much more convenient:

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class UpdateTextFieldOnToggle extends Application {

    @Override
    public void start(Stage primaryStage) {
        ToggleButton toggleButton = new ToggleButton("Generate");
        TextField textField = new TextField();

        Timeline changeTextField = new Timeline(
            new KeyFrame(Duration.millis(200), event -> {
                double rand = Math.random();
                textField.setText(String.format("%f", rand));
            })  
        );

        changeTextField.setCycleCount(Animation.INDEFINITE);

        toggleButton.setOnAction(event -> {
            if (toggleButton.isSelected()) {
                changeTextField.play();
            } else {
                changeTextField.stop();
            }
        });

        VBox root = new VBox(10, toggleButton, textField);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(24));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Upvotes: 2

Related Questions