So I have done some digging around on the JavaFx TableView and I have found some nice solutions to simple situations.
This article provides a nice explanation of how to
You don't have to use the default PropertyValueFactory, you can write your own callback.
import java.util.HashMap;
import java.util.LinkedHashMap;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class AssTable extends Application {
@Override
public void start(Stage primaryStage) {
ObservableList<Student> students = FXCollections.observableArrayList(
new Student("jack"),new Student("john"),new Student("jill"),new Student("jane"));
TableView<Student> studentTable = new TableView(students);
TableColumn<Student, String> firstNameColumn = new TableColumn("name");
firstNameColumn.setCellValueFactory(new PropertyValueFactory("firstName"));
studentTable.getColumns().add(firstNameColumn);
int maxAss = 0;
for (Student student : students)
maxAss = Math.max(maxAss, student.map.size());
Callback<TableColumn.CellDataFeatures<Student, String>, ObservableValue<String>> callBack =
new Callback<TableColumn.CellDataFeatures<Student, String>, ObservableValue<String>>() {
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Student, String> param) {
return param.getValue().map.containsKey(
"ass"+Integer.toString((int)param.getTableColumn().getUserData()))
? new SimpleStringProperty(String.format("%.1f",100d*param.getValue().map.get(
"ass"+Integer.toString((int)param.getTableColumn().getUserData()))))
:new SimpleStringProperty("");
}
};
ObservableList<TableColumn<Student, String>> assCols = FXCollections.observableArrayList();
for (int i = 1; i<=maxAss; i++){
TableColumn<Student, String> tmpCol = new TableColumn("ass"+Integer.toString(i));
tmpCol.setUserData(i);
tmpCol.setCellValueFactory(callBack);
assCols.add(tmpCol);
}
studentTable.getColumns().addAll(assCols);
VBox root = new VBox(studentTable);
Scene scene = new Scene(root, 500, 250);
primaryStage.setTitle("Table with map");
primaryStage.setScene(scene);
primaryStage.show();
}
public class Student {
private final StringProperty firstName = new SimpleStringProperty();
public StringProperty firstNameProperty(){return firstName;}
public final HashMap<String, Double> map;
public Student(String fn) {
firstName.set(fn);
map = new LinkedHashMap<>();
for (int i = 1; i <= 10; i++) {
double grade = Math.random();
if (grade > .5) {
map.put("ass" + Integer.toString(i), grade);
}
}
}
}
}
You can see it adds columns depending on how many assignments there are. Also nobody has done ass4 in this random sample. With this code and example you can't add an assignment like #8 without also adding a new column, or it won't show up.
I assumed that the size (# of Entries) of each Map does not change during runtime, or better: there is a fixed maximum of entries. If this is the case, the TableView
can access each Entry
the same way as it does with standard attributes (or a Property
).
Here is a modified class of Person
.
public class PersonSimple {
String firstName;
String lastName;
String age;
Map<Integer, Double> map;
public PersonSimple(String fn, String ln, String age, Double gr0, Double gr1, Double gr2)
{
this.firstName = fn;
this.lastName = ln;
this.age = age;
map = new LinkedHashMap<>();
map.put(0, gr0);
map.put(1, gr1);
map.put(2, gr2);
}
public String getFirstName()
{
return firstName;
}
public String getLastName()
{
return firstName;
}
public String getAge()
{
return age;
}
private Double getFromMap(Integer key)
{
Set<Entry<Integer, Double>> s = map.entrySet();
Iterator<Entry<Integer, Double>> iter = s.iterator();
int index = 0;
while(iter.hasNext())
{
Entry<Integer, Double> e = iter.next();
if(index == key.intValue())
{
return e.getValue();
}
index++;
}
return null;
}
public Double getFM0()
{
return getFromMap(0);
}
public Double getFM1()
{
return getFromMap(1);
}
public Double getFM2()
{
return getFromMap(2);
}
}
As you can see, every PersonSimple
has a Map
which must hold three entries. Now comes the trick. For each of these entries you have define a get-method. Be careful how you name them, because this part is crucial to the interaction with the TableView
.
The following code shows how you connect these new methods to the TableView
.
TableColumn firstNameCol = new TableColumn("First Name");
TableColumn lastNameCol = new TableColumn("Last Name");
TableColumn ageCol = new TableColumn("Age");
TableColumn aCol = new TableColumn("Assignment1");
TableColumn bCol = new TableColumn("Assignment2");
TableColumn cCol = new TableColumn("Assignment3");
table.getColumns().addAll(firstNameCol, lastNameCol, ageCol,aCol,bCol,cCol);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("firstName"));
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("lastName"));
ageCol.setCellValueFactory(new PropertyValueFactory<Person,String>("age"));
aCol.setCellValueFactory(new PropertyValueFactory<Person,Double>("FM0"));
bCol.setCellValueFactory(new PropertyValueFactory<Person,Double>("FM1"));
cCol.setCellValueFactory(new PropertyValueFactory<Person,Double>("FM2"));
It is highly important that each PropertyValueFactor
gets a name that fits to one of the get-methods in the class PersonSimple
. See the TableView-API for more information about that.
Of course, my approach does not solve the problem with getting the data from dynamic maps, because as far as i know it is not possible in Java to add new methods to a class during runtime. But there might be a trick using the reflection-api to circumvent this limitation.