JavaFX ListView:检索行的位置

时间:2016-01-29 03:17:02

标签: java listview javafx

JavaFX ListView是否有一个属性,该属性返回当前在其视图区域中显示的行?或者有没有办法将EventHandler分配给特定的行,当它在列表中的位置发生变化时发送事件(由于新对象被添加到List或从列表中删除的对象)?

本质上,我需要知道所选行的x,y坐标,以了解所选行是否在当前视图区域之外。因为如果发生这种情况,我需要调用ListView.scrollTo()方法将焦点返回到选定的行。如果用户选择一行,则程序会向列表中添加更多对象,从而将任何现有对象(包括所选对象)推离视图区域。

我尝试将ListChangeListener添加到下面的observable列表中,但似乎我无法将其应用于特定行。并且ListChangeListener似乎只指示行是否“添加/删除/更新/替换/置换”等。该事件处理程序/侦听器似乎没有给出特定行的x,y坐标,也没有给出一些一种布尔指示,表示该行位于ListView对象的当前视图区域内。

observableList.addListener(new ListChangeListener<Object>()
{
   @override
   public void onChanged(ListChangeListener.Change<? extends Object> c)
       {
           while(c.next())
           {
             if(c.wasAdded())
             {
             }
             else if(c.wasRemoved())
             {
             }
             .....
           }
       }
    }

我也看到获得ListViews.getSelectionModel().getSelectedItem().getObject().getLayoutX() or LayoutY() 出于某种原因,对于x-y坐标,总是返回0,0。

唯一能给我有效x-y坐标的是当我点击一行并启动OnMouseClickEvent(MouseEvent事件)回调时。该事件返回其单击位置的x,y位置。但遗憾的是,如果ListView动态地将新对象添加到列表顶部并且所选行的位置发生更改,则不会给出所选行的x-y位置。

非常感谢任何想法。

1 个答案:

答案 0 :(得分:1)

据我所知,没有API可以做到这一点。由于单元重用机制(在Cell documentation中描述),添加此功能有点棘手。

您需要在列表视图中使用单元工厂来跟踪列表中的哪些项目具有活动单元格,以及这些单元格的边界是什么(因为在滚动期间项目可能仍然具有单元格是可能的,但该单元格可能完全滚出视图)。您可以使用ObservableMap将项目映射到其单元格的边界。您需要在重复使用单元格时更新地图,删除旧项目并为新项目添加地图条目,如果单元格的边界发生更改或单元格在场景中移动,还需要更新地图。

以下是这样做的一个例子。我将核心功能放入自定义单元工厂并公开可观察的地图。例如,我创建了一个包含100个项目的ListView<Integer>,并演示该功能创建了第二个列表视图,其内容是具有完全显示的单元格的项目。第二个列表视图显示这些项及其边界(转换为包含单元格的列表视图中的坐标)。

理论上,这是相当高性能的,因为它通过观察大量数据来更新第二个列表视图;这在某种程度上得到了缓解,因为它只对每个细胞(不是每个项目)都这样做。在我的系统上它似乎运行正常,并且因为性能只是单元格数量的函数,它应该可以很好地扩展到大型列表。

import java.util.function.Function;
import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener.Change;
import javafx.collections.ObservableMap;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TrackCellsInListView extends Application {

    @Override
    public void start(Stage primaryStage) {

        // main list view:
        ListView<Integer> listView = new ListView<>();
        for (int i = 1 ; i <= 100; i++) {
            listView.getItems().add(i);
        }

        // create a cell factory that tracks items which have cells and the bounds of their cells:
        TrackingListCellFactory<Integer> cellFactory = new TrackingListCellFactory<>(i -> "Item "+i);
        listView.setCellFactory(cellFactory);

        // map from items with cells to bounds of the cells (in scene coordinates):
        ObservableMap<Integer, Bounds> boundsByItem = cellFactory.getBoundsByItem();

        // list view which will display which items have cells that are completely displayed
        // (i.e. whose bounds are completely contained in the list view bounds):

        ListView<Integer> visibleCells = new ListView<>();

        // cell factory for second list cell displays item and its bounds (translated to
        // list view coordinates):
        visibleCells.setCellFactory(lv -> {
            ListCell<Integer> cell = new ListCell<>();
            cell.textProperty().bind(Bindings.createStringBinding( 
                () -> {
                    if (cell.getItem()==null) {
                        return null ;
                    }
                    Bounds b = boundsByItem.get(cell.getItem());
                    if (b == null) {
                        return null ;
                    }
                    Bounds bounds = listView.sceneToLocal(b);
                    return String.format("%d: [%.1f, %.1f, %.1f, %.1f]", cell.getItem(), 
                            bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
                }, cell.itemProperty(), boundsByItem));
            return cell ;
        });

        // keep list of items in second list view up to date by observing map:
        boundsByItem.addListener((Change<? extends Integer, ? extends Bounds> c) -> {
            Bounds listBounds = listView.localToScene(listView.getBoundsInLocal());
            visibleCells.getItems().setAll(
                    boundsByItem.keySet().stream()
                    .filter(s -> listBounds.contains(boundsByItem.get(s)))
                    .sorted()
                    .collect(Collectors.toList()));
            System.out.println();
        });

        // usual UI setup:
        Scene scene = new Scene(new HBox(5, listView, visibleCells));
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static class TrackingListCellFactory<T> implements Callback<ListView<T>, ListCell<T>> {

        // function for mapping item to text to display:
        private Function<T,String> textFunction ;

        // map items which have cells to bounds of those cell in scene coordinates:
        private ObservableMap<T, Bounds> boundsByItem = FXCollections.observableHashMap();

        TrackingListCellFactory(Function<T,String> textFunction) {
            this.textFunction = textFunction ;
        }

        // default text function just calls toString():
        TrackingListCellFactory() {
            this(T::toString);
        }

        public ObservableMap<T, Bounds> getBoundsByItem() {
            return boundsByItem ;
        }


        @Override
        public ListCell<T> call(ListView<T> param) {

            //create cell that displays text according to textFunction:
            ListCell<T> cell = new ListCell<T>() {
                @Override
                protected void updateItem(T item, boolean empty) {
                    super.updateItem(item, empty);
                    setText(item == null ? null : textFunction.apply(item));
                }
            };

            // add and remove from map when cell is reused for different item:
            cell.itemProperty().addListener((obs, oldItem, newItem) -> {
                if (oldItem != null) {
                    boundsByItem.remove(oldItem);
                }
                if (newItem != null) {
                    boundsByItem.put(newItem, cell.localToScene(cell.getBoundsInLocal()));
                }
            });

            // update map when bounds of item change
            ChangeListener<Object> boundsChangeHandler = (obs, oldValue, newValue) -> {
                T item = cell.getItem() ;
                if (item != null) {
                    boundsByItem.put(item, cell.localToScene(cell.getBoundsInLocal()));
                }
            };

            // must update either if cell changes bounds, or if cell moves within scene (e.g.by scrolling):
            cell.boundsInLocalProperty().addListener(boundsChangeHandler);
            cell.localToSceneTransformProperty().addListener(boundsChangeHandler);

            return cell;
        }

    }

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