2

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.

midrare
  • 2,371
  • 28
  • 48

1 Answers1

7

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);
        }
    };
James_D
  • 201,275
  • 16
  • 291
  • 322
  • I can respond to scroll bar changes like this, but it's not quite what I'm looking for. I need to be able to calculate `left` and `right` in the `ChangeListener` (and `top` and `botttom`). When I scroll the scroll bar, `hval` is printed correctly, but `left` and `right` remain at their initial values. – midrare Oct 07 '14 at 21:48
  • Sure, you are just calculating those from the viewport bounds, which are the bounds of the viewport in its parent. Those don't change when you scroll. I updated the answer to show how to compute the visible portion of the content. – James_D Oct 07 '14 at 23:27
  • But why would `viewportBounds` only change when resizing? In my [test code](http://pastie.org/private/zsjaojwtytphephoxp3rg), my `viewport`'s `getMinX()` and `getMinY()` will return updated coordinates, but only after resizing. – midrare Oct 08 '14 at 21:43
  • 1
    The viewport is the part of the ScrollPane through which you view the content. Its bounds are its position and size relative to the ScrollPane. These don't change when you scroll, any more than the position of your windscreen relative to your car changes as the road you're looking at scroll by. – James_D Oct 08 '14 at 22:32
  • My bad, thinking that scrollPane's viewport is aloways updated. It seems like this viewPort is updated when scrolling using the mouse (click and drag) and not when using the sliders. I don't get the logic. – Antonin Dec 18 '21 at 21:31