1
votes

I want to make a ScrollPane with a custom Pane inside, that has two Children. One that holds my objects and one just for the background. I want to make it so if I zoom out, and the content is smaller than the viewport, then the size of the content would expand, filling in the new place in the viewport. And if I zoom back then it would remain the same, and I have now a larger content in area. The new width of the content would be: originalWidth + viewportWidth - scaledWidth.

I have made the grid, and the zooming works, but I can't make it so that it resizes the content. I have tried to set the content size when zooming to the current viewport size, but it does not work.

Question:

  • What am I doing wrong?

The layout is defined in fxml. Another than ScrollPane content set to fill height and width nothing out of ordinary there.

CustomPane class:

public class CustomPane extends StackPane implements Initializable {

@FXML
StackPane view;
@FXML
AnchorPane objectPane;
@FXML
GriddedPane background;

private DoubleProperty zoomFactor = new SimpleDoubleProperty(1.5);
private BooleanProperty altStatus = new SimpleBooleanProperty(false);

public CustomPane() {
    super();
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getClassLoader().getResource("CustomCanvas.fxml"));
    loader.setController(this);
    loader.setRoot(this);
    try {
        loader.load();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
public void initialize(URL location, ResourceBundle resources) {
    objectPane.setStyle("-fx-background-color: transparent");
    objectPane.prefWidthProperty().bind(prefWidthProperty());
    objectPane.prefHeightProperty().bind(prefHeightProperty());
    objectPane.getChildren().add(new Circle(512, 378, 20, Color.RED));
}

public void zoom(ScrollPane parent, Node node, double factor, double x, double y) {
    Timeline timeline = new Timeline(60);
    // determine scale
    double oldScale = node.getScaleX();
    double scale = oldScale * factor;
    double f = (scale / oldScale) - 1;

    // determine offset that we will have to move the node
    Bounds bounds = node.localToScene(node.getBoundsInLocal());


    double dx = (x - (bounds.getWidth() / 2 + bounds.getMinX()));
    double dy = (y - (bounds.getHeight() / 2 + bounds.getMinY()));

    // timeline that scales and moves the node

    timeline.getKeyFrames().clear();
    timeline.getKeyFrames().addAll(
            new KeyFrame(Duration.millis(100), new KeyValue(node.translateXProperty(), node.getTranslateX() - f * dx)),
            new KeyFrame(Duration.millis(100), new KeyValue(node.translateYProperty(), node.getTranslateY() - f * dy)),
            new KeyFrame(Duration.millis(100), new KeyValue(node.scaleXProperty(), scale)),
            new KeyFrame(Duration.millis(100), new KeyValue(node.scaleYProperty(), scale))
    );

    timeline.play();

    Bounds viewportBounds = parent.getViewportBounds();
    if (bounds.getWidth() < viewportBounds.getWidth()) {
        setMinWidth(viewportBounds.getWidth());
        requestLayout();
    }

    if (getMinHeight() < viewportBounds.getHeight()) {
        setMinHeight(viewportBounds.getHeight());
        requestLayout();
    }
}

public final Double getZoomFactor() {
    return zoomFactor.get();
}

public final void setZoomFactor(Double zoomFactor) {
    this.zoomFactor.set(zoomFactor);
}

public final DoubleProperty zoomFactorProperty() {
    return zoomFactor;
}

public boolean getAltStatus() {
    return altStatus.get();
}

public BooleanProperty altStatusProperty() {
    return altStatus;
}

public void setAltStatus(boolean altStatus) {
    this.altStatus.set(altStatus);
}
}

Controller class:

public class Controller implements Initializable {
public ScrollPane scrollPane;
public CustomPane customPane;
public AnchorPane anchorPane;
public Tab tab1;

@Override
public void initialize(URL location, ResourceBundle resources) {
    scrollPane.viewportBoundsProperty().addListener((observable, oldValue, newValue) -> {
        customPane.setMinSize(newValue.getWidth(), newValue.getHeight());
    });
    scrollPane.requestLayout();

    tab1.getTabPane().addEventFilter(KeyEvent.KEY_PRESSED, event1 -> {
        if (event1.getCode() == KeyCode.ALT)
            customPane.setAltStatus(true);
    });
    tab1.getTabPane().addEventFilter(KeyEvent.KEY_RELEASED, event1 -> {
        if (event1.getCode() == KeyCode.ALT)
            customPane.setAltStatus(false);
    });

    scrollPane.setOnScroll(event -> {
        double zoomFactor = 1.5;
        if (event.getDeltaY() <= 0)
            zoomFactor = 1 / zoomFactor;
        customPane.setZoomFactor(zoomFactor);
        if (customPane.getAltStatus())
            customPane.zoom(scrollPane, customPane, customPane.getZoomFactor(), event.getSceneX(), event.getSceneY());
    });
}
}

GriddedPane class:

public class GriddedPane extends Pane implements Initializable {

DoubleProperty gridWidth = new SimpleDoubleProperty(this, "gridWidth", 10);
DoubleProperty gridHeight = new SimpleDoubleProperty(this, "gridHeight", 10);

public GriddedPane() {
    super();
}

@Override
public void initialize(URL location, ResourceBundle resources) {

}

@Override
protected void layoutChildren() {
    getChildren().clear();
    setMouseTransparent(true);
    toBack();
    for (int i = 0; i < getHeight(); i += getGridWidth())
        getChildren().add(makeLine(0, i, getWidth(), i, "x"));
    for (int i = 0; i < getWidth(); i += getGridHeight())
        getChildren().add(makeLine(i, 0, i, getHeight(), "y"));
}

public void redrawLines() {
    for (Node n : getChildren()) {
        Line l = (Line) n;
        if (l.getUserData().equals("x")) {
            l.setEndX(getWidth());
        } else if (l.getUserData().equals("y")) {
            l.setEndY(getHeight());
        }
    }
}

private Line makeLine(double sx, double sy, double ex, double ey, String data) {
    final Line line = new Line(sx, sy, ex, ey);
    if (ex % (getGridWidth() * 10) == 0.0) {
        line.setStroke(Color.BLACK);
        line.setStrokeWidth(0.3);
    } else if (ey % (getGridHeight() * 10) == 0.0) {
        line.setStroke(Color.BLACK);
        line.setStrokeWidth(0.3);
    } else {
        line.setStroke(Color.GRAY);
        line.setStrokeWidth(0.1);
    }
    line.setUserData(data);
    return line;
}

public double getGridWidth() {
    return gridWidth.get();
}

public DoubleProperty gridWidthProperty() {
    return gridWidth;
}

public void setGridWidth(double gridWidth) {
    this.gridWidth.set(gridWidth);
}

public double getGridHeight() {
    return gridHeight.get();
}

public DoubleProperty gridHeightProperty() {
    return gridHeight;
}

public void setGridHeight(double gridHeight) {
    this.gridHeight.set(gridHeight);
}
}
1

1 Answers

0
votes

Not really sure if I unterstood what you want to achieve. But if your goal is to make the content of the scrollPane never get smaller than the scrollPane's width, this does the job for me:

public class Zoom extends Application {

    @Override
    public void start(Stage primaryStage) {

        ImageView imageView = new ImageView(new Image(someImage));
        ScrollPane scrollPane = new ScrollPane(imageView);
        StackPane root = new StackPane(scrollPane);

        imageView.fitWidthProperty().bind(scrollPane.widthProperty());
        imageView.fitHeightProperty().bind(scrollPane.heightProperty());

        scrollPane.setOnScroll(evt -> {
            boolean zoomOut = evt.getDeltaY() < 0;
            double zoomFactor = zoomOut ? -0.2 : 0.2;

            imageView.setScaleX(imageView.getScaleX() + zoomFactor);
            imageView.setScaleY(imageView.getScaleY() + zoomFactor);

            if (zoomOut) {
                Bounds bounds = imageView.getBoundsInParent();
                if (bounds.getWidth() < scrollPane.getWidth()) {
                    imageView.setScaleX(1);
                    imageView.setScaleY(1);

                }
            }
        });

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

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