ScrollPane内嵌带间距GridPane渲染异常问题求助
我来帮你搞定这三个GridPane和ScrollPane的问题,顺便给你聊聊后续单元格内图形缩放的最佳实践~
问题1:网格未显示全部间距
你的GridPane没有正确计算整体尺寸,导致单元格和间距没有完全展示。要知道,GridPane的hgap/vgap是单元格之间的间隔,20个单元格横向会有19个间隔,纵向同理。我们需要把GridPane的尺寸绑定到总宽度和高度,确保包含所有单元格和间距:
在initialize方法末尾加上这段代码:
// 计算GridPane的总宽度:单元格宽度×数量 + 间距×(数量-1) DoubleBinding gridWidth = cellSizeBinding.multiply(WIDTH).add(gridPane.hgapProperty().multiply(WIDTH - 1)); DoubleBinding gridHeight = cellSizeBinding.multiply(HEIGHT).add(gridPane.vgapProperty().multiply(HEIGHT - 1)); // 绑定GridPane的首选和最小尺寸,确保不会被压缩 gridPane.prefWidthProperty().bind(gridWidth); gridPane.prefHeightProperty().bind(gridHeight); gridPane.minWidthProperty().bind(gridWidth); gridPane.minHeightProperty().bind(gridHeight);
这样GridPane就能准确计算自己的大小,所有单元格和间距都会完整显示。
问题2:缩放后可拖拽至网格边界外
ScrollPane默认允许你把内容拖到边界外,这显然不符合预期。我们可以通过监听hvalue和vvalue的变化,限制拖拽范围,确保网格不会被拖出视口:
首先在Controller里注入ScrollPane:
@FXML private ScrollPane scrollPane;
然后在initialize方法中添加监听器:
scrollPane.hvalueProperty().addListener((obs, oldVal, newVal) -> { double viewportWidth = scrollPane.getViewportBounds().getWidth(); // 计算最大可拖拽的水平比例:(网格宽度-视口宽度)/网格宽度 double maxHValue = Math.max(0, (gridPane.getWidth() - viewportWidth) / gridPane.getWidth()); if (newVal.doubleValue() < 0) { scrollPane.setHvalue(0); } else if (newVal.doubleValue() > maxHValue) { scrollPane.setHvalue(maxHValue); } }); scrollPane.vvalueProperty().addListener((obs, oldVal, newVal) -> { double viewportHeight = scrollPane.getViewportBounds().getHeight(); double maxVValue = Math.max(0, (gridPane.getHeight() - viewportHeight) / gridPane.getHeight()); if (newVal.doubleValue() < 0) { scrollPane.setVvalue(0); } else if (newVal.doubleValue() > maxVValue) { scrollPane.setVvalue(maxVValue); } });
当网格比视口小时,maxHValue和maxVValue会是0,这时候拖拽会被固定在初始位置,结合后面的居中处理就能保持网格居中。
问题3:网格小于ScrollPane时无法居中
GridPane的alignment="CENTER"不生效,是因为ScrollPane的内容区域默认是左上角对齐。最简单的解决方法是把GridPane放在一个StackPane里——StackPane会自动把子元素居中,然后把StackPane作为ScrollPane的内容:
修改FXML代码:
<ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" hvalue="0.5" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" pannable="true" prefHeight="400.0" prefWidth="600.0" vbarPolicy="NEVER" vvalue="0.5" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="bug.app.Bug"> <StackPane fx:id="container" style="-fx-background-color: red;"> <GridPane fx:id="gridPane" hgap="1.0" vgap="1.0"> </GridPane> </StackPane> </ScrollPane>
然后在Controller里注入StackPane,并且让StackPane的最小尺寸绑定到ScrollPane的视口尺寸(这样当网格小时,StackPane会填充整个视口,GridPane就会在其中居中):
@FXML private StackPane container; // 在initialize方法中添加 container.minWidthProperty().bind(scrollPane.viewportBoundsProperty().map(bounds -> bounds.getWidth())); container.minHeightProperty().bind(scrollPane.viewportBoundsProperty().map(bounds -> bounds.getHeight()));
额外建议:单元格内图形的优雅缩放
你计划用Pane子类化并添加分割图形(比如三角形),最佳实践是把图形的尺寸/坐标绑定到Pane的尺寸,这样当Pane缩放时,图形会自动跟着调整:
举个例子,在单元格Pane中添加一个随Pane大小变化的三角形:
final Pane cell = new Pane(); // ... 其他单元格设置 ... // 创建三角形并绑定顶点 Polygon triangle = new Polygon(0, 0, 0, 0, 0, 0); // 绑定三角形的右上角到单元格的宽度,左下角到单元格的高度 triangle.getPoints().get(1).bind(cell.widthProperty()); triangle.getPoints().get(3).bind(cell.heightProperty()); triangle.setFill(Color.BLUE); triangle.setOpacity(0.5); cell.getChildren().add(triangle);
如果需要更复杂的分割(比如分成4部分),你可以用多个Polygon,每个的顶点都绑定到单元格的边界或中心点(比如cell.widthProperty().divide(2)),这样所有图形都会跟着单元格优雅缩放。
完整修改后的代码
FXML:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.ScrollPane?> <?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.StackPane?> <ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" hvalue="0.5" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" pannable="true" prefHeight="400.0" prefWidth="600.0" vbarPolicy="NEVER" vvalue="0.5" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="bug.app.Bug"> <StackPane fx:id="container" style="-fx-background-color: red;"> <GridPane fx:id="gridPane" hgap="1.0" vgap="1.0"> </GridPane> </StackPane> </ScrollPane>
Controller + Main:
package bug.app; import javafx.application.Application; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; import javafx.stage.Stage; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class Bug extends Application { private static final int HEIGHT = 20; private static final int WIDTH = 20; private static final double MIN_CELL_SIZE = 2; @FXML private ScrollPane scrollPane; @FXML private StackPane container; @FXML private GridPane gridPane; private DoubleProperty zoomRatio = new SimpleDoubleProperty(5); @FXML private void initialize() { final DoubleBinding cellSizeBinding = zoomRatio.multiply(MIN_CELL_SIZE); // 计算GridPane总尺寸,包含单元格和间距 DoubleBinding gridWidth = cellSizeBinding.multiply(WIDTH).add(gridPane.hgapProperty().multiply(WIDTH - 1)); DoubleBinding gridHeight = cellSizeBinding.multiply(HEIGHT).add(gridPane.vgapProperty().multiply(HEIGHT - 1)); gridPane.prefWidthProperty().bind(gridWidth); gridPane.prefHeightProperty().bind(gridHeight); gridPane.minWidthProperty().bind(gridWidth); gridPane.minHeightProperty().bind(gridHeight); // 让容器填充视口,确保GridPane居中 container.minWidthProperty().bind(scrollPane.viewportBoundsProperty().map(bounds -> bounds.getWidth())); container.minHeightProperty().bind(scrollPane.viewportBoundsProperty().map(bounds -> bounds.getHeight())); // 限制拖拽范围,防止网格超出视口 scrollPane.hvalueProperty().addListener((obs, oldVal, newVal) -> { double viewportWidth = scrollPane.getViewportBounds().getWidth(); double maxHValue = Math.max(0, (gridPane.getWidth() - viewportWidth) / gridPane.getWidth()); if (newVal.doubleValue() < 0) { scrollPane.setHvalue(0); } else if (newVal.doubleValue() > maxHValue) { scrollPane.setHvalue(maxHValue); } }); scrollPane.vvalueProperty().addListener((obs, oldVal, newVal) -> { double viewportHeight = scrollPane.getViewportBounds().getHeight(); double maxVValue = Math.max(0, (gridPane.getHeight() - viewportHeight) / gridPane.getHeight()); if (newVal.doubleValue() < 0) { scrollPane.setVvalue(0); } else if (newVal.doubleValue() > maxVValue) { scrollPane.setVvalue(maxVValue); } }); // 创建单元格 List<Node> cells = new ArrayList<>(HEIGHT * WIDTH); for (int row = 0; row < HEIGHT; row++) { for (int col = 0; col < WIDTH; col++) { final Pane cell = new Pane(); cell.setBackground(new Background(new BackgroundFill(Color.YELLOW, CornerRadii.EMPTY, Insets.EMPTY))); cell.setMinSize(MIN_CELL_SIZE, MIN_CELL_SIZE); cell.setMaxWidth(Region.USE_PREF_SIZE); cell.setMaxHeight(Region.USE_PREF_SIZE); cell.prefWidthProperty().bind(cellSizeBinding); cell.prefHeightProperty().bind(cellSizeBinding); // 添加示例缩放三角形 Polygon triangle = new Polygon(0, 0, 0, 0, 0, 0); triangle.getPoints().get(1).bind(cell.widthProperty()); triangle.getPoints().get(3).bind(cell.heightProperty()); triangle.setFill(Color.BLUE); triangle.setOpacity(0.5); cell.getChildren().add(triangle); GridPane.setConstraints(cell, col, row, 1, 1); cells.add(cell); } } gridPane.getChildren().clear(); gridPane.getChildren().addAll(cells); } public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws IOException { Parent root = FXMLLoader.load(getClass().getResource("/view/bugView.fxml")); final Scene scene = new Scene(root, 600, 400); primaryStage.setScene(scene); primaryStage.show(); } }
内容的提问来源于stack exchange,提问作者Hawk

