2016-01-29 58 views
1

JavaFX ListView是否有一個屬性,它返回哪些行當前顯示在其視圖區域中?或者是否有辦法將EventHandler分配給特定的行,以便在列表中的位置發生更改時(由於將新對象添加到列表或將對象從列表中刪除)發生事件?JavaFX ListView:檢索某一行的位置

實質上,我需要知道所選行的x,y座標,以瞭解選定行是否與當前視圖區域關閉。因爲如果發生這種情況,我需要調用ListView.scrollTo()方法將焦點返回給選定的行。如果用戶選擇一行,則會出現這種情況,程序會向列表中添加更多對象,以便將所有現有對象(包括所選對象)從視圖區域中推出。

我試着將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() 出於某種原因總是返回0,0 X-Y座標。

唯一看起來給我有效的x-y座標的是當我點擊一行並啓動了OnMouseClickEvent(MouseEvent事件)回調。該事件返回其點擊的x,y位置。但不幸的是,如果ListView動態地將新對象添加到列表的頂部並且所選行的位置發生更改,那麼不會給我所選行的x-y位置。

任何想法將不勝感激。

+0

沒有您需要的API的API。 Tomas Mikula的第三方庫[Flowless](https:// github。com/TomasMikula/Flowless)提供了一些類似於你正在尋找的功能(我認爲)。 –

+1

請注意,在很多問題中,您會將數據與數據視圖混淆。例如:「我嘗試向'ObservableList'添加一個'ListChangeListener' ...該事件處理程序似乎沒有給出特定行的x,y座標」。問題在於列表中的項目沒有座標(*單元格*有座標,而不是項目):該項目可能甚至沒有任何相應的單元格或任何UI。請參閱['Cell'文檔](http://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Cell.html)。這些都不會阻止'getVisibleRowIndices()'API;但它只是不存在 –

+0

另請參見http://stackoverflow.com/questions/18965871/how-to-get-position-of-an-item-in-listview-in-javafx –

回答

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); 
    } 
}