-1

I have two variables inside a class: the amount and the maximum possible value for that amount.

I need a TableCell capable of receiving any number, as long as it doesn't surpass the maximum value present in each item. I also need both values for other stuff, such as implementing a custom StringConverter, a custom TextField, etc.

However, I can't access the item value inside setCellFactory() !!!

Here it is more or less what I have done:

public class Item {
    
    private SimpleIntegerProperty amount;
    private SimpleIntegerProperty max; // maximum
    
    Item(int max,int amount){
        this.max = new SimpleIntegerProperty(max);
        this.amount = new SimpleIntegerProperty(amount);
    }
    
    
    public static TableView<Item> getTable(){
        TableColumn<Item,Number> colAmnt = new TableColumn<Item, Number>("column");
        colAmnt.setCellValueFactory(c -> c.getValue().amount); // I CAN access the item object (c.getValue()) here!
        colAmnt.setCellFactory(d->{
            // But I can't access it from here!!!
            return new MaxNumberTableCell();
        });
        
        // unimportant stuff
        colAmnt.setOnEditCommit((TableColumn.CellEditEvent<Item, Number> t) -> {
            Item o = ((Item) t.getTableView().getItems().get(
                    t.getTablePosition().getRow()));
            o.amount.setValue(t.getNewValue().intValue());
        });
        
        TableView<Item> t = new TableView<Item>();
        t.getColumns().add(colAmnt);
        t.setEditable(true);
        return t;
    }
    public int getMax() {
        return max.get();
    }
}

I have tried to get the value by extending TableCell<S,T> class as shown below, but I receive NullPointerException.

class MaxNumberTableCell extends TableCell<Item,Number>{
    public MaxNumberTableCell(){
        super();
        // Can't access from here either
        int a = this.getTableRow().getItem().getMax(); // NullPointerException
        // tldr: I need the value of a !
    }
}

Edit: If you want a minimal work example, here is the Main

public class Main extends Application{
    @Override
    public void start(Stage stage) {
        TableView<Item> t = Item.getTable();
        ObservableList<Item> ol = FXCollections.observableArrayList();
        ol.add(new Item(5,1));
        t.setItems(ol);
        
        Scene scene = new Scene(t);
        stage.setScene(scene);
        stage.show();
}
    public static void main(String[] args) {
        launch(args);
    }
}
  • [mcve] please.. and do not use static scope – kleopatra Mar 15 '22 at 09:05
  • as to your first problem: most probably, you meed some further understanding of java language basics, in particular scoping/access rules. As to your second problem: what you are trying is utterly wrong - work through a tutorial on how to use TableView, in particular on how to implement custom cells (note: they are re-used during their lifetime!) – kleopatra Mar 15 '22 at 12:21
  • @kleopatra I posted the Main class so you can reproduce if you want. I don't understand what my "two problems" are. I have only one: I need to access max from inside TableCell – João Schmidt Mar 15 '22 at 16:07
  • please read my comments thoroughly (you fixed only the first part of my first comment, thanks. Three more parts to go :) – kleopatra Mar 15 '22 at 16:27
  • @kleopatra Once again, I failed to visualize what are you talking about, I have an working solution and I didn't have "3 other parts to fix". You could answer so I could understand better – João Schmidt Mar 15 '22 at 17:45

2 Answers2

1

It looks like you're building an editable property sheet using TableView. As each Item in the model may have different bounds, you need access to the item's max attribute while editing in the TableCell. Your current approach seems awkward, duplicating existing properties in the model. Instead, aggregate the value with its bounds as a BoundedValue and let the model contain a corresponding property:

private final ObjectProperty<BoundedValue> quantity;

In the example below, the amount and fraction columns each have the same cell value factory: quantityProperty(). In contrast, each column has a custom cell factory that displays the aspect of interest: an editable amount or a non-editable fraction displayed as a percentage. In either case, the factory's TableCell has access to any needed fields of the BoundedValue.

Going forward, you'll want to handle any NumberFormatException thrown by Integer.valueOf() or consider a TextFormatter, seen here. This related example illustrates using color to highlight cell state, and this example examines using a Spinner as a TableCell.

image

import java.text.NumberFormat;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;

/**
 * @see https://stackoverflow.com/a/71542867/230513
 * @see https://stackoverflow.com/q/45200797/230513
 * @see https://stackoverflow.com/q/41702771/230513
 */
public class BoundedValueTest extends Application {

    private static class BoundedValue {

        private final Integer value;
        private final Integer min;
        private final Integer max;

        public BoundedValue(int value, int min, int max) {
            if (value < min || value > max) {
                throw new IllegalArgumentException("Value out of bounds.");
            }
            this.value = value;
            this.min = min;
            this.max = max;
        }
    }

    private static class Item {

        private final ObjectProperty<BoundedValue> quantity;

        Item(int quantity, int min, int max) {
            this.quantity = new SimpleObjectProperty<>(
                new BoundedValue(quantity, min, max));
        }

        private ObjectProperty<BoundedValue> quantityProperty() {
            return quantity;
        }
    }

    private TableCell<Item, BoundedValue> createQuantityCell() {
        TextFieldTableCell<Item, BoundedValue> cell = new TextFieldTableCell<>();
        cell.setConverter(new StringConverter<BoundedValue>() {
            @Override
            public BoundedValue fromString(String string) {
                int min = cell.getItem().min;
                int max = cell.getItem().max;
                //TODO NumberFormatException vs TextFormatter  
                int value = Integer.valueOf(string);
                if (value < min || value > max) {
                    return cell.getItem();
                } else {
                    return new BoundedValue(value, min, max);
                }
            }

            @Override
            public String toString(BoundedValue item) {
                return item == null ? "" : item.value.toString();
            }
        });
        return cell;
    }

    private TableCell<Item, BoundedValue> createFractionCell() {
        NumberFormat f = NumberFormat.getPercentInstance();
        TextFieldTableCell<Item, BoundedValue> cell = new TextFieldTableCell<>(
            new StringConverter<BoundedValue>() {
            @Override
            public String toString(BoundedValue t) {
                return f.format((double) t.value / t.max);
            }

            @Override
            public BoundedValue fromString(String string) {
                return null;
            }
        });
        return cell;
    }

    @Override
    public void start(Stage stage) {
        ObservableList<Item> list = FXCollections.observableArrayList();
        for (int i = 0; i < 10; i++) {
            list.add(new Item(i, 0, 10));
        }
        TableView<Item> table = new TableView<>(list);
        TableColumn<Item, BoundedValue> amountCol = new TableColumn<>("Amount");
        amountCol.setCellValueFactory(cd -> cd.getValue().quantityProperty());
        amountCol.setCellFactory(tc -> createQuantityCell());
        table.getColumns().add(amountCol);
        TableColumn<Item, BoundedValue> fractionCol = new TableColumn<>("Fraction");
        fractionCol.setCellValueFactory(cd -> cd.getValue().quantityProperty());
        fractionCol.setCellFactory(tc -> createFractionCell());
        fractionCol.setEditable(false);
        table.getColumns().add(fractionCol);
        table.setEditable(true);
        Scene scene = new Scene(table);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
0

Anyway, turns out I can make an extra field and bind to amount and max. Although I think it's a bit ugly, it works perfectly.

I going to let this question open so maybe someone can come with a more elegant way, maybe without the extra variable.

public class Item {
    
    private SimpleIntegerProperty amount;
    private SimpleIntegerProperty max;
    private SimpleObjectProperty<Integer[]> fraction;
    
    // I have changed the order here
    Item(int amount,int max){
        this.max = new SimpleIntegerProperty(max);
        this.amount = new SimpleIntegerProperty(amount);
        // Extra variable
        this.fraction = new SimpleObjectProperty<Integer[]>();
        this.fraction.bind(Bindings.createObjectBinding(()->{
            return new Integer[] {this.amount.get(),this.max.get()};
        }, this.amount,this.max));
    }
    
    public static TableView<Item> getTable(){
        TableColumn<Item,Integer[]> colAmnt = new TableColumn<Item, Integer[]>("column");
        colAmnt.setCellValueFactory(c -> c.getValue().fraction);
        // setCellFactory with fraction variable
        colAmnt.setCellFactory(d->new MaxNumberTableCell());
        colAmnt.setOnEditCommit((TableColumn.CellEditEvent<Item, Integer[]> t) -> {
            Item o = ((Item) t.getTableView().getItems().get(
                    t.getTablePosition().getRow()));
            o.amount.setValue(t.getNewValue()[0]);
        });
        
        TableView<Item> t = new TableView<Item>();
        t.getColumns().add(colAmnt);
        t.setEditable(true);
        return t;
    }
    void setFraction(int amount, int max){
        this.max.set(max);
        this.amount.set(amount);
    }
}
class MaxNumberTableCell extends TableCell<Item,Integer[]>{
    @Override
    public void updateItem(Integer[] item,boolean empty) {
        super.updateItem(item, empty);
        if (item == null) {
            setText(null);
            setGraphic(null);
        } else {
            // I can acess both values and further modify if I want
            setText(item[0]+"/"+item[1]);
            setGraphic(null);
        }
    }
}