javajavafx

JavaFX layout children in custom layout component


I am trying to render a lot of nodes and after a certain amount, scrolling becomes very slow.

Therefore, I'm using a virtualisation approach, similar to TableView. As I have complex layout requirements (e.g. some nodes can be next to others, some need to stay on their own line), TableView is not suitable.

I've created my own component, but the layout of children doesn't work. When scrolling, one can see the light red background from the BorderPane, but the label background and the text of the label are not rendered at all.

Any ideas?

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class VirtualNodesDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        int virtualNodes = 100;
        double nodeWidth = 100;
        double nodeHeight = 100;

        StackPane root = new StackPane();

        Pane content = new Pane() {
            @Override
            protected void layoutChildren() {
                System.out.println("content.layoutChildren() ...");

                getChildren().clear();

                ScrollPane scrollPane = (ScrollPane) getParent().getParent().getParent();

                double viewportHeight = scrollPane.getViewportBounds().getHeight();
                double scrollTop = scrollPane.getVvalue() * (getPrefHeight() - viewportHeight);

                double y = 0;

                for (int i = 0; i < virtualNodes; i++) {
                    if (y + nodeHeight > scrollTop && y < scrollTop + viewportHeight) {
                        Pane pane = createRealNode(i);
                        pane.resizeRelocate(10, y, nodeWidth, nodeHeight);
                        pane.layout();
                        getChildren().add(pane);
                    } else {
                        break;
                    }
                    y += nodeHeight + 10;
                }

                setPrefHeight(y);
            }

            private Pane createRealNode(int i) {
                BorderPane borderPane = new BorderPane();
                borderPane.setManaged(false);
                Label label = new Label("Node " + i);
                label.setStyle("-fx-background-color: #aaf;");
                borderPane.setCenter(label);
                borderPane.setStyle("-fx-background-color: #faa;");
                return borderPane;
            }
        };

        ScrollPane scrollPane = new ScrollPane(content);
        scrollPane.setFitToWidth(true);
        scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        scrollPane.vvalueProperty().addListener(observable -> content.layout());
        root.getChildren().add(scrollPane);

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

When changing createRealNode as below, the label and label text are shown as expected. However, I need to be able to use BorderPane (and related, like StackPane, GridPane) because the nodes I need to render are quite big and have a lot of children/sub-panes.

            private Pane createRealNode(int i) {
                Label label = new Label("Node " + i);
                label.setStyle("-fx-background-color: #aaf;");
                Pane p = new Pane();
                p.setManaged(false);
                p.getChildren().add(label);
                return p;
            }

Solution

  • I agree with all the comments mentioned. Don't reinvent the wheel !!

    To deal with virtualization, it is not as simple as you think . Think for altering your approach to model(data objects) your items and then you can use ListView to use cell factories to render the nodes.

    For the quick fix what you are encountering with, include pane.applyCss(); after you add the pane to the children. This will fix your issue and the label will be displayed in the BorderPane. But as said, this is very wrong approach.

    Pane pane = createRealNode(i);
    pane.resizeRelocate(10, y, nodeWidth, nodeHeight);
    pane.layout();
    getChildren().add(pane); 
    pane.applyCss();
    

    You can think of something like below as I mentioned above using ListView approach: (just to give you some idea how to approach)

    enter image description here

    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class VirtualNodesDemo_Update extends Application {
        private final static String CSS = "data:text/css," +
                """
                    .list-view .list-cell{
                        -fx-background-color: transparent !important;
                    }
        """;
        double nodeWidth = 100;
        double nodeHeight = 100;
    
        @Override
        public void start(Stage primaryStage) {
            ObservableList<DataDetails> data = FXCollections.observableArrayList();
            for(int i=0;i<100;i++){
                data.add(new DataDetails("Node "+i, "#aaf", "#faa", nodeWidth, nodeHeight));
            }
            ListView<DataDetails> listView = new ListView<>();
            listView.setItems(data);
            listView.setMaxWidth(Double.MAX_VALUE);
            listView.setMaxHeight(Double.MAX_VALUE);
            listView.setCellFactory(param -> new CustomListCell());
    
            StackPane root = new StackPane();
            root.getChildren().add(listView);
            Scene scene = new Scene(root, 800, 600);
            scene.getStylesheets().add(CSS);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
        
        public static void main(String[] args) {
            launch(args);
        }
    
        class CustomListCell extends ListCell<DataDetails>{
    
            BorderPane pane;
            Label label;
    
            public CustomListCell(){
                /* Build the structure as you like...*/
                pane = new BorderPane();
                label = new Label();
                pane.setCenter(label);
            }
    
            @Override
            protected void updateItem(DataDetails item, boolean empty) {
                super.updateItem(item, empty);
                if(item!=null && !empty){
                    label.setText(item.getTitle());
                    pane.setPrefSize(item.getWidth(), item.getHeight());
                    pane.setMaxSize(item.getWidth(), item.getHeight());
                    label.setStyle("-fx-background-color: "+item.getLabelBg());
                    pane.setStyle("-fx-background-color: "+item.getDataBg());
                    setGraphic(pane);
                }else{
                    setGraphic(null);
                }
            }
        }
        class DataDetails{
            String title;
            String labelBg;
            String dataBg;
            double width;
            double height;
    
            /* Build your data object with all the details that you need to render.. */
            public DataDetails(String title, String labelBg, String dataBg, double width, double height) {
                this.title = title;
                this.labelBg = labelBg;
                this.dataBg = dataBg;
                this.width = width;
                this.height = height;
            }
    
            public String getTitle() {
                return title;
            }
    
            public String getLabelBg() {
                return labelBg;
            }
    
            public String getDataBg() {
                return dataBg;
            }
    
            public double getWidth() {
                return width;
            }
    
            public double getHeight() {
                return height;
            }
        }
    }