如何获取JavaFX ScrollPane中内容节点(Page)的像素坐标?
解决ScrollPane中缩放页面的坐标与背景可见区域计算问题
需求概述
我正在实现类似CorelDraw/InDesign的ScrollPane组件:内部页面(Page)支持拖拽移动、缩放。需要实现的核心功能是:当页面放大到上下超出视口,但左右两侧的VBox黑色背景可见时,计算视口内页面左右两侧的VBox可见像素数。当前代码只能获取页面左上角与视口左边缘的距离,但页面完全可见时该值始终为0,0,无法满足需求。
问题根源
直接使用scrollPane.getViewportBounds()结合缩放系数计算的方式,没有考虑VBox的居中布局、页面的缩放变换以及坐标空间的转换,导致无法准确获取页面在视口中的实际位置。
解决方案
通过JavaFX的坐标空间转换API,将视口边界转换到VBox的局部坐标系,再结合页面的缩放与布局信息,计算出页面在视口中的可见范围,进而得到左右两侧VBox的可见像素数。具体步骤:
- 将ScrollPane视口的边界转换为
centeredVBox的局部坐标 - 获取页面缩放后的实际尺寸与在VBox中的位置
- 计算视口内页面的左右可见边缘,用视口宽度减去页面可见部分的宽度,得到左右VBox的可见像素数
修改后的代码
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); // 居中 this.scrollPane.setFitToWidth(true); // 居中 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 -> { // 转换视口边界到centeredVBox的局部坐标系 Bounds viewportInVBox = scrollPane.getViewportBounds(); Point2D viewportMinInVBox = scrollPane.localToScene(new Point2D(viewportInVBox.getMinX(), viewportInVBox.getMinY())); viewportMinInVBox = centeredVBox.sceneToLocal(viewportMinInVBox); Point2D viewportMaxInVBox = scrollPane.localToScene(new Point2D(viewportInVBox.getMaxX(), viewportInVBox.getMaxY())); viewportMaxInVBox = centeredVBox.sceneToLocal(viewportMaxInVBox); double viewportWidthInVBox = viewportMaxInVBox.getX() - viewportMinInVBox.getX(); // 获取页面缩放后的实际边界(在centeredVBox中的坐标) Bounds pageBoundsInVBox = page.getBoundsInParent(); // 计算页面在视口中的左右可见边缘 double pageLeftInViewport = Math.max(pageBoundsInVBox.getMinX(), viewportMinInVBox.getX()); double pageRightInViewport = Math.min(pageBoundsInVBox.getMaxX(), viewportMaxInVBox.getX()); // 计算左右两侧可见的VBox像素数 double leftVBoxVisible = pageLeftInViewport - viewportMinInVBox.getX(); double rightVBoxVisible = viewportMaxInVBox.getX() - pageRightInViewport; System.out.println("左侧VBox可见像素数: " + leftVBoxVisible); System.out.println("右侧VBox可见像素数: " + rightVBoxVisible); System.out.println("页面在视口中的左上角位置(VBox局部坐标): " + pageLeftInViewport + "/" + Math.max(pageBoundsInVBox.getMinY(), viewportMinInVBox.getY())); }); 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) { double zoomFactor = Math.exp(wheelDelta * 0.02); Bounds innerBounds = this.zoomGroup.getLayoutBounds(); Bounds viewportBounds = this.scrollPane.getViewportBounds(); // 计算[0,1]范围的像素偏移 double valX = this.scrollPane.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth()); double valY = this.scrollPane.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight()); // 转换鼠标坐标到page的局部坐标系 Point2D posInZoomTarget = this.page.parentToLocal(this.zoomGroup.parentToLocal(mousePoint)); // 计算滚动位置的调整量(像素) Point2D adjustment = this.page.getLocalToParentTransform() .deltaTransform(posInZoomTarget.multiply(zoomFactor - 1)); // 转换回[0,1]范围 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(); } }
代码说明
- 通过
localToScene和sceneToLocal完成视口边界到VBox局部坐标系的转换,确保坐标空间统一 page.getBoundsInParent()获取页面在VBox中的实际边界(已考虑缩放变换)- 通过比较视口边界与页面边界,计算出页面在视口中的可见范围,进而得到左右VBox的可见像素数
内容的提问来源于stack exchange,提问作者user17160846
相关产品推荐
相关产品推荐

