ovi_b
ovi_b

Reputation: 169

How to dynamically add columns to TableView and update their content

I have the following TableView: enter image description here

Each row represents an Ingredient which have two properties, a name and a ArrayList of type Nutrient with the nutrients (name and weight) that composes the ingredient.

My goal is to update the weight property of each Nutrient in the Array List of each Ingredient by clicking on the corresponding cell and entering the new value (same behavior as the Name column).

I suspect that I need to set for each column a custom CellValueFactory and a CellFactory, but I'm new to JavaFx and I don't know exactly ho do do it.

This is the Controller class where most of the magic (should) happen:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;

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


public class Controller implements Initializable {
    @FXML private TableView<Ingredient> tableView;
    @FXML private TableColumn<Ingredient, String> nameColumn;

    private final ObservableList<Ingredient> ingredientsList = FXCollections.observableArrayList();
    private final ArrayList<Nutrient> nutrientsList = new ArrayList<>();

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        
        // add ingredients and nutrients
        ingredientsList.add(new Ingredient("ingredient 1"));
        ingredientsList.add(new Ingredient("ingredient 2"));
        ingredientsList.add(new Ingredient("ingredient 3"));
        nutrientsList.add(new Nutrient("proteins", 0.0));
        nutrientsList.add(new Nutrient("fats", 0.0));
        nutrientsList.add(new Nutrient("carbs", 0.0));

        // add the list of nutrients to each ingredient
        ingredientsList.forEach(nutrient -> nutrient.setNutrients(nutrientsList));

        ingredientsList.forEach(System.out::println);

        //add ingredients (rows) to the table
        tableView.setItems(ingredientsList);

        // make name column editable
        tableView.setEditable(true);
        nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
        nameColumn.setCellValueFactory(new PropertyValueFactory<Ingredient, String>("name"));

        for (int i = 0; i < nutrientsList.size(); i++) {
            // add a column for each ingredient
            TableColumn<Ingredient, Nutrient> column = new TableColumn<>(nutrientsList.get(i).getName());
            tableView.getColumns().add(column);

            Ingredient selectedIngredient = tableView.getSelectionModel().getSelectedItem();

            int currentNutrientIndex = i;
            column.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Ingredient, Nutrient>>() {
                @Override
                public void handle(TableColumn.CellEditEvent<Ingredient, Nutrient> ingredientNutrientCellEditEvent) {
                    selectedIngredient.updateNutrientValue(currentNutrientIndex, ingredientNutrientCellEditEvent.getNewValue().getWeight());
                }
            });

            // column.setCellFactory and column.setCellValueFactory here???
        }
    }

    /**
     * This method allows the user to double click on an cell and update the ingredient name.
     * It is linked to the UI by setting the attribute onEditCommit of the Column the the name of the method in the fxml file
     */
    public void changeIngredientNameCellEvent(TableColumn.CellEditEvent editedCell){
        Ingredient selectedIngredient = tableView.getSelectionModel().getSelectedItem();
        selectedIngredient.setName(editedCell.getNewValue().toString());
        ingredientsList.forEach(System.out::println);
    }

}

sample.fxml

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

<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>

<TableView fx:id="tableView" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
  <columns>
    <TableColumn fx:id="nameColumn" onEditCommit="#changeIngredientNameCellEvent" prefWidth="75.0" text="Name" />
  </columns>
</TableView>

Ingredient class:

package sample;
import javafx.beans.property.SimpleStringProperty;
import java.util.ArrayList;

public class Ingredient {
    private SimpleStringProperty name;
    private ArrayList<Nutrient> nutrients;

    public Ingredient(String name) {
        this.name = new SimpleStringProperty(name);
    }

    public void setNutrients(ArrayList<Nutrient> list){
        this.nutrients = list;
    }

    public void updateNutrientValue(int nutrientIndex, double newValue){
        nutrients.get(nutrientIndex).setWeight(newValue);
    }

    public Nutrient getNutrientAt(int index){
        return nutrients.get(index);
    }

    public String getName() {
        return name.get();
    }

    public SimpleStringProperty nameProperty() {
        return name;
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public String toString(){
        return "name = '" + name.get() + "' nutrients = " + nutrients;
    }
}

Nutrient class:

package sample;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;

public class Nutrient {
    private final SimpleStringProperty name;
    private final SimpleDoubleProperty weight;

    public Nutrient(String name, double weight) {
        this.name = new SimpleStringProperty(name);
        this.weight = new SimpleDoubleProperty(weight);
    }

    public String getName() {
        return name.get();
    }

    public SimpleStringProperty nameProperty() {
        return name;
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public double getWeight() {
        return weight.get();
    }

    public SimpleDoubleProperty weightProperty() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight.set(weight);
    }

    public String toString(){
        return  name.get() + ", " + weight.get() ;
    }
}

Main class:

package sample;

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

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();
    }


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

Upvotes: 0

Views: 490

Answers (1)

James_D
James_D

Reputation: 209225

You can do

    for (int i = 0; i < nutrientsList.size(); i++) {
        // add a column for each ingredient
        TableColumn<Ingredient, Double> column = new TableColumn<>(nutrientsList.get(i).getName());
        tableView.getColumns().add(column);


        int currentNutrientIndex = i;
        

        // column.setCellFactory and column.setCellValueFactory here???

       column.setCellValueFactory(data -> 
           data.getValue().getNutrientAt(currentNutrientIndex).weightProperty().asObject());
       column.setCellFactory(tc -> new TextFieldTableCell<>(new DoubleStringConverter()));
    }

You may prefer to provide a custom StringConverter in place of the DoubleStringConverter to provide, e.g., localized parsing etc.

Note that the setup you have won't work correctly, but I assume this is just example code. You can't use the same list of Nutrients for different ingredients (or indeed the same Nutrient instances, even in different lists). You need to create new instances for each Ingredient.

A fully working example can be made using

private final ObservableList<Ingredient> ingredientsList = FXCollections.observableArrayList();

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
    
    // add ingredients and nutrients
    ingredientsList.add(new Ingredient("ingredient 1"));
    ingredientsList.add(new Ingredient("ingredient 2"));
    ingredientsList.add(new Ingredient("ingredient 3"));
    
    List<String> nutrientNames = List.of("proteins", "fats", "carbs");
    
    // add a list of nutrients to each ingredient

    ingredientsList.forEach(ingredient -> ingredient.setNutrients(
        nutrientNames.stream()
            .map(name -> new Nutrient(name, 0.0))
            .collect(Collectors.toList())
        )
    );
    


    ingredientsList.forEach(System.out::println);

    //add ingredients (rows) to the table
    tableView.setItems(ingredientsList);

    // make name column editable
    tableView.setEditable(true);
    nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
    

    for (int i = 0; i < nutrientNames.size(); i++) {
        // add a column for each ingredient
        TableColumn<Ingredient, Double> column = new TableColumn<>(nutrientNames.get(i));
        tableView.getColumns().add(column);

        Ingredient selectedIngredient = tableView.getSelectionModel().getSelectedItem();

        int currentNutrientIndex = i;
        

        // column.setCellFactory and column.setCellValueFactory here???

       column.setCellValueFactory(data -> 
           data.getValue().getNutrientAt(currentNutrientIndex).weightProperty().asObject());
       column.setCellFactory(tc -> new TextFieldTableCell<>(new DoubleStringConverter()));
    }
}

with the additional change of changing the type of Ingredient.nutrients to List<Nutrient> (which is good practice anyway).

Upvotes: 4

Related Questions