multithreadingjavafxcontrolsprogress-bar

JavaFX - Updating ProgressBar on Nodes Creation


I've seen a lot of code on the platform regarding updating the ProgressBar using thread, but all of it is related to some calculation performed or updating of some control property. I don't know if it's possible, but what I need is for the ProgressBar to show the progress in creating 225 (15 X 15) to 2500 (50 x 50) TextFields. Below are the files and the .fxml. When I included the thread code part, the grid stopped to appears. Thank you in advance.

The application

enter image description here

GridApplication.java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class GridCenterApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(GridCenterApplication.class.getResource("main-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load());
        stage.setTitle("Grid");
        stage.setScene(scene);
        stage.setMaximized(true);
        stage.show();
    }

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

GridController.java

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;

import java.net.URL;
import java.util.ResourceBundle;

public class GridCenterController implements Initializable {

    @FXML
    private ScrollPane scpGrid;

    @FXML
    private Spinner<Integer> spnCols;

    @FXML
    private Spinner<Integer> spnRows;

    @FXML
    private ProgressBar pgbProgress;

    GridPane gridPane;

    private final int CELL_HORIZONTAL_GAP = 1;
    private final int CELL_VERTICAL_GAP = 1;
    private final int CELL_HORIZONTAL_SIZE = 40;
    private final int CELL_VERTICAL_SIZE = 40;

    private int totalCols = 0;
    private int totalRows = 0;

    @FXML
    void onMnuItemNewGridAction(ActionEvent event) {

        if(!(scpGrid.getContent() == null)){
            scpGrid.setContent(null);
        }

        totalCols = spnCols.getValue();
        totalRows = spnRows.getValue();

        var newGrid = new Grid(totalCols, totalRows, CELL_HORIZONTAL_GAP, CELL_VERTICAL_GAP, CELL_HORIZONTAL_SIZE,
                CELL_VERTICAL_SIZE, pgbProgress);
        gridPane = newGrid.getGrid();
        scpGrid.setContent(gridPane);
    }

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        SpinnerValueFactory<Integer> numberOfCols = new SpinnerValueFactory.IntegerSpinnerValueFactory(15, 50);
        SpinnerValueFactory<Integer> numberOfRows = new SpinnerValueFactory.IntegerSpinnerValueFactory(15, 50);
        spnCols.setValueFactory(numberOfCols);
        spnRows.setValueFactory(numberOfRows);
        scpGrid.contentProperty().addListener((observableValue, oldValue, newValue) -> {
            if (newValue != null && newValue.isVisible()) {
               pgbProgress.setProgress(0);
            }
        });

    }
}

Grid.java

import javafx.concurrent.Task;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class Grid {

    private final GridPane grid;

    public Grid(int totalCols, int totalRows, int CELL_HORIZONTAL_GAP, int CELL_VERTICAL_GAP, int CELL_HORIZONTAL_SIZE,
                int CELL_VERTICAL_SIZE, ProgressBar pgbProgress) {

        grid = new GridPane();
        grid.setHgap(CELL_HORIZONTAL_GAP);
        grid.setVgap(CELL_VERTICAL_GAP);
        TextField[][] arrayLetterField = new TextField[totalCols][totalRows];

        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {

                pgbProgress.setProgress(0);
                double total = totalCols * totalRows;
                double i = 1.0;

                for (int row = 0; row < totalRows; row++) {
                    for (int col = 0; col < totalCols; col++) {
                        arrayLetterField[col][row] = new TextField();
                        arrayLetterField[col][row].setMinSize(CELL_HORIZONTAL_SIZE, CELL_VERTICAL_SIZE);
                        arrayLetterField[col][row].setMaxSize(CELL_HORIZONTAL_SIZE, CELL_VERTICAL_SIZE );
                        grid.add(arrayLetterField[col][row], col, row);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

                        updateProgress(i, total);
                        i++;
                    }
                }
                return null;
            }
        };

        pgbProgress.progressProperty().bind(task.progressProperty());
        new Thread(task).start();

    }

    public GridPane getGrid() {
        return grid;
    }

}

main-view.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>

<BorderPane prefHeight="767.0" prefWidth="1053.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.gridpanetest.GridCenterController">
    <top>
        <VBox prefWidth="100.0" BorderPane.alignment="CENTER">
            <children>
                <MenuBar fx:id="mnuBar" prefHeight="25.0" prefWidth="360.0">
                    <menus>
                        <Menu mnemonicParsing="false" text="Grid">
                            <items>
                                <MenuItem mnemonicParsing="false" onAction="#onMnuItemNewGridAction" text="New grid" />
                            </items>
                        </Menu>
                    </menus>
                </MenuBar>
            <Pane prefHeight="80.0" prefWidth="1053.0">
               <children>
                  <Label layoutX="26.0" layoutY="15.0" text="Columns" />
                  <Label layoutX="26.0" layoutY="46.0" text="Rows" />
                  <Spinner fx:id="spnCols" layoutX="79.0" layoutY="11.0" prefHeight="25.0" prefWidth="57.0" />
                  <Spinner fx:id="spnRows" layoutX="79.0" layoutY="42.0" prefHeight="25.0" prefWidth="57.0" />
                  <ProgressBar fx:id="pgbProgress" focusTraversable="false" layoutX="185.0" layoutY="31.0" prefWidth="200.0" progress="0.0" />
               </children>
            </Pane>
            </children>
        </VBox>
    </top>
   <center>
        <ScrollPane fx:id="scpGrid" style="-fx-background-color: #dbbb92; -fx-background: #dbbb92;" BorderPane.alignment="CENTER" />
   </center>
</BorderPane>

Solution

  • I believe the main problem in your code is the part where you add the text field to the grid. It is because, you are trying to add child node in different thread and not in JavaFX thread.

    A quick fix can be wrapping that code in Platform.runLater as below:

    int c = col;
    int r = row;
    Platform.runLater(() -> grid.add(arrayLetterField[c][r], c, r));
    

    But this will not be as the behaviour you want, Because you are adding the grid too early, so you will see the nodes adding one by one..

    To update the scrollPane at the end, you can try the below code for getting the desired behaviour. I also included the logic of instant updating of nodes, just for reference to give you some idea.

    enter image description here

    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.concurrent.Task;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    public class NodesProgressBarDemo extends Application {
        @Override
        public void start(Stage primaryStage) throws Exception {
            TextField cols = new TextField();
            cols.setPrefWidth(100);
            TextField rows = new TextField();
            rows.setPrefWidth(100);
            Button generateLazily = new Button("Generate Lazily");
            Button generateInstantly = new Button("Generate Instantly");
            HBox row = new HBox(20, new HBox(10, new Label("Columns:"), cols),
                    new HBox(10, new Label("Rows:"), rows),
                    generateLazily, generateInstantly);
            row.setAlignment(Pos.CENTER_LEFT);
    
            ProgressBar progressBar = new ProgressBar();
            progressBar.setProgress(0);
            progressBar.setMaxWidth(Double.MAX_VALUE);
    
            ScrollPane scrollPane = new ScrollPane();
            scrollPane.setFitToHeight(true);
            scrollPane.setFitToWidth(true);
            VBox.setVgrow(scrollPane, Priority.ALWAYS);
    
            generateLazily.setOnAction(e -> updateGridLazily(cols.getText(), rows.getText(), scrollPane, progressBar));
            generateInstantly.setOnAction(e -> updateGridInstantly(cols.getText(), rows.getText(), scrollPane, progressBar));
    
            VBox root = new VBox(10, row, progressBar, scrollPane);
            root.setPadding(new Insets(10));
            Scene scene = new Scene(root, 700, 500);
            primaryStage.setScene(scene);
            primaryStage.setTitle("Nodes ProgressBar Demo");
            primaryStage.show();
        }
    
        private void updateGridLazily(String col, String row, ScrollPane scrollPane, ProgressBar progressBar) {
            int columns = Integer.parseInt(col);
            int rows = Integer.parseInt(row);
    
            Task<GridPane> task = new Task<>() {
                @Override
                protected GridPane call() {
                    int total = columns * rows;
                    int count = 0;
                    GridPane gridPane = new GridPane();
                    gridPane.setVgap(5);
                    gridPane.setHgap(5);
                    for (int r = 0; r < rows; r++) {
                        for (int c = 0; c < columns; c++) {
                            TextField textField = new TextField();
                            textField.setMinSize(40, 40);
                            textField.setMaxSize(40, 40);
                            gridPane.add(textField, c, r);
    
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
    
                            count++;
                            updateProgress(count, total);
                        }
                    }
                    return gridPane;
                }
            };
            task.setOnSucceeded(e -> {
                scrollPane.setContent(task.getValue());
                progressBar.setProgress(0);
            });
            task.progressProperty().addListener((obs, old, val) -> progressBar.setProgress(val.doubleValue()));
            new Thread(task).start();
        }
    
        private void updateGridInstantly(String col, String row, ScrollPane scrollPane, ProgressBar progressBar) {
            int columns = Integer.parseInt(col);
            int rows = Integer.parseInt(row);
    
            GridPane gridPane = new GridPane();
            gridPane.setVgap(5);
            gridPane.setHgap(5);
            scrollPane.setContent(gridPane);
    
            Task<Void> task = new Task<>() {
                @Override
                protected Void call() {
                    int total = columns * rows;
                    int count = 0;
                    for (int r = 0; r < rows; r++) {
                        for (int c = 0; c < columns; c++) {
                            TextField textField = new TextField();
                            textField.setMinSize(40, 40);
                            textField.setMaxSize(40, 40);
                            int r1 = r;
                            int c1 = c;
    
                            Platform.runLater(() -> gridPane.add(textField, c1, r1));
    
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
    
                            count++;
                            updateProgress(count, total);
                        }
                    }
                    return null;
                }
            };
            task.setOnSucceeded(e -> progressBar.setProgress(0));
            task.progressProperty().addListener((obs, old, val) -> progressBar.setProgress(val.doubleValue()));
            new Thread(task).start();
        }
    }
    

    Related Task Javadoc