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);
}
}
}
}
[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:
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
andresources
properties into the controller.FXMLLoader
will now automatically call any suitably annotated no-arginitialize()
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
}
}