3
votes

I have a simple JavaFX application with a Canvas and a ScrollBar. The Canvas acts as a virtualized canvas whose contents can be scrolled with the ScrollBar.

The width of the Canvas is 500 pixels but the logical width of the Canvas is 1000 pixels. I've set the ScrollBar's max to 1000 and the ScrollBar's visible amount to 500. The problem is that when the ScrollBar is scrolled all the way to the right the value of the ScrollBar is 1000, not 500. Logically when a ScrollBar is scrolled all the way to the right there should be some way to determine the actual scrolled offset and that does not seem possible. Please suggest how the amount of scroll required can be obtained. Thank you

Here is the example scrolled all the way to the left. The scrollbar looks good. Its visible width is 50% of the window size.

enter image description here

Here is the example scrolled halfway to the right. Here the problem is clear. The Canvas has been scrolled 500 pixels to the right, but if the Canvas was correctly scrolled halfway it would be scrolled only 250 pixels.

enter image description here

Here is the example scrolled all the way to the right. enter image description here

import javafx.application.Application;
import javafx.beans.value.*;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;

public class Sc extends Application {

    public ScrollBar scrollBar;
    double scrolled;
    Canvas canvas;

    @Override
    public void start(Stage stage) {
        VBox vbox = new VBox();
        Scene scene = new Scene(vbox);
        stage.setScene(scene);

        canvas = new Canvas(500, 100);
        scrollBar = new ScrollBar();
        scrollBar.setMin(0);
        scrollBar.setMax(1000);
        scrollBar.setVisibleAmount(500);
        vbox.getChildren().addAll(canvas, scrollBar);
        draw();

        scrollBar.valueProperty().addListener(new ChangeListener<Number>() {
            public void changed(ObservableValue<? extends Number> ov,
                Number old_val, Number new_val) {
                scrolled = new_val.doubleValue();
                draw();
            }
        });

        stage.show();
    }

    private void draw()
    {
        GraphicsContext graphics = canvas.getGraphicsContext2D();
        graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
        Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.RED)};
        LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
        graphics.setFill(lg);
        graphics.fillRect(0-scrolled, 30, 1000, 40);
    }

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

1 Answers

3
votes

Think of it this way:

min, max, and value are the logical values of the scrollbar. value ranges between min and max, and is equal to min if and only if the scrollbar is scrolled all the way to the left (or top, for a vertical scrollbar). It is equal to max if and only if the scrollbar is scrolled as far as possible to the right (or bottom).

The visibleAmount property is really just a visual property that determines the size of the "thumb". The relationship is something like

thumbSize / trackSize = visibleAmount / (max - min)

The visibleAmount will affect the relationship between actual pixels and "logical units"; however it will not change the fact that value can vary along the full range of [min, max].

So the only change you need to make to your code is to set the maximum value of the scrollbar to the maximum it can be scrolled by (not the size of the image you are drawing), which is 1000 - canvas.getWidth():

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;

public class ScrollBarExample extends Application {

    private final static double TOTAL_WIDTH = 1000 ;


    @Override
    public void start(Stage stage) {
        VBox vbox = new VBox();

        Scene scene = new Scene(vbox, 500, 130);
        stage.setScene(scene);

        Canvas canvas = new Canvas(500, 100);
        ScrollBar scrollBar = new ScrollBar();
        scrollBar.setMin(0);
        scrollBar.setMax(TOTAL_WIDTH - canvas.getWidth());

        scrollBar.setVisibleAmount(scrollBar.getMax() * canvas.getWidth() / TOTAL_WIDTH);

        vbox.getChildren().addAll(canvas, scrollBar);
        draw(canvas, scrollBar.getValue());

        scrollBar.valueProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> ov,
                Number old_val, Number new_val) {
                draw(canvas, scrollBar.getValue());
            }
        });


        stage.show();
    }

    private void draw(Canvas canvas, double scrollAmount)
    {
        GraphicsContext graphics = canvas.getGraphicsContext2D();
        graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
        Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.RED)};
        LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
        graphics.setFill(lg);
        graphics.fillRect(0-scrollAmount, 30, TOTAL_WIDTH, 40);
    }

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