我正在尝试实现一个具有与 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();
}
}
我从这里提取了代码:
并修改它来报告坐标。也许它会报告您正在寻找的值。
我不太理解你的应用程序代码,所以我没有使用它。
这个答案中有很多代码,但大部分只是从链接的答案中复制的。
唯一的新功能是报告场景根、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";
}