提问者:小点点

为什么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);
    }
}

运行时,它会做我期望的事情:

当我增大窗口时,X就会增大。

但是当我缩小窗户时,我得到了侧面板的工件。

我本以为擦除背景会解决这个问题,但我想还有一些顺序问题。但即使如此,当你拖动角落时,所有的XPanes都会改变大小,它们都被重新绘制,但工件仍然存在。

我尝试将XPanes包装到StackPane中,但这并没有做任何事情(我不认为它会,但还是尝试了)。

我该如何补救?这是macOS Big Sur上JDK16上的JavaFX 13。


共2个答案

匿名用户

为什么你会得到文物

我认为应该使用不同的方法,而不是修复你的方法,但是如果你愿意,你可以修复它。

您正在侦听器中向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,可用于实现NumberExtionBindings中定义的方法的属性。

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();
    }
}