You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 00:54:58