2

I have an observable list containing persons:

ObservableList<Person> persons = FXCollections.observableArrayList();

And a class Person:

public class Person {
    private final StringProperty name = new SimpleStringProperty();
    private final IntegerProperty count = new SimpleIntegerProperty();
    // ...
}

Now I can display them in a table like this:

TableView<Person> table = new TableView<>(persons);

TableColumn<Person, String> name = new TableColumn<>("Name");
name.setCellValueFactory(cell -> cell.getValue().nameProperty());

TableColumn<Person, Number> count = new TableColumn<>("Count");
count.setCellValueFactory(cell -> cell.getValue().countProperty());

table.getColumns().addAll(name, count);

If there are multiple persons with the same name, it will appear multiple times.

Now I'd like to display each name in the table only once and sum the count together.

In SQL for example it would be a group by function.

How can I do this in JavaFX?

5
  • Well then don't allow duplicates on the ObservableList , based on the Name . So if two Person have the same name then which one will appear ? Only one will appear with the number=sum numbers of all persons ? How you populate the person ObservableList with items ? Commented May 31, 2017 at 14:33
  • 2
    How big is the underlying list likely to be? Specifically, is it acceptable to recompute the entire table if there are any changes to the underlying list? Or would that be prohibitive, performance-wise? (Also, would a TreeTableView be more appropriate here?) Commented May 31, 2017 at 14:48
  • @GOXR3PLUS the list will be populated at the start of the application. Yes, only one would appear and the number should be the sum of the counts. Commented Jun 1, 2017 at 6:48
  • @James_D The list can be quite big (Up to 10'000 entries). It can't be changed but filtered (code.makery.ch/blog/javafx-8-tableview-sorting-filtering). Commented Jun 1, 2017 at 6:54
  • Nibor James_D answer is the correct one . Also you can develop a mechanism for showing only 300-400 elements and the user can move to next or previous page :) . Ι have made something for it (show only 200-300 elements per page and then populate with 200-300 next or 200-300 previous), but i need to make it more generic before publishing it . Commented Jun 1, 2017 at 17:27

2 Answers 2

3

If you need a solution that will update the table if the properties in the Person instances are changed, you can do:

ObservableList<Person> persons = FXCollections
        .observableArrayList(p -> new Observable[] { p.nameProperty(), p.countProperty() });
ObservableList<String> uniqueNames = FXCollections.observableArrayList();

persons.addListener((Change<? extends Person> c) -> uniqueNames
        .setAll(persons.stream().map(Person::getName).distinct().collect(Collectors.toList())));

TableView<String> table = new TableView<>(uniqueNames);
TableColumn<String, String> name = new TableColumn<>("Name");
name.setCellValueFactory(n -> new SimpleStringProperty(n.getValue()));
TableColumn<String, Number> count = new TableColumn<>("Count");
count.setCellValueFactory(n -> Bindings.createIntegerBinding(() -> persons.stream()
        .filter(p -> p.getName().equals(n.getValue())).collect(Collectors.summingInt(Person::getCount)), persons));

This solution is not particularly efficient: all visible cells will recompute if the data in the underlying list changes (including changes in the properties in the Person instances). This should be viable for reasonably small lists; but if you have large amounts of data you may need to process the changes in the list (persons.addListener(...)) in a more intelligent way (which would likely be quite complex).

Here is an SSCCE. It displays two tables: one is a regular table with the full persons list; the other is the table as set up above that shows the "grouped" list. You can add items to the main list by filling in the text fields (the second needs to be an integer) and pressing "Add", or delete entries by selecting an item and pressing delete. The "full table" is also editable, so you can test altering existing values.

import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;

public class GroupedTable extends Application {

    @Override
    public void start(Stage primaryStage) {
        ObservableList<Person> persons = FXCollections
                .observableArrayList(p -> new Observable[] { p.nameProperty(), p.countProperty() });
        ObservableList<String> uniqueNames = FXCollections.observableArrayList();

        persons.addListener((Change<? extends Person> c) -> uniqueNames
                .setAll(persons.stream().map(Person::getName).distinct().collect(Collectors.toList())));

        TableView<String> table = new TableView<>(uniqueNames);
        TableColumn<String, String> name = new TableColumn<>("Name");
        name.setCellValueFactory(n -> new SimpleStringProperty(n.getValue()));
        TableColumn<String, Number> count = new TableColumn<>("Count");
        count.setCellValueFactory(n -> Bindings.createIntegerBinding(() -> persons.stream()
                .filter(p -> p.getName().equals(n.getValue())).collect(Collectors.summingInt(Person::getCount)), persons));

        table.getColumns().add(name);
        table.getColumns().add(count);

        TableView<Person> fullTable = new TableView<>(persons);
        fullTable.setEditable(true);
        TableColumn<Person, String> allNamesCol = new TableColumn<>("Name");
        TableColumn<Person, Integer> allCountsCol = new TableColumn<>("Count");
        allNamesCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
        allNamesCol.setCellFactory(TextFieldTableCell.forTableColumn());
        allCountsCol.setCellValueFactory(cellData -> cellData.getValue().countProperty().asObject());
        allCountsCol.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        fullTable.getColumns().add(allNamesCol);
        fullTable.getColumns().add(allCountsCol);

        TextField nameTF = new TextField();
        TextField countTF = new TextField();
        Button add = new Button("Add");
        add.setOnAction(e -> {
            persons.add(new Person(nameTF.getText(), Integer.parseInt(countTF.getText())));
            nameTF.clear();
            countTF.clear();
        });
        Button delete = new Button("Delete");
        delete.setOnAction(e -> persons.remove(fullTable.getSelectionModel().getSelectedItem()));
        delete.disableProperty().bind(fullTable.getSelectionModel().selectedItemProperty().isNull());

        HBox controls = new HBox(5, new Label("Name:"), nameTF, new Label("Count:"), countTF, add, delete);
        BorderPane root = new BorderPane(new HBox(5, fullTable, table));
        root.setBottom(controls);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    public static class Person {
        private final StringProperty name = new SimpleStringProperty();
        private final IntegerProperty count = new SimpleIntegerProperty();

        public Person(String name, int count) {
            setName(name);
            setCount(count);
        }


        public final StringProperty nameProperty() {
            return this.name;
        }

        public final String getName() {
            return this.nameProperty().get();
        }

        public final void setName(final String name) {
            this.nameProperty().set(name);
        }

        public final IntegerProperty countProperty() {
            return this.count;
        }

        public final int getCount() {
            return this.countProperty().get();
        }

        public final void setCount(final int count) {
            this.countProperty().set(count);
        }





    }
}

Sequence of screen shots:

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

The data (persons) will load in the beginning and won't be changed again. But I'd like to be able to filter the list like this: (code.makery.ch/blog/javafx-8-tableview-sorting-filtering). The data should be based on the the filtered list and update if the filter predicate changes.
Your solution works quite good. It's like this now: sortedPersons.addListener((Change<? extends Person> c) -> uniqueNames.setAll(sortedPersons.stream().map(Person::getName).distinct().collect(Collectors.toList()))); with your integer binding for the setCellValueFactory. Is it possible in an easy way to add a lastname to the table and do the grouping/summing based on the name and lastname?
2

Does JavaFX support Java-8 stream API? If yes you could just use Collectors.groupingBy

List<Person> persons  = Arrays.asList(
        new Person("a", 10),
        new Person("b", 20),
        new Person("c", 10),
        new Person("a", 10),
        new Person("d", 20),
        new Person("b", 10),
        new Person("e", 10)
);
Map<String, Integer> sum = persons.stream().
        collect(
               Collectors.groupingBy(
                          Person::getName,
                          Collectors.summingInt(Person::getCount)
               )
         );
System.out.println(sum); // {a=20, b=30, c=10, d=20, e=10}

4 Comments

The problem is, that map won't update if the properties change (e.g. persons.get(0).setCount(20)). So (even if you get past the issue of what your cell value factories are going to be set to), this doesn't really work.
Perfecto i was trying to do it with Java-8 Stream half an hour now :).
@James_D, I thinks that the change in the properties is not going to be reflected with tables name.setCellValueFactory(cell -> cell.getValue().nameProperty()); either.
@AntonBalaniuc Sure it is. That's the whole point of the StringProperty. The table cells register a listener with those properties.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.