0

I posted a question similar to this earlier but I wasn't specific enough. Here is a simplified version of my code:

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.ProgressBar;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.layout.HBox;
import javafx.scene.Scene;
import javafx.concurrent.Task;
   import javafx.stage.Stage;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.MenuBar;
import javafx.scene.control.Menu;
 import javafx.scene.control.MenuItem;
import javafx.scene.control.Separator;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.text.Text;
 import javafx.scene.text.Font;
import javafx.geometry.Pos;
import java.net.URL;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
import javafx.scene.control.Slider;

public class ProgressTest extends Application {

boolean play = true;
int x = 0;
@Override
public void start(Stage stage) {
GridPane pane = new GridPane(); //pane and Hboxes
HBox hbox = new HBox();

Button update = new Button("Start");
update.setOnAction( e -> {
    while(play == true)
    {
        System.out.println(++x);
    }
});

Button pause = new Button("Pause");
pause.setOnAction( e -> {
    if(play == true)
    {
        pause.setText("Play");
        play = false;
    }

    else
    {
        pause.setText("Pause");
        play = true;
    }
});


hbox.getChildren().addAll(update, pause);
pane.add(hbox, 0, 1);
Scene scene = new Scene(pane);
stage.setMaxWidth(655);
stage.setMaxHeight(620);

stage.setTitle("Gallery!");
stage.setScene(scene);

stage.sizeToScene();

stage.show();

}


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

}

The idea with this code is that, when the user clicks the "Start" button, the program should print out x going up, and pause whenever the user hits "Pause", and resume again when the user hits "Play".

The problem is that whenever I click the "Play" button, the program goes into an infinite loop and I am unable to press the pause button to stop it. Is there something wrong with the way I am going about this? Are there any tricks to getting this to work? Any help would be very appreciated.

Also, I know that some of this may have syntax errors but I know it's correct on my copy of the code, I'm more considered with the logic behind how to get this to work.

  • Do you know what the keyword `volatile` does? And do you know how to start a new `Thread`? – Dawood ibn Kareem Nov 05 '17 at 19:26
  • No, I'm fairly new to Javafx and GUI's. – Mohd Hasan Nov 05 '17 at 19:39
  • The `while(true)` loop is a bad idea. It can max out the CPU usage and bring any other part of your code to a halt. Instead, you should use an event-driven design. What really do you want to happen when you press your buttons? – user1803551 Nov 05 '17 at 19:39
  • OK, well those are the two things you need to research. I'll try to find time to write a proper answer later, God willing. – Dawood ibn Kareem Nov 05 '17 at 19:40
  • @user1803551 I basically want to do the following: When the user hits the play button, the program should check to see if the "play" boolean is true (which it always starts as such). After the check it will begin printing the numbers starting from 1 and going up to the INT MAX. At any time during this, the user should be able to hit "Pause" and change the play boolean to false, which should result in stopping the printing until the player hits that same button again, setting the play boolean back to true. – Mohd Hasan Nov 05 '17 at 19:47
  • OK, post a [mcve] so I can help you modify your existing code. It should include just the `Application` class of JavaFX, a simple scene with your buttons and your logic. To solve the problem, you will need to perform the loop on a different thread - one which does not block the GUI thread and make is unresponsive. – user1803551 Nov 05 '17 at 19:50
  • No problem. I have the post typed out and am just waiting for the 90 minute timer to finish before I post it. @user1803551 – Mohd Hasan Nov 05 '17 at 20:29
  • What timer? [Edit] your question. – user1803551 Nov 05 '17 at 20:31
  • here's the link to the working code @user1803551 https://stackoverflow.com/questions/47126480/infinite-loop-error-with-buttons-in-javafx-working-code – Mohd Hasan Nov 05 '17 at 20:48
  • You're not supposed to create a new question with the code. Just [edit] your current question so it's relevant and delete the new one. – user1803551 Nov 05 '17 at 21:03
  • Ok ive gone ahead and edited this question. Sorry im still new to StackOverflow – Mohd Hasan Nov 05 '17 at 21:08
  • 1
    Consider using an [animation](https://docs.oracle.com/javase/9/docs/api/javafx/animation/package-summary.html) of some kind, e.g. a [`Timeline`](https://docs.oracle.com/javase/9/docs/api/javafx/animation/Timeline.html) or an [`AnimationTimer`](https://docs.oracle.com/javase/9/docs/api/javafx/animation/AnimationTimer.html). Your current code blocks the FX Application thread (because there is an infinite loop), so the FX Application Thread never gets a chance to respond to user events. – James_D Nov 05 '17 at 21:14
  • @James_D I'm interested in how you would do this as these classes are duration based while the requirement is (seemingly) "as fast as possible". – user1803551 Nov 05 '17 at 23:22
  • @user1803551 Presumably though the real requirement isn't simply to spit numbers to stdout. Is there any reason not to simply poll, say, an `AtomicBoolean` (or `volatile boolean`, if you prefer) on each iteration? But I still suspect that an animation meets the real requirements (whatever they are) better. – James_D Nov 05 '17 at 23:31
  • it's obvious that you'd get an infinite loop here when the start button is pressed. At the start "play" is set to be "true" and your while-loop checks whether "play" is "true" which it is and it does not change within the body of the while-loop; hence the loop will not stop. Consider re-coding your while-loop – Oshan Mendis Nov 06 '17 at 14:48
  • @MohdHasan If my answer solved your problem consider accepting it by checking the green checkmark next to it. – user1803551 Nov 09 '17 at 15:01

1 Answers1

2

There are 2 big problems with your implementation:

  1. The GUI runs on the JavaFX thread. For it to remain responsive any operations in it must finish quickly. When you are running a long computation like your loop on this thread the whole GUI is blocked.
  2. After you manage to set play to false the loop will just exit and clicking on resume will do nothing.

There are several ways to approach this. I will demonstrate one using Thread and CountDownLatch.

I can't explain what a thread is in the scope of the question (tons of material on SO and everywhere), but what is relevant here is that executing the costly operation on a thread which is not the JavaFX thread will solve point 1.

A CountDownLatch is used to block a thread's (or more than one) execution until the latch is released/broken, upon which the thread will continue. It is initialized with an int representing the number of times it needs to count down before it is released. A thread reaching the latch's await method blocks until the latch's countDown method is called the specified amount of times.

Here is a sample code:

import java.util.concurrent.CountDownLatch;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ProgressTest extends Application {

    volatile CountDownLatch cl = new CountDownLatch(1);

    @Override
    public void start(Stage stage) {
        Thread thread = new Thread(() -> {
            int x = 0;
            while (true) {
                try {
                    cl.await();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                System.out.println(++x);
            }
        });
        thread.setDaemon(true);

        Button update = new Button("Start");
        update.setOnAction(e -> {
            if (!thread.isAlive()) {
                cl.countDown();
                thread.start();
            }
        });

        BooleanProperty running = new SimpleBooleanProperty(true);

        Button pause = new Button("Pause");
        pause.textProperty().bind(Bindings.when(running).then("Pause").otherwise("Play"));
        pause.setOnAction(e -> {
            if (running.get()) {
                cl = new CountDownLatch(1);
            }
            else {
                cl.countDown();
            }
            running.set(!running.get());
        });

        HBox hbox = new HBox(update, pause);
        Scene scene = new Scene(hbox);
        stage.setScene(scene);
        stage.setTitle("Gallery!");
        stage.sizeToScene();
        stage.show();
    }

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

The thread spits out numbers "as fast as possible" with the while(true) loop as you did, only it can be paused by reaching the await method. When you press Start the latch is broken and the thread starts and executes continuously. When you press Pause a new latch is created in its place (a latch is a 1-time thing, it can't be reset), which causes the thread to wait in await until someone breaks it with the countDown method (one call is enough because we instantiated it with 1). That is what pressing Resume does.

Calling setDaemon(true) on the thread makes sure it allows the JVM to exit without waiting for it. If the thread must finish before the JVM exists (e.g., it is not a background thread), you can remove it.

Iv'e made the latch volatile which guarantees that different threads will see the same value for it. See also Do you ever use the volatile keyword in Java? and other available sources. In this specific case you don't need poinpoint thread synchronization so it won't have a noticeable effect, but it should be there nonetheless.

Note that I've added a small check on Start that the thread is not already running because starting a thread while it's running throws an exception. You didn't specify what to do if Start is pressed during execution.


While irrelevant to your question, Iv'e demonstrated how you can utilize the JavaFX binding API to have the button's text update automatically with the boolean's value. I "promoted" the control boolean to a property and bound the button's text to its value. It might not be so useful for this situation though.

Notes:

  • You're setting the height and width of the scene and then call sizeToScene, which makes the previous calls redundant.
  • You don't need to check a boolean with ==, you can use it directly: if (b == true) is equivalent to if (b) and if (b == false) is equivalent to if (!b).
  • A better name for your boolean would be one that represents a state ("running"/"paused") rather than an action ("run"/"pause").
user1803551
  • 12,965
  • 5
  • 47
  • 74