Populate TableView with Map<String, Map<String, String>> JavaFX

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 :)

    链接地址: http://www.djcxy.com/p/80154.html

    上一篇: 如何在运行时使用C ++获取内存使用情况?

    下一篇: 使用Map <String,Map <String,String>填充TableView JavaFX