首页 > 解决方案 > 对整个列表进行分页 TableView 排序时出错

问题描述

我正在尝试实现一个分页表视图,它允许按 JavaFX 中的所有项目进行排序。我从这里实现了分页表视图:https ://stackoverflow.com/a/25424208/12181863 。由jewelsea 和tim buthe 提供。

我在想,因为表视图只访问项目的子列表,所以我想根据我对 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());

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

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

我还参考了其他参考资料,例如:https ://incepttechnologies.blogspot.com/p/javafx-tableview-with-pagination-and.html但我发现很难理解,因为一切都在解释模糊的地方.

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

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

单独的类:

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

标签: javasortingjavafxpaginationtableview

解决方案


不直接支持 TableView 的分页,所以我们必须自己做。请注意,问题中引用的解决方案早于(重新引入 Sorted-/FilteredList

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

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

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

  • 负责排序的协作者是 sortPolicy
  • 默认策略检查表的项目是否是排序列表:如果是(并且它的比较器绑定到表的),则排序留给该列表,否则回退到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;
}

出路是实现自定义排序策略:它不仅检查表的项目是否为 sortedList,而是沿着 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);
    }

}

推荐阅读