获取平移和缩放节点的视口

Get Viewport of translated and scaled node

问:如何在变换和缩放节点的坐标中获取查看矩形?

代码附在下面,它基于此答案中的代码:

详情:

我有一个简单的窗格,BigGridPane,其中包含一组正方形,均为 50x50。

我把它放在这个 PanAndZoomPane 结构中,它是从上面引用的答案中提取出来的。老实说,我不能完全理解 PanAndZoomPane 的实现。例如,我不清楚为什么它根本需要 ScrollPane,但我没有深入研究没有它的尝试。

PanAndZoomPane 让我可以平移和缩放 BigGridPane。这很管用。

这个总结构涉及 4 个窗格,在这个层次结构中:ScrollPane 包含 PanAndZoomPane,其中 Group 包含 BigGridPane

ScrollPane
  PanAndZoomPane
    Group
      BigGridPane

我已经将听众放在所有这些的 boundsInLocalPropertyboundsInParentProperty 上,其中唯一一个在平移和缩放时发生变化的是 boundsInParentProperty PanAndZoomPane。 (出于某种原因,我看到它在滚动窗格上触发,但所有值都是相同的,所以我不在此处包含它)。

随着 boundsInParentProperty 的变化,PanAndZoomPanetranslateXtranslateYmyScale 属性也会随着事物的移动而变化。当然,这是预料之中的。 myScale 绑定到 PanAndZoomPane.

scaleXscaleY 属性

这是启动时的样子。

如果我如图所示平移网格,将 2-2 放在左上角:

我们可以看到PanAndZoomPane.

的属性
panAndZoom in parent: BoundingBox [minX:-99.5, minY:-99.5, minZ:0.0, 
                                   width:501.5, height:501.5, depth:0.0, 
                                   maxX:402.0, maxY:402.0, maxZ:0.0]
paz scale = 1.0 - tx: -99.0 - ty: -99.0

比例为 1(无缩放),我们翻译了 ~100x100。也就是说,BigGridPane 的原点在 -100,-100。这一切都是完全有道理的。同样,边界框显示相同的东西。原点在-100,-100。

在这种情况下,我想派生一个矩形,显示我在 window 中看到的内容,在 BigGridPane 的坐标中。那将意味着

的矩形

x:100 y:100 width:250 height:250

通常情况下,我认为这将是 ScrollPane 的视口,但由于此代码实际上并未使用 ScrollPane 进行滚动(同样,我不太清楚它在这里的作用),ScrollPane视口永远不会改变。

我应该注意到,由于我 mac 上的视网膜显示器,现在正在发生恶作剧。如果你看一下显示 5x5 的矩形,它们是 50x50 的矩形,所以我们应该看到 10x10,但由于我的 iMac 上的 Retina 显示器,一切都加倍了。我们在 BigGridPane 坐标中看到的是一个由 5 个正方形组成的 250x250 块,偏移量为 100x100。这是在 500x500 的 window 中显示的事实,这是一个细节(但我们可以忽略的不太可能)。

但重申一下我的问题是什么,这就是我想要得到的:100x100 处的 250x250 正方形。

奇怪的是它偏移了 100x100,即使框架是两倍大(500 对 250)。如果我平移到左上角 1-1 的位置,则偏移量为 -50,-50,就像它应该的那样。

现在,让我们添加缩放,然后再次平移到 2-2。

单击滚轮 1 次,刻度跳至 1.5。

panAndZoom in parent: BoundingBox [minX:-149.375, minY:-150.375, minZ:0.0,     
                                   width:752.25, height:752.25, depth:0.0, 
                                   maxX:602.875, maxY:601.875, maxZ:0.0]
paz scale = 1.5 - tx: -23.375 - ty: -24.375

在这种情况下,我再次想要的是 BigGridPane 坐标中的矩形。大致:

x:100 y:100 w:150 h:150

我们看到我们偏移了 2x2 个框 (100x100),我们看到 3+ 个框 (150x150)。

所以。回到边界框。 MinX 和 minY = -150,-150。这很好。 100 x 1.5 = 150。同样宽度和高度为750。500 x 1.5 = 750。所以,这很好。

翻译是我们离开 rails 的地方。 -23.375,-24.375。我不知道这些数字是从哪里来的。我似乎无法将它们与 100、150、1.5 变焦等相关联。

更糟糕的是,如果我们平移(仍然以 1.5 比例)到“0,0”,之前,在比例 = 1 时,tx 和 ty 都是 0。这很好。

panAndZoom in parent: BoundingBox [minX:0.625, minY:0.625, minZ:0.0, 
                                   width:752.25, height:752.25, depth:0.0, 
                                   maxX:752.875, maxY:752.875, maxZ:0.0]
paz scale = 1.5 - tx: 126.625 - ty: 126.625

现在,它们是 126.625(可能应四舍五入为 125)。我不知道这些数字是从哪里来的。

我已经对这些数字进行了各种尝试,以了解这些数字的来源。

JavaFX 知道数字是多少! (即使整个视网膜的东西有点让我头疼,我暂时会忽略它)。

而且我在任何窗格的变换中都看不到任何东西。

所以,我的坐标系遍布整个地图,我想知道我的 BigGridPane 的哪一部分显示在我的平移和缩放视图中。

代码:

package pkg;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

public class PanZoomTest extends Application {

    private ScrollPane scrollPane = new ScrollPane();
    private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
    private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
    private final Group group = new Group();
    PanAndZoomPane panAndZoomPane = null;
    BigGridPane1 bigGridPane = new BigGridPane1(10, 10, 50);

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

        scrollPane.setPannable(true);
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);

        group.getChildren().add(bigGridPane);

        panAndZoomPane = new PanAndZoomPane();
        zoomProperty.bind(panAndZoomPane.myScale);
        deltaY.bind(panAndZoomPane.deltaY);
        panAndZoomPane.getChildren().add(group);

        SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);

        scrollPane.setContent(panAndZoomPane);
        panAndZoomPane.toBack();

        addListeners("panAndZoom", panAndZoomPane);

        scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
        scrollPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
        scrollPane.addEventFilter(ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());

        AnchorPane anchorPane = new AnchorPane();

        anchorPane.getChildren().add(scrollPane);

        anchorPane.setTopAnchor(scrollPane, 1.0d);
        anchorPane.setRightAnchor(scrollPane, 1.0d);
        anchorPane.setBottomAnchor(scrollPane, 1.0d);
        anchorPane.setLeftAnchor(scrollPane, 1.0d);

        BorderPane root = new BorderPane(anchorPane);
        Label label = new Label("Pan and Zoom Test");
        root.setTop(label);

        Scene scene = new Scene(root, 250, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

    private void addListeners(String label, Node node) {
        node.boundsInLocalProperty().addListener((o) -> {
            System.out.println(label + " in local: " + node.getBoundsInLocal());
        });

        node.boundsInParentProperty().addListener((o) -> {
            System.out.println(label + " in parent: " + node.getBoundsInParent());
            System.out.println("paz scale = " + panAndZoomPane.getScale() + " - "
                    + panAndZoomPane.getTranslateX() + " - "
                    + panAndZoomPane.getTranslateY());
            System.out.println(group.getTransforms());
        });
    }

    class BigGridPane extends Region {

        int rows;
        int cols;
        int size;

        Font numFont = Font.font("sans-serif", 8);
        FontMetrics numMetrics = new FontMetrics(numFont);

        public BigGridPane(int cols, int rows, int size) {
            this.rows = rows;
            this.cols = cols;
            this.size = size;
            int sizeX = cols * size;
            int sizeY = rows * size;
            setMinSize(sizeX, sizeY);
            setMaxSize(sizeX, sizeY);
            setPrefSize(sizeX, sizeY);
            populate();
        }

        @Override
        protected void layoutChildren() {
            System.out.println("grid layout");
            super.layoutChildren();
        }

        private void populate() {
            ObservableList<Node> children = getChildren();
            children.clear();
            for (int i = 0; i < cols; i++) {
                for (int j = 0; j < rows; j++) {
                    Rectangle r = new Rectangle(i * size, j * size, size, size);
                    r.setFill(null);
                    r.setStroke(Color.BLACK);
                    String label = i + "-" + j;
                    Point2D p = new Point2D(r.getBoundsInLocal().getCenterX(), r.getBoundsInLocal().getCenterY());
                    Text t = new Text(label);
                    t.setX(p.getX() - numMetrics.computeStringWidth(label) / 2);
                    t.setY(p.getY() + numMetrics.getLineHeight() / 2);
                    t.setFont(numFont);
                    children.add(r);
                    children.add(t);
                }
            }
        }
    }

    class PanAndZoomPane extends Pane {

        public static final double DEFAULT_DELTA = 1.5d; //1.3d
        DoubleProperty myScale = new SimpleDoubleProperty(1.0);
        public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
        private Timeline timeline;

        public PanAndZoomPane() {

            this.timeline = new Timeline(30);//60

            // add scale transform
            scaleXProperty().bind(myScale);
            scaleYProperty().bind(myScale);
        }

        public double getScale() {
            return myScale.get();
        }

        public void setScale(double scale) {
            myScale.set(scale);
        }

        public void setPivot(double x, double y, double scale) {
            // note: pivot value must be untransformed, i. e. without scaling
            // timeline that scales and moves the node
            timeline.getKeyFrames().clear();
            timeline.getKeyFrames().addAll(
                    new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
                    new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
                    new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale)) //200
            );
            timeline.play();

        }

        public double getDeltaY() {
            return deltaY.get();
        }

        public void setDeltaY(double dY) {
            deltaY.set(dY);
        }
    }

    /**
     * Mouse drag context used for scene and nodes.
     */
    class DragContext {

        double mouseAnchorX;
        double mouseAnchorY;

        double translateAnchorX;
        double translateAnchorY;

    }

    /**
     * Listeners for making the scene's canvas draggable and zoomable
     */
    public class SceneGestures {

        private DragContext sceneDragContext = new DragContext();

        PanAndZoomPane panAndZoomPane;

        public SceneGestures(PanAndZoomPane canvas) {
            this.panAndZoomPane = canvas;
        }

        public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
            return onMousePressedEventHandler;
        }

        public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
            return onMouseDraggedEventHandler;
        }

        public EventHandler<ScrollEvent> getOnScrollEventHandler() {
            return onScrollEventHandler;
        }

        private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {

            public void handle(MouseEvent event) {

                sceneDragContext.mouseAnchorX = event.getX();
                sceneDragContext.mouseAnchorY = event.getY();

                sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
                sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();

            }

        };

        private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
            public void handle(MouseEvent event) {

                panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
                panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);

                event.consume();
            }
        };

        /**
         * Mouse wheel handler: zoom to pivot point
         */
        private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {

            @Override
            public void handle(ScrollEvent event) {

                double delta = PanAndZoomPane.DEFAULT_DELTA;

                double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
                double oldScale = scale;

                panAndZoomPane.setDeltaY(event.getDeltaY());
                if (panAndZoomPane.deltaY.get() < 0) {
                    scale /= delta;
                } else {
                    scale *= delta;
                }

                double f = (scale / oldScale) - 1;

                double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth() / 2 + panAndZoomPane.getBoundsInParent().getMinX()));
                double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight() / 2 + panAndZoomPane.getBoundsInParent().getMinY()));

                panAndZoomPane.setPivot(f * dx, f * dy, scale);

                event.consume();

            }
        };
    }

    class FontMetrics {

        final private Text internal;
        public float lineHeight;

        public FontMetrics(Font fnt) {
            internal = new Text();
            internal.setFont(fnt);
            Bounds b = internal.getLayoutBounds();
            lineHeight = (float) b.getHeight();
        }

        public float computeStringWidth(String txt) {
            internal.setText(txt);
            return (float) internal.getLayoutBounds().getWidth();
        }

        public float getLineHeight() {
            return lineHeight;
        }
    }
}

一般情况下,在node2的坐标系中,如果两者在同一场景中,则可以使用

获取node1的边界
node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));

我不明白你发布的所有代码;当您似乎自己实现所有平移和缩放时,我真的不知道为什么要使用滚动窗格。这是一个更简单的 PanZoomPane 版本,然后是一个测试,它展示了如何使用上面的想法在 panning/zooming 内容的坐标系中获取视口的边界。 “视口”只是内容坐标系中 panning/zooming 窗格的边界。

如果您需要在您的平移和缩放版本中添加其他功能,您应该能够使这个想法适应那个;但是我要花很长时间才能理解你在那里所做的一切。

import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;

public class PanZoomPane extends Region {
    
    private final Node content ;
    
    private final Rectangle clip ;
    
    private Affine transform ;
    
    private Point2D mouseDown ;
    
    private static final double SCALE = 1.01 ; // zoom factor per pixel scrolled
    
    public PanZoomPane(Node content) {
        this.content = content ;
        getChildren().add(content);
        clip = new Rectangle();
        setClip(clip);
        transform = Affine.affine(1, 0, 0, 1, 0, 0);
        content.getTransforms().setAll(transform);
        
        content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
        content.setOnMouseDragged(event -> {
            double deltaX = event.getX() - mouseDown.getX();
            double deltaY = event.getY() - mouseDown.getY();
            translate(deltaX, deltaY);
        });
        content.setOnScroll(event -> {
            double pivotX = event.getX();
            double pivotY = event.getY();
            double scale = Math.pow(SCALE, event.getDeltaY());
            scale(pivotX, pivotY, scale);
        });
    }
    
    public Node getContent() {
        return content ;
    }
    
    @Override
    protected void layoutChildren() {
        clip.setWidth(getWidth());
        clip.setHeight(getHeight());        
    }
    
    public void scale(double pivotX, double pivotY, double scale) {
        transform.append(Transform.scale(scale, scale, pivotX, pivotY));
    }
    
    public void translate(double x, double y) {
        transform.append(Transform.translate(x, y));
    }
    
    public void reset() {
        transform.setToIdentity();
    }

}
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.stage.Stage;



public class PanZoomTest extends Application {
    
    private Binding<Bounds> viewport ;

    @Override
    public void start(Stage stage) {
        Node content = createContent(50, 50, 50) ;
        PanZoomPane pane = new PanZoomPane(content);
        
        viewport = new ObjectBinding<>() {
            {
                bind(
                    pane.localToSceneTransformProperty(),
                    pane.boundsInLocalProperty(),
                    content.localToSceneTransformProperty()
                );
            }

            @Override
            protected Bounds computeValue() {
                return content.sceneToLocal(pane.localToScene(pane.getBoundsInLocal()));
            }
        };
        
        viewport.addListener((obs, oldViewport, newViewport)  -> System.out.println(newViewport));
        
        BorderPane root = new BorderPane(pane);
        Button reset = new Button("Reset");
        reset.setOnAction(event -> pane.reset());
        
        HBox buttons = new HBox(reset);
        buttons.setAlignment(Pos.CENTER);
        buttons.setPadding(new Insets(10));
        
        root.setTop(buttons);
        
        Scene scene = new Scene(root, 800, 800);
        stage.setScene(scene);
        stage.show();
    }
    
    private Node createContent(int columns, int rows, double cellSize) {
        
        GridPane grid = new GridPane() ;
        ColumnConstraints cc = new ColumnConstraints();
        cc.setMinWidth(cellSize);
        cc.setPrefWidth(cellSize);
        cc.setMaxWidth(cellSize);
        cc.setFillWidth(true);
        cc.setHalignment(HPos.CENTER);
        for (int column = 0 ; column < columns ; column++) {
            grid.getColumnConstraints().add(cc);
        }
        RowConstraints rc = new RowConstraints();
        rc.setMinHeight(cellSize);
        rc.setPrefHeight(cellSize);
        rc.setMaxHeight(cellSize);
        rc.setFillHeight(true);
        rc.setValignment(VPos.CENTER);
        for (int row = 0 ; row < rows ; row++) {
            grid.getRowConstraints().add(rc);
        }
        for (int x = 0 ; x < columns ; x++) {
            for (int y = 0 ; y < rows ; y++) {
                Label label = new Label(String.format("[%d, %d]", x, y));
                label.setBackground(new Background(
                    new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY),
                    new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1,1,0,0))
                ));
                label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
                grid.add(label, x, y);
            }
        }
        return grid ;
    }

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

}