TreeView - 不允许选择某些 TreeItems

TreeView - Certain TreeItems are not allowed to be selected

我创建了一个 Treeview (javafx),它看起来像:

我现在想要只有 "Tour"-TreeItems 是可选的。 但是我不知道怎么办。

我已经尝试使用 ChangeListener,但我只能用它刷新选项卡 (TabPane) 的内容...刷新工作正常...但是可以选择 "Delivery"-TreeItems :(

代码:

public void showTours(List<Tour> pTours) {

    treeViewPane.getSelectionModel().selectedItemProperty().addListener(treeItemChangeListener);

    TreeItem tTreeRoot = new TreeItem<>("Root", new ImageView(Icons.getIcon24("truck_blue.png")));
    tTreeRoot.setExpanded(true);
    treeViewPane.setRoot(tTreeRoot);

    for (Tour tTour : pTours) {

        TreeItem<Object> tTourItem = new TreeItem<>(tTour);
        tTreeRoot.getChildren().add(tTourItem);

        if (tTour.getDeliveries() != null) {
            for (Delivery tDelivery : tTour.getDeliveries()) {

                TreeItem<Object> tDeliveryItem = new TreeItem<>(tDelivery);
                tTourItem.getChildren().add(tDeliveryItem);
            }
        }
    }
}

private final ChangeListener<TreeItem> treeItemChangeListener = (observable, oldValue, newValue) -> {

    if (newValue != null && newValue.getValue() instanceof Tour){
        Tour selectedTour = (Tour) newValue.getValue();
        reloadTabContent(selectedTour);
    }
};

希望你能帮助我。 如果你能给我看示例代码,我会很高兴:)

谢谢

在 JavaFX 的任何控件中修改 selection 行为似乎有点痛苦;但是 "proper" 方法是为树定义自定义 selection 模型。最简单的方法是包装默认的 selection 模型,并将方法调用委托给它,如果 selection 索引用于不应该的项目,则否决 selection不会 selected.

在调用 select 方法时尽可能 select 是个好主意,否则键盘导航会中断。

这是一个实现:

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class CustomTreeSelectionModelExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TreeItem<Object> root = new TreeItem<>("Root");
        for (int i = 1 ; i <= 5 ; i++) {
            TreeItem<Object> item = new TreeItem<>(new Tour("Tour "+i));
            for (int j = 1 ; j <= 5; j++) {
                Delivery delivery = new Delivery("Delivery "+j);
                item.getChildren().add(new TreeItem<>(delivery));
            }
            root.getChildren().add(item);
        }
        TreeView<Object> tree = new TreeView<>();
        tree.setSelectionModel(new TourSelectionModel(tree.getSelectionModel(), tree));
        tree.setRoot(root);

        primaryStage.setScene(new Scene(new BorderPane(tree), 400, 400));
        primaryStage.show();
    }

    public static class TourSelectionModel extends MultipleSelectionModel<TreeItem<Object>> {

        private final MultipleSelectionModel<TreeItem<Object>> selectionModel ;
        private final TreeView<Object> tree ;

        public TourSelectionModel(MultipleSelectionModel<TreeItem<Object>> selectionModel, TreeView<Object> tree) {
            this.selectionModel = selectionModel ;
            this.tree = tree ;
            selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
        }

        @Override
        public ObservableList<Integer> getSelectedIndices() {
            return selectionModel.getSelectedIndices() ;
        }

        @Override
        public ObservableList<TreeItem<Object>> getSelectedItems() {
            return selectionModel.getSelectedItems() ;
        }

        @Override
        public void selectIndices(int index, int... indices) {

            List<Integer> indicesToSelect = Stream.concat(Stream.of(index), IntStream.of(indices).boxed())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .collect(Collectors.toList());


            if (indicesToSelect.isEmpty()) {
                return ;
            }
            selectionModel.selectIndices(indicesToSelect.get(0), 
                    indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());

        }

        @Override
        public void selectAll() {
            List<Integer> indicesToSelect = IntStream.range(0, tree.getExpandedItemCount())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .boxed()
                    .collect(Collectors.toList());
            if (indicesToSelect.isEmpty()) {
                return ;
            }
            selectionModel.selectIndices(0, 
                    indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());
        }

        @Override
        public void selectFirst() {
            IntStream.range(0, tree.getExpandedItemCount())
                .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                .findFirst()
                .ifPresent(selectionModel::select);
        }

        @Override
        public void selectLast() {
            IntStream.iterate(tree.getExpandedItemCount() - 1, i -> i - 1)
                .limit(tree.getExpandedItemCount())
                .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                .findFirst()
                .ifPresent(selectionModel::select);
        }

        @Override
        public void clearAndSelect(int index) {
            int toSelect = index ;
            int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
            while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
                toSelect = toSelect + direction  ;
            }
            if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
                selectionModel.clearAndSelect(toSelect);
            }
        }

        @Override
        public void select(int index) {
            int toSelect = index ;
            int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
            while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
                toSelect = toSelect + direction  ;
            }
            if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
                selectionModel.select(toSelect);
            }
        }

        @Override
        public void select(TreeItem<Object> obj) {
            if (obj.getValue() instanceof Tour) {
                selectionModel.select(obj);
            }
        }

        @Override
        public void clearSelection(int index) {
            selectionModel.clearSelection(index);
        }

        @Override
        public void clearSelection() {
            selectionModel.clearSelection();
        }

        @Override
        public boolean isSelected(int index) {
            return selectionModel.isSelected(index);
        }

        @Override
        public boolean isEmpty() {
            return selectionModel.isEmpty();
        }

        @Override
        public void selectPrevious() {
            int current = selectionModel.getSelectedIndex() ;
            if (current > 0) {
                IntStream.iterate(current - 1, i -> i - 1).limit(current)
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .findFirst()
                    .ifPresent(selectionModel::select);
            }
        }

        @Override
        public void selectNext() {
            int current = selectionModel.getSelectedIndex() ;
            if (current < tree.getExpandedItemCount() - 1) {
                IntStream.range(current + 1, tree.getExpandedItemCount())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .findFirst()
                    .ifPresent(selectionModel::select);
            }
        }

    }

    public static class Tour {

        private final String name ;

        public Tour(String name) {
            this.name = name ;
        }

        public String getName() {
            return name ;
        }

        @Override
        public String toString() {
            return getName();
        }

    }

    public static class Delivery {
        private final String name;

        public Delivery(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return getName();
        }
    }

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

我修改了 James_D 发布的 selection-model,使其更通用一些,以便您可以指定自定义过滤器。实现是:

public class FilteredTreeViewSelectionModel<S> extends MultipleSelectionModel<TreeItem<S>> {

    private final TreeView<S> treeView;
    private final MultipleSelectionModel<TreeItem<S>> selectionModel;
    private final TreeItemSelectionFilter<S> filter;

    public FilteredTreeViewSelectionModel(
            TreeView<S> treeView,
            MultipleSelectionModel<TreeItem<S>> selectionModel, 
            TreeItemSelectionFilter<S> filter) {
        this.treeView = treeView;
        this.selectionModel = selectionModel;
        this.filter = filter;
        selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
    }

    @Override
    public ObservableList<Integer> getSelectedIndices() {
        return this.selectionModel.getSelectedIndices();
    }

    @Override
    public ObservableList<TreeItem<S>> getSelectedItems() {
        return this.selectionModel.getSelectedItems();
    }

    private int getRowCount() {
        return this.treeView.getExpandedItemCount();
    }

    @Override
    public boolean isSelected(int index) {
        return this.selectionModel.isSelected(index);
    }

    @Override
    public boolean isEmpty() {
        return this.selectionModel.isEmpty();
    }

    @Override
    public void select(int index) {
        // If the row is -1, we need to clear the selection.
        if (index == -1) {
            this.selectionModel.clearSelection();
        } else if (index >= 0 && index < getRowCount()) {
            // If the tree-item at the specified row-index is selectable, we
            // forward select call to the internal selection-model.
            TreeItem<S> treeItem = this.treeView.getTreeItem(index);
            if (this.filter.isSelectable(treeItem)) {
                this.selectionModel.select(index);
            }
        }
    }

    @Override
    public void select(TreeItem<S> treeItem) {
        if (treeItem == null) {
            // If the provided tree-item is null, and we are in single-selection
            // mode we need to clear the selection.
            if (getSelectionMode() == SelectionMode.SINGLE) {
                this.selectionModel.clearSelection();
            }
            // Else, we just forward to the internal selection-model so that
            // the selected-index can be set to -1, and the selected-item
            // can be set to null.
            else {
                this.selectionModel.select(null);
            }
        } else if (this.filter.isSelectable(treeItem)) {
            this.selectionModel.select(treeItem);
        }
    }

    @Override
    public void selectIndices(int index, int... indices) {
        // If we have no trailing rows, we forward to normal row-selection.
        if (indices == null || indices.length == 0) {
            select(index);
            return;
        }

        // Filter indices so that we only end up with those indices whose
        // corresponding tree-items are selectable.
        int[] filteredIndices = IntStream.concat(IntStream.of(index), Arrays.stream(indices)).filter(indexToCheck -> {
            TreeItem<S> treeItem = treeView.getTreeItem(indexToCheck);
            return (treeItem != null) && filter.isSelectable(treeItem);
        }).toArray();

        // If we have indices left, we proceed to forward to internal selection-model.
        if (filteredIndices.length > 0) {
            int newIndex = filteredIndices[0];
            int[] newIndices = Arrays.copyOfRange(filteredIndices, 1, filteredIndices.length);
            this.selectionModel.selectIndices(newIndex, newIndices);
        }
    }

    @Override
    public void clearAndSelect(int index) {
        // If the index is out-of-bounds we just clear and return.
        if (index < 0 || index >= getRowCount()) {
            clearSelection();
            return;
        }

        // Get tree-item at index.
        TreeItem<S> treeItem = this.treeView.getTreeItem(index);

        // If the tree-item at the specified row-index is selectable, we forward
        // clear-and-select call to the internal selection-model.
        if (this.filter.isSelectable(treeItem)) {
            this.selectionModel.clearAndSelect(index);
        }
        // Else, we just do a normal clear-selection call.
        else {
            this.selectionModel.clearSelection();
        }
    }

    @Override
    public void selectAll() {
        int rowCount = getRowCount();

        // If we are in single-selection mode, we exit prematurely as
        // we cannot select all rows.
        if (getSelectionMode() == SelectionMode.SINGLE) {
            return;
        }

        // If we only have a single index to select, we forward to the
        // single-index select-method.
        if (rowCount == 1) {
            select(0);
        }
        // Else, if we have more than one index available, we construct an array
        // of all the indices and forward to the selectIndices-method.
        else if (rowCount > 1) {
            int index = 0;
            int[] indices = IntStream.range(1, rowCount).toArray();
            selectIndices(index, indices);
        }
    }

    @Override
    public void clearSelection(int index) {
        this.selectionModel.clearSelection(index);
    }

    @Override
    public void clearSelection() {
        this.selectionModel.clearSelection();
    }

    @Override
    public void selectFirst() {
        Optional<TreeItem<S>> firstItem = IntStream.range(0, getRowCount()).
            mapToObj(this.treeView::getTreeItem).
            filter(this.filter::isSelectable).
            findFirst();
        firstItem.ifPresent(this.selectionModel::select);
    }

    @Override
    public void selectLast() {
        int rowCount = getRowCount();
        Optional<TreeItem<S>> lastItem = IntStream.iterate(rowCount - 1, i -> i - 1).
            limit(rowCount).
            mapToObj(this.treeView::getTreeItem).
            filter(this.filter::isSelectable).
            findFirst();
        lastItem.ifPresent(this.selectionModel::select);
    }

    private int getFocusedIndex() {
        FocusModel<TreeItem<S>> focusModel = this.treeView.getFocusModel();
        return (focusModel == null) ? -1 : focusModel.getFocusedIndex();
    }

    @Override
    public void selectPrevious() {
        int focusIndex = getFocusedIndex();
        // If we have nothing selected, wrap around to the last index.
        int startIndex = (focusIndex == -1) ? getRowCount() : focusIndex;
        if (startIndex > 0) {
            Optional<TreeItem<S>> previousItem = IntStream.iterate(startIndex - 1, i -> i - 1).
                limit(startIndex).
                mapToObj(this.treeView::getTreeItem).
                filter(this.filter::isSelectable).
                findFirst();
            previousItem.ifPresent(this.selectionModel::select);
        }
    }

    @Override
    public void selectNext() {
        // If we have nothing selected, starting at -1 will work out correctly
        // because we'll search from 0 onwards.
        int startIndex = getFocusedIndex();
        if (startIndex < getRowCount() - 1) {
            Optional<TreeItem<S>> nextItem = IntStream.range(startIndex + 1, getRowCount()).
                mapToObj(this.treeView::getTreeItem).
                filter(this.filter::isSelectable).
                findFirst();
            nextItem.ifPresent(this.selectionModel::select);
        }
    }
}

我更改了 selectIndex(int) 方法,因为如果过滤器允许,此方法应该将基于索引的 selection 转发到其内部 selection 模型。我不同意 while 循环逻辑,因为您明确将要 selected 的索引传递给此方法,希望它可以 select 它。预期的行为应该是它应该忽略 select 如果过滤器不允许它。我还通过为 index == -1 案例添加一个 catch 来充实该方法,因为我们需要在发生这种情况时清除 selection。

select(TreeItem) 方法也发生了很大的变化,检查 null 参数并单独处理它,因此如果我们处于单 selection 模式,我们需要清除 selection,否则我们调用 select(null) 以便内部 selection-model 正确处理它。如果我们确实有一个树项,我们只需检查过滤器并传递给内部 selection-model。

selectIndices(int, int[]) 方法的不同之处还在于它应该处理 indices 数组可能为空或长度为 0 的情况。如果是这种情况,select(index)应该调用方法。

我实现 clearAndSelect(int) 方法的方式与其他方法略有不同。我在开始时进行边界检查,看看我们是否需要立即调用 clearSelection()。否则,我会通过过滤器检查索引处的 TreeItem 是否 select 可用。如果是我们转发到内部 selection-model,否则我们就清除。我也不同意其他实现中使用的 while 循环方法。

James_D 实现的 selectPrevious()selectNext() 方法实际上存在一个错误。如果什么都没有 selected 你需要在调用 selectPrevious() 时捕捉到最后一个索引。 selectFirst() 的情况正好相反,如果没有 selected,则需要捕捉到第一个索引。然后,您可以根据这些新索引找到过滤器允许的第一个项目。您还需要使用 focus-index 而 not selected-index。如果您查看 MultipleSelectionModelBase class 以供参考,您可以看到此行为。

TreeItemSelectionFilter指定为:

public interface TreeItemSelectionFilter<S> {

    public boolean isSelectable(TreeItem<S> treeItem);
}

对于您的特定情况,您可以将它们连接在一起:

....
MultipleSelectionModel<TreeItem<Object>> selectionModel = tree.getSelectionModel();
TreeItemSelectionFilter<Object> filter = treeItem -> treeItem.getValue() instanceof Tour;
FilteredTreeViewSelectionModel<Object> filteredSelectionModel = new FilteredTreeViewSelectionModel<>(tree, selectionModel, filter);
tree.setSelectionModel(filteredSelectionModel);
....

我已经上传了示例应用程序 here 的源代码,以便您可以轻松地自己测试 FilteredTreeViewSelectionModel 的行为。将其与默认的 selection-model 进行比较,看看您是否对行为感到满意。