I have a tabelview that displays a list of appointees. Each appointe has a group assigned to it, the id of that group is saved in the appointe class.
I want to display a combobox inside a tablecell that displays the selected group and all other groups that exist. I can set the items of the combobox in the cell factory but i cant set the selected value of the respective appointee.
I have a method that returns the Group from the observable list when i provide it with the id. Thats means i need the id in the cellfactory but i didnt find a way to do this. I also need to display the name of the group and not the refernce to the clas. Is there a way to do this, or should i change my approach?
The Appointee class
public class Appointee {
private SimpleIntegerProperty id;
private SimpleStringProperty firstname;
private SimpleStringProperty lastname;
private SimpleIntegerProperty group;
private SimpleIntegerProperty assigned;
public Appointee(int id, String firstname, String lastname, int group, int assigned){
this.id = new SimpleIntegerProperty(id);
this.firstname = new SimpleStringProperty(firstname);
this.lastname = new SimpleStringProperty(lastname);
this.group = new SimpleIntegerProperty(group);
this.assigned = new SimpleIntegerProperty(assigned);
}
The Group class
public class Group {
private IntegerProperty id;
private StringProperty name;
private IntegerProperty members;
private IntegerProperty assigned;
public Group(int id, String name, int members, int assigned) {
this.id = new SimpleIntegerProperty(id);
this.name = new SimpleStringProperty(name);
this.members = new SimpleIntegerProperty(members);
this.assigned = new SimpleIntegerProperty(assigned);
}
The appointe table view
public AppointeeTableView() {
// define table view
this.setPrefHeight(800);
this.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
this.setItems(MainController.appointeeObervableList);
this.setEditable(true);
// define columns
...
TableColumn groupCol = new TableColumn("Group"); // group
groupCol.setCellFactory(col -> {
TableCell<Group, StringProperty> c = new TableCell<>();
final ComboBox<String> comboBox = new ComboBox(MainController.groupObservableList);
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
groupCol.setEditable(false);
...
}
Override the updateItem method of the TableCell to update the cell, make sure the new value is saved on a change of the TableCell value and use a cellValueFactory.
final Map<Integer, Group> groupById = ...
final ObservableList<Integer> groupIds = ...
TableColumn<Group, Number> groupCol = new TableColumn<>("Group");
groupCol.setCellValueFactory(cd -> cd.getValue().groupProperty());
class GroupCell extends ListCell<Integer> {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
Group group = groupById.get(item);
if (empty || group == null) {
setText("");
} else {
setText(group.getName());
}
}
}
groupCol.setCellFactory(col -> new TableCell<Group, Integer>() {
private final ComboBox<Integer> comboBox = new ComboBox<>(groupIds);
private final ChangeListener<Integer> listener = (o, oldValue, newValue) -> {
Group group = (Group) getTableView().getItems().get(getIndex());
group.setGroup(newValue);
};
{
comboBox.setCellFactory(lv -> new GroupCell());
comboBox.setButtonCell(new GroupCell());
}
#Override
protected void updateItem(Number item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
comboBox.valueProperty().removeListener(listener);
setGraphic(comboBox);
comboBox.setValue((Integer) item);
comboBox.valueProperty().addListener(listener);
}
}
});
It's a bit hard to tell from only some small code snippets, but my general recommendation when working with frontends is to distinguish between the model and the rendering on each level. This applies to JavaFX, Swing and Angular applications alike.
The appointee TableView should likely be TableView<Appointee>.
For the appointee.group property you have two options: either use Group or (e.g. when this would generate too many duplicate data when de-/serializing from/ to JSON) then use a business key. The first option is usually easier to implement and work with. With the second option you'll need some service / code to convert back to a Group and have to think about where/ at what level exactly you want to do the conversion.
Let's go on here with the second option as you currently have specified appointee.group to be an integer.
In this case the group column should be TableColum<Appointee, Integer>.
The group cell then should be TableCell<Appointee, Integer>.
So far we've only talked about the model, not about rendering except that we want to display the appointees in a table.
I recommend to do this also on the next level.
Don't use a ComboBox<String> for a groups comboBox but a ComboBox<Group>. String is how you want to render the group inside the comboBox but the Group is the model. Also ComboBox<Integer>, the type of the business key, is a bit misleading (as you want a Groups comboBox, not an integer comboBox) and limits the flexibility of your code.
Use the converting service / code I've mentioned when pre-selecting a value in the comboBox.
The group cell should have the type ListCell<Group> and in the updateItem method, which concerns about how to render a Group, you could e.g. use the name property to get the String representation.
Of course there are variations of this approach, but make sure that on each level you know what the model of the control is and what the renderer of the control is. Always design your code using the model and use the rendering types only at the lowest rendering level.
Related
I am trying to automatically update a JavaFX ListView when a change occurs on a Property located within an ObservableMap.
Below is my model, where I have a Project, containing a list of Seats, and each Seat in turn contains a Map of type <Layer, ObjectProperty<Category>>.
What I am trying to achieve is to bind an ui element to that ObjectProperty<Category> within the Map.
Here is the Model:
public class Seat {
private final DoubleProperty positionX;
private final DoubleProperty positionY;
private final MapProperty<Layer, ObjectProperty<Category>> categoryMap;
public Seat() {
this.positionX = new SimpleDoubleProperty();
this.positionY = new SimpleDoubleProperty();
this.categoryMap = new SimpleMapProperty(FXCollections.observableHashMap());
}
}
public class Project {
private ObservableList<Seat> seatList;
public Project() {
seatList = FXCollections.observableArrayList(
new Callback<Seat, Observable[]>() {
#Override
public Observable[] call(Seat seat) {
return new Observable[]{
seat.categoryMapProperty()
};
}
}
);
}
The UI element I want to bind is a ListView with a custom cell as follows:
public class CategoryCell extends ListCell<Category>{
private ToggleButton viewButton;
private Rectangle colorRect;
private Label name;
private Label count;
private GridPane pane;
public CategoryCell(ObservableList<Seat> seatList) {
super();
buildGui();
itemProperty().addListener((list, oldValue, newValue) -> {
if (newValue != null) {
//Bind color
colorRect.fillProperty().bind(newValue.colorProperty());
//Bind category name
name.textProperty().bind(newValue.nameProperty());
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
if (oldValue != null) {
name.textProperty().unbind();
count.textProperty().unbind();
colorRect.fillProperty().unbind();
}
});
}
private void buildGui() {
FontIcon hidden = new FontIcon("mdi-eye-off");
viewButton = new ToggleButton("");
viewButton.setGraphic(hidden);
viewButton.selectedProperty().addListener((observable,oldValue, newValue) -> {
Category category = itemProperty().get();
if (newValue == true) {
category.shownColorProperty().unbind();
category.setShownColor(Color.TRANSPARENT);
}else {
category.shownColorProperty().bind(category.colorProperty());
}
});
colorRect = new Rectangle(30,30);
name = new Label();
name.setMaxWidth(120);
pane = new GridPane();
count = new Label();
count.setPadding(new Insets(0,0,0,10));
ColumnConstraints nameCol = new ColumnConstraints();
nameCol.setHgrow( Priority.ALWAYS );
pane.getColumnConstraints().addAll(
new ColumnConstraints(40),
new ColumnConstraints(40),
nameCol,
new ColumnConstraints(40));
pane.addColumn(0, viewButton);
pane.addColumn(1, colorRect);
pane.addColumn(2, name );
pane.addColumn(3, count);
this.setText(null);
name.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount()==2) {
//launch the category editor TODO
}
}
});
}
The problem is that the code below is not triggered when I change the Category value of a CategoryProperty within the MapProperty of a Seat.
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
Any advice on how to achieve this?
===== Clarifications following James_D comment ====
1) About the model: I have actually thought and hesitated quite a bit about this. I want to allocate categories to seats in concert halls, and do this on multiple "layers/levels". Say for example a price "layer" where I could have four price tag categories, and "selling company" layer where I would have 3 companies, etc... In order to model this in my Seat class I have a Map<Layer, Category> which looks like a good choice as a seat should only be assigned to one unique category per layer. Then my Project class keeps track of Layers and their respective Categories, which is not really needed but handy to keep their user-specified display order.
2) Thank you for spotting that bug in the CategoryCell! The order of if (oldValue != null) and if (newValue != null) should indeed be reversed.
3) Now what I need to answer my initial question is a way to trigger a notification when the categoryProperty in the Map of the Seat class is modified.
Actually, just refreshing the listview whenever I make a change to my Map solves the issue, but it kinds of defeat the purpose of having a Observable property...
Answering myself now that I understand a little more.
1) How to bind to a property within a ObservableMap?
By using the valueAt() method of a MapProperty.
Instead of using ObservableMap<Layer, ObjectProperty<Category>>, use
MapProperty<Layer, Category>.
2) How to trigger a notification when the objectProperty in the ObservableMap is modified?
Since we are now using a MapProperty where the value is the object and not the property wrapping it, we can just use the addListener() method of the MapProperty.
I have a restaurant menu with dishes and categories implemented as a treeTableView in javaFX.
I want to make the the category rows appear different with CSS but I just can't find a way to filter them out and apply a class. Moving the images a bit to the left would also be nice. I also had no luck using a rowFactory. I've seen this answer but I don't understand it.
This is how I fill the table. I've left out the column- and cellfactories.
private void fillDishes(List<Dish> dishes){
root.getChildren().clear();
Map<String,TreeItem<Dish>> categoryMap = new HashMap<>();
for (Category c: allCats) {
TreeItem<Dish> newCat = new TreeItem<>(new Dish(c.getName(),null,null,null));
//newCat.getGraphic().getStyleClass().add("category");
categoryMap.put(c.getName(),newCat);
root.getChildren().add(newCat);
}
for (Dish d: dishes) {
categoryMap.get(d.getCategory()).getChildren().add(new TreeItem<>(d));
}
}
TreeTableView uses the rowFactory to create the TreeTableRows. At some time later it assigns a TreeItem to a TreeTableRow. This may happen again with different TreeItems for the same row. For this reason you need to handle changes those changes which can be done by adding a ChangeHandler to the TreeTableRow.treeItem property. If a new TreeItem is assigned to the row, you can check for top-level nodes by checking the children of the (invisible) root item for the row item.
I prefer the approach that does not require searching the child list though. It's possible to compare the parent of the item with the root.
public static class Item {
private final String value1;
private final String value2;
public Item(String value1, String value2) {
this.value1 = value1;
this.value2 = value2;
}
public String getValue1() {
return value1;
}
public String getValue2() {
return value2;
}
}
#Override
public void start(Stage primaryStage) {
final TreeItem<Item> root = new TreeItem<>(null);
TreeTableView<Item> ttv = new TreeTableView<>(root);
ttv.setShowRoot(false);
TreeTableColumn<Item, String> column1 = new TreeTableColumn<>();
column1.setCellValueFactory(new TreeItemPropertyValueFactory<>("value1"));
TreeTableColumn<Item, String> column2 = new TreeTableColumn<>();
column2.setCellValueFactory(new TreeItemPropertyValueFactory<>("value2"));
ttv.getColumns().addAll(column1, column2);
final PseudoClass topNode = PseudoClass.getPseudoClass("top-node");
ttv.setRowFactory(t -> {
final TreeTableRow<Item> row = new TreeTableRow<>();
// every time the TreeItem changes, check, if the new item is a
// child of the root and set the pseudoclass accordingly
row.treeItemProperty().addListener((o, oldValue, newValue) -> {
boolean tn = false;
if (newValue != null) {
tn = newValue.getParent() == root;
}
row.pseudoClassStateChanged(topNode, tn);
});
return row;
});
// fill tree structure
TreeItem<Item> c1 = new TreeItem<>(new Item("category 1", null));
c1.getChildren().addAll(
new TreeItem<>(new Item("sub1.1", "foo")),
new TreeItem<>(new Item("sub1.2", "bar")));
TreeItem<Item> c2 = new TreeItem<>(new Item("category 2", null));
c2.getChildren().addAll(
new TreeItem<>(new Item("sub2.1", "answer")),
new TreeItem<>(new Item("sub2.2", "42")));
root.getChildren().addAll(c1, c2);
Scene scene = new Scene(ttv);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
style.css
.tree-table-row-cell:top-node {
-fx-background: orange;
}
Moving the images a bit to the left would also be nice.
Usually you do this from a custom TreeTableCell returned by a TreeTableColumn.cellFactory. Depending on the behavior you want to implement setting fitWidth/fitHeight may be sufficient, but in other cases dynamically modifying those values based on the cell size may be required.
I used ObservableList to populate the TableView but the problem is that the data is not showing in the table I don't know what is the problem because the number of rows is exactly like I added them capture but there is nothing in the cells!
here is the code of the controller:
public class EnlistDim {
private static final String DEFAULT="-fx-text-background-color: black; -fx-background-color: steelblue;-fx-fill: red ;";
#FXML
private TableView<Parameter> tab;
#FXML
public void initialize() {
final ObservableList<Parameter> data = FXCollections.observableArrayList(
new Parameter("Query","Access method","Sequential scan"),
new Parameter("Query","Access method","in memory"),
new Parameter("Query","Operation","join"),
new Parameter("Query","Operation","Scan"),
new Parameter("Query","Operation","Sort"),
new Parameter("Database","Buffer management","Without buffer"),
new Parameter("Database","Buffer management","FIFO"),
new Parameter("Database","Buffer management","LIFO"),
new Parameter("Database","Buffer management","LRU"),
new Parameter("Database","Buffer management","Other"),
new Parameter("Database","Optimization structure","Not used"),
new Parameter("Database","Optimization structure","Partionning"),
new Parameter("Database","Optimization structure","Materialized View"),
new Parameter("Database","Optimization structure","compresssion"),
new Parameter("Database","System storage type","Database SQL"),
new Parameter("Database","System storage type","New SQL"),
new Parameter("Database","System storage type","Document"),
new Parameter("Database","System storage type","Graph"),
new Parameter("Database","System storage type","NVRAM"),
new Parameter("Database","System storage type","key value store"),
new Parameter("Database","Data storage type","Row Oriented"),
new Parameter("Database","Data storage type","Column Oriented"),
new Parameter("Database","Data storage type","Hybrid Oriented"),
new Parameter("Hardware","Processing device","CPU"),
new Parameter("Hardware","Processing device","GPU"),
new Parameter("Hardware","Processing device","FPGA"),
new Parameter("Hardware","Storage device","RAM"),
new Parameter("Hardware","Storage device","SSD"),
new Parameter("Hardware","Storage device","NVRAM"),
new Parameter("Hardware","Communication device","Modem"),
new Parameter("Hardware","Communication device","Cable"),
new Parameter("Hardware","Communication device","FaxModem"),
new Parameter("Hardware","Communication device","Router")
);
tab.setEditable(true);
tab.setItems(data);
tab.setStyle(DEFAULT);
}
}
and the code of Parameter class:
class Parameter {
SimpleStringProperty cat;
SimpleStringProperty subCat;
SimpleStringProperty subSubCat;
Parameter(String cat, String subCat, String subSubCat) {
this.cat = new SimpleStringProperty(cat);
this.subCat = new SimpleStringProperty(subCat);
this.subSubCat = new SimpleStringProperty(subSubCat);
}
public String getCat() {
return cat.get();
}
public void setCat(String c) {
cat.set(c);
}
public String getSubCat() {
return subCat.get();
}
public void setSubCat(String sc) {
subCat.set(sc);
}
public String getSubSubCat() {
return subSubCat.get();
}
public void setSubSubCat(String ssc) {
subSubCat.set(ssc);
}
}
You need to actually tell the TableView HOW to display the data. This is done using a CellValueFactory. Basically, you need to tell each column of the table what type of data it holds and where it gets that data from.
You need to start by defining your columns (give them an fx:id either in the FXML file or in SceneBuilder):
#FXML
TableColumn<Parameter, String> colCategory;
#FXML
TableColumn<Parameter, String> colSubCategory;
#FXML
TableColumn<Parameter, String> colSubSubCategory;
Each TableColumn takes two Type parameters. The first defines the object being displayed (Parameter). The second is the data type for this column (all yours are String).
Once the columns are defined, you need to set their CellValueFactory in your initialize() method:
colCategory.setCellValueFactory(new PropertyValueFactory<Parameter, String>("cat"));
colSubCategory.setCellValueFactory(new PropertyValueFactory<Parameter, String>("subCat"));
colSubSubCategory.setCellValueFactory(new PropertyValueFactory<Parameter, String>("subSubCat"));
Here you are telling each column where to find the data to be displayed. The last argument on the line, in the quotes, is the name of your property within the Parameter object.
So, when JavaFX populates your table, it will takes these steps to populate each column (colCategory, for example):
Get the CellValueFactory for colCategory.
The factory is a PropertyValueFactory, so determine which class holds the property (in this case it is the Parameter class)
Look in the Parameter class for a String property by the name of "cat"
Populate the column's cell with the value of the cat property.
my brain is burning already and I cannot find correct way to populate TableView in JavaFX. My data map is Map<String, Map<String, String>> . First key is a state name, value is map that has key as variable and value as variable value. I need a table like
| States | x | y | ...
| state 1 | 5 | 6 | ...
etc.
EDIT: This is my last solution that populate only one column and other are populated by same data. This can be in another foreach with values.
for (TableColumn<ObservableList<String>, ?> column : table.getColumns()) {
TableColumn<ObservableList<String>, String> col = (TableColumn<ObservableList<String>, String>) column;
col.setCellValueFactory(data -> new ReadOnlyObjectWrapper<>(someValue));
}
I think about solution with something like this, but it populates rows by last value only:
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
for (Map<String, String> map : map.values()) {
for (Map.Entry<String, String> entry : map.entrySet()) {
if (Utils.getTableColumnByName(table, entry.getKey()) != null) {
TableColumn<ObservableList<String>, String> column = (TableColumn<ObservableList<String>, String>) Utils.getTableColumnByName(table, entry.getKey());
column.setCellValueFactory(data -> new ReadOnlyObjectWrapper<>(entry.getValue()));
}
}
}
for (Integer stateIndex : states) {
tableData.add(FXCollections.observableArrayList("state " + stateIndex));
}
table.setItems(tableData);
I am looking for only any suggestions, no complete solutions :)
EDIT 2: With this I populate only first row at beginning of execution. I don't know how populate another rows after complete of execution. This is in foreach:
TableColumn<ObservableList<String>, String> varColumn = new TableColumn();
varColumn.setText(variable.getText());
varColumn.setCellValueFactory(data -> new ReadOnlyObjectWrapper<>(value.getText()));
table.getColumns().add(varColumn);
And this after foreach:
table.setItems(getTableData());
And getTableData():
ObservableList<ObservableList<String>> data = FXCollections.observableArrayList();
for (String row : map.keySet()) {
data.add(FXCollections.observableArrayList(row));
}
return data;
I hope that is clear... thanks!
The data structure for a TableView is an ObservableList<SomeObject>, which is different from the data structure of your model, which is Map<String, Map<String, String>>. So you need some way to transform the model data structure into an ObservableList which can be used in the TableView.
A couple of ways I can think of doing this are:
Create a set of dummy objects which go in the list, one for each row which will correspond to a real item in your model and provide cell value factories which dynamically pull the data you require out of your model.
Create a parallel ObservableList data structure and sync the underlying data between your model and your ObservableList as required.
Option 2 of the above is the sample which I provide here. It is a kind of MVVM (model, view, view model) architecture approach. The model is your underlying map-based structure, the view model is the observable list that is consumed by the view which is the TableView.
Here is a sample.
import javafx.application.Application;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
public class StateView extends Application {
#Override
public void start(Stage stage) {
ObservableMap<String, ObservableMap<String, String>> states = populateStates();
final TableView<StateItem> tableView = new TableView<>();
tableView.setItems(extractItems(states));
final TableColumn<StateItem, String> stateCol = new TableColumn<>("State");
final TableColumn<StateItem, String> variableCol = new TableColumn<>("Variable");
final TableColumn<StateItem, String> valueCol = new TableColumn<>("Value");
stateCol.setCellValueFactory(new PropertyValueFactory<>("stateName"));
variableCol.setCellValueFactory(new PropertyValueFactory<>("variableName"));
valueCol.setCellValueFactory(new PropertyValueFactory<>("variableValue"));
tableView.getColumns().setAll(stateCol, variableCol, valueCol);
states.addListener((MapChangeListener<String, ObservableMap<String, String>>) change ->
tableView.setItems(extractItems(states))
);
Scene scene = new Scene(tableView);
stage.setScene(scene);
stage.show();
}
private ObservableList<StateItem> extractItems(ObservableMap<String, ObservableMap<String, String>> states) {
return FXCollections.observableArrayList(
states.keySet().stream().sorted().flatMap(state -> {
Map<String, String> variables = states.get(state);
return variables.keySet().stream().sorted().map(
variableName -> {
String variableValue = variables.get(variableName);
return new StateItem(state, variableName, variableValue);
}
);
}).collect(Collectors.toList())
);
}
private static final Random random = new Random(42);
private static final String[] variableNames = { "red", "green", "blue", "yellow" };
private ObservableMap<String, ObservableMap<String, String>> populateStates() {
ObservableMap<String, ObservableMap<String, String>> states = FXCollections.observableHashMap();
for (int i = 0; i < 5; i ++) {
ObservableMap<String, String> variables = FXCollections.observableHashMap();
for (String variableName: variableNames) {
variables.put(variableName, random.nextInt(255) + "");
}
states.put("state " + i, variables);
}
return states;
}
public static void main(String[] args) {
launch(args);
}
public static class StateItem {
private String stateName;
private String variableName;
private String variableValue;
public StateItem(String stateName, String variableName, String variableValue) {
this.stateName = stateName;
this.variableName = variableName;
this.variableValue = variableValue;
}
public String getStateName() {
return stateName;
}
public void setStateName(String stateName) {
this.stateName = stateName;
}
public String getVariableName() {
return variableName;
}
public void setVariableName(String variableName) {
this.variableName = variableName;
}
public String getVariableValue() {
return variableValue;
}
public void setVariableValue(String variableValue) {
this.variableValue = variableValue;
}
}
}
What I do is provide a new StateItem class which feeds into the observable list for the view model and contains the stateName, variableName and variableValue values used for each row of the table. There is a separate extraction function which extracts data from the model map and populates the view model observable list as needed.
What "as needed" means for you will depend upon what you need to accomplish. If you only need to populate the data up-front at initialization, a single call to extract the data to the view model is all that is required.
If you need the view model to change dynamically based on changes to the underlying data, then you need to either:
Perform some binding of values from the view model to the model OR
Add some listeners for changes to the model which you then use to update the view model OR
Make sure you make a direct call to update the view model whenever the underlying model changes.
For the sample, I have provided an example of a listener based approach. I changed the underlying model class from Map<String, Map<String, String> to ObservableMap<String, ObservableMap<String, String>> and then use a MapChangeListener to listen for changes of the outermost ObservableMap (in your case this is would correspond to the addition of an entirely new state or removal of an existing state).
If you need to maintain additional synchronicity between the two structures, for instance reflecting dynamically that variables are added or removed, or variables or states are renamed or variable values are updated, then you would need to apply additional listeners for the inner-most ObservableMap which is maintaining your variable list. You would likely also change the types from String to StringProperty so that you could bind values in the model view StateItem class to values in your model, and you would also add property accessors to the StateItem class.
Anyway, the above code is unlikely to completely solve your problem but may assist in better understanding potential approaches you might wish to evaluate to solve it.
As an aside, perhaps using a TreeTableView, might be a better control for your implementation than a TableView. Just depends on your needs.
Thanks guys! I did it! Tables in JavaFX are so annoying, but my solution is here for anyone who will need it :)
public class Row {
private String state;
private String[] values;
public Row(String state, String... values) {
this.state = state;
this.values = values;
}
public String getState() {
return state;
}
public List<String> getValues() {
return Arrays.asList(values);
}
Set columns:
column.setCellValueFactory(data -> new ReadOnlyObjectWrapper<>(data.getValue().getValues().get(index)));
Get data from map:
public ObservableList<Row> getTableData() {
ObservableList<Row> data = FXCollections.observableArrayList();
for (Map.Entry<String, TreeMap<String, String>> entry : map.entrySet()) {
String[] values = new String[entry.getValue().values().size()];
int index = 0;
for (String value : entry.getValue().values()) {
values[index] = value;
index++;
}
Row row = new Row(entry.getKey(), values);
data.add(row);
}
return data;
}
And at last:
table.setItems(getTableData());
Table with expected output
Of course it wants some fixes with undefined values and so on but it works finally :)
I have a TreeTableView<MyCustomRow> and I want to add columns dynamically. In MyCustomRow i have a Map<Integer, SimpleBooleanProperty> with the values in the row. I'm adding the new column this way:
private TreeTableColumn<MyCustomRow, Boolean> newColumn() {
TreeTableColumn<MyCustomRow, Boolean> column = new TreeTableColumn<>();
column.setId(String.valueOf(colNr));
column.setPrefWidth(150);
column.setCellValueFactory(data -> data.getValue().getValue().getValue(colNr));
column.setCellFactory(factory -> new CheckBoxTreeTableCell());
column.setEditable(true);
colNr++;
return column;
}
Then table.getColumns().add(newColumn()).
The problem is when I check a CheckBox in a row, all CheckBoxes in that row become checked. Here is the code for my row:
public class MyCustomRow {
private Map<Integer, SimpleBooleanProperty> values = new HashMap<>();
public MyCustomRow(Map<Integer, Boolean> values) {
values.entrySet().forEach(entry -> this.values
.put(entry.getKey(), new SimpleBooleanProperty(entry.getValue())));
}
public SimpleBooleanProperty getValue(Integer colNr) {
if (!values.containsKey(colNr)) {
values.put(colNr, new SimpleBooleanProperty(false));
}
return values.get(colNr);
}
}
So I set the value of the cell depending on the colNr, i also tried to debug and it seems the values are different in the values map, so I have no idea why all the checkBoxes are checked when I check just one.
In this line,
column.setCellValueFactory(data -> data.getValue().getValue().getValue(colNr));
The handler is called when the cells are shown. Hence all of colNr are the newest value, the boolean property of the last index is associated with all cells.
To call the handler with the value at the time of newColumn() is called, for example:
final Integer colNrFixed = colNr;
column.setCellValueFactory(data -> data.getValue().getValue().getValue(colNrFixed));
// ...
colNr++;