在 javafx 的 tableView 中自定义 TableCell

Customise TableCell in a tableView in javafx

假设我们有以下信息:

如您所见,一篇文章可以存储在许多商店中,反之亦然:一个商店可以存储许多文章:这就是 class 模型 (UML)

一些代码: FXML 部分:

 @FXML
     private  TableView<Article> tblArticles;
    @FXML
    private TableColumn<Article, String> colStore;

    @FXML
    private TableColumn<Article, Integer> colQuantity;

吸气剂和 setter :

colStore.setCellValueFactory(new PropertyValueFactory<>("store"));
colStore.setCellValueFactory(new PropertyValueFactory<>("quantity"));

我收到了第一个 table 中的结果,但我无法执行第二个 table 中的结果。

而我想要的应该提供以下信息:

所以我的问题是可以在 TableView 中执行此操作吗?

这是一个示例应用程序。它后面有一个MVVM style,适合这种工作。该应用程序是使用 Java 13 构建的,无法在 Java 早期版本(例如 Java 8)中运行。这是一个相对较长的答案,但是,嗯,有时这就是它所需要的。

总体方法是不是为存储文章的每个商店创建一个table视图行。相反,我们只为每篇文章创建一行我们有一个自定义单元格渲染器,它为存储该项目的所有商店和数量生成一个单一格式的单元格。

现在,您可以根据 custom rowFactory 进行替代实施。但是,我 推荐针对此特定任务的方法,因为我认为如果没有提供足够的价值,实施和维护会不必要地复杂。

另一种方法是使用嵌套列。如果采取适当的措施,这种方法 确实 允许您为存储文章的每个商店创建一个 table 视图行。如果这样做,您需要一些根据行是否是组中的第一行来填充不同数据的方法。您不允许用户对 table 中的数据重新排序和排序,因为这很难满足,因为 "first row in the group" 的概念将永远改变。为了允许使用嵌套列进行适当的呈现,您最终会得到一个略有不同的视图模型(下面的 FlatLineItem class 以及 LineItemService 中检索它们的附带方法)。

下图展示了左侧带有自定义单元格渲染器的 TableView 和右侧使用嵌套列的 TableView 的输出。请注意选择在每种情况下的工作方式不同。在左侧选择一行时,它包括附加到该行的所有商店。右侧使用嵌套列时,行选择仅为给定商店选择行。

主应用程序class

这将设置几个 TableView。

对于第一个 table 视图,它所做的只是为要显示的每个元素创建一个包含一列的 TableView。所有数据都是使用标准 PropertyValueFactoryLineItem 视图模型 class 中提取的。稍有不同的是通过 StoredQuantityTableCellStoredQuantity 字段自定义单元格渲染器,这将在稍后解释。

第二个视图使用 nested columns 并基于 FlatLineItem 视图模型 class 工作,也使用标准 PropertyValueFactory 并且不使用自定义单元格渲染器。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.List;

public class AggregateViewApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        LineItemService lineItemService = new LineItemService();

        TableView<LineItem> tableView = createArticleTableView();
        tableView.getItems().setAll(lineItemService.fetchAllLineItems());

        TableView<FlatLineItem> nestedTableView = createNestedArticleTableView();
        nestedTableView.getItems().setAll(lineItemService.fetchAllFlatLineItems());

        HBox layout = new HBox(
                40,
                tableView,
                nestedTableView
        );

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

    @SuppressWarnings("unchecked")
    private TableView<LineItem> createArticleTableView() {
        TableView tableView = new TableView();

        TableColumn<LineItem, Long> articleIdCol = new TableColumn<>("Article ID");
        articleIdCol.setCellValueFactory(new PropertyValueFactory<>("articleId"));

        TableColumn<LineItem, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(new PropertyValueFactory<>("articleName"));

        TableColumn<LineItem, List<StoredQuantity>> storedArticleCol = new TableColumn<>("Store Quantities");
        storedArticleCol.setCellValueFactory(new PropertyValueFactory<>("storedQuantities"));
        storedArticleCol.setCellFactory(lineItemStringTableColumn -> new StoredQuantityTableCell());

        TableColumn<LineItem, DB.StoredArticle> totalCol = new TableColumn<>("Total");
        totalCol.setCellValueFactory(new PropertyValueFactory<>("total"));

        tableView.getColumns().addAll(articleIdCol, nameCol, storedArticleCol, totalCol);

        tableView.setPrefSize(400, 150);

        return tableView;
    }

    @SuppressWarnings("unchecked")
    private TableView<FlatLineItem> createNestedArticleTableView() {
        TableView tableView = new TableView();

        TableColumn<FlatLineItem, Long> articleIdCol = new TableColumn<>("Article ID");
        articleIdCol.setCellValueFactory(new PropertyValueFactory<>("articleId"));
        articleIdCol.setSortable(false);

        TableColumn<FlatLineItem, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(new PropertyValueFactory<>("articleName"));
        nameCol.setSortable(false);

        TableColumn<FlatLineItem, String> storeCol = new TableColumn<>("Store");
        storeCol.setCellValueFactory(new PropertyValueFactory<>("storeName"));
        storeCol.setSortable(false);
        TableColumn<FlatLineItem, String> storeQuantityCol = new TableColumn<>("Quantity");
        storeQuantityCol.setCellValueFactory(new PropertyValueFactory<>("storeQuantity"));
        storeQuantityCol.setSortable(false);

        TableColumn<FlatLineItem, List<StoredQuantity>> storedArticleCol = new TableColumn<>("Store Quantities");
        storedArticleCol.getColumns().setAll(
                storeCol,
                storeQuantityCol
        );
        storedArticleCol.setSortable(false);

        TableColumn<LineItem, DB.StoredArticle> totalCol = new TableColumn<>("Total");
        totalCol.setCellValueFactory(new PropertyValueFactory<>("total"));
        totalCol.setSortable(false);

        tableView.getColumns().setAll(articleIdCol, nameCol, storedArticleCol, totalCol);

        tableView.setPrefSize(400, 200);

        return tableView;
    }

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

}

StoredQuantityTableCell.java

这需要一个 StoredQuantities 列表,它是商店名称和存储在该商店的物品数量的元组,然后将该列表呈现到单个单元格中,在 GridView 内部格式化显示。您可以使用任何您想要的内部节点布局或格式,并在必要时添加 CSS 样式来增加趣味性。

import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.layout.GridPane;

import java.util.List;

class StoredQuantityTableCell extends TableCell<LineItem, List<StoredQuantity>> {
    private GridPane storedQuantityPane;

    public StoredQuantityTableCell() {
        storedQuantityPane = new GridPane();
        storedQuantityPane.setHgap(10);
        storedQuantityPane.setVgap(5);
    }

    @Override
    protected void updateItem(List<StoredQuantity> storedQuantities, boolean empty) {
        super.updateItem(storedQuantities, empty);

        if (storedQuantities == null)  {
            setGraphic(null);
            return;
        }

        storedQuantityPane.getChildren().removeAll(storedQuantityPane.getChildren());
        int row = 0;
        for (StoredQuantity storedQuantity: storedQuantities) {
            storedQuantityPane.addRow(
                    row,
                    new Label(storedQuantity.getStoreName()),
                    new Label("" + storedQuantity.getQuantity())
            );

            row++;
        }

        setGraphic(storedQuantityPane);
    }
}

LineItem.java

视图模型 class 表示 table 中的一行。

import java.util.Collections;
import java.util.List;

public class LineItem {
    private long articleId;
    private String articleName;
    private List<StoredQuantity> storedQuantities;

    public LineItem(long articleId, String articleName, List<StoredQuantity> storedQuantities) {
        this.articleId = articleId;
        this.articleName = articleName;
        this.storedQuantities = storedQuantities;
    }

    public long getArticleId() {
        return articleId;
    }

    public String getArticleName() {
        return articleName;
    }

    public List<StoredQuantity> getStoredQuantities() {
        return Collections.unmodifiableList(storedQuantities);
    }

    public int getTotal() {
        return storedQuantities.stream()
                .mapToInt(StoredQuantity::getQuantity)
                .sum();
    }
}

StoredQuantity.java

一个视图模型class 表示商店名称和商店中物品的数量。 StoredQuantityTableCell 使用它来呈现订单项的存储数量。

public class StoredQuantity implements Comparable<StoredQuantity> {
    private String storeName;
    private int quantity;

    StoredQuantity(String storeName, int quantity) {
        this.storeName = storeName;
        this.quantity = quantity;
    }

    public String getStoreName() {
        return storeName;
    }

    public int getQuantity() {
        return quantity;
    }

    @Override
    public int compareTo(StoredQuantity o) {
        return storeName.compareTo(o.storeName);
    }
}

平坦LineItem.java

视图模型 class 支持带有嵌套列的 table 视图。可以为存储文章的每个商店创建一个平面项目。

public class FlatLineItem {
    private Long articleId;
    private String articleName;
    private final String storeName;
    private final Integer storeQuantity;
    private final Integer total;
    private final boolean firstInGroup;

    public FlatLineItem(Long articleId, String articleName, String storeName, Integer storeQuantity, Integer total, boolean firstInGroup) {
        this.articleId = articleId;
        this.articleName = articleName;
        this.storeName = storeName;
        this.storeQuantity = storeQuantity;
        this.total = total;
        this.firstInGroup = firstInGroup;
    }

    public Long getArticleId() {
        return articleId;
    }

    public String getArticleName() {
        return articleName;
    }

    public String getStoreName() {
        return storeName;
    }

    public Integer getStoreQuantity() {
        return storeQuantity;
    }

    public Integer getTotal() {
        return total;
    }

    public boolean isFirstInGroup() {
        return firstInGroup;
    }
}

LineItemService.java

这会将数据库中的值转换为可以由视图呈现的视图模型对象(LineItems 或 FlatLineItems)。请注意为嵌套列 table 视图构造 FlatLineItems 的 getFlatLineItemsForLineItem 如何知道它是一组行项目中的第一行并基于此适当地传播 FlatLineItem,留下一些值如果它们只是从组中的第一项开始重复,则为 null,这会导致显示清晰。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class LineItemService {
    private final DB db = DB.instance();

    public List<LineItem> fetchAllLineItems() {
        return db.findAllArticles()
                .stream()
                .map(article -> createLineItemForArticle(article.getArticleId()))
                .collect(Collectors.toList());
    }

    public List<FlatLineItem> fetchAllFlatLineItems() {
        return fetchAllLineItems().stream()
                .flatMap(lineItem -> getFlatLineItemsForLineItem(lineItem).stream())
                .collect(Collectors.toList());
    }

    private List<FlatLineItem> getFlatLineItemsForLineItem(LineItem lineItem) {
        ArrayList<FlatLineItem> flatLineItems = new ArrayList<>();

        boolean firstStore = true;
        for (StoredQuantity storedQuantity: lineItem.getStoredQuantities()) {
            FlatLineItem newFlatLineItem;

            if (firstStore) {
                newFlatLineItem = new FlatLineItem(
                        lineItem.getArticleId(),
                        lineItem.getArticleName(),
                        storedQuantity.getStoreName(),
                        storedQuantity.getQuantity(),
                        lineItem.getTotal(),
                        true
                );

                firstStore = false;
            } else {
                newFlatLineItem = new FlatLineItem(
                        null,
                        null,
                        storedQuantity.getStoreName(),
                        storedQuantity.getQuantity(),
                        null,
                        false
                );
            }

            flatLineItems.add(newFlatLineItem);
        }

        return flatLineItems;
    }

    private LineItem createLineItemForArticle(long articleId) {
        DB.Article article =
                db.findArticleById(
                        articleId
                ).orElse(
                        new DB.Article(articleId, "N/A")
                );

        List<DB.StoredArticle> storedArticles =
                db.findAllStoredArticlesForArticleId(articleId);

        return new LineItem(
                article.getArticleId(),
                article.getName(),
                getStoredQuantitesForStoredArticles(storedArticles)
        );
    }

    private List<StoredQuantity> getStoredQuantitesForStoredArticles(List<DB.StoredArticle> storedArticles) {
        return storedArticles.stream()
                .map(storedArticle ->
                        new StoredQuantity(
                                db.findStoreById(storedArticle.getStoreId())
                                        .map(DB.Store::getName)
                                        .orElse("No Store"),
                                storedArticle.getQuantity()
                        )
                )
                .sorted()
                .collect(
                        Collectors.toList()
                );
    }
}

模拟数据库class

只是数据库的简单内存表示 class。在真实的应用程序中,您可能会使用类似 SpringData with hibernate 的东西来提供使用基于 JPA 的对象到关系映射的数据访问存储库。

数据库 class 与视图完全无关,只是在此处显示,以便可以在 MVVM 样式框架内创建 运行 应用程序。

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

class DB {
    private static final DB instance = new DB();
    public static DB instance() {
        return instance;
    }

    private List<Article> articles = List.of(
            new Article(1, "Hp101"),
            new Article(3, "Lenovo303"),
            new Article(4, "Asus404")
    );

    private List<Store> stores = List.of(
            new Store(1, "S1"),
            new Store(2, "S2")
    );

    private List<StoredArticle> storedArticles = List.of(
            new StoredArticle(1, 1, 30),
            new StoredArticle(1, 2, 70),
            new StoredArticle(3, 1, 50),
            new StoredArticle(4, 2, 70)
    );

    public Optional<Article> findArticleById(long articleId) {
        return articles.stream()
                .filter(article -> article.getArticleId() == articleId)
                .findFirst();
    }

    public Optional<Store> findStoreById(long storeId) {
        return stores.stream()
                .filter(store -> store.getStoreId() == storeId)
                .findFirst();
    }

    public List<StoredArticle> findAllStoredArticlesForArticleId(long articleId) {
        return storedArticles.stream()
                .filter(storedArticle -> storedArticle.articleId == articleId)
                .collect(Collectors.toList());
    }

    public List<Article> findAllArticles() {
        return Collections.unmodifiableList(articles);
    }

    static class Article {
        private long articleId;
        private String name;

        public Article(long articleId, String name) {
            this.articleId = articleId;
            this.name = name;
        }

        public long getArticleId() {
            return articleId;
        }

        public String getName() {
            return name;
        }
    }

    static class Store {
        private long storeId;
        private String name;

        public Store(long storeId, String name) {
            this.storeId = storeId;
            this.name = name;
        }

        public long getStoreId() {
            return storeId;
        }

        public String getName() {
            return name;
        }
    }

    static class StoredArticle {
        private long articleId;
        private long storeId;
        private int quantity;

        public StoredArticle(long articleId, long storeId, int quantity) {
            this.articleId = articleId;
            this.storeId = storeId;
            this.quantity = quantity;
        }

        public long getArticleId() {
            return articleId;
        }

        public long getStoreId() {
            return storeId;
        }

        public int getQuantity() {
            return quantity;
        }
    }
}

一些后续问题的答案

Which Approach is the best for updating data ?

我展示的所有方法都使用只读数据模型和视图。使其可读写需要更多的工作(并且超出了我准备添加到这个已经很长的答案的范围)。可能,在上面概述的两种方法中,为每个包含商品的商店使用单独的行的方法最容易适应使数据更新table.

Which approach in general I should use to update data ( data are stored for sure in db) ?

定义更新数据库中数据的一般方法超出了我在这里要回答的范围(这纯粹是基于意见的答案,因为有许多不同的方法可以实现这一点,因此不在主题之列对于 Whosebug)。如果是我,我会设置一个连接到数据库的基于 Spring 引导的休息服务,并让我的客户端应用程序与之通信。如果该应用程序不需要通过 Internet 进行通信,而仅通过 LAN 与本地数据库进行通信,则通过使该应用程序成为 Spring 启动应用程序并使用 Spring 数据存储库来添加直接数据库访问我会使用嵌入式 H2 数据库。

Is when modifying in a specific row modify in db or wait until user modify in the whole tableview and click on a save button ?

任何一种方法都行,我对其中一个和另一个没有任何强烈的意见。我可能倾向于立即更新方案而不是延迟保存方案,但这取决于应用程序和所需的用户体验。

Please can you provide me with some code for either to draw a line under every cell or to make it just like usual tableView ( one row gray and one not etc ...)

您可以将其作为一个单独的问题提出。但是,一般来说,使用 CSS styling。如果您使用上面概述的第二种方法,每个商店有一行,那么就样式而言,一切都已经是 "usual tableView",一行是灰色,一行不是,等等,所以我不知道有没有在这种情况下确实需要额外的样式。