Search code examples
javajavafxcenteringscrollpanegridpane

Center GridPane Square in ScrollPane Viewport


I have a 20x20 GridPane of uniformly sized squares which is representing data from a two-dimensional array of the same size, placed in a ScrollPane such that one can use it to pan around. The width and height in pixels of the GridPane are both much greater than the respective dimensions of the ScrollPane.

My problem is that I have been unable to make a functional method that centers the viewport of the ScrollPaneon the GridPane square located at a set of designated coordinates.

My understanding of the scrollPane.setHvalue(double) and scrollPane.setVvalue(double)which I have been attempting to utilize is that the double value passed into them is the percentage of the axis that it sets the scroll to. However, when I write the code assuming that, it fails to center the view on the intended square, sometimes with the square not even being anywhere on the viewport:

private void centerViewOn(double x, double y){
    scrollPane.setHvalue(x/grid.getDimensionX());
    scrollPane.setVvalue(y/grid.getDimensionY());
}

Where grid is an instance of a class whose only relevance to this question is that it has the dimensions of the grid, and double x and double y are the x,y coordinates in the GridPane of the square that is intended to be centered.

What am I doing wrong, and how can I fix this?

EDIT: In accordance with James_D's suggestion, I'm including what I made as a Minimal, Complete, and Verifiable example:

private ScrollPane scrollPane;
private GridPane gridPane;
private double dimensionX;
private double dimensionY;

@Override
public void start(Stage primaryStage) {
    scrollPane = new ScrollPane();
    gridPane   = new GridPane();
    dimensionX = 20;
    dimensionY = 20;

    Pane temp;
    for(int x = 0; x < dimensionX; ++x){
        for(int y = 0; y < dimensionY; ++y){
            temp = new Pane();
            temp.setPrefSize(100, 100);
            temp.setOnMouseClicked( e -> {
                centerViewOn(GridPane.getColumnIndex((Pane)e.getSource()), GridPane.getRowIndex((Pane)e.getSource()));
                ((Pane)e.getSource()).setStyle("-fx-background-color: blue");
            });
            gridPane.add(temp, x, y);
        }
    }

    gridPane.setGridLinesVisible(true);
    scrollPane.setContent(gridPane);

    primaryStage.setScene(new Scene(scrollPane));
    primaryStage.show();
}

private void centerViewOn(double x, double y){
    double viewportWidth    = scrollPane.getViewportBounds().getWidth();
    double maxHscrollPixels = gridPane.getWidth() - viewportWidth;
    double hscrollPixels    = viewportWidth / 2 - (x + 0.5) * dimensionX / 20;
    scrollPane.setHvalue(hscrollPixels / maxHscrollPixels);

    double viewportHeight   = scrollPane.getViewportBounds().getHeight();
    double maxVscrollPixels = gridPane.getHeight() - viewportHeight;
    double vscrollPixels    = viewportHeight / 2 - (y + 0.5) * dimensionY / 20;
    scrollPane.setVvalue(vscrollPixels / maxVscrollPixels);
}

Solution

  • The scroll pane's horizontal scroll values vary between scrollPane.getHmin() and scrollPane.getHmax(). Those are in arbitrary units, with the amount scrolled in pixels being linearly interpolated across the range.

    The possible horizontal scroll amount in pixels ranges from zero to (grid pane width - scrollpane viewport width).

    So you have

    (hvalue - hmin) / (hmax - hmin) = scrollPixels / (gridPaneWidth - viewportWidth)
    

    and so after some algebra you get

    hvalue = hmin + (hmax - hmin) * scrollPixels / (gridPaneWidth - viewportWidth)
    

    Assuming each column is the same width, and there are dimensionX columns, x columns will have total width x * gridPaneWidth / dimensionX pixels. Since you want to scroll to the center of the cell, add another half cell, and since you want the center of the cell to be in the center of the viewport, subtract viewportWidth / 2. So you have:

    hscrollPixels =  (x + 0.5) * gridPane.getWidth() / dimensionX - viewportWidth / 2
    

    Scrolling vertically is exactly analogous.

    The default values for hmin and hmax are 0 and 1, respectively, so you can simplify things a bit if you assume these don't change.

    So I think you end up with

    private void centerViewOn(double x, double y){
        double viewportWidth    = scrollPane.getViewportBounds().getWidth();
        double maxHscrollPixels = gridPane.getWidth() - viewportWidth;
        double hscrollPixels    =  (x + 0.5) * gridPane.getWidth() / dimensionX - viewportWidth / 2;
        scrollPane.setHvalue(hscrollPixels / maxHscrollPixels);
    
        double viewportHeight   = scrollPane.getViewportBounds().getHeight();
        double maxVscrollPixels = gridPane.getHeight() - viewportHeight;
        double vscrollPixels    =  (y + 0.5) * gridPane.getHeight() / dimensionY - viewportHeight / 2;
        scrollPane.setVvalue(vscrollPixels / maxVscrollPixels);
    }
    

    Note that not all cells can be scrolled to the center: if they are too close to the edges, they will be scrolled as close as possible to the center, but the scroll pane will not allow blank space (unless the content is smaller than the viewport).

    Here's the complete example:

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.ScrollPane;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.Pane;
    import javafx.stage.Stage;
    
    public class ScrollToCenter extends Application {
    
        private ScrollPane scrollPane;
        private GridPane gridPane;
        private double dimensionX;
        private double dimensionY;
    
        @Override
        public void start(Stage primaryStage) {
            scrollPane = new ScrollPane();
            gridPane   = new GridPane();
            dimensionX = 20;
            dimensionY = 20;
    
            for(int x = 0; x < dimensionX; ++x){
                for(int y = 0; y < dimensionY; ++y){
                    Pane temp = new Pane();
                    temp.setPrefSize(100, 100);
                    temp.setOnMouseClicked( e -> {
                        centerViewOn(GridPane.getColumnIndex(temp), GridPane.getRowIndex(temp));
                        temp.setStyle("-fx-background-color: blue");
                    });
                    gridPane.add(temp, x, y);
                }
            }
    
            gridPane.setGridLinesVisible(true);
            scrollPane.setContent(gridPane);
    
            primaryStage.setScene(new Scene(scrollPane));
            primaryStage.show();
        }
    
        private void centerViewOn(double x, double y){
            double viewportWidth    = scrollPane.getViewportBounds().getWidth();
            double maxHscrollPixels = gridPane.getWidth() - viewportWidth;
            double hscrollPixels    =  (x + 0.5) * gridPane.getWidth() / dimensionX - viewportWidth / 2;
            scrollPane.setHvalue(hscrollPixels / maxHscrollPixels);
    
            double viewportHeight   = scrollPane.getViewportBounds().getHeight();
            double maxVscrollPixels = gridPane.getHeight() - viewportHeight;
            double vscrollPixels    =  (y + 0.5) * gridPane.getHeight() / dimensionY - viewportHeight / 2;
            scrollPane.setVvalue(vscrollPixels / maxVscrollPixels);
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }