Java 如何修改tableview上单个单元格的属性

Java 如何修改tableview上单个单元格的属性,java,javafx,Java,Javafx,这是我第一次使用JavaFX(希望是最后一次),所以我不太清楚一切是如何工作的。我将简要地总结一下我的情况 我试图使我的表格突出显示特定列上的重复单元格 我需要可编辑的单元格,而不是我在工作中遇到的TableCell扩展,我今天大部分时间都在试图修复它们的bug,但毫无效果。我已经放弃了那种方法 我找到了TextFieldTableCell,但这不允许我扩展和重写像updateItem这样的函数。在这一点上,我对重新实现这些功能没有兴趣 目前我所做的工作如下: CollectionName.se

这是我第一次使用JavaFX(希望是最后一次),所以我不太清楚一切是如何工作的。我将简要地总结一下我的情况

  • 我试图使我的表格突出显示特定列上的重复单元格
  • 我需要可编辑的单元格,而不是我在工作中遇到的TableCell扩展,我今天大部分时间都在试图修复它们的bug,但毫无效果。我已经放弃了那种方法
  • 我找到了TextFieldTableCell,但这不允许我扩展和重写像updateItem这样的函数。在这一点上,我对重新实现这些功能没有兴趣
  • 目前我所做的工作如下:

    CollectionName.setCellValueFactory(new PropertyValueFactory<>("CollectionName"));
    CollectionName.setCellFactory(EditingCell.<Item>forTableColumn(this)); //At the moment this just passes though TextFieldTableCell, the parameter is totally inconsequential
    CollectionName.setOnEditCommit((CellEditEvent<Item, String> t) ->
    {
        ((Item) t.getTableView().getItems().get(
                t.getTablePosition().getRow())
                ).setCollectionName(t.getNewValue());
        System.out.println("Set on edit commit");
        if(isDuplicateName(t.getNewValue()))
        {
            t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().add("duplicate-cell");
            System.out.println("Duplicate");
        }
        else
        {
            t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().remove("duplicate-cell");
            System.out.println("Not duplicate");
        }
    });
    
    CollectionName.setCellValueFactory(新的PropertyValueFactory(“CollectionName”);
    CollectionName.setCellFactory(EditingCell.forTableColumn(this))//目前,它只是通过TextFieldTableCell,参数是完全无关紧要的
    CollectionName.setOnEditCommit((CellEditEvent t)->
    {
    ((项)t.getTableView().getItems().get(
    t、 getTablePosition().getRow())
    ).setCollectionName(t.getNewValue());
    System.out.println(“编辑提交时设置”);
    if(isDuplicateName(t.getNewValue()))
    {
    t、 getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().add(“重复单元格”);
    系统输出打印项次(“副本”);
    }
    其他的
    {
    t、 getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().remove(“重复单元格”);
    系统输出打印项次(“不重复”);
    }
    });
    

    这将按预期运行,但会突出显示整个列。我需要它只突出显示特定的单元格。我希望有一种方法可以简单地调用myTable.getCell(x,y).getStyleClass().add(“复制单元格”)或其他什么。我的意思是它毕竟是一张桌子

    任何涉及根据单元格项的特定状态和其他数据更改表单元格外观的问题的解决方案都是使用单元格工厂,该工厂返回相应更新其外观的单元格

    您尝试的方法的问题在于忽略了表视图重用单元格的事实。例如,如果表格包含大量数据且用户滚动,则不会创建新单元格,但滚动出视图的单元格将被重新用于滚动到视图中的新项目。由于发生这种情况时不更新单元格的样式,滚动将使错误的单元格高亮显示

    这里的逻辑有点复杂,因为每个单元格基本上都必须观察列中的所有值(无论它们当前是否显示)。我认为这里最简单的解决方案是独立维护一个
    observateSet
    ,它保存一个重复条目的列表,并让单元格观察它。这是一个实现。您可能可以将其分解为一个单独的类,用于cell factory(或一些方便的东西),以使其更加优雅和可重用

    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    import javafx.application.Application;
    import javafx.beans.Observable;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.BooleanBinding;
    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.collections.ObservableSet;
    import javafx.css.PseudoClass;
    import javafx.scene.Scene;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.TextFieldTableCell;
    import javafx.stage.Stage;
    import javafx.util.StringConverter;
    
    public class HighlightDuplicateTableCells extends Application {
    
        // create an observable list that fires events if the dataProperty of any elements change:
    
        private final ObservableList<Item> items = 
                FXCollections.observableArrayList(item -> new Observable[]{item.dataProperty()});
    
        // collection of strings that are duplicated in the data properties of all the items:
    
        private final ObservableSet<String> duplicateData = FXCollections.observableSet();
    
        private static final PseudoClass DUPLICATE_PC = PseudoClass.getPseudoClass("duplicate");
    
        private final StringConverter<String> identityStringConverter = new StringConverter<String>() {
    
            @Override
            public String toString(String object) {
                return object;
            }
    
            @Override
            public String fromString(String string) {
                return string;
            }
    
        };
    
        @Override
        public void start(Stage primaryStage) {
    
            // listener to maintain collection of duplicates:
            items.addListener((Change<? extends Item> change) -> updateDuplicateData());
    
            TableView<Item> table = new TableView<>();
            table.setEditable(true);
            table.setItems(items);
    
            TableColumn<Item, Number> idColumn = new TableColumn<>("Id");
            idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
    
            TableColumn<Item, String> dataColumn = new TableColumn<>("Data");
            dataColumn.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
    
            dataColumn.setCellFactory(tc -> {
    
                TextFieldTableCell<Item, String> cell = new TextFieldTableCell<Item, String>(identityStringConverter) {
    
                    // boolean binding that indicates if the current item is contained in the duplicateData set:
                    private BooleanBinding duplicate = Bindings.createBooleanBinding(
                            () -> duplicateData.contains(getItem()),
                            duplicateData, itemProperty());
    
                    // anonymous constructor just updates CSS pseudoclass if above binding changes:
                    {
                        duplicate.addListener((obs, wasDuplicate, isNowDuplicate) -> 
                            pseudoClassStateChanged(DUPLICATE_PC, isNowDuplicate));
                    }
                };
    
                return cell ;
            });
    
            table.getColumns().add(idColumn);
            table.getColumns().add(dataColumn);
    
            // note best to minimize changes to items.
            // creating a temp list and using items.setAll(...) achieves this:
    
            List<Item> tmp = new ArrayList<>();
            for (int i = 1 ; i <= 70; i++) {
                char c = (char)('@' + (i % 60));
                String data = Character.toString(c) ;
                tmp.add(new Item(i, data));
            }
    
            items.setAll(tmp);
    
            Scene scene = new Scene(table, 600, 600);
            scene.getStylesheets().add("duplicate-cell-example.css");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private void updateDuplicateData() {
    
            // TODO: may not be most efficient implementation
    
            // all data:
            List<String> data = items.stream().map(Item::getData).collect(Collectors.toList());
            // unique data:
            Set<String> uniqueData = new HashSet<>(data);
            // remove unique values from data:
            uniqueData.forEach(data::remove);
            // remaining values are duplicates: replace contents of duplicateData with these:
            duplicateData.clear();
            duplicateData.addAll(data);
        }
    
        public static class Item {
            private final int id ;
            private final StringProperty data = new SimpleStringProperty();
    
            public Item(int id, String data) {
                this.id = id ;
                setData(data);
            }
    
            public final StringProperty dataProperty() {
                return this.data;
            }
    
    
            public final String getData() {
                return this.dataProperty().get();
            }
    
    
            public final void setData(final String data) {
                this.dataProperty().set(data);
            }
    
            public int getId() {
                return id ;
            }
    
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    任何涉及根据单元格项和其他数据的特定状态更改表单元格外观的问题的解决方案都是使用单元格工厂,该工厂返回相应更新其外观的单元格

    您尝试的方法的问题在于忽略了表视图重用单元格的事实。例如,如果表格包含大量数据且用户滚动,则不会创建新单元格,但滚动出视图的单元格将被重新用于滚动到视图中的新项目。由于发生这种情况时不更新单元格的样式,滚动将使错误的单元格高亮显示

    这里的逻辑有点复杂,因为每个单元格基本上都必须观察列中的所有值(无论它们当前是否显示)。我认为这里最简单的解决方案是独立维护一个
    observateSet
    ,它保存一个重复条目的列表,并让单元格观察它。这是一个实现。您可能可以将其分解为一个单独的类,用于cell factory(或一些方便的东西),以使其更加优雅和可重用

    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    import javafx.application.Application;
    import javafx.beans.Observable;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.BooleanBinding;
    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.collections.ObservableSet;
    import javafx.css.PseudoClass;
    import javafx.scene.Scene;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.TextFieldTableCell;
    import javafx.stage.Stage;
    import javafx.util.StringConverter;
    
    public class HighlightDuplicateTableCells extends Application {
    
        // create an observable list that fires events if the dataProperty of any elements change:
    
        private final ObservableList<Item> items = 
                FXCollections.observableArrayList(item -> new Observable[]{item.dataProperty()});
    
        // collection of strings that are duplicated in the data properties of all the items:
    
        private final ObservableSet<String> duplicateData = FXCollections.observableSet();
    
        private static final PseudoClass DUPLICATE_PC = PseudoClass.getPseudoClass("duplicate");
    
        private final StringConverter<String> identityStringConverter = new StringConverter<String>() {
    
            @Override
            public String toString(String object) {
                return object;
            }
    
            @Override
            public String fromString(String string) {
                return string;
            }
    
        };
    
        @Override
        public void start(Stage primaryStage) {
    
            // listener to maintain collection of duplicates:
            items.addListener((Change<? extends Item> change) -> updateDuplicateData());
    
            TableView<Item> table = new TableView<>();
            table.setEditable(true);
            table.setItems(items);
    
            TableColumn<Item, Number> idColumn = new TableColumn<>("Id");
            idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
    
            TableColumn<Item, String> dataColumn = new TableColumn<>("Data");
            dataColumn.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
    
            dataColumn.setCellFactory(tc -> {
    
                TextFieldTableCell<Item, String> cell = new TextFieldTableCell<Item, String>(identityStringConverter) {
    
                    // boolean binding that indicates if the current item is contained in the duplicateData set:
                    private BooleanBinding duplicate = Bindings.createBooleanBinding(
                            () -> duplicateData.contains(getItem()),
                            duplicateData, itemProperty());
    
                    // anonymous constructor just updates CSS pseudoclass if above binding changes:
                    {
                        duplicate.addListener((obs, wasDuplicate, isNowDuplicate) -> 
                            pseudoClassStateChanged(DUPLICATE_PC, isNowDuplicate));
                    }
                };
    
                return cell ;
            });
    
            table.getColumns().add(idColumn);
            table.getColumns().add(dataColumn);
    
            // note best to minimize changes to items.
            // creating a temp list and using items.setAll(...) achieves this:
    
            List<Item> tmp = new ArrayList<>();
            for (int i = 1 ; i <= 70; i++) {
                char c = (char)('@' + (i % 60));
                String data = Character.toString(c) ;
                tmp.add(new Item(i, data));
            }
    
            items.setAll(tmp);
    
            Scene scene = new Scene(table, 600, 600);
            scene.getStylesheets().add("duplicate-cell-example.css");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private void updateDuplicateData() {
    
            // TODO: may not be most efficient implementation
    
            // all data:
            List<String> data = items.stream().map(Item::getData).collect(Collectors.toList());
            // unique data:
            Set<String> uniqueData = new HashSet<>(data);
            // remove unique values from data:
            uniqueData.forEach(data::remove);
            // remaining values are duplicates: replace contents of duplicateData with these:
            duplicateData.clear();
            duplicateData.addAll(data);
        }
    
        public static class Item {
            private final int id ;
            private final StringProperty data = new SimpleStringProperty();
    
            public Item(int id, String data) {
                this.id = id ;
                setData(data);
            }
    
            public final StringProperty dataProperty() {
                return this.data;
            }
    
    
            public final String getData() {
                return this.dataProperty().get();
            }
    
    
            public final void setData(final String data) {
                this.dataProperty().set(data);
            }
    
            public int getId() {
                return id ;
            }
    
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    这基本上是James_D的方法,但它将更新所需的时间从
    Ω(n²)
    最坏情况(
    n
    =列表大小)提高到
    O(m)
    ,其中
    m
    是更改的数量(
    1
    用于属性更新;列表更新时添加/删除的元素数量)

    此性能是通过将发生次数存储在
    可观察地图中来实现的:


    请注意,有些部分(创建和填充表格,创建列)是从@James_D的答案中复制的,因为这样做是最佳实践。

    这基本上是James_D的方法,但它将更新所需的时间从
    Ω(n²)
    最坏情况(
    n
    =列表大小)提高到
    O(m)
    其中
    m
    是更改数(
    1
    用于属性更新;列表更新中添加/删除的元素数)

    此性能是通过将发生次数存储在
    可观察地图中来实现的:


    请注意,有些部分(创建和填充表格、创建列)是从@James_D的答案中复制的,因为这样做是最佳做法。

    不确定为什么说不能覆盖
    TextFieldTableCell.updateItem(…)
    。这不是一个
    最终的
    方法。只需重写该方法即可添加或删除
    重复单元格
    样式类。(没有
    getCell(x,y)
    方法的原因是
    (x,y)
    可能没有单元格,是否有取决于视图的状态(滚动位置等)。任何逻辑都应该基于数据,而不是视图。)我将第一个参数显式地表示为字符串,其中它需要是T
    private final ObservableMap<String, Integer> valueOccuranceCounts = FXCollections.observableHashMap();
    
    private final ChangeListener<String> changeListener = (observable, oldValue, newValue) -> {
        valueOccuranceCounts.computeIfPresent(oldValue, REMOVE_UPDATER);
        valueOccuranceCounts.merge(newValue, 1, ADD_MERGER);
    };
    
    private static final BiFunction<Integer, Integer, Integer> ADD_MERGER = (oldValue, newValue) -> oldValue + 1;
    private static final BiFunction<String, Integer, Integer> REMOVE_UPDATER = (key, value) -> {
        int newCount = value - 1;
        // remove mapping, if the value would become 0
        return newCount == 0 ? null : newCount;
    };
    
    private final ListChangeListener<Item> listChangeListener = (ListChangeListener.Change<? extends Item> c) -> {
        while (c.next()) {
            if (c.wasRemoved()) {
                for (Item r : c.getRemoved()) {
                    // decrease count and remove listener
                    this.valueOccuranceCounts.computeIfPresent(r.getData(), REMOVE_UPDATER);
                    r.dataProperty().removeListener(this.changeListener);
                }
            }
            if (c.wasAdded()) {
                for (Item a : c.getAddedSubList()) {
                    // increase count and add listener
                    this.valueOccuranceCounts.merge(a.getData(), 1, ADD_MERGER);
                    a.dataProperty().addListener(this.changeListener);
                }
            }
        }
    };
    
    private final ObservableList<Item> items;
    
    {
        items = FXCollections.observableArrayList();
        items.addListener(listChangeListener);
    }
    
    private static final PseudoClass DUPLICATE = PseudoClass.getPseudoClass("duplicate");
    private static final String FIRST_COLUMN_CLASS = "first-column";
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        TableView<Item> tableView = new TableView<>(items);
    //    tableView.getSelectionModel().setCellSelectionEnabled(true);
        tableView.setEditable(true);
    
        TableColumn<Item, String> column = new TableColumn<>("data");
        column.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
    
        column.setCellFactory(col -> new TextFieldTableCell<Item, String>() {
    
            // boolean binding that indicates if the current item is contained in the duplicateData set:
            private final BooleanBinding duplicate = Bindings.createBooleanBinding(
                    () -> valueOccuranceCounts.getOrDefault(getItem(), 1) >= 2,
                    valueOccuranceCounts, itemProperty());
    
            // anonymous constructor just updates CSS pseudoclass if above binding changes:
            {
                duplicate.addListener((observable, oldValue, newValue)
                        -> pseudoClassStateChanged(DUPLICATE, newValue));
            }
        });
    
        TableColumn<Item, Number> idColumn = new TableColumn<>("id");
        idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
    
        tableView.getColumns().addAll(idColumn, column);
        tableView.getColumns().addListener((Observable observable) -> {
            // keep style class marking the cells of the column as
            // belonging to the first column up to date 
            if (tableView.getColumns().get(0) == column) {
                if (!column.getStyleClass().contains(FIRST_COLUMN_CLASS)) {
                    column.getStyleClass().add(FIRST_COLUMN_CLASS);
                }
            } else {
                column.getStyleClass().remove(FIRST_COLUMN_CLASS);
            }
        });
    
        // note best to minimize changes to items.
        // creating a temp list and using items.setAll(...) achieves this:
        final int count = 70;
        List<Item> tmp = Arrays.asList(new Item[count]);
        for (int i = 0; i < count; i++) {
            tmp.set(i, new Item(Integer.toString(i % 60)));
        }
    
        items.setAll(tmp);
    
        Scene scene = new Scene(tableView);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static class Item {
    
        private static int counter = 0;
    
        private final StringProperty data;
        private final int id = counter++;
    
        public Item(String data) {
            this.data = new SimpleStringProperty(data);
        }
    
        public final StringProperty dataProperty() {
            return this.data;
        }
    
        public final String getData() {
            return this.dataProperty().get();
        }
    
        public final void setData(final String data) {
            this.dataProperty().set(data);
        }
    
        public int getId() {
            return id ;
        }
    
    }