1

I'm running into a problem where I have an application that needs around 40 unique textfields (along with their associated label).

However, the problem I'm running into is that I can't think of a clean way to set this up AND get their fx:id fields in the controller.

If I create all the textfields and label in a HBox in FXML, then I don't have a way to get all the fx:id from the controller and so currently I just copy and paste the id from the FXML to the controller and then set up code to map the Text Field to their id. This sounds stupid as it will never scale and also means if I change a fx:id on the FXML, then I must change it in the controller as well.

Is there a better way to do this?

cheunste
  • 13
  • 4
  • What kind of data is this? Would you be better off using a table? – Mick Mnemonic Aug 27 '18 at 22:54
  • 1
    Create the `TextFields` in your code using a loop. – SedJ601 Aug 28 '18 at 00:24
  • 1
    I concur with Sedrick. The only scalable approach is to create by code (in controller) instead of FXML. The fact is, if you ever need to add anything, you will still need to manually add `Label` and `TextField` into your FXML - so the claim of adding `@FXML ...` in your controller being messy is no longer valid. – Jai Aug 28 '18 at 01:52
  • 1
    To add to what Sedrick and Jai said - this may be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). Creating the fields in a loop sounds like the right approach, but it may help if you describe what the original problem/goal is. – Itai Aug 28 '18 at 06:25
  • Using the `initialize` method to create the code should be preferred which allows you to use a loop and store the nodes in some data structure. You could also create a `List` in the fxml and inject it to the controller (see https://stackoverflow.com/a/34470630/2991525). Afaik you won't be able to open such a fxml in SceneBuilder however... – fabian Aug 28 '18 at 08:00

1 Answers1

2

While your question does not make it really clear what your end goal is, some other users suggested using a loop in your Java code to create the TextFields.

I agree, since what you describe is probably not best suited for FXML as it is much simpler to accomplish using Java code.

The sample below uses a custom data model object called TextInput that contains both the TextField and its associated Label.

The code itself is also commented with additional insight.

THE CODE

Main.java - Simply used to start the application and show the Stage:

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 {

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

    @Override
    public void start(Stage primaryStage) {

        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("Layout.fxml"));
            loader.setController(new Controller());

            primaryStage.setScene(new Scene(loader.load()));
            primaryStage.setTitle("Dynamic TextFields Example");
            primaryStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

Layout.fxml - a simple GUI layout containing the GridPane used to display the TextInput data:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
<VBox alignment="TOP_CENTER" prefHeight="400.0" prefWidth="300.0" spacing="10.0" xmlns="http://javafx.com/javafx/9.0.1"
      xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
            <content>
                <GridPane fx:id="gridPane" hgap="10.0" vgap="5.0">
                    <columnConstraints>
                        <ColumnConstraints hgrow="NEVER" minWidth="-Infinity"/>
                        <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
                    </columnConstraints>
                    <rowConstraints>
                        <RowConstraints minHeight="-Infinity" prefHeight="30.0" vgrow="NEVER"/>
                    </rowConstraints>
                </GridPane>
            </content>
            <padding>
                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
            </padding>
        </ScrollPane>
        <Button fx:id="btnGetInputText" mnemonicParsing="false" text="Get Input 4 Text"/>
    </children>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</VBox>

Controller.java - the controller for the FXML layout

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

import java.util.ArrayList;
import java.util.List;

public class Controller {

    @FXML
    private GridPane gridPane;
    @FXML
    private Button btnGetInputText;

    // List of new TextInput objects
    List<TextInput> textInputs = new ArrayList<>();

    @FXML
    private void initialize() {

        // Here we will generate 40 new TextInput objects.
        for (int i = 0; i < 40; i++) {
            TextInput input = new TextInput(
                    "Input " + i,
                    new Label("Input " + i + ":"),
                    new TextField()
            );

            // Now, add the new TextInput Label and TextField to the GridPane
            gridPane.add(input.getLabel(), 0, i);
            gridPane.add(input.getTextField(), 1, i);

            // Finally, add the input to the list so they can be retrieved later using the input's Name
            textInputs.add(input);
        }

        // Use the button to print out the text from Input #4
        btnGetInputText.setOnAction(e -> {
            System.out.println("Input #4: " + getInputTextByName("Input 4"));
        });
    }

    /**
     * Helper method to get the value of a specific TextField
     */
    private String getInputTextByName(String name) {

        // Loop through the list of TextInput objects and get the one with the desired name
        for (TextInput input :
                textInputs) {
            if (input.getName().equalsIgnoreCase(name)) {
                return input.getTextField().getText();
            }
        }

        // If not found, return null (or empty String, if desired)
        return null;

    }
}

/**
 * Custom data structure to hold both the Label and TextFields for your inputs. There is also a Name field that can
 * be any type you wish, but provides a way of locating the right TextField when you need to. This could just as
 * easily be done with an ID or searching label.getText(), but I'm using a separate field in this sample for simplicity.
 */
class TextInput {

    private final String name;
    private final Label label;
    private final TextField textField;

    public TextInput(String name, Label label, TextField textField) {
        this.name = name;
        this.label = label;
        this.textField = textField;
    }

    public String getName() {
        return name;
    }

    public Label getLabel() {
        return label;
    }

    public TextField getTextField() {
        return textField;
    }
}
Zephyr
  • 9,885
  • 4
  • 28
  • 63