Implement Spinner in TableView to show/edit value

2019-08-20 02:29发布

问题:

I have an ObservableList of custom objects RecipeObject_Fermentable, whose properties I am displaying to the user through a TableView. For the most part it works well; when I populate the ObservableList with a new item, the TableView displays its contents.

Using the .setCellValueFactory() method for each column allows me to display the simple objects (Strings and Doubles) as text in my TableView very easily. For instance, I can access property 'name' of type String with...

private TableColumn<RecipeObject_Fermentable, String> tableColumn_rf_name;
tableColumn_rf_name.setCellValueFactory(new PropertyValueFactory<>("name"));

The problem is, I want to show the 'weight' property (of type Double) inside a Spinner for column tableColumn_rf_weight (and have it display units and be editable by text, but that’s not the main issue), and I cannot work out how I can actually do this, and what is best practise.

I have tried to come up with a solution through other people’s posts, but I cannot get it to work. I attribute this partly to not understanding what the .setCellFactory() and .setCellValueFactory() methods for each TableColumn actually are and do, and how they link with the properties of the custom objects in my ObservableList. If someone could briefly explain this, or point to something that does, I would be grateful. [EDIT: I DID try researching this but I didn't find any full explanation that I could grasp from my current understanding of Java and JavaFX]

So how can I get the TableView to display a Spinner for each data entry of that column?

The following code snippets are cut down to show only the useful information. Let me know if i've missed anything.

The custom object

public class RecipeObject_Fermentable {

// Object properties
private String name;            // Displayed and referenced name of the fermentable
private Double srm;             // Colour of fermentable in SRM
private Double pkgl;            // Specific gravity points per kg per liter
private Double weight;          // Weight in kilograms
private Double percent;         // Total percent to grain bill as percentage
private BooleanProperty lateadd;// Late addition toggle

// Constructor
public RecipeObject_Fermentable(String name, Double weight, Double srm, Double pkgl, Double contribution, boolean lateadd) {
    this.name = name;
    this.srm = srm;
    this.pkgl = pkgl;
    this.weight = weight;
    this.percent = contribution;
    this.lateadd = new SimpleBooleanProperty(lateadd);
}

// Constructor from fermentable object
public RecipeObject_Fermentable(Fermentable f) {
    this.name = f.getName();
    this.srm = f.getSrm();
    this.pkgl = f.getPkgl();
    if (f.getType().equals(FermType.GRAIN)) {
        if (f.getSubtype().equals(FermSubtype.BASE_MALT)) {
            this.weight = Double.valueOf(5);
        } else {
            this.weight = Double.valueOf(1);
        }
    } else {
        this.weight = Double.valueOf(0);
    }
    this.percent = Double.valueOf(0);
    this.lateadd = new SimpleBooleanProperty(false);
}

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

public Double getSrm() {
    return this.srm;
}

public Double getPkgl() {
    return this.pkgl;
}

public Double getWeight() {
    return this.weight;
}

public Double getContribution() {
    return this.percent;
}

public ObservableBooleanValue isLateadd() {
    return lateadd;
}

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

public void setSrm(Double srm) {
    this.srm = srm;
}

public void setPkgl(Double pkgl) {
    this.pkgl = pkgl;
}

public void setWeight(Double value) {
    this.weight = value;
}

public void setContribution(Double contribution) {
    this.percent = contribution;
}

public void setLateadd(Boolean checked) {
    this.lateadd.set(checked);
}
}

Other code in my controller

// Link and define FXML objects for tableView and its columns
@FXML
private TableView<RecipeObject_Fermentable> tableview_recipeFermentables;
@FXML
private TableColumn<RecipeObject_Fermentable, String> tableColumn_rf_name;
@FXML
private TableColumn<RecipeObject_Fermentable, Double> tableColumn_rf_weight;
@FXML
private TableColumn<RecipeObject_Fermentable, Double> tableColumn_rf_percent;
@FXML
private TableColumn<RecipeObject_Fermentable, Boolean> tableColumn_rf_lateAddition;

// Set up observable list of custom object
private ObservableList<RecipeObject_Fermentable> fermentables_recipe = 
FXCollections.observableArrayList()

// Set up table data
tableview_recipeFermentables.setItems(fermentables_recipe);
tableColumn_rf_name.setCellValueFactory(new PropertyValueFactory<>("name"));
// ---> tableColumn_rf_weight.setCellValueFactory();
// ---> tableColumn_rf_weight.setCellFactory();
tableColumn_rf_percent.setCellValueFactory(new PropertyValueFactory<>("percent"));
tableColumn_rf_lateAddition.setCellValueFactory(p->p.getValue().isLateadd());
tableColumn_rf_lateAddition.setCellFactory(CheckBoxTableCell.forTableColumn( tableColumn_rf_lateAddition));

Thanks in advance.

回答1:

You need a custom TableCell. Additionally there should be a way to pass the data back to the item. This is best done by using a DoubleProperty but in your case you could work with JavaBeanDoubleProperty, if null values are not allowed).

final JavaBeanDoublePropertyBuilder weightBuilder
                   = JavaBeanDoublePropertyBuilder.create()
                                                  .beanClass(RecipeObject_Fermentable.class)
                                                  .name("weight");
tableColumn_rf_weight.setCellValueFactory(cd -> weightBuilder.bean(cd.getValue()).build());
public class DoubleSpinnerCell<T> extends TableCell<T, Number> {
    private final Spinner<Double> spinner = new Spinner​(0, Double.MAX_VALUE, 0, 0.01);
    private boolean ignoreUpdate; // flag preventing updates triggered from ui/initialisation

    {
        spinner.valueProperty().addListener((o, oldValue, newValue) -> {
            if (!ignoreUpdate) {
                ignoreUpdate = true;
                WritableValue<Number> property = (WritableValue<Number>) getTableColumn().getCellObservableValue((T) getTableRow().getItem());
                property.setValue(newValue);
                ignoreUpdate = false;
            }
        });
    }


    @Override
    protected void updateItem(Number item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null) {
            setGraphic(null);
        } else {
            ignoreUpdate = true;
            spinner.getValueFactory().setValue(item.doubleValue());
            setGraphic(spinner);
            ignoreUpdate = false;
        }
    }
}
tableColumn_rf_weight.setCellFactory(c -> new DoubleSpinnerCell<RecipeObject_Fermentable>());

This requires the weight to be set to a non-null value though. (I recommend using primitive double instead of Double.)

Edit

You also need to change the item type of the column to Number:

@FXML
private TableColumn<RecipeObject_Fermentable, Number> tableColumn_rf_weight;