有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

JavaFXListView:检索行的位置

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

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

我尝试将ListChangeListener添加到下面的可观察列表中,但似乎无法将其应用于特定行。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) 个答案

  1. # 1 楼答案

    据我所知,没有API可以做到这一点。添加此功能有点棘手,因为单元重用机制(在^{} 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);
        }
    }