整个列表的分页 TableView 排序错误

Error with Paginated TableView sort for whole list

我正在尝试实现一个允许按 JavaFX 中的所有项目排序的分页 TableView。我从这里实现了分页 table 视图:。由 jewelsea 和 tim buthe 提供。

我在想,因为 table 视图只访问项目的子列表,所以我想根据我对关于 Java 文档排序的部分:https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/TableView.html#setItems-javafx.collections.ObservableList-

 // bind the sortedList comparator to the TableView comparator
 //i m guessing it extends the sorting from the table to the actual list?
 sortedList.comparatorProperty().bind(tableView.comparatorProperty());

然后为相同的子列表索引刷新 table视图(由于整个列表已排序,现在应该对其进行排序)。

基本上,我想使用 table 列比较器对完整列表进行排序,然后使用新的排序列表“刷新”table 视图。这可行吗?或者有更简单的方法来解决这个问题吗?

我还参考了其他参考资料 material,例如:https://incepttechnologies.blogspot.com/p/javafx-tableview-with-pagination-and.html 但我发现它很难理解,因为到处都是含糊不清的解释。

快速提取我的 TouchDisplayEmulatorController 中的核心组件 class

import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Pagination;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.List;

public class TouchDisplayEmulatorController extends Application {
    public TableView sensorsTable;
    public List<Sensor> sensors;
    public int rowsPerPage = 14;
    public GridPane grids = new GridPane();
    public long timenow;

    public void start(final Stage stage) throws Exception {
        grids = new GridPane();
        setGridPane();

        Scene scene = new Scene(grids, 1024, 768);
        stage.setScene(scene);
        stage.setTitle("Table pager");
        stage.show();
    }

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

    public void setGridPane(){
        processSensors();
        sensorsGrid();
    }

    public void sensorsGrid(){
        buildTable();
        int numOfPages = 1;
        if (sensors.size() % rowsPerPage == 0) {
            numOfPages = sensors.size() / rowsPerPage;
        } else if (sensors.size() > rowsPerPage) {
            numOfPages = sensors.size() / rowsPerPage + 1;
        }
        Pagination pagination = new Pagination((numOfPages), 0);
        pagination.setPageFactory(this::createPage);
        pagination.setMaxPageIndicatorCount(numOfPages);
        grids.add(pagination, 0, 0);
    }

    private Node createPage(int pageIndex) {
        int fromIndex = pageIndex * rowsPerPage;
        int toIndex = Math.min(fromIndex + rowsPerPage, sensors.size());
        sensorsTable.setItems(FXCollections.observableArrayList(sensors.subList(fromIndex, toIndex)));

        return new BorderPane(sensorsTable);
    }

    public void processSensors(){
        sensors = new ArrayList<>();
//        long timenow = OffsetDateTime.now(ZoneOffset.UTC).toInstant().toEpochMilli()/1000;
//        StringTokenizer hildetoken = new StringTokenizer(msg);

        for (int i=0; i<20; i++) {
            sensors.add(new Sensor(String.valueOf(i), "rid-"+i, "sid-"+i, "0", "0", "no condition"));
        }
    }


    public void buildTable() {
        sensorsTable = new TableView();
        TableColumn<Sensor, String> userid = new TableColumn<>("userid");
        userid.setCellValueFactory(param -> param.getValue().userid);
        userid.setPrefWidth(100);
        TableColumn<Sensor, String> resourceid = new TableColumn<>("resourceid");
        resourceid.setCellValueFactory(param -> param.getValue().resourceid);
        resourceid.setPrefWidth(100);
        TableColumn<Sensor, String> column1 = new TableColumn<>("sid");
        column1.setCellValueFactory(param -> param.getValue().sid);
        column1.setPrefWidth(100);
        TableColumn<Sensor, String> column2 = new TableColumn<>("timestamp");
        column2.setCellValueFactory(param -> param.getValue().timestamp);
        column2.setPrefWidth(100);
        TableColumn<Sensor, String> column3 = new TableColumn<>("reading");
        column3.setCellValueFactory(param -> param.getValue().reading);
        column3.setPrefWidth(100);
        TableColumn<Sensor, String> column4 = new TableColumn<>("last contacted");
        column4.setCellFactory(new Callback<TableColumn<Sensor, String>, TableCell<Sensor, String>>() {
            @Override
            public TableCell<Sensor, String> call(TableColumn<Sensor, String> sensorStringTableColumn) {
                return new TableCell<Sensor, String>() {
                    public void updateItem(String item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!isEmpty()) {
                            this.setTextFill(Color.WHITE);
                            if (item.contains("@")) {
                                this.setTextFill(Color.BLUEVIOLET);
                            } else if (item.equals("> 8 hour ago")) {
                                this.setStyle("-fx-background-color: red;");
                            } else if (item.equals("< 8 hour ago")) {
                                this.setStyle("-fx-background-color: orange;");
                                //this.setTextFill(Color.ORANGE);
                            } else if (item.equals("< 4 hour ago")) {
                                this.setStyle("-fx-background-color: yellow;");
                                this.setTextFill(Color.BLACK);
                            } else if (item.equals("< 1 hour ago")) {
                                this.setStyle("-fx-background-color: green;");
                                //this.setTextFill(Color.GREEN);
                            }
                            setText(item);
                        }
                    }
                };
            }
        });
        column4.setCellValueFactory(param -> param.getValue().condition);
        column4.setPrefWidth(100);
        sensorsTable.getColumns().addAll(userid, resourceid, column1, column2, column3, column4);
    }
}
class Sensor {
    public SimpleStringProperty userid;
    public SimpleStringProperty resourceid;
    public SimpleStringProperty sid;
    public SimpleStringProperty timestamp;
    public SimpleStringProperty reading;
    public SimpleStringProperty condition;


    public Sensor(String userid, String resourceid, String sid, String timestamp, String reading, String condition){
        this.userid = new SimpleStringProperty(userid);
        this.resourceid = new SimpleStringProperty(resourceid);
        this.sid = new SimpleStringProperty(sid);
        this.timestamp = new SimpleStringProperty(timestamp);
        this.reading = new SimpleStringProperty(reading);
        this.condition = new SimpleStringProperty(condition);
        //we can use empty string or condition 3 here
    }

    public Sensor(String sid, String timestamp, String reading, String condition){
        this.userid = new SimpleStringProperty("-1");
        this.resourceid = new SimpleStringProperty("-1");
        this.sid = new SimpleStringProperty(sid);
        this.timestamp= new SimpleStringProperty(timestamp);
        this.reading= new SimpleStringProperty(reading);
        this.condition = new SimpleStringProperty(condition);
    }

    public String getUserid() { return this.userid.toString(); }
    public String getResourceid() { return this.resourceid.toString(); }
    public String getSid() { return this.sid.toString(); }
    public String getTimestamp() { return this.timestamp.toString(); }
    public String getReading() { return this.reading.toString(); }
    public String getCondition() { return this.condition.toString(); }
    public String toString() { return "userid: "+getUserid()+" resourceid: "+getResourceid()+" sid: "+getSid()+
            "\ntimestamp: "+getTimestamp()+" reading: "+getReading()+" condition: "+getCondition();}
}

单独 class:

public class tester {
    public static void main(String[] args) {
        Application.launch(TouchDisplayEmulatorController.class, args);
    }
}

首先,不清楚 Pagination 是否是正确的控件,尽管第 selection 页的 UI 非常好。您的问题可能源于您每次都将 TableView 放入新的 BorderPane,或者可能源于您正在使用 TableView.setItems().

一种有效的方法是使用 FilteredList 来处理分页,并且只将 TableView 作为布局中的静态元素,而不是在分页的动态图形区域中。为了满足让 Pagination 做某事的需要,创建了带有页码的 Text

新的 属性 已添加到传感器 - ordinalNumber。这用于控制分页的过滤器。过滤器将动态更改为 select 只有那些在特定范围内具有 ordinalNumber 的传感器。范围由分页的 currentPageIndexProperty 控制。 属性 上有一个侦听器,每次更改页面时都会重新生成 FilteredList 的谓词 属性。

这处理了页面更改,但是对整个列表进行排序呢?首先,FilteredList 被包裹在 SortedList 中,SortedList 被设置到 TableView 中。 SortedListComparator 绑定到 TableViewComparator

但是 SortedList 只能看到包含在当前过滤器下的传感器。因此 TableViewcomparatorProperty 添加了一个监听器。此侦听器的操作流式传输底层 ObservableList,使用新的 Comparator 对其进行排序,并根据新的排序顺序重置每个传感器的 ordinalNumber

最后,为了让 FilteredList 重新评估 ObservableList,这些 ordinalNumber 更改需要触发 ListChange 事件。因此,在 ordinalNumber.

的基础上,向 ObservableList 添加了一个提取器

结果非常好,除了愚蠢的页码 Text 每次换页都会滑到屏幕上。

为提高可读性清理了整个代码,并删除了未使用的内容以保持示例最少。

这是传感器 class:

    class Sensor {
      public SimpleStringProperty userid;
      public SimpleStringProperty resourceid;
      public SimpleStringProperty sid;
      public SimpleStringProperty timestamp;
      public SimpleStringProperty reading;
      public IntegerProperty ordinalNumber = new SimpleIntegerProperty(0);
       
      public Sensor(int userid, String resourceid, String sid, String timestamp, String reading, String condition) {
          this.userid = new SimpleStringProperty(Integer.toString(userid));
          this.resourceid = new SimpleStringProperty(resourceid);
          this.sid = new SimpleStringProperty(sid);
          this.timestamp = new SimpleStringProperty(timestamp);
          this.reading = new SimpleStringProperty(reading);
          this.ordinalNumber.set(userid);
      }
    }
     

布局代码如下:

public class PaginationController extends Application {
    public TableView<Sensor> sensorsTable = new TableView<>();
    public ObservableList<Sensor> sensorObservableList = FXCollections.observableArrayList(sensor -> new Observable[]{sensor.ordinalNumber});
    public FilteredList<Sensor> sensorFilteredList = new FilteredList<>(sensorObservableList);
    public SortedList<Sensor> sensorSortedList = new SortedList<>(sensorFilteredList);
    public IntegerProperty currentPage = new SimpleIntegerProperty(0);
    public int rowsPerPage = 14;

    public void start(final Stage stage) throws Exception {
        processSensors();
        stage.setScene(new Scene(buildScene(), 1024, 768));
        stage.setTitle("Table pager");
        stage.show();
    }

    public Region buildScene() {
        buildTable();
        int numOfPages = calculateNumOfPages();
        Pagination pagination = new Pagination((numOfPages), 0);
        pagination.setPageFactory(pageIndex -> {
            Text text = new Text("This is page " + (pageIndex + 1));
            return text;
        });
        pagination.setMaxPageIndicatorCount(numOfPages);
        currentPage.bind(pagination.currentPageIndexProperty());
        sensorFilteredList.predicateProperty().bind(Bindings.createObjectBinding(() -> createPageFilter(pagination.getCurrentPageIndex()), pagination.currentPageIndexProperty()));
        return new VBox(sensorsTable, pagination);
    }

    @NotNull
    private Predicate<Sensor> createPageFilter(int currentPage) {
        int lowerLimit = (currentPage) * rowsPerPage;
        int upperLimit = (currentPage + 1) * rowsPerPage;
        return sensor -> (sensor.ordinalNumber.get() >= lowerLimit) &&
                (sensor.ordinalNumber.get() < upperLimit);
    }

    private int calculateNumOfPages() {
        int numOfPages = 1;
        if (sensorObservableList.size() % rowsPerPage == 0) {
            numOfPages = sensorObservableList.size() / rowsPerPage;
        } else if (sensorObservableList.size() > rowsPerPage) {
            numOfPages = sensorObservableList.size() / rowsPerPage + 1;
        }
        return numOfPages;
    }

    public void processSensors() {
        Random random = new Random();
        for (int i = 0; i < 60; i++) {
            sensorObservableList.add(new Sensor(i, "rid-" + i, "sid-" + i, Integer.toString(random.nextInt(100)), "0", "no condition"));
        }
    }


    public void buildTable() {
        addStringColumn("userid", param1 -> param1.getValue().userid);
        addStringColumn("resourceid", param1 -> param1.getValue().resourceid);
        addStringColumn("sid", param1 -> param1.getValue().sid);
        addStringColumn("timestamp", param1 -> param1.getValue().timestamp);
        addStringColumn("reading", param1 -> param1.getValue().reading);
        TableColumn<Sensor, Number> ordinalCol = new TableColumn<>("ordinal");
        ordinalCol.setCellValueFactory(param -> param.getValue().ordinalNumber);
        ordinalCol.setPrefWidth(100);
        sensorsTable.getColumns().add(ordinalCol);
        sensorsTable.setItems(sensorSortedList);
        sensorSortedList.comparatorProperty().bind(sensorsTable.comparatorProperty());
        sensorSortedList.comparatorProperty().addListener(x -> renumberRecords());
        
    }

    private void renumberRecords() {
        AtomicInteger counter = new AtomicInteger(0);
        Comparator<Sensor> newValue = sensorsTable.getComparator();
        if (newValue != null) {
            sensorObservableList.stream().sorted(newValue).forEach(sensor -> sensor.ordinalNumber.set(counter.getAndIncrement()));
        } else {
            sensorObservableList.forEach(sensor -> sensor.ordinalNumber.set(counter.getAndIncrement()));
        }
    }

    @NotNull
    private void addStringColumn(String columnTitle, Callback<TableColumn.CellDataFeatures<Sensor, String>, ObservableValue<String>> callback) {
        TableColumn<Sensor, String> column = new TableColumn<>(columnTitle);
        column.setCellValueFactory(callback);
        column.setPrefWidth(100);
        sensorsTable.getColumns().add(column);
    }
}

出于演示目的,传感器中的时间戳字段被初始化为随机数,以便在对该列进行排序时给出明显的变化。此外,ordinalNumber 字段已添加到 table,以便在选择新的排序列时可以轻松验证它们是否已重新评估。

不直接支持TableView的分页,所以我们必须自己做。注意问题中引用的解决方案 pre-date the (re-introduction of Sorted-/FilteredList)

如今,基本方法是使用仅包含当前页面上的行的 FilteredList。此 filteredList 必须是 table 的 itemsProperty 的值。为了也允许排序,我们需要将原始数据包装到一个 SortedList 并将其比较器绑定到 table 提供的比较器。合并所有:

items = observableArrayList(... //my data);
sortedList = new SortedList(items);
filteredList = new FilteredList(sortedList);
table.setItems(filteredList);
sortedList.comparatorProperty().bind(table.comparatorProperty());

看起来不错,不是吗?不幸的是,单击列 header 时没有任何反应。原因:

  • 负责排序的协作者是 sortPolicy
  • 默认策略检查 table 的项目是否是排序列表:如果是(并且其比较器绑定到 table),则排序留给该列表,否则它会回到 FXCollections.sort(items, ...)
  • collections.sort 无法执行任何操作,因为筛选列表不可修改

在伪代码中:

if (items instanceof SortedList) {
    return sortedList.getComparator().isBoundTo(table.getComparator());   
}
try {
    FXCollections.sort(items);
    // sorting succeeded
    return true;
} catch (Exception ex) {
    // sorting failed
    return false;
}

出路是实施自定义排序策略:它不是只检查 table 的项目是否为排序列表,而是沿着 transformationList(如果可用)源链向上移动,直到找到一个已排序(或未排序):

ObservableList<?> lookup = items;
while (lookup instanceof TransformationList) {
    if (lookup instanceof SortedList) {
        items = lookup;
        break;
    } else {
        lookup = ((TransformationList<?, ?>) lookup).getSource();
    }
}
// ... same as original policy

现在我们已经准备好了(完整列表的)排序 - 下一个问题是排序后分页视图应该发生什么。选项:

  • 保持页面不变并更新过滤器
  • 保持任何当前项目可见并更新页面

两者都需要在列表的排序状态发生变化时触发更新,具体实现取决于用户体验指南。

一个可运行的例子:

public class TableWithPaginationSO extends Application {

    public static <T> Callback<TableView<T>, Boolean> createSortPolicy(TableView<T> table) {
        // c&p of DEFAULT_SORT_POLICY except adding search up a chain
        // of transformation lists until we find a sortedList
        return new Callback<TableView<T>, Boolean>() {

            @Override
            public Boolean call(TableView<T> table) {
                try {
                    ObservableList<?> itemsList = table.getItems();
                    
                    // walk up the source lists to find the first sorted
                    ObservableList<?> lookup = itemsList;
                    while (lookup instanceof TransformationList) {
                        if (lookup instanceof SortedList) {
                            itemsList = lookup;
                            break;
                        } else {
                            lookup = ((TransformationList<?, ?>) lookup).getSource();
                        }
                    }

                    if (itemsList instanceof SortedList) {
                        SortedList<?> sortedList = (SortedList<?>) itemsList;
                        boolean comparatorsBound = sortedList.comparatorProperty()
                                .isEqualTo(table.comparatorProperty()).get();

                        return comparatorsBound;
                    } else {
                        if (itemsList == null || itemsList.isEmpty()) {
                            // sorting is not supported on null or empty lists
                            return true;
                        }

                        Comparator comparator = table.getComparator();
                        if (comparator == null) {
                            return true;
                        }

                        // otherwise we attempt to do a manual sort, and if successful
                        // we return true
                        FXCollections.sort(itemsList, comparator);
                        return true;
                    }
                } catch (UnsupportedOperationException e) {
                    return false;
                }
            };

        };
    }

    private Parent createContent() {
        initData();
        // wrap sorted list around data
        sorted = new SortedList<>(data);
        // wrap filtered list around sorted
        filtered = new FilteredList<>(sorted);
        // use filtered as table's items
        table = new TableView<>(filtered);
        addColumns();
        page = new BorderPane(table);

        // install custom sort policy
        table.setSortPolicy(createSortPolicy(table));
        // bind sorted comparator to table's
        sorted.comparatorProperty().bind(table.comparatorProperty());

        pagination = new Pagination(rowsPerPage, 0);
        pagination.setPageCount(sorted.size() / rowsPerPage);;
        pagination.setPageFactory(this::createPage);

        sorted.addListener((ListChangeListener<Locale>) c -> {
            // update page after changes to list 
            updatePage(true);
        });
        return pagination;
    }

    private Node createPage(int pageIndex) {
        updatePredicate(pageIndex);
        return page;
    }

    /**
     * Update the filter to show the current page.
     */
    private void updatePredicate(int pageIndex) {
        int first = rowsPerPage * pageIndex;
        int last = Math.min(first + rowsPerPage, sorted.size());
        Predicate<Locale> predicate = loc -> {
            int index = sorted.indexOf(loc);
            return index >= first && index < last;
        };
        filtered.setPredicate(predicate);
        // keep reference to first on page
        firstOnPage = filtered.get(0);
    }

    /** 
     * Update the page after changes to the list. 
     */
    private void updatePage(boolean keepItemVisible) {
        if (keepItemVisible) {
            int sortedIndex = sorted.indexOf(firstOnPage);
            int pageIndex = sortedIndex >= 0 ? sortedIndex / rowsPerPage : 0;
            pagination.setCurrentPageIndex(pageIndex);
        } else {
            updatePredicate(pagination.getCurrentPageIndex());
        }
    }

    private void addColumns() {
        TableColumn<Locale, String> name = new TableColumn<>("Name");
        name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
        TableColumn<Locale, String> country = new TableColumn<>("Country");
        country.setCellValueFactory(new PropertyValueFactory<>("displayCountry"));
        table.getColumns().addAll(name, country);
    }

    private void initData() {
        Locale[] availableLocales = Locale.getAvailableLocales();
        data = observableArrayList(
                Arrays.stream(availableLocales)
                .filter(e -> e.getDisplayName().length() > 0)
                .limit(120)
                .collect(toList())
                );
    }

    private TableView<Locale> table;
    private Pagination pagination;
    private BorderPane page;
    private ObservableList<Locale> data;
    private FilteredList<Locale> filtered;
    private SortedList<Locale> sorted;
    private Locale firstOnPage;
    private int rowsPerPage = 15;

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.show();
    }

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

}