如何向 TableView 动态添加列并更新其内容

How to dynamically add columns to TableView and update their content

我有以下表格视图:

每一行代表一个成分,它有两个属性,一个名称和一个营养类型的数组列表,其中包含构成该成分的营养素(名称和重量)。

我的目标是通过点击相应的单元格并输入新值(相同Name column).

的行为

我怀疑我需要为每一列设置一个自定义的 CellValueFactory 和一个 CellFactory,但我是 JavaFx 的新手,我不知道该怎么做。

这是 控制器 class 大多数魔术(应该)发生的地方:

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>

成分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;
    }
}

营养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() ;
    }
}

主要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);
    }
}

你可以做到

    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()));
    }

您可能更愿意提供自定义 StringConverter 来代替 DoubleStringConverter 以提供例如本地化解析等

请注意,您的设置将无法正常工作,但我认为这只是示例代码。您不能对不同的成分使用相同的 Nutrient 列表(或者实际上是相同的 Nutrient 实例,即使在不同的列表中)。您需要为每个 Ingredient.

创建新实例

可以使用

制作一个完整的示例
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()));
    }
}

Ingredient.nutrients 的类型更改为 List<Nutrient>(无论如何这是一个很好的做法)。