2

I'd like to make a javafx TableView with rows that may (or may not) have "related" rows. Basically, I think it might be a tableview of tableviews, but (a) I'm not sure that would work and (b) my gut tells me there's an easier way.

Imagine a bill or materials like

item    material    quantity    name
1       wood        1           base
2       wood        4           drawer
        wood        4           drawer sides
        wood        1           drawer base
        hardware    1           pull
        hardware    8           nails
3       aluminum    4           leg
        plastic     1           foot
        hardware    1           screws

Right now, it's sorted by name (base, drawer, leg). If I sort it by material, I want the items to be 3, 1, 2 (or 3, 2, 1): aluminum, wood, wood. I need the "subitems" to remain with the numbered items.

Can I make "complex rows"? (And I don't even know what I'd call it!) Or do I need a custom sort that will keep groups together?

Marco R.
  • 2,667
  • 15
  • 33
  • 1
    Looking for [`TreeTableView`](https://openjfx.io/javadoc/11/javafx.controls/javafx/scene/control/TreeTableView.html)? – fabian May 23 '19 at 02:27

2 Answers2

5

You may want to check out TreeTableView. With it, you may be able to sort by whatever criteria you like, while at the same time having the records grouped by any other criteria (for example by name)

This component gets its data from a tree structure model built on the TreeItem class.

With it a root TreeItem can be built for the root object of your bill of materials having as children a TreeItem for each group you want (name of the material?). Then, each of these grouping TreeItem objects should contain as children the corresponding TreeItem children for each of the actual material in the bill of materials being displayed.

There are multiple good tutorials out there that cover in detail the technical aspects of this:

Hope this helps.

Marco R.
  • 2,667
  • 15
  • 33
  • 1
    When I looked through the javafx controls, I saw TreeTableView and thought it was a TreeView, ie, no columns. This is much easier than my options and I thank you, Marco. – Megabuttons May 23 '19 at 11:13
2

This answer uses a TreeTableView to represent the data rather than a TableView.

It uses a demonstration data model with hard-coded data.

The complicated nested lambda expressions could be replaced by procedural code mapping your actual model data to the TreeItem data structure which backs the TreeTableView.

screenshot

import java.util.*;

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

@SuppressWarnings("unchecked")
public class TreeTableViewSample extends Application {

    private final List<String> employeesNames = Arrays.asList(
            "Ethan Williams",
            "Emma Jones",
            "Michael Brown"
    );

    private final Random random = new Random(42);

    @Override
    public void start(Stage stage) {
        ObservableList<DutyAssignment> assignments =
                determineAssignments(employeesNames);

        TreeItem<DutyAssignment> assignmentTree = createAssignmentTree(assignments);
        TreeTableView<DutyAssignment> dutyAssignmentView = createAssigmentTreeTableView(assignmentTree);

        stage.setScene(new Scene(dutyAssignmentView));
        stage.show();
    }

    private TreeTableView<DutyAssignment> createAssigmentTreeTableView(TreeItem<DutyAssignment> root) {
        TreeTableColumn<DutyAssignment, String> employeeColumn =
                new TreeTableColumn<>("Employee");
        employeeColumn.setPrefWidth(120);
        employeeColumn.setCellValueFactory(
                param -> param.getValue().getValue().nameProperty()
        );

        TreeTableColumn<DutyAssignment, Workday> dayColumn =
                new TreeTableColumn<>("Day");
        dayColumn.setPrefWidth(100);
        dayColumn.setCellValueFactory(param ->
                param.getValue().getValue().dayProperty()
        );

        TreeTableColumn<DutyAssignment, Duty> dutyColumn =
                new TreeTableColumn<>("Duty");
        dutyColumn.setPrefWidth(100);
        dutyColumn.setCellValueFactory(param ->
                param.getValue().getValue().dutyProperty()
        );

        TreeTableView<DutyAssignment> dutyAssignmentView = new TreeTableView<>(root);
        dutyAssignmentView.getColumns().setAll(employeeColumn, dayColumn, dutyColumn);
        dutyAssignmentView.setShowRoot(false);
        dutyAssignmentView.setPrefSize(380, 500);
        
        return dutyAssignmentView;
    }

    private TreeItem<DutyAssignment> createAssignmentTree(ObservableList<DutyAssignment> assignments) {
        TreeItem<DutyAssignment> root = new TreeItem<>(
                new DutyAssignment("All Assignments", null, null)
        );
        root.setExpanded(true);
        employeesNames.stream()
                .sorted()
                .forEach(employeeName -> {
                    TreeItem<DutyAssignment> employeeTitleItem = new TreeItem<>(
                            new DutyAssignment(employeeName, null, null)
                    );
                    root.getChildren().add(employeeTitleItem);
                    employeeTitleItem.setExpanded(true);

                    assignments.stream()
                            .sorted()
                            .filter(assignment -> employeeName.equals(assignment.getName()))
                            .forEach(dutyAssignment -> {
                                TreeItem<DutyAssignment> assignmentLineItem = new TreeItem<>(
                                        new DutyAssignment(null, dutyAssignment.getDay(), dutyAssignment.getDuty())
                                );
                                employeeTitleItem.getChildren().add(assignmentLineItem);
                            });
                });
        
        return root;
    }

    private ObservableList<DutyAssignment> determineAssignments(List<String> employeesNames) {
        ObservableList<DutyAssignment> assignments = FXCollections.observableArrayList();

        for (String employeeName : employeesNames) {
            for (Workday day : Workday.values()) {
                assignments.add(
                        new DutyAssignment(
                                employeeName,
                                day,
                                selectRandomDuty()
                        )
                );
            }
        }

        return assignments;
    }

    private Duty selectRandomDuty() {
        return
                Duty.values()[
                        random.nextInt(
                                Duty.values().length
                        )
                ];
    }

    public static void main(String[] args) {
        Application.launch(TreeTableViewSample.class, args);
    }

    public class DutyAssignment implements Comparable<DutyAssignment> {
        final private SimpleStringProperty name = new SimpleStringProperty();
        final private SimpleObjectProperty<Workday> day = new SimpleObjectProperty<>();
        final private SimpleObjectProperty<Duty> duty = new SimpleObjectProperty<>();

        final private Comparator<DutyAssignment> nameDayDutyComparator =
                Comparator.comparing(DutyAssignment::getName)
                        .thenComparing(DutyAssignment::getDay)
                        .thenComparing(DutyAssignment::getDuty);

        public DutyAssignment(String name, Workday day, Duty duty) {
            setName(name);
            setDay(day);
            setDuty(duty);
        }

        public String getName() {
            return name.get();
        }

        public SimpleStringProperty nameProperty() {
            return name;
        }

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

        public Workday getDay() {
            return day.get();
        }

        public SimpleObjectProperty<Workday> dayProperty() {
            return day;
        }

        public void setDay(Workday day) {
            this.day.set(day);
        }

        public Duty getDuty() {
            return duty.get();
        }

        public SimpleObjectProperty<Duty> dutyProperty() {
            return duty;
        }

        public void setDuty(Duty duty) {
            this.duty.set(duty);
        }

        @Override
        public int compareTo(DutyAssignment o) {
            return nameDayDutyComparator.compare(this, o);
        }
    }

    public enum Duty {
        WRITING, EDITING, COLORING, COMPOSITING
    }

    public enum Workday {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406