0

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.

Joe
  • 45
  • 1
  • 5
  • 2
    unrelated: please learn java naming conventions and stick to them – kleopatra Apr 23 '18 at 10:01
  • Could you list the terms which I need to correct/edit? – Joe Apr 23 '18 at 10:47
  • hmm ... don't understand why that's needed: simply search for "java naming conventions" and compare your code against the rules .. you'll see at a glance which don't fit ;) – kleopatra Apr 23 '18 at 11:21
  • All I can see is my egregious use of underscores in the names, but I felt the need to use them since so many of the elements (there are a lot you don't see) are similar yet distinct. Are there any other problems that I've missed? – Joe Apr 23 '18 at 11:56
  • don't know - didn't read further when I saw them ;) Well, code is for communication (besides functionality, of course) - it doesn't matter so much what you feel when starting with a language but how others can easily, quickly grasp what you coded. Those underscores are very disruptive .. sooner or later you will be forced to apply conventions, the sooner the better for all :) – kleopatra Apr 23 '18 at 12:01
  • https://en.wikipedia.org/wiki/Naming_convention_(programming)#Java – James_D Apr 23 '18 at 12:23
  • On a side note, I'm not sure why my question has two down votes. I believe I posted a legitimate question and detailed it sufficiently. How could I improve it? – Joe Apr 24 '18 at 07:33

1 Answers1

0

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;
Community
  • 1
  • 1
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thank you for providing such a detailed answer and example. I'll test it out asap. – Joe Apr 23 '18 at 11:19
  • I've tried implementing your solution and It's almost accepted by the compiler, but the line `tableColumn_rf_weight.setCellValueFactory(cd -> weightBuilder.bean(cd.getValue()).build());` gives _"JavaBeanDoubleProperty cannot be converted to ObservableValue"_. The next line `tableColumn_rf_weight.setCellFactory(c -> new DoubleSpinnerCell<>());` gives _"cannot infer type arguments for DoubleSpinnerCell<>"_ and i'm not sure what arguments to give it. I apologise if these are trivial questions but i'm not very familiar with the `.setCellFactory()` and `.setCellValueFactory()` methods. – Joe Apr 23 '18 at 14:45
  • @Joe I completely overlooked that you use `Double` as type parameter for the `TableColumn`. It needs to be changed to `Number`, since double properties implement `ObservableValue`. – fabian Apr 23 '18 at 14:51
  • It works! But trying to increment/decrement the value displayed by the spinner gives me a `NumberFormatException` which is trickling down through various standard java libraries. I'll have to try and figure out why this is happening... any ideas? – Joe Apr 23 '18 at 15:38
  • @Joe OK seems like `Spinner` has a issue with `Double.POSITIVE_INFINITY`. (Fixed this and some other errors.) – fabian Apr 23 '18 at 21:26
  • WORKS BEAUTIFULLY. Thanks for all the time and effort you put into this answer! very much appreciated. – Joe Apr 24 '18 at 07:30