简体   繁体   中英

JavaFX- center selected row in TableView

In JavaFX 8 is was able to do the following to always center a selected row in a TableView in the middle of the viewport:

    TableView<T> tv = getTableView();
    // Position selection in the middle of the viewPort.
    if (tv.getSelectionModel().getSelectedItem() != null) {
        
        TableViewSkin<?> ts = (TableViewSkin<?>) tv.getSkin();
        Optional<VirtualFlow> vfOpt = ts.getChildren().stream()
            .filter(child -> child instanceof VirtualFlow)
            .map(VirtualFlow.class::cast)
            .findFirst();

        // TODO sometimes not centering correctly. The scrollTo used in JavaFX 17
        // differs from that used in JavaFX 8!
        if (vfOpt.isPresent()) {
            VirtualFlow vf = vfOpt.get(); 
            int first = vf.getFirstVisibleCell().getIndex();
            int last = vf.getLastVisibleCell().getIndex();
            int selectedIndex = tv.getSelectionModel().getSelectedIndex();
            
            int scrollPosition = selectedIndex - ((last - first) / 2) > 0 ? selectedIndex - ((last - first) / 2) : 0;
            vf.scrollTo(scrollPosition); 
        }
    }

In JavaFX 17, this no longer works. I tracked it down to the implementation of the vf.scrollTo(int) method, that has gone through some changes compared to JavaFX 8. The code above will sometimes work and sometimes it won't (depending on the first and last index).

I noted down the following (FI = first, LA = last, SEL = selectedIndex, POS = calculated scroll position, RES result):

FI = 0,  LA = 16, SEL = 13, POS = 5, RES = to top of viewport
FI = 12, LA = 29, SEL = 13, POS = 5, RES = to middle of viewport
FI = 5,  LA = 21, SEL = 13, POS = 5, RES = to top of viewport

So, it appears to have something to do with the calculated position falling already inside the viewport, causing the selected row to go to the top.

Can anyone offer any help?

VirtualFlow.scrollTo(int) only guarantees the item at the specified index will be visible in the viewport; it makes no guarantees about where in the viewport it will be positioned.

Instead, you can use VirtualFlow.scrollToTop(int) to move the selected item to the top of the viewport (if possible), and then use Viewport.scrollPixels(double) to adjust by half the height of the viewport. You need to layout the viewport in between (I think otherwise the second call overrides the first, though I am not entirely clear).

This approach should be more robust than your original approach, as it relies only on the specification, instead of the actual implementation, though I have not tested on versions prior to JavaFX 18.

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        TableView<Item> table = new TableView<>();
        for (int i = 1 ; i <= 1000 ; i++) {
            table.getItems().add(new Item("Item "+i));
        }
        TableColumn<Item, String> col = new TableColumn<>("Item");
        col.setCellValueFactory(data -> data.getValue().nameProperty());
        table.getColumns().add(col);

        Button scrollSelected = new Button("Scroll to Selected");
        scrollSelected.setOnAction(e -> {
            int selected = table.getSelectionModel().getSelectedIndex();
            if (selected == -1) return ;
            TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
            skin.getChildren().stream()
                    .filter(VirtualFlow.class::isInstance)
                    .map(VirtualFlow.class::cast)
                    .findAny()
                    .ifPresent(vf -> {
                        vf.scrollToTop(selected);
                        vf.layout();
                        vf.scrollPixels(-vf.getHeight()/2);
                    });
        });

        HBox controls = new HBox(scrollSelected);
        controls.setAlignment(Pos.CENTER);
        controls.setPadding(new Insets(5));

        BorderPane root = new BorderPane(table);
        root.setBottom(controls);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();

        public Item(String name) {
            this.name.set(name);
        }

        public StringProperty nameProperty() {
            return name ;
        }
    }

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

There are various other solutions, eg

    vf.scrollTo(selected);
    vf.layout();
    Cell<?> cell = vf.getCell(selected);
    double y = cell.getBoundsInParent().getCenterY();
    vf.scrollPixels(y - vf.getHeight()/2);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM