0

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.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • Have you considered making designs using scenebuilder ? – Tcheutchoua Steve Sep 03 '21 at 22:35
  • forget the idea - absolute _fixed size_ always was and still is the complete wrong direction to think. Instead formulate your problem as _layout_, that is think about relations (in size, locations) – kleopatra Sep 03 '21 at 22:49
  • I think we need to know more to guide you. In Swing, you add components to a layout; you override `paintComponent()` to alter a component's appearance. In [tag:javafx] you add nodes to a graph, e.g. `Line`s to a `Control` in your example; a `Canvas` _is a_ `Node` that lets you schedule rendering operations in a `GraphicsContext`. Here are some [examples](https://stackoverflow.com/search?tab=votes&q=user%3a230513%20%5bjavafx%5d%20canvas) comparing the two. – trashgod Sep 03 '21 at 23:10
  • Are you looking for `GridPane`? Add a `Pane` to each cell in the `GridPane`. Draw on each `Pane` using your mouse. https://stackoverflow.com/questions/53708509/randomly-displaying-circles-within-gridpane-cells-in-javafx/53709006#53709006 – SedJ601 Sep 04 '21 at 02:26
  • @Sedrick no, the grid is completely synthetic, just a simple test substituting for something more complex. – Will Hartung Sep 04 '21 at 03:07
  • Note that transforms don’t affect the layout bounds of most nodes (exceptions include `Group`), so the strategy of using a translation to enforce the clip is likely to produce unexpected results if you place this component in a layout pane (`FlowPane`, etc.). I’m not at a computer to test right now, but try subclassing `Pane` instead of `Control`, and setting the clip instead of using a translation. – James_D Sep 04 '21 at 14:04
  • @James_D Subclassing `Pane` made no difference, I'm using translate to shift the elements within, not the wrapping structure. – Will Hartung Sep 04 '21 at 14:17
  • 1
    @WillHartung Yes, that's my point. The layout bounds do not include the transforms. So your custom component will be laid out in the parent container, and *then* the transform will be applied. So your approach will not play nicely with the layout pane in which it's placed. See "Visual Bounds vs. Layout Bounds" in https://openjfx.io/javadoc/16/javafx.graphics/javafx/scene/layout/package-summary.html – James_D Sep 04 '21 at 14:30
  • One trick to enable the transformed versions nodes effect the layout is to wrap the transformed nodes in a group. – jewelsea Sep 04 '21 at 21:43
  • Unfortunately, creating custom components and layouts by subclassing Region or Pane is not well documented in tutorials anywhere on the net, as far as I know. The best source of information on this is the code in the JavaFX library itself, if you can find a layout or control skin that somewhat matched what you want achieve, and can then study that to get a better idea of how to implement your widget. – jewelsea Sep 04 '21 at 21:55
  • What is well documented is using existing panes and controls to achieve layout, usually combined with invoking various APIs on the layout panes to supply hints for customizing layouts. For the vast majority of layout tasks, this is the preferred approach. However, for your particular task, a subclass of region might be best, unless you could find away to repurpose a scroll pane to achieve what you want (which may be possible). – jewelsea Sep 04 '21 at 21:59
  • @jewelsea As is the ScrollPane could work, but it would require (in theory) that the entirety of the model is in the scene graph (since the ScrollPane does not alter the scene graph, it simply moves the viewport over it and clips it). I'd rather not have the entire model "rendered" like this. I will take a look at the ScrollPane to see if I can understand what it is doing. Perhaps that combined with James_D answer will let me do what I want, otherwise I'll just translate everything myself. – Will Hartung Sep 05 '21 at 02:03

1 Answers1

2

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

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you again @James_D. At a high level, so the populate method should build the scene graph, and the layoutChildren should place the nodes. I assume it's untoward for the layoutChildren to change the scene graph? I have a larger logical model. The goal is to offer a viewport in to that model. I had hoped to translate the upper left, then simply fill the graph with the elements within the viewport (in their "global" coordinates), and have it clipped to the rectangle. So, how would I apply the translation in this case or do i have to do it myself? (this why I had translate in the original.) – Will Hartung Sep 04 '21 at 16:26
  • I think it is OK to change the scene graph while laying out the children. Layout passes are executed on a top down basis. What you can’t do invoke the layout() method to request a layout when you are already in the middle of a layout. – jewelsea Sep 04 '21 at 21:50
  • 1
    @WillHartung For reasons I outlined in the answer, I wouldn’t use a translation. Just reposition things as I showed. Another option would be to apply the translation to the child nodes (lines here), wrap those in a group, and add the group to the region. But it just seems simpler to compute the locations of the child nodes directly. – James_D Sep 05 '21 at 00:04
  • @WillHartung As for manipulating the scene graph in `layoutChildren`, I think that’s ok, and I have done it sometimes. Here I didn’t as it seemed better to build the scene graph just once, not every time layout is performed. In your case, if your model is observable, just manipulate the scene graph in response to changes in the model, then layout whatever is currently displayed in `layoutChildren()` – James_D Sep 05 '21 at 00:07