javajavafxtextfieldgridpane

Insert composite object (GridPane with TextFields) into ScrolPane


I have a class that builds a grid with an array of TextFields using GridPane. I need to insert this grid into a ScrollPane that only accepts Node in the setContent() method. So I extend this class from GridPane. The Grid class is instantiated and set in the ScrollPane by the onMnuItemNewAction method of the MainViewController.java class, but the grid is not shown. Thanks for your help.

MainView.fxml

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

<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>

<BorderPane prefHeight="277.0" prefWidth="495.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="br.com.ablogic.crossword.MainViewController">
    <top>
       <VBox prefWidth="100.0" BorderPane.alignment="CENTER">
         <children>
            <MenuBar fx:id="mnuBar" prefHeight="25.0" prefWidth="360.0">
              <menus>
                <Menu mnemonicParsing="false" text="File">
                  <items>
                    <MenuItem fx:id="mnuItemNew" mnemonicParsing="false" onAction="#onMnuItemNewAction" text="New grid" />
                  </items>
                </Menu>
              </menus>
            </MenuBar>
         </children>
      </VBox>
   </top>
   <center>
      <ScrollPane fx:id="scpGrid" fitToHeight="true" fitToWidth="true" pannable="true" style="-fx-background-color: #dbbb92; -fx-background: #dbbb92;" BorderPane.alignment="CENTER" />
   </center>
</BorderPane>

Main.java

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

public class Main extends Application {
    @Override
    public void start(Stage stage) throws IOException {

        FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("MainView.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 800, 600);
        stage.setTitle("Grid Demo");
        stage.setScene(scene);
        stage.show();
    }

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

}

MainViewController.java (the calling method)

import javafx.geometry.Pos;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;

public class MainViewController implements Initializable {

    @FXML
    private MenuItem mnuItemNew;

    @FXML
    private ScrollPane scpGrid;

    @FXML
    public void onMnuItemNewAction() {
        int cols = 10;
        int rows = 10;
        int horizontalGap = 1;
        int verticalGap = 1;
        int fieldHorizontalSize = 40;
        int fieldVerticalSize = 40;
        var newGrid = new Grid(cols, rows, horizontalGap, verticalGap, fieldHorizontalSize, fieldVerticalSize);
        scpGrid.setContent(newGrid);
        newGrid.setAlignment(Pos.CENTER);
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {

    }

}

Grid.java

import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import java.net.URL;
import java.util.ResourceBundle;

public class Grid extends GridPane implements Initializable {
    private final int totalColumnFields;
    private final int totalRowFields;
    private final int horizontalGap;
    private final int verticalGap;
    private final int fieldHorizontalSize;
    private final int fieldVerticalSize;
        
    public Grid(int totalColumnFields, int totalRowFields, int horizontalGap, int verticalGap, int fieldHorizontalSize, int fieldVerticalSize) {
        this.totalColumnFields = totalColumnFields;
        this.totalRowFields = totalRowFields;
        this.horizontalGap = horizontalGap;
        this.verticalGap = verticalGap;
        this.fieldHorizontalSize = fieldHorizontalSize;
        this.fieldVerticalSize = fieldVerticalSize;
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {

        this.setHgap(horizontalGap);
        this.setVgap(verticalGap);
        TextField[][] arrayLetterField = new TextField[totalColumnFields][totalRowFields];

        for (int row = 0; row < totalRowFields; row++) {
            for (int col = 0; col < totalColumnFields; col++) {
                arrayLetterField[col][row] = new TextField();
                arrayLetterField[col][row].setMinSize(fieldHorizontalSize, fieldVerticalSize);
                arrayLetterField[col][row].setMaxSize(fieldHorizontalSize, fieldVerticalSize );
                this.add(arrayLetterField[col][row], col, row);
            }
        }            
    }    
}

Solution

  • [TLDR]: The initialize(...) method in Grid is never called, so the text fields are never created and added to the grid pane. Consequently, even though the grid pane is displayed, there is nothing in it and so nothing is visible.


    The Initializable interface and its corresponding void initialize(URL, ResourceBundle) method are intended for use by controller classes acting as controllers for FXML documents. When the FXMLLoader loads an FXML file which specifies a class in its fx:controller attribute, the FXMLLoader instantiates that class and then invokes the initialize(...) method on it.1

    Your Grid class is not a controller class for any FXML document. It is not instantiated by an FXMLLoader (you instantiate it directly by calling var newGrid = new Grid(...) in the MainViewController class) and so the initialize(...) method is not automatically invoked for you at any point.

    Consequently, the initialize() method in Grid is never called, so the text fields are never created and never added to the grid pane. So the grid you add to the scroll pane is empty, and nothing is visible.

    There is no need for the Grid class to implement Initializable, since it is not associated with an FXML file. The code in the initialize() method is code you want to be executed when a Grid is created, so move it to the constructor:

    import javafx.scene.control.TextField;
    import javafx.scene.layout.GridPane;
    
    public class Grid extends GridPane {
        private final int totalColumnFields;
        private final int totalRowFields;
        private final int horizontalGap;
        private final int verticalGap;
        private final int fieldHorizontalSize;
        private final int fieldVerticalSize;
    
        public Grid(int totalColumnFields, int totalRowFields, int horizontalGap, int verticalGap, int fieldHorizontalSize, int fieldVerticalSize) {
            this.totalColumnFields = totalColumnFields;
            this.totalRowFields = totalRowFields;
            this.horizontalGap = horizontalGap;
            this.verticalGap = verticalGap;
            this.fieldHorizontalSize = fieldHorizontalSize;
            this.fieldVerticalSize = fieldVerticalSize;
            this.setHgap(horizontalGap);
            this.setVgap(verticalGap);
            TextField[][] arrayLetterField = new TextField[totalColumnFields][totalRowFields];
    
            for (int row = 0; row < totalRowFields; row++) {
                for (int col = 0; col < totalColumnFields; col++) {
                    arrayLetterField[col][row] = new TextField();
                    arrayLetterField[col][row].setMinSize(fieldHorizontalSize, fieldVerticalSize);
                    arrayLetterField[col][row].setMaxSize(fieldHorizontalSize, fieldVerticalSize );
                    this.add(arrayLetterField[col][row], col, row);
                }
            }
        }
    
    }
    

    This gives the desired result:

    Screenshot of the grid of text fields displayed in a window


    Other comments on your code:

    Note there is no need to replicate the values horizontalGap and verticalGap in your class, since these are already stored as the hgap and vgap properties inherited from GridPane. So you can reduce the size of your class a little with:

    public class Grid extends GridPane {
        private final int totalColumnFields;
        private final int totalRowFields;
        private final int fieldHorizontalSize;
        private final int fieldVerticalSize;
    
        public Grid(int totalColumnFields, int totalRowFields, int horizontalGap, int verticalGap, int fieldHorizontalSize, int fieldVerticalSize) {
            this.totalColumnFields = totalColumnFields;
            this.totalRowFields = totalRowFields;
            this.fieldHorizontalSize = fieldHorizontalSize;
            this.fieldVerticalSize = fieldVerticalSize;
            this.setHgap(horizontalGap);
            this.setVgap(verticalGap);
            TextField[][] arrayLetterField = new TextField[totalColumnFields][totalRowFields];
    
            for (int row = 0; row < totalRowFields; row++) {
                for (int col = 0; col < totalColumnFields; col++) {
                    arrayLetterField[col][row] = new TextField();
                    arrayLetterField[col][row].setMinSize(fieldHorizontalSize, fieldVerticalSize);
                    arrayLetterField[col][row].setMaxSize(fieldHorizontalSize, fieldVerticalSize );
                    this.add(arrayLetterField[col][row], col, row);
                }
            }
        }
    }
    

    If you need to reference those values at any time, you can do so with getHgap() and getVgap().

    I also recommend not subclassing GridPane here. You should reserve subclassing existing classes when you are adding functionality to them. Here you are really only configuring an instance of the existing class. Subclassing GridPane also exposes the internal details of the layout strategy to the rest of your application, potentially making it much more difficult to change the layout later (e.g. to a TilePane or some other strategy) if you wanted to do so. I recommend "favoring aggregation over inheritance" here and just giving access to an aggregated GridPane without exposing details of which layout you are using:

    import javafx.scene.Node;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.GridPane;
    
    public class Grid {
        private final int totalColumnFields;
        private final int totalRowFields;
        private final int fieldHorizontalSize;
        private final int fieldVerticalSize;
    
        private final GridPane grid;
    
        public Grid(int totalColumnFields, int totalRowFields, int horizontalGap, int verticalGap, int fieldHorizontalSize, int fieldVerticalSize) {
            this.totalColumnFields = totalColumnFields;
            this.totalRowFields = totalRowFields;
            this.fieldHorizontalSize = fieldHorizontalSize;
            this.fieldVerticalSize = fieldVerticalSize;
    
            grid = new GridPane();
            grid.setHgap(horizontalGap);
            grid.setVgap(verticalGap);
            TextField[][] arrayLetterField = new TextField[totalColumnFields][totalRowFields];
    
            for (int row = 0; row < totalRowFields; row++) {
                for (int col = 0; col < totalColumnFields; col++) {
                    arrayLetterField[col][row] = new TextField();
                    arrayLetterField[col][row].setMinSize(fieldHorizontalSize, fieldVerticalSize);
                    arrayLetterField[col][row].setMaxSize(fieldHorizontalSize, fieldVerticalSize );
                    grid.add(arrayLetterField[col][row], col, row);
                }
            }
        }
    
        public Node getView() {
            return grid;
        }
    
    }
    

    And then the slight corresponding change to the client code:

        @FXML
        public void onMnuItemNewAction() {
            int cols = 10;
            int rows = 10;
            int horizontalGap = 1;
            int verticalGap = 1;
            int fieldHorizontalSize = 40;
            int fieldVerticalSize = 40;
            var newGrid = new Grid(cols, rows, horizontalGap, verticalGap, fieldHorizontalSize, fieldVerticalSize);
            var gridView = newGrid.getView();
            scpGrid.setContent(gridView);
            gridView.setStyle("-fx-alignment: center;");
        }
    

    (1) Note that as of JavaFX 2.1 the Initializable interface is essentially redundant. From the documentation:

    NOTE This interface has been superseded by automatic injection of location and resources properties into the controller. FXMLLoader will now automatically call any suitably annotated no-arg initialize() method defined by the controller. It is recommended that the injection approach be used whenever possible.

    This means that even a controller class for an FXML document does not need to implement Initializable. If you need to perform initialization after the @FXML-annotated fields have been injected, just define a no-arg initialize() method to do so. You can even make this method private if you annotate it @FXML, better enforcing encapsulation. If you need access to the location or resources properties, those can be injected in the same way as the elements of the FXML file. For example:

    public class MainViewController {
    
        @FXML
        private MenuItem mnuItemNew;
    
        @FXML
        private ScrollPane scpGrid;
        
        @FXML
        // Can omit this field if it is not needed
        // (It is very rare to need this.)
        private URL location;
    
        @FXML
        private void onMnuItemNewAction() {
            int cols = 10;
            int rows = 10;
            int horizontalGap = 1;
            int verticalGap = 1;
            int fieldHorizontalSize = 40;
            int fieldVerticalSize = 40;
            var newGrid = new Grid(cols, rows, horizontalGap, verticalGap, fieldHorizontalSize, fieldVerticalSize);
            var gridView = newGrid.getView();
            scpGrid.setContent(gridView);
            gridView.setStyle("-fx-alignment: center;");
        }
    
        @FXML
        private void initialize() {
            // Any required initialization code here
            // If no intialization needed, this method can be omitted
        }
    
    }