我有一个简单的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);
}
}
运行时,它会做我期望的事情:
当我增大窗口时,X就会增大。
但是当我缩小窗户时,我得到了侧面板的工件。
我本以为擦除背景会解决这个问题,但我想还有一些顺序问题。但即使如此,当你拖动角落时,所有的XPanes都会改变大小,它们都被重新绘制,但工件仍然存在。
我尝试将XPanes包装到StackPane中,但这并没有做任何事情(我不认为它会,但还是尝试了)。
我该如何补救?这是macOS Big Sur上JDK16上的JavaFX 13。
为什么你会得到文物
我认为应该使用不同的方法,而不是修复你的方法,但是如果你愿意,你可以修复它。
您正在侦听器中向XPane添加新的矩形和线条。每次高度或宽度更改时,您都会添加一组新的节点,但旧高度和宽度的旧节点集仍然存在。最终,如果您调整足够大,性能将下降,或者您将运行内存溢出或资源,使程序无法使用。
BorderPane按照添加的顺序绘制其子级(中心和XPanes),而不进行裁剪,因此这些旧线条将保留,渲染器将在您调整大小时在某些窗格上绘制它们。类似地,一些窗格将在某些线条上绘制,因为您正在窗格中构建潜在的大量填充矩形,并且它们部分重叠了创建的大量线条。
要解决此问题,请在添加任何新节点之前清除()填充()方法中的子节点列表。
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
替代解决方案
IMO,更改监听器的宽度和高度并不是向自定义区域添加内容的地方。
我认为最好利用场景图,让它在你改变那些节点的属性后,处理现有节点的重新绘制和更新,而不是一直创建新的节点。
这是一个子类化Region并在调整大小时进行精细绘制的示例。
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);
}
}
区域vs窗格
我不确定你是应该子类化窗格还是区域,两者之间的主要区别是窗格有一个可修改子列表的公共访问器,但区域没有。所以这取决于你想做什么。如果它只是像示例一样画X,那么区域是合适的。
在layout儿童()与绑定
区域留档指出:
默认情况下,Region会继承其超类父节点的布局行为,这意味着它会将任何可调整大小的子节点调整为其首选大小,但不会重新定位它们。如果应用程序需要更具体的布局行为,那么它应该使用Region子类之一:StackPane、HBox、VBox、TilePane、FlowPane、BorderPane、GridPane或AnchorPane。
要实现更自定义的布局,Region子类必须覆盖computePrefWidth、computePrefHeight和layout儿童。请注意,layout儿童是在执行自上而下的布局传递时由场景图自动调用的,不应由region子类直接调用。
布局其子级的区域子类将通过设置layoutX/layoutY来定位节点,并且不会更改translateX/translateY,它们保留用于调整和动画。
我在这里实际上并没有这样做,相反,我在构造函数中绑定而不是覆盖layout儿童()。您可以实现一个替代解决方案,该解决方案按照留档讨论的方式运行,覆盖layout儿童()而不是使用绑定,但它更复杂,并且关于如何做到这一点的文档更少。
将Region子类化并重写layout儿童()是不常见的。相反,通常会使用标准布局窗格的组合,并在窗格和节点上设置约束以获得所需的布局。这让布局引擎可以完成许多工作,例如捕捉到像素、计算边距和插入、尊重约束、重新定位内容等,其中许多工作需要手动为layout儿童()实现完成。
一种常见的方法是将相关的几何属性绑定到封闭容器的所需属性。这里检查了一个相关的示例,这里收集了其他示例。
下面的变体将几个Shape
实例的顶点绑定到Pane
宽度和高度属性。调整封闭的阶段
的大小,以查看BorderPane
子级如何符合BorderPane Resize Table中的条目。该示例还添加了一个红色的Circle
,它在每个子级中保持居中,在中心增长和收缩以填充宽度或高度中较小的一个。该方法依赖于流畅的算术API,可用于实现NumberExtion
或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 https://stackoverflow.com/q/70311488/230513
*/
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();
}
}