Issue
I have a simple FX example with a simple component.
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);
}
}
When run, it does what I expect:
When I grow the window, the X's grow.
But when I shrink the window, I get artifacts of the side panels.
I would have thought erasing the backgrounds would have fixed this, but I guess there's some ordering issue. But even still, when you drag the corner, all of the XPanes change size, and they all get repainted, but the artifacts remain.
I tried wrapping the XPanes in to a StackPane, but that didn't do anything (I didn't think it would, but tried it anyway).
How do I remedy this? This is JavaFX 13 on JDK 16 on macOS Big Sur.
Solution
Why you get artifacts
I think a different approach should be used rather than fixing the approach you have, but you could fix it if you want.
You are adding new rectangles and lines to your XPane in listeners. Every time the height or width changes, you add a new set of nodes, but the old set of nodes at the old height and widths remains. Eventually, if you resize enough, performance will drop or you will run out of memory or resources, making the program unusable.
A BorderPane paints its children (the center and the XPanes) in the order they were added without clipping, so these old lines will remain and the renderer will paint them over some panes as you resize. Similarly, some panes will paint over some lines because you are building up potentially lots of filled rectangles in the panes and they are partially overlapping lots of lines created.
To fix this, clear() the child node list in your populate() method before you add any new nodes.
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
Alternate Solution
Change listeners on widths and heights aren't really the place to add content to a custom region, IMO.
I think that it is best to take advantage of the scene graph and let it handle the repainting and updating of existing nodes after you change the attributes of those nodes, instead of creating new nodes all the time.
Here is an example that subclasses Region and paints fine when a resize occurs.
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);
}
}
On Region vs Pane
I'm not sure if you should be subclassing Pane or Region, the main difference between the two is that a Pane has a public accessor for a modifiable child list, but a Region does not. So it would depend on what you are trying to do. If it is just drawing X's like the example, then Region is appropriate.
On layoutChildren() vs binding
The Region documentation states:
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.
I am not actually doing that here, instead, I am binding in the constructor rather than overriding layoutChildren(). You could implement an alternate solution that operates as the documentation discusses, overriding layoutChildren() rather than using binding, but it is more complicated and less well documented on how to do that.
It is uncommon to subclass Region and override layoutChildren(). Instead, usually, a combination of standard layout Panes will be used and constraints set on the panes and nodes to get the desired layout. This lets the layout engine do a lot of the work such as snapping to pixels, calculating margins and insets, respecting constraints, repositioning content, etc, a lot of which would need to be done manually for a layoutChildren() implementation.
Answered By - jewelsea
Answer Checked By - Terry (JavaFixing Volunteer)