为什么 JavaFX 中出现奇怪的绘画行为
Why strange painting behavior in JavaFX
我有一个带有简单组件的简单 FX 示例。
package fxtest;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage stage) {
var bp = new BorderPane();
var r = new Rectangle(0, 0, 200, 200);
r.setFill(Color.GREEN);
var sp = new StackPane(r);
bp.setCenter(sp);
bp.setTop(new XPane());
bp.setBottom(new XPane());
bp.setLeft(new XPane());
bp.setRight(new XPane());
var scene = new Scene(bp, 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
package fxtest;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
setMinSize(100, 100);
setPrefSize(100, 100);
widthProperty().addListener((o) -> {
populate();
});
heightProperty().addListener((o) -> {
populate();
});
populate();
}
private void populate() {
ObservableList<Node> children = getChildren();
Rectangle r = new Rectangle(getWidth(), getHeight());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
children.add(r);
Line line = new Line(0, 0, getWidth(), getHeight());
children.add(line);
line = new Line(0, getHeight(), getWidth(), 0);
children.add(line);
}
}
当 运行 时,它符合我的预期:
当我长 window 时,X 也会长。
但是当我缩小 window 时,我得到了侧面板的瑕疵。
我原以为擦除背景可以解决这个问题,但我猜是顺序问题。但即便如此,当您拖动角时,所有 XPanes 的大小都会改变,并且它们都会重新绘制,但瑕疵仍然存在。
我尝试将 XPanes 包装到 StackPane 中,但这没有做任何事情(我认为不会,但无论如何都试过了)。
我该如何补救?这是 macOS Big Sur 上 JDK 16 上的 JavaFX 13。
为什么会得到神器
我认为应该使用不同的方法,而不是修复现有的方法,但如果需要,您可以修复它。
您正在向侦听器中的 XPane 添加新的矩形和线条。每次高度或宽度发生变化时,您都会添加一组新节点,但旧高度和宽度的旧节点集仍然存在。最终,如果您调整的大小足够大,性能将会下降,或者您将 运行 内存或资源不足,从而使程序无法使用。
BorderPane 按照添加它们的顺序绘制其子项(中心和 XPanes)而不进行裁剪,因此这些旧行将保留并且渲染器将在您调整大小时将它们绘制在某些窗格上。同样,一些窗格会覆盖一些线条,因为您可能会在窗格中构建大量填充矩形,并且它们与创建的大量线条部分重叠。
要解决此问题,请在添加任何新节点之前在 populate() 方法中清除 () 子节点列表。
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
备用解决方案
更改宽度和高度的侦听器并不是真正向自定义区域添加内容的地方,IMO。
我认为最好利用场景图,让它在更改这些节点的属性后处理现有节点的重绘和更新,而不是一直创建新节点。
这是一个子类区域的示例,并在发生调整大小时绘制得很好。
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
super();
Rectangle border = new Rectangle();
Line topLeftToBottomRight = new Line();
Line bottomLeftToTopRight = new Line();
getChildren().addAll(
border,
topLeftToBottomRight,
bottomLeftToTopRight
);
border.setStroke(Color.BLACK);
border.setFill(Color.WHITE);
border.widthProperty().bind(
widthProperty()
);
border.heightProperty().bind(
heightProperty()
);
topLeftToBottomRight.endXProperty().bind(
widthProperty()
);
topLeftToBottomRight.endYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.startYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.endXProperty().bind(
widthProperty()
);
setMinSize(100, 100);
setPrefSize(100, 100);
}
}
区域与窗格
我不确定您是否应该子类化 Pane 或 Region,两者之间的主要区别是 Pane 有一个 public 可修改子列表的访问器,但 Region 没有。所以这取决于你想做什么。如果像例子一样只是画X,那么Region是合适的。
关于 layoutChildren() 与绑定
Region 文档指出:
By default a Region inherits the layout behavior of its superclass,
Parent, which means that it will resize any resizable child nodes to
their preferred size, but will not reposition them. If an application
needs more specific layout behavior, then it should use one of the
Region subclasses: StackPane, HBox, VBox, TilePane, FlowPane,
BorderPane, GridPane, or AnchorPane.
To implement a more custom layout, a Region subclass must override
computePrefWidth, computePrefHeight, and layoutChildren. Note that
layoutChildren is called automatically by the scene graph while
executing a top-down layout pass and it should not be invoked directly
by the region subclass.
Region subclasses which layout their children will position nodes by
setting layoutX/layoutY and do not alter translateX/translateY, which
are reserved for adjustments and animation.
我实际上并没有在这里这样做,而是在构造函数中进行绑定,而不是重写 layoutChildren()。您可以实现一个替代解决方案,该解决方案按照文档讨论的方式运行,覆盖 layoutChildren() 而不是使用绑定,但它更复杂,而且关于如何做到这一点的文档也较少。
继承 Region 并覆盖 layoutChildren() 并不常见。相反,通常会使用标准布局窗格的组合,并在窗格和节点上设置约束以获得所需的布局。这让布局引擎可以做很多工作,例如对齐像素、计算边距和插图、遵守约束、重新定位内容等,其中很多工作需要手动完成 layoutChildren() 实现。
一种常见的方法是将相关的几何属性绑定到封闭容器的所需属性。一个相关的例子被检查 , and others are collected here.
下面的变体绑定了几个 Shape
instances to the Pane
width and height properties. Resize the enclosing stage
to see how the BorderPane
children conform to entries in the BorderPane Resize Table. The example also adds a red Circle
, which stays centered in each child, growing and shrinking in the center to fill the smaller of the width or height. The approach relies on the fluent arithmetic API available to properties that implement NumberExpression
or methods defined in Bindings
.
的顶点
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
/**
* @see
*/
public class App extends Application {
@Override
public void start(Stage stage) {
var bp = new BorderPane(new XPane(), new XPane(),
new XPane(), new XPane(), new XPane());
stage.setScene(new Scene(bp, 640, 480));
stage.show();
}
private static class XPane extends Pane {
private final Rectangle r = new Rectangle();
private final Circle c = new Circle(8, Color.RED);
private final Line line1 = new Line();
private final Line line2 = new Line();
public XPane() {
setPrefSize(100, 100);
r.widthProperty().bind(this.widthProperty());
r.heightProperty().bind(this.heightProperty());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
getChildren().add(r);
line1.endXProperty().bind(widthProperty());
line1.endYProperty().bind(heightProperty());
getChildren().add(line1);
line2.startXProperty().bind(widthProperty());
line2.endYProperty().bind(heightProperty());
getChildren().add(line2);
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
getChildren().add(c);
}
}
public static void main(String[] args) {
launch();
}
}
我有一个带有简单组件的简单 FX 示例。
package fxtest;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage stage) {
var bp = new BorderPane();
var r = new Rectangle(0, 0, 200, 200);
r.setFill(Color.GREEN);
var sp = new StackPane(r);
bp.setCenter(sp);
bp.setTop(new XPane());
bp.setBottom(new XPane());
bp.setLeft(new XPane());
bp.setRight(new XPane());
var scene = new Scene(bp, 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
package fxtest;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
setMinSize(100, 100);
setPrefSize(100, 100);
widthProperty().addListener((o) -> {
populate();
});
heightProperty().addListener((o) -> {
populate();
});
populate();
}
private void populate() {
ObservableList<Node> children = getChildren();
Rectangle r = new Rectangle(getWidth(), getHeight());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
children.add(r);
Line line = new Line(0, 0, getWidth(), getHeight());
children.add(line);
line = new Line(0, getHeight(), getWidth(), 0);
children.add(line);
}
}
当 运行 时,它符合我的预期:
当我长 window 时,X 也会长。
但是当我缩小 window 时,我得到了侧面板的瑕疵。
我原以为擦除背景可以解决这个问题,但我猜是顺序问题。但即便如此,当您拖动角时,所有 XPanes 的大小都会改变,并且它们都会重新绘制,但瑕疵仍然存在。
我尝试将 XPanes 包装到 StackPane 中,但这没有做任何事情(我认为不会,但无论如何都试过了)。
我该如何补救?这是 macOS Big Sur 上 JDK 16 上的 JavaFX 13。
为什么会得到神器
我认为应该使用不同的方法,而不是修复现有的方法,但如果需要,您可以修复它。
您正在向侦听器中的 XPane 添加新的矩形和线条。每次高度或宽度发生变化时,您都会添加一组新节点,但旧高度和宽度的旧节点集仍然存在。最终,如果您调整的大小足够大,性能将会下降,或者您将 运行 内存或资源不足,从而使程序无法使用。
BorderPane 按照添加它们的顺序绘制其子项(中心和 XPanes)而不进行裁剪,因此这些旧行将保留并且渲染器将在您调整大小时将它们绘制在某些窗格上。同样,一些窗格会覆盖一些线条,因为您可能会在窗格中构建大量填充矩形,并且它们与创建的大量线条部分重叠。
要解决此问题,请在添加任何新节点之前在 populate() 方法中清除 () 子节点列表。
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
备用解决方案
更改宽度和高度的侦听器并不是真正向自定义区域添加内容的地方,IMO。
我认为最好利用场景图,让它在更改这些节点的属性后处理现有节点的重绘和更新,而不是一直创建新节点。
这是一个子类区域的示例,并在发生调整大小时绘制得很好。
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
super();
Rectangle border = new Rectangle();
Line topLeftToBottomRight = new Line();
Line bottomLeftToTopRight = new Line();
getChildren().addAll(
border,
topLeftToBottomRight,
bottomLeftToTopRight
);
border.setStroke(Color.BLACK);
border.setFill(Color.WHITE);
border.widthProperty().bind(
widthProperty()
);
border.heightProperty().bind(
heightProperty()
);
topLeftToBottomRight.endXProperty().bind(
widthProperty()
);
topLeftToBottomRight.endYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.startYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.endXProperty().bind(
widthProperty()
);
setMinSize(100, 100);
setPrefSize(100, 100);
}
}
区域与窗格
我不确定您是否应该子类化 Pane 或 Region,两者之间的主要区别是 Pane 有一个 public 可修改子列表的访问器,但 Region 没有。所以这取决于你想做什么。如果像例子一样只是画X,那么Region是合适的。
关于 layoutChildren() 与绑定
Region 文档指出:
By default a Region inherits the layout behavior of its superclass, Parent, which means that it will resize any resizable child nodes to their preferred size, but will not reposition them. If an application needs more specific layout behavior, then it should use one of the Region subclasses: StackPane, HBox, VBox, TilePane, FlowPane, BorderPane, GridPane, or AnchorPane.
To implement a more custom layout, a Region subclass must override computePrefWidth, computePrefHeight, and layoutChildren. Note that layoutChildren is called automatically by the scene graph while executing a top-down layout pass and it should not be invoked directly by the region subclass.
Region subclasses which layout their children will position nodes by setting layoutX/layoutY and do not alter translateX/translateY, which are reserved for adjustments and animation.
我实际上并没有在这里这样做,而是在构造函数中进行绑定,而不是重写 layoutChildren()。您可以实现一个替代解决方案,该解决方案按照文档讨论的方式运行,覆盖 layoutChildren() 而不是使用绑定,但它更复杂,而且关于如何做到这一点的文档也较少。
继承 Region 并覆盖 layoutChildren() 并不常见。相反,通常会使用标准布局窗格的组合,并在窗格和节点上设置约束以获得所需的布局。这让布局引擎可以做很多工作,例如对齐像素、计算边距和插图、遵守约束、重新定位内容等,其中很多工作需要手动完成 layoutChildren() 实现。
一种常见的方法是将相关的几何属性绑定到封闭容器的所需属性。一个相关的例子被检查
下面的变体绑定了几个 Shape
instances to the Pane
width and height properties. Resize the enclosing stage
to see how the BorderPane
children conform to entries in the BorderPane Resize Table. The example also adds a red Circle
, which stays centered in each child, growing and shrinking in the center to fill the smaller of the width or height. The approach relies on the fluent arithmetic API available to properties that implement NumberExpression
or methods defined in Bindings
.
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
/**
* @see
*/
public class App extends Application {
@Override
public void start(Stage stage) {
var bp = new BorderPane(new XPane(), new XPane(),
new XPane(), new XPane(), new XPane());
stage.setScene(new Scene(bp, 640, 480));
stage.show();
}
private static class XPane extends Pane {
private final Rectangle r = new Rectangle();
private final Circle c = new Circle(8, Color.RED);
private final Line line1 = new Line();
private final Line line2 = new Line();
public XPane() {
setPrefSize(100, 100);
r.widthProperty().bind(this.widthProperty());
r.heightProperty().bind(this.heightProperty());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
getChildren().add(r);
line1.endXProperty().bind(widthProperty());
line1.endYProperty().bind(heightProperty());
getChildren().add(line1);
line2.startXProperty().bind(widthProperty());
line2.endYProperty().bind(heightProperty());
getChildren().add(line2);
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
getChildren().add(c);
}
}
public static void main(String[] args) {
launch();
}
}