Will Hartung
Will Hartung

Reputation: 118794

Fixed size JavaFX component

Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.

I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.

But here's my problem.

The fixed size I want is smaller than the contents of the element.

The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.

This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):

enter image description here

I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.

        FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
        var scene = new Scene(fp, 640, 480);
        stage.setScene(scene);

I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.

        TestPane pane = new TestPane();
        ScrollPane sp = new ScrollPane(pane);
        var scene = new Scene(sp, 640, 480);
        stage.setScene(scene);

And I struggle to discern whether I should be extending Region or Control in these cases.

I am missing something fundamental.

package pkg;

import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;

public class TestPane extends Control {

    public TestPane() {
        setMinHeight(150);
        setMaxHeight(150);
        setMinWidth(150);
        setMaxWidth(150);
        setPrefHeight(150);
        setPrefWidth(150);
        populate();
    }

    @Override
    protected double computePrefHeight(double width) {
        return 150;
    }

    @Override
    protected double computePrefWidth(double height) {
        return 150;
    }

    @Override
    protected double computeMaxHeight(double width) {
        return 150;
    }

    @Override
    protected double computeMaxWidth(double height) {
        return 150;
    }

    @Override
    protected double computeMinHeight(double width) {
        return 150;
    }

    @Override
    protected double computeMinWidth(double height) {
        return 150;
    }

    
    @Override
    public boolean isResizable() {
        return false;
    }

    private void populate() {
        Translate translate = new Translate();
        translate.setX(-25);
        translate.setY(-25);

        getTransforms().clear();
        getTransforms().addAll(translate);

        ObservableList<Node> children = getChildren();

        for (int i = 0; i < 4; i++) {
            Line line = new Line(0, i * 50, 200, i * 50);
            children.add(line);
            line = new Line(i * 50, 0, i * 50, 200);
            children.add(line);
        }
    }
}

Addenda, to clarify.

I want a fixed sized component. It's a rectangle. I want it X x Y big.

I want to draw things in my box. Lines, circles, text.

I want the things I draw to clip to the boundaries of the component.

I don't want to use Canvas.

More addenda.

What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.

Upvotes: 1

Views: 1033

Answers (1)

James_D
James_D

Reputation: 209694

TLDR:

  • Subclass Region,
  • make isResizable() return true to respect pref, min, and max sizes,
  • explicitly set a clip to avoid painting outside the local bounds.

Most of the documentation for this is in the package documentation for javafx.scene.layout

First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.

Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)

So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).

Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.

As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).

Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.

Here's a refactoring of your example:

import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;

public class TestPane extends Region {
    
    private Line[] verticalLines ;
    private Line[] horizontalLines ;
    
    private static final int WIDTH = 150 ;
    private static final int HEIGHT = 150 ;
    private static final int LINE_GAP = 50 ;

    public TestPane() {
        populate();
    }

    @Override
    protected double computePrefHeight(double width) {
        return HEIGHT;
    }

    @Override
    protected double computePrefWidth(double height) {
        return HEIGHT;
    }

    @Override
    protected double computeMaxHeight(double width) {
        return HEIGHT;
    }

    @Override
    protected double computeMaxWidth(double height) {
        return WIDTH;
    }

    @Override
    protected double computeMinHeight(double width) {
        return WIDTH;
    }

    @Override
    protected double computeMinWidth(double height) {
        return WIDTH;
    }

    
    @Override
    public boolean isResizable() {
        return true;
    }
    
    @Override
    public void layoutChildren() {
        double w = getWidth();
        double h = getHeight() ;
        
        double actualWidth = verticalLines.length * LINE_GAP ;
        double actualHeight = horizontalLines.length * LINE_GAP ;
        
        double hOffset = (actualWidth - w) / 2 ;
        double vOffset = (actualHeight - h) / 2 ;
                
        for (int i = 0 ; i < verticalLines.length ; i++)  {
            double x = i * LINE_GAP - hOffset;
            verticalLines[i].setStartX(x);
            verticalLines[i].setEndX(x);
            verticalLines[i].setStartY(0);
            verticalLines[i].setEndY(h);
        }
        
        for (int i = 0 ; i < horizontalLines.length ; i++)  {
            double y = i * LINE_GAP - vOffset;
            horizontalLines[i].setStartY(y);
            horizontalLines[i].setEndY(y);
            horizontalLines[i].setStartX(0);
            horizontalLines[i].setEndX(w);
        }
        
        setClip(new Rectangle(0, 0, w, h));
    }

    private void populate() {
        
        verticalLines = new Line[4] ;
        horizontalLines = new Line[4] ;
        
        for (int i = 0 ; i <verticalLines.length ; i++) {
            verticalLines[i] = new Line();
            getChildren().add(verticalLines[i]);
        }
        
        for (int i = 0 ; i <horizontalLines.length ; i++) {
            horizontalLines[i] = new Line();
            getChildren().add(horizontalLines[i]);
        }
        
        
    }
}

A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.

Here's a quick test case:

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;



public class App extends Application {

    @Override
    public void start(Stage stage) {
        FlowPane root = new FlowPane();
        root.setAlignment(Pos.TOP_LEFT);
        root.setPadding(new Insets(10));
        root.setHgap(5);
        root.setVgap(5);
        for (int i = 0; i < 6 ; i++) {
            root.getChildren().add(new TestPane());
        }
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

    }

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

}

Which results in:

enter image description here

This plays nicely with the layout pane; resizing the window gives

enter image description here

Upvotes: 2

Related Questions