Search code examples
javajavafxresizeautoresize

JavaFX resize children relative to parent


Within a resizable Pane that shows an imageView i'd like to select multiple rectangles.

To do so I created a class SelectableImageViewPane. The two classes ImageViewPane and RubberBandSelection are from the com.bitplan.javafx Project where I am a committer.

When the window is resized the relevant panes are resized nicely as you can see from the debug output produced by e.g. SelectableImageViewPaneDemo and the following example screenshots. Unfortunately the selection rectangles are not resized / repositioned.

enter image description here enter image description here

How could i make sure the child - rectangles are resized/repositioned relative to the surrounding glasspane and in sync with the ImageViewPane/ImageView?

package com.bitplan.javafx;

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

/**
 * an ImageViewPane with a RubberBandSelection that properly resizes the rectangles
 * @author wf
 *
 */
public class SelectableImageViewPane extends StackPane {
  protected static Logger LOGGER = Logger.getLogger("com.bitplan.javafx");
  public static boolean debug=true;
  private RubberBandSelection selection;
  private ImageViewPane imageViewPane;
  private Pane glassPane;

  /**
   * get my selection
   * @return
   */
  public RubberBandSelection getSelection() {
    return selection;
  }

  /**
   * show the bound of the given node with the given title
   * @param title
   * @param n
   */
  public void showBounds(String title,Node n) {
    if (debug) {
      Bounds b = n.getLayoutBounds();
      LOGGER.log(Level.INFO,String.format("%s: min %.0f,%.0f max %.0f,%.0f",title,b.getMinX(),b.getMinY(),b.getMaxX(),b.getMaxY()));        
    }
  }

  @Override
  protected void layoutChildren() {
    super.layoutChildren();
    int index=1;
    if (debug) {
      showBounds("pane",this);
      showBounds("imageViewPane",imageViewPane);
      showBounds("glassPane",glassPane);
      showBounds("imageView",imageViewPane.imageViewProperty().get());
    }
    for (Node n:selection.selected) {
     showBounds(""+(index++),n);
    }
  }

  /**
   * create me
   * @param imageView
   */
  public SelectableImageViewPane(ImageView imageView) {
    imageViewPane=new ImageViewPane(imageView);
    getChildren().add(imageViewPane);
    StackPane.setAlignment(imageViewPane, Pos.CENTER);
    glassPane = new AnchorPane();
    glassPane.setStyle("-fx-background-color: rgba(0, 0, 0, 0.1);");
    getChildren().add(glassPane);
    //glassPane.prefWidthProperty().bind(imageViewPane.widthProperty());
    //glassPane.prefHeightProperty().bind(imageViewPane.heightProperty());
    selection = new RubberBandSelection(glassPane);
    selection.setSelectButton(true);
  }

}

Update In the meantime I modified RubberBandSelection to remember the relative bounds of the rectangles within the parent (which I'll need later anyway when using the rectangles in my application). This information can be used to properly layout the rectangles in the parent should the parent's dimension change.

Bounds rB = s.relativeBounds;
                double w = getWidth();
                double h=getHeight();
                double minX=rB.getMinX()*w;
                double minY=rB.getMinY()*h;
                double width=rB.getWidth()*w;
                double height=rB.getHeight()*h;
                layoutInArea(s.node, minX,minY,width,height, 0, HPos.LEFT,
                          VPos.TOP);

The solution works better but there is still some issue probably involving the x/y offset within the parent.

Updated SelectableImageViewPane

package com.bitplan.javafx;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.bitplan.javafx.RubberBandSelection.Selection;

import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

/**
 * an ImageViewPane with a RubberBandSelection that properly resizes the
 * rectangles
 * 
 * @author wf
 *
 */
public class SelectableImageViewPane extends StackPane {
    protected static Logger LOGGER = Logger.getLogger("com.bitplan.javafx");
    public static boolean debug = true;
    private RubberBandSelection selection;
    private ImageViewPane imageViewPane;
    private Pane glassPane;

    /**
     * get my selection
     * 
     * @return
     */
    public RubberBandSelection getSelection() {
        return selection;
    }

    /**
     * show the bound of the given node with the given title
     * 
     * @param title
     * @param n
     */
    public void showBounds(String title, Node n) {
        if (debug) {
            Bounds b = n.getLayoutBounds();
            LOGGER.log(Level.INFO, String.format("%s: min %.0f,%.0f max %.0f,%.0f", title, b.getMinX(), b.getMinY(),
                    b.getMaxX(), b.getMaxY()));
        }
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();
        int index = 1;
        if (debug) {
            showBounds("pane", this);
            showBounds("imageViewPane", imageViewPane);
            showBounds("glassPane", glassPane);
            showBounds("imageView", imageViewPane.imageViewProperty().get());
        }
        for (Selection s : selection.selected.values()) {

            if (debug) {
                showBounds("" + (index++), s.node);
                LOGGER.log(Level.INFO, s.asPercent());
            }
            Bounds rB = s.relativeBounds;
            double w = getWidth();
            double h=getHeight();
            double minX=rB.getMinX()*w;
            double minY=rB.getMinY()*h;
            double width=rB.getWidth()*w;
            double height=rB.getHeight()*h;
            layoutInArea(s.node, minX,minY,width,height, 0, HPos.LEFT,
                      VPos.TOP);
        }
    }

    /**
     * create me
     * 
     * @param imageView
     */
    public SelectableImageViewPane(ImageView imageView) {
        imageViewPane = new ImageViewPane(imageView);
        getChildren().add(imageViewPane);
        StackPane.setAlignment(imageViewPane, Pos.CENTER);
        glassPane = new AnchorPane();
        glassPane.setStyle("-fx-background-color: rgba(0, 0, 0, 0.1);");
        getChildren().add(glassPane);
        // glassPane.prefWidthProperty().bind(imageViewPane.widthProperty());
        // glassPane.prefHeightProperty().bind(imageViewPane.heightProperty());
        selection = new RubberBandSelection(glassPane);
        selection.setSelectButton(true);
    }

}

Solution

  • There are two main ingredients to make this working:

    1. Tracking the size of the image - I modified ImageViewPane accordingly.
    2. making sure the selected rectangles within the image are resized in sync with changes of the Image. The RelativePane below is a helper class in which the rectangles (buttons in fact) can be positioned relatively.

    Selection Resized

    Please note that there are two changes to the original code by chriscamacho:

    RelativePane

    package com.bitplan.javafx;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    import javafx.beans.Observable;
    import javafx.geometry.BoundingBox;
    import javafx.geometry.Bounds;
    import javafx.scene.Parent;
    import javafx.scene.control.Control;
    import javafx.scene.layout.Pane;
    
    /**
     * We want a Pane that will automatically resize its components The components
     * width height and position are all expressed as a relative of the width and
     * height of the Pane from 0.0 to 1.0 As the Pane resizes the position and sizes
     * will all remain relative to the Pane's new width and height. Aspect ratio is
     * not respected
     * 
     * see https://gist.github.com/chriscamacho/4f8b2e3e8f8340278b7c
     * 
     * @author chriscamacho
     * @author wf - modified to relative instead of percent
     *
     */
    public class RelativePane extends Pane implements RelativeSizer {
      protected static Logger LOGGER = Logger.getLogger("com.bitplan.javafx");
      public static boolean debug=false;
    
      /**
       * helper class to keep track of relative position
       */
      public class ControlBundle {
        public double rx, ry, rw, rh;
        public Control control;
    
        /**
         * construct me for the given control c and the given relative positions
         * 
         * @param c
         * @param rX
         * @param rY
         * @param rW
         * @param rH
         */
        ControlBundle(Control c, double rX, double rY, double rW, double rH) {
          control = c;
          rx = rX;
          ry = rY;
          rw = rW;
          rh = rH;
        }
      }
    
      Map<Control, ControlBundle> controls = new HashMap<Control, ControlBundle>();
    
      /**
       * create a relative Pane
       */
      public RelativePane() {
        super();
        widthProperty().addListener(o -> sizeListener(o));
        heightProperty().addListener(o -> sizeListener(o));
      }
    
      /**
       * show the bounds of the given node with the given title
       * 
       * @param title
       * @param b
       */
      public static void showBoundsPercent(String title, Bounds b) {
        LOGGER.log(Level.INFO,
            String.format("%s: min %.0f%%,%.0f%% max %.0f%%,%.0f%%", title,
                b.getMinX() * 100.0, b.getMinY() * 100.0, b.getMaxX() * 100.0,
                b.getMaxY() * 100.0));
      }
    
      /**
       * show the given bounds with the given title
       * 
       * @param title
       * @param b
       */
      public static void showBounds(String title, Bounds b) {
        LOGGER.log(Level.INFO, String.format("%s: min %.0f,%.0f max %.0f,%.0f",
            title, b.getMinX(), b.getMinY(), b.getMaxX(), b.getMaxY()));
      }
    
    
      /**
       * listen to a change of the given observable
       * @param o
       */
      void sizeListener(Observable o) {
        int index=0;
        for (ControlBundle cb : controls.values()) {
          index++;
          double w = getWidth() * cb.rw;
          double h = getHeight() * cb.rh;
          double x = getWidth() * cb.rx;
          double y = getHeight() * cb.ry;
          if (debug) {
            showBoundsPercent("r"+index,new BoundingBox(cb.rx,cb.ry,cb.rw,cb.rh));
            showBounds("a"+index,new BoundingBox(x,y,w,h));
          }
    
          cb.control.setPrefWidth(w);
          cb.control.setMinWidth(w);
          cb.control.setMaxWidth(w);
    
          cb.control.setPrefHeight(h);
          cb.control.setMinHeight(h);
          cb.control.setMaxHeight(h);
    
          cb.control.setLayoutX(x);
          cb.control.setLayoutY(y);
    
        }
    
      }
    
      /**
       * add a control with the given percentages
       * 
       * @param ctrl
       * @param x
       * @param y
       * @param w
       * @param h
       */
      public void addControl(Control ctrl, double x, double y, double w, double h) {
        this.getChildren().add(ctrl);
        controls.put(ctrl, new ControlBundle(ctrl, x, y, w, h));
      }
    
      public void removeControl(Control ctrl) {
        this.getChildren().remove(ctrl);
        controls.remove(ctrl);
      }
    
      @Override
      public Parent getSizer() {
        return this;
      }
    
    }