如何获取ScrollPane中内容节点的坐标?

问题描述 投票:0回答:1

我正在尝试实现一个具有与 CorelDraw/InDesign 类似功能的 ScrollPane:ScrollPane 中的一张纸,可以四处移动(更改视口)并且可以放大/缩小。

我的问题是我不知道如何获取 ScrollPane 中页面的坐标。我阅读了许多有关如何实现具有可缩放内容的 ScrollPane 的文章,并了解到需要一个组和一个附加节点才能使整个页面在 ScrollPane 中可见。

我想要什么:

如果页面被放大,使得上部和下部位于视口之外,但背景(在我的例子中是VBox)左右可见,我想知道VBox的多少像素在ScrollPanes中可见页面内容的视口左侧(右侧相同)。

使用当前代码,我知道页面的左上角距 ScrollPanes 视口的左边缘有多远,但如果整个页面可见,它总是显示 0,0。

有什么想法可以通过我的组件安排来实现这一点吗?

到目前为止我的代码:

package demo;

import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class MVP extends Application {

    private Stage stage = null;
    private Group zoomGroup = null;
    private VBox centeredVBox = null;
    private ScrollPane scrollPane = null;
    private StackPane page = null;

    @Override
    public void start(Stage primaryStage) throws Exception {

        this.stage = primaryStage;
        this.page = new StackPane();
        this.page.setPrefWidth(42840);
        this.page.setPrefHeight(60624);
        this.page.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));

        this.zoomGroup = new Group(page);

        this.centeredVBox = new VBox(zoomGroup);
        this.centeredVBox.setAlignment(Pos.CENTER);
        this.centeredVBox
                .setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));

        this.scrollPane = new ScrollPane(centeredVBox);
        this.scrollPane.setPannable(true);
        this.scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        this.scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        this.scrollPane.setFitToHeight(true); // center
        this.scrollPane.setFitToWidth(true); // center
        this.scrollPane.setPrefSize(800, 600);

        this.centeredVBox.setOnScroll(evt -> {
            if (evt.isControlDown()) {
                this.onScroll(evt.getTextDeltaY(), new Point2D(evt.getX(), evt.getY()));
            }
            this.scrollPane.layout();
        });

        this.scrollPane.setOnMouseReleased(evt -> {

            // HOW TO GET THE POSITION OF THE PAGE WITHIN VIEWPORT OF SCROLLPANE IN PIXEL COORDINATES?
            System.out.println(
                    "Upper left: " + (this.scrollPane.getViewportBounds().getMinX() * (1 / this.page.getScaleX())) + "/"
                            + (this.scrollPane.getViewportBounds().getMinY() * (1 / this.page.getScaleY())));
            System.out.println(
                    "Lower Right: " + (this.scrollPane.getViewportBounds().getMaxX() * (1 / this.page.getScaleX()))
                            + "/" + (this.scrollPane.getViewportBounds().getMaxY() * (1 / this.page.getScaleY())));
        });

        this.page.setOnMouseMoved(evt -> {

            this.stage.setTitle(String.valueOf(evt.getX() + "/" + evt.getY()));
        });

        Scene scene = new Scene(this.scrollPane);

        primaryStage.setTitle("Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

    private void onScroll(double wheelDelta, Point2D mousePoint) {
        // this.pageRepresentation.setLastClickedPoint(mousePoint);

        double zoomFactor = Math.exp(wheelDelta * 0.02);

        Bounds innerBounds = this.zoomGroup.getLayoutBounds();
        Bounds viewportBounds = this.scrollPane.getViewportBounds();

        // calculate pixel offsets from [0, 1] range
        double valX = this.scrollPane.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
        double valY = this.scrollPane.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());

        // convert target coordinates to zoomTarget coordinates
        Point2D posInZoomTarget = this.page.parentToLocal(this.zoomGroup.parentToLocal(mousePoint));
        // this.pageRepresentation.setLastClickedPoint(posInZoomTarget);

        // calculate adjustment of scroll position (pixels)
        Point2D adjustment = this.page.getLocalToParentTransform()
                .deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
        // this.pageRepresentation.setLastClickedPoint(adjustment);

        // convert back to [0, 1] range
        // (too large/small values are automatically corrected by ScrollPane)
        Bounds updatedInnerBounds = zoomGroup.getBoundsInLocal();
        this.scrollPane
                .setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
        this.scrollPane
                .setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));

        double zoomValue = this.page.getScaleX() * zoomFactor;
        this.page.setScaleX(zoomValue);
        this.page.setScaleY(zoomValue);
        this.page.layout();
        this.scrollPane.layout();
    }
}
javafx zooming viewport scrollpane
1个回答
0
投票

我从这里提取了代码:

并修改它来报告坐标。也许它会报告您正在寻找的值。

我不太理解你的应用程序代码,所以我没有使用它。

这个答案中有很多代码,但大部分只是从链接的答案中复制的。

唯一的新功能是报告场景根、ScrollPane 视口和视口中各个节点的坐标。

private void reportCoords() {
    Node viewport = scene.lookup(".viewport");
    Bounds sceneBounds = scene.getRoot().getLayoutBounds();
    Bounds viewportBoundsInScene = viewport.localToScene(viewport.getBoundsInLocal());
    Bounds starBoundsInScene = star.localToScene(star.getBoundsInParent());
    Bounds curveBoundsInScene = curve.localToScene(curve.getBoundsInParent());

    String alertText = """
            Root:     %s
            Viewport: %s
            Star:     %s
            Curve:    %s
            """.formatted(
                    formatBounds(sceneBounds),
                    formatBounds(viewportBoundsInScene),
                    formatBounds(starBoundsInScene),
                    formatBounds(curveBoundsInScene)
    );

    System.out.println(alertText);

    Alert displayCoordsAlert = new Alert(
            Alert.AlertType.INFORMATION,
            alertText
    );
    displayCoordsAlert.initOwner(stage);
    displayCoordsAlert.setHeaderText("Coordinate bounds in scene coordinates");
    displayCoordsAlert.getDialogPane()
            .lookup(".content")
            .setStyle("-fx-font-family: monospace");
    displayCoordsAlert.getDialogPane().setPrefSize(550, 200);
    displayCoordsAlert.showAndWait();
}

private String formatBounds(Bounds bounds) {
    return "minX: %5d, minY: %5d, maxX: %5d, maxY: %5d".formatted(
            (int) bounds.getMinX(),
            (int) bounds.getMinY(),
            (int) bounds.getMaxX(),
            (int) bounds.getMaxY()
    );
}

要使用它,请运行应用程序,然后使用菜单或按

r
键生成警报和系统输出,报告有趣事物的当前坐标(在场景坐标中)。

使用场景坐标,以便所有坐标都在同一坐标系中,您可以使用它们来比较绝对位置,如果我理解您的问题,这就是您想要做的事情。

示例中的 ScrollPane 是可平移和缩放的,在计算视点中项目的坐标时会考虑到这一点(通过使用其转换后的boundsInParent,而不是其未转换的boundsInLocal)。因此,滚动和平移以移动内容,然后按

r
键报告所有内容的当前场景坐标。

这是使用以下代码完成的:

star.localToScene(star.getBoundsInParent())

示例应用程序

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;

public class GraphicsScalingApp extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    private final Node star = createStar();
    private final Node curve = createCurve();
    private Stage stage;
    private Scene scene;

    @Override
    public void start(final Stage stage) {
        this.stage = stage;

        final Group group = new Group(star, curve);

        Parent zoomPane = createZoomPane(group);

        VBox layout = new VBox();
        layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);

        VBox.setVgrow(zoomPane, Priority.ALWAYS);

        scene = new Scene(layout);/**/

        stage.setTitle("Zoomy");
        stage.getIcons().setAll(new Image(APP_ICON));
        stage.setScene(scene);
        stage.show();
    }

    private Parent createZoomPane(final Group group) {
        final double SCALE_DELTA = 1.1;
        final StackPane zoomPane = new StackPane();

        zoomPane.getChildren().add(group);

        final ScrollPane scroller = new ScrollPane();
        final Group scrollContent = new Group(zoomPane);
        scroller.setContent(scrollContent);

        scroller.viewportBoundsProperty().addListener((observable, oldValue, newValue) ->
                zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight())
        );

        scroller.setPrefViewportWidth(256);
        scroller.setPrefViewportHeight(256);

        zoomPane.setOnScroll(event -> {
            event.consume();

            if (event.getDeltaY() == 0) {
                return;
            }

            double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
                    : 1 / SCALE_DELTA;

            // amount of scrolling in each direction in scrollContent coordinate
            // units
            Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);

            group.setScaleX(group.getScaleX() * scaleFactor);
            group.setScaleY(group.getScaleY() * scaleFactor);

            // move viewport so that old center remains in the center after the
            // scaling
            repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);

        });

        // Panning via drag....
        final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
        scrollContent.setOnMousePressed(event ->
                lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()))
        );

        scrollContent.setOnMouseDragged(event -> {
            double deltaX = event.getX() - lastMouseCoordinates.get().getX();
            double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
            double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
            double desiredH = scroller.getHvalue() - deltaH;
            scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));

            double deltaY = event.getY() - lastMouseCoordinates.get().getY();
            double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
            double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
            double desiredV = scroller.getVvalue() - deltaV;
            scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
        });

        return scroller;
    }

    private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
        double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
        double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
        double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
        double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
        double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
        double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
        return new Point2D(scrollXOffset, scrollYOffset);
    }

    private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
        double scrollXOffset = scrollOffset.getX();
        double scrollYOffset = scrollOffset.getY();
        double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
        if (extraWidth > 0) {
            double halfWidth = scroller.getViewportBounds().getWidth() / 2;
            double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
            scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
        } else {
            scroller.setHvalue(scroller.getHmin());
        }
        double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
        if (extraHeight > 0) {
            double halfHeight = scroller.getViewportBounds().getHeight() / 2;
            double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
            scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
        } else {
            scroller.setHvalue(scroller.getHmin());
        }
    }

    private SVGPath createCurve() {
        SVGPath ellipticalArc = new SVGPath();
        ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
        ellipticalArc.setStroke(Color.LIGHTGREEN);
        ellipticalArc.setStrokeWidth(4);
        ellipticalArc.setFill(null);
        return ellipticalArc;
    }

    private SVGPath createStar() {
        SVGPath star = new SVGPath();
        star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
        star.setStrokeLineJoin(StrokeLineJoin.ROUND);
        star.setStroke(Color.BLUE);
        star.setFill(Color.DARKBLUE);
        star.setStrokeWidth(4);
        return star;
    }

    private MenuBar createMenuBar(final Stage stage, final Group group) {
        Menu fileMenu = new Menu("_File");
        MenuItem exitMenuItem = new MenuItem("E_xit");
        exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
        exitMenuItem.setOnAction(event -> stage.close());
        fileMenu.getItems().setAll(exitMenuItem);

        Menu zoomMenu = new Menu("_Zoom");
        MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
        zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
        zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
        zoomResetMenuItem.setOnAction(event -> {
            group.setScaleX(1);
            group.setScaleY(1);
        });

        MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
        zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
        zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
        zoomInMenuItem.setOnAction(event -> {
            group.setScaleX(group.getScaleX() * 1.5);
            group.setScaleY(group.getScaleY() * 1.5);
        });

        MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
        zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
        zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
        zoomOutMenuItem.setOnAction(event -> {
            group.setScaleX(group.getScaleX() * 1 / 1.5);
            group.setScaleY(group.getScaleY() * 1 / 1.5);
        });
        zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
                zoomOutMenuItem);

        Menu coordsMenu = new Menu("_Coords");
        MenuItem reportCoordsMenuItem = new MenuItem("_Report");
        reportCoordsMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.R));
        reportCoordsMenuItem.setOnAction(event -> reportCoords());
        coordsMenu.getItems().setAll(reportCoordsMenuItem);

        MenuBar menuBar = new MenuBar();
        menuBar.getMenus().setAll(fileMenu, zoomMenu, coordsMenu);

        return menuBar;
    }

    private void reportCoords() {
        Node viewport = scene.lookup(".viewport");
        Bounds sceneBounds = scene.getRoot().getLayoutBounds();
        Bounds viewportBoundsInScene = viewport.localToScene(viewport.getBoundsInLocal());
        Bounds starBoundsInScene = star.localToScene(star.getBoundsInParent());
        Bounds curveBoundsInScene = curve.localToScene(curve.getBoundsInParent());

        String alertText = """
                Root:     %s
                Viewport: %s
                Star:     %s
                Curve:    %s
                """.formatted(
                        formatBounds(sceneBounds),
                        formatBounds(viewportBoundsInScene),
                        formatBounds(starBoundsInScene),
                        formatBounds(curveBoundsInScene)
        );

        System.out.println(alertText);

        Alert displayCoordsAlert = new Alert(
                Alert.AlertType.INFORMATION,
                alertText
        );
        displayCoordsAlert.initOwner(stage);
        displayCoordsAlert.setHeaderText("Coordinate bounds in scene coordinates");
        displayCoordsAlert.getDialogPane()
                .lookup(".content")
                .setStyle("-fx-font-family: monospace");
        displayCoordsAlert.getDialogPane().setPrefSize(550, 200);
        displayCoordsAlert.showAndWait();
    }

    private String formatBounds(Bounds bounds) {
        return "minX: %5d, minY: %5d, maxX: %5d, maxY: %5d".formatted(
                (int) bounds.getMinX(),
                (int) bounds.getMinY(),
                (int) bounds.getMaxX(),
                (int) bounds.getMaxY()
        );
    }

    // icons source from:
    // http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
    // icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
    // http://creativecommons.org/licenses/by-nc-nd/3.0/
    // icon Commercial usage: Allowed (Author Approval required -> Visit artist
    // website for details).

    public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
    public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
    public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
    public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
    public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
© www.soinside.com 2019 - 2024. All rights reserved.