Very large component in Javafx

When learning javafx, I liked a number of things, and in particular the consistence of the naming and MVC mechanism (vs. the multiplicity of mechanisms used by Swing).

However, two points bothered me:

The large component problem

Suppose you have a component which must display lots of data. It might be a text component loading a whole book, a detailled map for a large zone, or a tabular-like display.

Typically (but not always) this zone will be embedded in a scroll panel.

The normal Node-oriented solution of Java FX supposes you create nodes for each item you want to display. But it's too much considering the amount of data you have. What you want is to create nodes only for the data which is currently displayed.

A number of solutions are available (https://github.com/TomasMikula/Flowless for instance) but I wanted to see how to code them from the ground up.

The JavaFX ScrollPane is not very useful there: it doesn't send events to its children (or at least, not automatically) when it is scrolled.

In Swing, any object, even if it is very large, can benefit from a JScrollPane. A good use of revalidate and a redefinition of getPreferredSize are sufficient. For JavaFx, Scrollbars work correctly on small objects, but an object in a ScrollPane is completely unaware of the said scrollpane.

As a result, partial drawing is difficult. Having looked at FlowLayout and FLowless implementation, the solution chosen by those two classes is to integrate the scrollbar in the component itself. That way, the component can listen to scrollbars modifications, and react accordingly.

A large component implementation.

Usually, very large components are used for text or grids... those are complex classes. As a result, the code related to virtual component management is difficult to separate from the rest of the class, and the learning process is more complex than necessary.

As a matter of fact, I was unable to find any tutorial on the subject (admitedly, it's not something programmer do that often).

So, I have tried to create a minimal class which explains a solution to this problem. Some points are can probably be improved, and maybe I have done some errors. Mails about it (at public@qenherkhopeshef.org) are welcome.

The component itself: I have chosen to provide a tabular component, displaying the coordinate of points every 200 pixels for a component size of 9,000,000 pixels x 9,000,000 pixels.

That's clearly a bit too many nodes (2,025,000,000) to be stored in memory.

So, my component will build them on demand. The trick is to listen to the events from the hbar and vbar.

The result is quite fast, but notice that making it robust might be some work. I might try to use Flowless or VirtualFlow if I needed to.

The control

So this component :

 Large Component

let you scroll freely, with a virtual 9000000 pixels width and height.

The following code is not the absolute best: the idea is not to build a bulletproof component, but to explain how such a component can work.

The component is built in two parts: the component itself, a Control1 (which is supposed to hold the relevant data), and the skin, which performs the actual display business. Here, the control is minimal.

Normally, a Control object holds some data from the model, and specifies a skin, which is in charge of the actual drawing.

Here, the only task of our control is to return the correct skin.

package largeComponent;

import javafx.scene.control.Control;
import javafx.scene.control.Skin;


public class LargeComponent extends Control {
    @Override
    protected Skin<LargeComponent> createDefaultSkin() {
        return new LargeComponentSkin(this);
    }  
}

The Skin.

The actual display is performed by the skin. It contains two nodes:

In javafx, a ScrollPane can be used in two ways:

Note that this choice means that we must tightly couple the scrollpane and the rest of our component, in contrast with Swing, where a component would have "free" virtual rendering.

**the meaning of vvalue: ** if we consider vvalue, when it's 0, then the top of the panel is visible in the scrollpane, at position 0 - that's easy to understand. Now, when vvalue is 1.0, it means that the bottom of the panel is visible. But, if we are interested about what is displayed at the top-left of the scrollpane window, it's definitly not the bottom of our panel. The bottom at the panel is displayed at the bottom of the scrollpane, (height of scrollpane) points below. Hence, the top-left point in the scrollpane correspond to a point for which y is (height of pane - height of scrollpane).

For more details about computing the viewport bounds, see http://stackoverflow.com/questions/26240501/javafx-scrollpane-update-viewportbounds-on-scroll.

The central method in the skin is updateNodes(). Basically, it will be called when the display window changes (that is, when the scrollbars move).

It computes the theoretical clip window (which is not available in Javafx), using the formula we have just explained. Then, in a very rough way, it removes the current content of pane, and replace it by the content which should be displayed.

The various listeners ensure the display is updated when needed:

pane.layoutBoundsProperty().addListener((o) -> updateNodes());

prepare the display when the actual size of the pane is set. Our control of the size is in fact indirect. We are not supposed to set the size directly. Instead, in our case, the ScrollPane will give the pane its preferredSize (9000000x9000000). The correct rendering of the pane will be possible only after setting the size. The change in layoutBoundsProperty will signal this change.

scrollPane.hvalueProperty().addListener((h) -> updateNodes());
scrollPane.vvalueProperty().addListener((v) -> updateNodes());

Any move on the scrollbar will trigger a recomputation of the drawing.

scrollPane.widthProperty().addListener((e) -> updateNodes());
scrollPane.heightProperty().addListener((e) -> updateNodes());

A change in the ScrollPane dimensions should also trigger a redrawing.

The complete code is below:

package largeComponent;

import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;

class LargeComponentSkin extends SkinBase<LargeComponent> {

    // Create the two main component we need
    private final Pane pane = new Pane();
    private final ScrollPane scrollPane = new ScrollPane(pane);

    public LargeComponentSkin(LargeComponent control) {
        super(control);
        init();
        initGraphics();
        registerListeners();
    }

    private void init() {
        // Should be set only if unset by getSkinnable(), see below. 
        scrollPane.setPrefSize(300, 300);
        pane.setPrefSize(9_000_000, 9_000_000);

        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);

        if (getSkinnable().getMinWidth() <= 0.0
                || getSkinnable().getMinHeight() <= 0.0) {
            getSkinnable().setMinSize(100, 100);
        }

        if (getSkinnable().getMaxWidth() <= 0.0
                || getSkinnable().getMaxHeight() <= 0.0) {
            getSkinnable().setMaxSize(100_000_000, 100_000_000);
        }
    }

    private void initGraphics() {
        getChildren().setAll(scrollPane);
        updateNodes();
    }

    private void registerListeners() {
        pane.layoutBoundsProperty().addListener((o) -> updateNodes());
        scrollPane.hvalueProperty().addListener((h) -> updateNodes());
        scrollPane.vvalueProperty().addListener((v) -> updateNodes());
        scrollPane.widthProperty().addListener((e) -> updateNodes());
        scrollPane.heightProperty().addListener((e) -> updateNodes());
    }


    private void updateNodes() {
        double dx = 200;
        double dy = 100;
        double hval = scrollPane.getHvalue();
        double vval = scrollPane.getVvalue();
        double displayW = scrollPane.getViewportBounds().getWidth();
        double displayH = scrollPane.getViewportBounds().getHeight();
        pane.getChildren().clear();
        // Note: when h=1.0, the top is at prefHeight - containerHeight. Hence the formula.
        double startX = dx * Math.floor((pane.getPrefWidth() - displayW) * hval / dx);
        double startY = dy * Math.floor((pane.getPrefHeight() - displayH) * vval / dy);
        for (double x = startX; x < startX + 2 * displayW; x += dx) {
            for (double y = startY; y < startY + 2 * displayH; y += dy) {
                Text text = new Text(x, y, "(" + x + "," + y + ")");
                pane.getChildren().add(text);
            }
        }
    }

}

And a small application to display the result

package largeComponent;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class LargeComponentMain extends Application {

    LargeComponent largeLayout;

    @Override
    public void start(Stage primaryStage) {
        BorderPane borderPane= new BorderPane();
        largeLayout= new LargeComponent();
        borderPane.setCenter(largeLayout);        
        Scene scene = new Scene(borderPane, 300, 250);        
        primaryStage.setTitle("Large Component demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }

}

Note that, despite the crude redrawing system (deleting and then re-creating everything), the component is very very fast.

S. Rosmorduc


  1. I appreciate javafx attempt at creating a more logical system than Swing. In this respect, I'm not sure that calling "Control" something which is a View (from the M/V/C standpoint) is a very good idea, especially for the poor fellows (me at some point?) who might want to teach it.