'How to set a CellFactory with access to two specific fields
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);
}
}
Solution 1:[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.
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);
}
}
Solution 2:[2]
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);
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 |

