JavaFX ScrollPane update viewportBounds on scroll


Question

I have a very large image I'm trying to display in JavaFX. To do this, I've split the image into several smaller ones, and only loading/displaying the parts that are visible in my ScrollPane.

To detect the area visible in the ScrollPane, I'm adding listener to ScrollPane.viewportBounds. However, the viewportBounds is only updated when I resize the window, but not when I scroll the scroll bars.

If I'm to be scrolling around my large image, I need viewportBounds to be updated when I scroll the scroll bars as well. How do I do this?


I have some test code below. Clicking the Button works, but not through the ChangeListener - left and right still remain unchanged using the ChangeListener.

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class TestClass extends Application {
    @Override
    public void start(Stage stage) {
        VBox vbox = new VBox();

        ScrollPane scrollPane = new ScrollPane(new Rectangle(1000, 700, Color.GREEN));
        scrollPane.setPrefSize(500, 300);

        // Using a ChangeListener on viewportBounds doesn't work.
        scrollPane.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
            @Override
            public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) {
                int left = -1 * (int) bounds.getMinX();
                int right = left + (int) bounds.getWidth();
                System.out.println("hval:" + scrollPane.getHvalue() + " left:" + left + " right:" + right);
            }
        });

        // Neither does this.
        scrollPane.hvalueProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                Bounds bounds = scrollPane.getViewportBounds();
                int left = -1 * (int) bounds.getMinX();
                int right = left + (int) bounds.getWidth();
                System.out.println("hval:" + scrollPane.getHvalue() + " left:" + left + " right:" + right);
            }
        });

        // Clicking the button works.
        Button printButton = new Button("Print");

        printButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                Bounds bounds = scrollPane.getViewportBounds();
                int left = -1 * (int) bounds.getMinX();
                int right = left + (int) bounds.getWidth();
                System.out.println("hval:" + scrollPane.getHvalue() + " left:" + left + " right:" + right);
                event.consume();
            }
        });

        vbox.getChildren().addAll(scrollPane, printButton);

        Scene scene = new Scene(vbox, 640, 480);
        stage.setScene(scene);

        stage.show();
    }

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

EDIT Updated test code.

1
2
10/7/2014 10:23:55 PM

Accepted Answer

The viewportBounds are just the bounds of the viewport, i.e. the bounds of the portion of the scrollable content that is visible. These won't change as you scroll (but will usually change as you resize the window, as the size changes).

To respond to the scroll content being moved within the viewport, you need to observe the hvalueProperty and vvalueProperty of the ScrollPane. You can use the same change listener, with minor modifications to the types of the parameters:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class TestClass extends Application {
    @Override
    public void start(Stage stage) {
        ScrollPane scrollPane = new ScrollPane(new Rectangle(1000, 700, Color.GREEN));
        scrollPane.setPrefSize(500, 300);

        ChangeListener<Object> changeListener = new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
                Bounds bounds = scrollPane.getViewportBounds();
                int left = -1 * (int) bounds.getMinX();
                int right = left + (int) bounds.getWidth();
                System.out.println("hval:" + scrollPane.getHvalue() + " left:" + left + " right:" + right);
            }
        };
        scrollPane.viewportBoundsProperty().addListener(changeListener);
        scrollPane.hvalueProperty().addListener(changeListener);
        scrollPane.vvalueProperty().addListener(changeListener);

        Scene scene = new Scene(scrollPane, 640, 480);
        stage.setScene(scene);

        stage.show();
    }

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

Note that when you scroll, the viewport (of course) does not move in its parent; the visible portion of the scrollpane's content changes. There's no simple way I can see to compute the visible portion of the content: you just have to do a bit of maths.

(Note: there may be an easier way, but I am not able to see it.)

The API docs for ScrollPane state

The ScrollPane allows the application to set the current, minimum, and maximum values for positioning the contents in the horizontal and vertical directions. These values are mapped proportionally onto the layoutBounds of the contained node.

So you have to interpret that a bit, but it means that

(hvalue - hmin) / (hmax - hmin) = hoffset / freeHspace

where hmin, hvalue, and hmax are scroll pane property values, hoffset is the amount by which the content is shifted horizontally, and freeHspace is the total horizontal amount the content can move. This is

freeHspace = contentWidth - viewportWidth

(Obviously if contentWidth <= viewportWidth, no horizontal scrolling is possible, and hoffset is necessarily zero.)

So if you want the horizontal offset you have

hoffset = Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin)

And a similar formula holds for the vertical offset.

So you can replace the change listener above with

    ChangeListener<Object> changeListener = new ChangeListener<Object>() {
        @Override
        public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
            double hmin = scrollPane.getHmin();
            double hmax = scrollPane.getHmax();
            double hvalue = scrollPane.getHvalue();
            double contentWidth = content.getLayoutBounds().getWidth();
            double viewportWidth = scrollPane.getViewportBounds().getWidth();

            double hoffset = 
                Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin);

            double vmin = scrollPane.getVmin();
            double vmax = scrollPane.getVmax();
            double vvalue = scrollPane.getVvalue();
            double contentHeight = content.getLayoutBounds().getHeight();
            double viewportHeight = scrollPane.getViewportBounds().getHeight();

            double voffset = 
                Math.max(0,  contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin);

            System.out.printf("Offset: [%.1f, %.1f] width: %.1f height: %.1f %n", 
                    hoffset, voffset, viewportWidth, viewportHeight);
        }
    };
6
10/7/2014 11:26:29 PM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon