【问题标题】:Customise TableCell in a tableView in javafx在 javafx 中的 tableView 中自定义 TableCell
【发布时间】:2020-01-21 16:41:30
【问题描述】:

假设我们有以下信息:

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

一些代码: FXML 部分:

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

    @FXML
    private TableColumn<Article, Integer> colQuantity;

getter 和 setter:

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

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

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

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

【问题讨论】:

  • 是的,有可能。您需要定义自定义表格单元格来执行此操作。有tutorials for that。显然,对于您的实现,它将比基本教程更复杂。如果您不为您已经尝试做的事情提供一些基本代码minimal reproducible example,我不知道是否有人会为您发布解决方案。我觉得要在这方面取得成功,您需要一个实际的具体解决方案示例,而不是一组关于如何做到这一点的指针。
  • 是的,我看过教程,我想这不是我要找的。我进行了编辑。

标签: uitableview tableview javafx-8 uml


【解决方案1】:

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

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

现在,您可以基于custom rowFactory 进行替代实现。但是,我推荐该方法用于此特定任务,因为我认为实施和维护它会不必要地复杂,而且没有提供足够的价值。

另一种方法是使用嵌套列。这种方法,如果采取适当的措施,确实允许您为存储文章的每个商店创建一个 tableview 行。如果您这样做,您需要某种方式来填充不同的数据,具体取决于是否一行要么是组中的第一行,要么不是。您不允许用户对表中的数据进行重新排序和排序,因为这将很难满足,因为“组中的第一行”的概念将永远改变。为了允许使用嵌套列进行适当的渲染,您最终会得到一个稍微不同的视图模型(下面的 FlatLineItem 类以及检索它们的 LineItemService 中的随附方法)。

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

主应用类

这设置了几个 TableView。

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

第二个视图使用 nested columns 并基于 FlatLineItem 视图模型类工作,也使用标准 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

表示表格中一行的视图模型类。

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

代表商店名称和商店中物品数量的视图模型类。 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);
    }
}

FlatLineItem.java

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

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),这些对象可以由视图呈现。请注意,为嵌套列表视图构造 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()
                );
    }
}

模拟数据库类

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

数据库类与视图完全无关,只是在此处显示,以便可以在 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;
        }
    }
}

对一些后续问题的回答

哪种方法最适合更新数据?

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

我一般应该使用哪种方法来更新数据(数据肯定存储在数据库中)?

定义更新数据库中数据的通用方法超出了我在这里要回答的范围(这是一个纯粹基于意见的答案,因为有很多不同的方法可以实现这一点,因此对于 StackOverflow 来说不是主题)。如果是我,我会建立一个基于 SpringBoot 的休息服务,它连接到数据库并让我的客户端应用程序与之通信。如果应用程序不需要通过 Internet 进行通信,而只需要通过 LAN 与本地数据库进行通信,那么我将使用通过使应用程序成为 SpringBoot 应用程序并使用带有嵌入式 H2 数据库的 Spring Data 存储库来添加直接数据库访问.

是在特定行中修改时在db中修改还是等到用户在整个tableview中修改并单击保存按钮?

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

请您提供一些代码,以便在每个单元格下画一条线或使其像通常的 tableView 一样(一行灰色,一行不等...)

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

【讨论】:

  • 我想你已经给了我所有我需要的东西。谢谢,我想问你另一个问题,1-wich 方法最适合更新数据? 2-通常我应该使用哪种方法来更新数据(数据肯定存储在数据库中),3-是在修改数据库中的特定行修改或等到用户在整个表视图中修改并单击保存按钮时?
  • 请您提供一些代码,以便在每个单元格下画一条线或使其像通常的 tableView 一样(一行灰色,一行不等...)
  • 更新了解决后续问题的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多