JavaFX Pane Clipping

Most JavaFX layout containers (base class Region) automatically position and size their children, so clipping any child contents that might protrude beyond the container’s layout bounds is never an issue. The big exception is Pane, a direct subclass of Region and the base class for all layout containers with publicly accessible children. Unlike its subclasses Pane does not attempt to arrange its children but simply accepts explicit user positioning and sizing.

This makes Pane suitable as a drawing surface, similar to Canvas but rendering user-defined Shape children rather than direct drawing commands. The problem is that drawing surfaces are usually expected to automatically clip their contents at their bounds. Canvas does this by default but Pane does not. From the last paragraph of the Javadoc entry for Pane:

Pane does not clip its content by default, so it is possible that children’s bounds may extend outside its own bounds, either if children are positioned at negative coordinates or the pane is resized smaller than its preferred size.

This quote is somewhat misleading. Children are rendered (wholly or partially) outside their parent Pane whenever their combination of position and size extends beyond the parent’s bounds, regardless of whether the position is negative or the Pane is ever resized. Quite simply, Pane only provides a coordinate shift to its children, based on its upper-left corner – but its layout bounds are completely ignored while rendering children. Note that the Javadoc for all Pane subclasses (that I checked) includes a similar warning. They don’t clip their contents either, but as mentioned above this is not usually a problem for them because they automatically arrange their children.

So to properly use Pane as a drawing surface for Shapes, we need to manually clip its contents. This is somewhat complex, especially when a visible border is involved. I wrote a small demo application to illustrate the default behavior and various steps to fix it. You can download it as PaneDemo.zip which contains a project for NetBeans 8.2 and Java SE 8u112. The following sections explain each step with screenshots and pertinent code snippets.

Default Behavior

Starting up, PaneDemo shows what happens when you put an Ellipse shape into a Pane that’s too small to contain it entirely. The Pane has a nice thick rounded Border to visualize its area. The application window is resizable, with the Pane size following the window size. The three buttons on the left are used to switch to the other steps in the demo; click Default (Alt+D) to revert to the default output from a later step.

Pane Demo (Default)

As you can see, the Ellipse overwrites its parent’s Border and protrudes well beyond it. The following code is used to generate the default view. It’s split into several smaller methods, and a constant for the Border corner radius, because they will be referenced in the next steps.

static final double BORDER_RADIUS = 4;

static Border createBorder() {
    return new Border(
            new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID,
            new CornerRadii(BORDER_RADIUS), BorderStroke.THICK));
}

static Shape createShape() {
    final Ellipse shape = new Ellipse(50, 50);
    shape.setCenterX(80);
    shape.setCenterY(80);
    shape.setFill(Color.LIGHTCORAL);
    shape.setStroke(Color.LIGHTCORAL);
    return shape;
}

static Region createDefault() {
    final Pane pane = new Pane(createShape());
    pane.setBorder(createBorder());
    pane.setPrefSize(100, 100);
    return pane;
}

Simple Clipping

Surprisingly, there is no predefined option to have a resizable Region automatically clip its children to its current size. Instead, you need to use the basic clipProperty defined on Node and keep it updated manually to reflect changing layout bounds. Method clipChildren below show how this works (with Javadoc because you may want to reuse it in your own code):

/**
 * Clips the children of the specified {@link Region} to its current size.
 * This requires attaching a change listener to the region’s layout bounds,
 * as JavaFX does not currently provide any built-in way to clip children.
 * 
 * @param region the {@link Region} whose children to clip
 * @param arc the {@link Rectangle#arcWidth} and {@link Rectangle#arcHeight}
 *            of the clipping {@link Rectangle}
 * @throws NullPointerException if {@code region} is {@code null}
 */
static void clipChildren(Region region, double arc) {

    final Rectangle outputClip = new Rectangle();
    outputClip.setArcWidth(arc);
    outputClip.setArcHeight(arc);
    region.setClip(outputClip);

    region.layoutBoundsProperty().addListener((ov, oldValue, newValue) -> {
        outputClip.setWidth(newValue.getWidth());
        outputClip.setHeight(newValue.getHeight());
    });        
}

static Region createClipped() {
    final Pane pane = new Pane(createShape());
    pane.setBorder(createBorder());
    pane.setPrefSize(100, 100);

    // clipped children still overwrite Border!
    clipChildren(pane, 3 * BORDER_RADIUS);

    return pane;
}

Choose Clipped (Alt+C) in PaneDemo to render the corresponding output. Here’s how that looks:

Pane Demo (Clipped)

That’s better. The Ellipse no longer protrudes beyond the Pane – but still overwrites its Border. Also note that we had to manually specify an estimated corner rounding for the clipping Rectangle in order to reflect the rounded Border corners. This estimate is 3 * BORDER_RADIUS because the corner radius specified on Border actually defines its inner radius, and the outer radius (which we need here) will be greater depending on the Border thickness. (You could compute the outer radius exactly if you really wanted to, but I skipped that for the demo application.)

Nested Panes

Can we somehow specify a clipping region that excludes a visible Border? Not on the drawing Pane itself, as far as I can tell. The clipping region affects the Border as well as other contents, so if you were to shrink the clipping region to exclude it you would no longer see any Border at all. Instead, the solution is to create two nested panes: one inner drawing Pane without Border that clips exactly to its bounds, and one outer StackPane that defines the visible Border and also resizes the drawing Pane. Here is the final code:

static Region createNested() {
    // create drawing Pane without Border or size
    final Pane pane = new Pane(createShape());
    clipChildren(pane, BORDER_RADIUS);

    // create sized enclosing Region with Border
    final Region container = new StackPane(pane);
    container.setBorder(createBorder());
    container.setPrefSize(100, 100);
    return container;
}

Choose Nested (Alt+N) in PaneDemo to render the corresponding output. Now everything looks as it should:

Pane Demo (Nested)

As an added bonus, we no longer need to guesstimate a correct corner radius for the clipping Rectangle. We now clip to the inner rather than outer circumference of our visible Border, so we can directly reuse its inner corner radius. Should you specify multiple different corner radii or an otherwise more complex Border you’d have to define a correspondingly more complex clipping Shape.

There is one small caveat. The top-left corner of the drawing Pane to which all child coordinates are relative now starts within the visible Border. If you retroactively change a single Pane with visible Border to nested panes as shown here, all children will exhibit a slight positioning shift corresponding to the Border thickness.

2016-11-07: Carl Walker of Bekwam was kind enough to add this post to the JavaFX Documentation Project where you can find it under Section 4.3: Clipping. There’s plenty of other useful JavaFX information in this fast-growing project, so be sure to check it out!

2017-01-16: Commenter Anne W discovered that my “nested” solution only works for small corner radii. The reasons are twofold: a Rectangle’s arcWidth and arcHeight properties specify diameters rather than radii as I had mistakenly assumed – but those diameters also seem to be measured somewhat smaller than twice the inner radius of a Border.

So when the corner radii grow large enough, using BORDER_RADIUS itself for the Rectangle arcs, as in the test program, leads to a visibly thinned border around the ellipse as the clipping rectangle’s radii are too small. Yet specifying 2 * BORDER_RADIUS leads to a visible empty pixel gap between ellipse and border.

For a BORDER_RADIUS of 10, an intermediate factor of 1.6 seems to give good visual results. If you have fixed corner radii you could simply experiment and hard-code the best-looking factor. Otherwise, you might follow Anne’s advice and use another overlaid Rectangle instead of a Border so you don’t run into this annoying conflict between the rounded corner measurements of these two classes.

5 thoughts on “JavaFX Pane Clipping

  1. Pingback: JavaFX links of the week, November 14 | JavaFX News, Demos and Insight // FX Experience

  2. Daniel Zimmermann

    Thanks!
    This tipp came just about at the correct time: I’ve created an extended StackPane with some swipping effect for it’s Pane-extending children and it was kind of annoying, that children currently out of view where visible left and right the current one (I heavily relied on translate-x…). I played around with opacity binding.
    But with the clipping it looks a lot more “natural”.

    Reply
  3. Anne W

    Hi,
    unfortunately, it seems the Nested Panes solution does not actually work correctly – this is easy to see by increasing the BORDER_RADIUS. It looks fine for small radii up to about 10, but for radii > 10, the clip of the pane is visibly not identical to the inner border line anymore. :-(

    Reply
  4. Anne W

    One issue that stands out is that Border expects corner RADII, while arc width/height of rectangle describe the [horicontal/vertical] DIAMATER of the corner arc (https://docs.oracle.com/javase/8/javafx/api/javafx/scene/layout/CornerRadii.html , https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/Rectangle.html ).

    In the Nested Panes solution, using clipChildren(pane, BORDER_RADIUS * 2) creates a much better clip even for higher values of BORDER_RADIUS. However, there is a tiny but visible pixel gap between the border and the clip, so it is still not quite perfect.

    I found that in some cases, using Rectangles as simplified “pseudo borders” instead of true Borders can work. By setting the StrokeType of the pseudo border Rectangle to INSIDE and creating a clip Rectangle with the exact same x, y, width, height, arcWidth, and arcHeight (by binding), it is possible to add the pseudo border Rectangle to a pane’s children (on the very top) and clip the pane to the desired extent.

    Reply
    1. Christoph Nahr Post author

      Thank you, excellent observations! You are quite right, I missed that Rectangle corner arcs are diameters rather than radii – but as you also noticed, those aren’t really diameters compared to the inner corner radius of a Border, i.e. exactly twice as big. For a Border radius of 10 a factor of 1.6 seems to give good visual results, though I don’t know why. I’ve updated my post according to your comments.

      Reply

Leave a Reply