resizable and movable rectangle

Stéphane GROSSMANN picture Stéphane GROSSMANN · Oct 10, 2014 · Viewed 12.7k times · Source

With the following code (thanks to several posts here), I draw a rectangle, that I want to be resizable and movable. Two anchors (the upper left and lower right) do what I want, and the last one (lower middle) moves the rectangle, but the two first anchors do not follow the rectangle.

When I make them move, the Listener of them, resizes the rectangle.

package application;

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class Main extends Application {

    private Rectangle rectangle;
    private Group group;
    private Scene scene;
    private Stage primaryStage;
    private ObservableList<Double> Coins;

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

  @Override
  public void start(Stage primaryStage) {
        group = new Group();        
        rectangle = new Rectangle(200,200,400,300);


        Coins = FXCollections.observableArrayList();
        //UpperLeft
        Coins.add(rectangle.getX());
        Coins.add(rectangle.getY());
        //LowerRight
        Coins.add(rectangle.getX() + rectangle.getWidth());
        Coins.add(rectangle.getY()+ rectangle.getHeight());
        //Moving
        Coins.add(rectangle.getX() + (rectangle.getWidth()/2));
        Coins.add(rectangle.getY()+ (rectangle.getHeight()));


        group.getChildren().addAll(createControlAnchorsFor(Coins));
        group.getChildren().add(rectangle);
        scene = new Scene(group,800,800);
        primaryStage.setScene(scene);
        primaryStage.show();
  }


//@return a list of anchors which can be dragged around to modify points in the format [x1, y1, x2, y2...]
 private ObservableList<Anchor> createControlAnchorsFor(final ObservableList<Double> points) {
   ObservableList<Anchor> anchors = FXCollections.observableArrayList();

   //Coin GaucheHaut
   DoubleProperty xProperty = new SimpleDoubleProperty(points.get(0));
   DoubleProperty yProperty = new SimpleDoubleProperty(points.get(1));

   xProperty.addListener(new ChangeListener<Number>() {
       @Override public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
           System.out.println(oldX + " et " + x);
           rectangle.setX((double) x);  
           rectangle.setWidth((double) rectangle.getWidth() -((double) x- (double) oldX)); 
           anchors.get(2).setCenterX((double) x + rectangle.getWidth()/2 );
       }
     });

     yProperty.addListener(new ChangeListener<Number>() {
       @Override public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) {
           rectangle.setY((double) y);  
           rectangle.setHeight((double) rectangle.getHeight() -((double) y- (double) oldY)); 
       }
     });
     anchors.add(new Anchor(Color.GOLD, xProperty, yProperty));

     //Coin DroiteBas
     DoubleProperty xProperty2 = new SimpleDoubleProperty(points.get(2));
     DoubleProperty yProperty2 = new SimpleDoubleProperty(points.get(3));

     xProperty2.addListener(new ChangeListener<Number>() {
         @Override public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
           rectangle.setWidth((double) rectangle.getWidth() -((double) oldX- (double) x)); 
           anchors.get(2).setCenterX((double) x - rectangle.getWidth()/2 );
         }
       });

       yProperty2.addListener(new ChangeListener<Number>() {
         @Override public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) {
           rectangle.setHeight((double) rectangle.getHeight() -((double) oldY- (double) y));
           anchors.get(2).setCenterY((double) y);
         }
       });
       anchors.add(new Anchor(Color.GOLD, xProperty2, yProperty2));

       //Moving
       DoubleProperty xPropertyM = new SimpleDoubleProperty(points.get(4));
       DoubleProperty yPropertyM = new SimpleDoubleProperty(points.get(5));
       xPropertyM.addListener(new ChangeListener<Number>() {
           @Override public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
               rectangle.setX((double) x - rectangle.getWidth()/2 );
               //anchors.get(0).setCenterX((double) x- rectangle.getWidth()/2);
               //anchors.get(0).setVisible(false);
           }
         });

         yPropertyM.addListener(new ChangeListener<Number>() {
           @Override public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) { 
               rectangle.setY((double) y - rectangle.getHeight() ); 
               Coins.set(1, (double) y);
           }
         });
       anchors.add(new Anchor(Color.GOLD, xPropertyM, yPropertyM));

   return anchors;
 }

//a draggable anchor displayed around a point.
class Anchor extends Circle {
  private final DoubleProperty x, y;

  Anchor(Color color, DoubleProperty x, DoubleProperty y) {
    super(x.get(), y.get(), 20);
    setFill(color.deriveColor(1, 1, 1, 0.5));
    setStroke(color);
    setStrokeWidth(2);
    setStrokeType(StrokeType.OUTSIDE);

    this.x = x;
    this.y = y;

    x.bind(centerXProperty());
    y.bind(centerYProperty());
    enableDrag();
  }

//make a node movable by dragging it around with the mouse.
  private void enableDrag() {
    final Delta dragDelta = new Delta();
    setOnMousePressed(new EventHandler<MouseEvent>() {
      @Override public void handle(MouseEvent mouseEvent) {
        // record a delta distance for the drag and drop operation.
        dragDelta.x = getCenterX() - mouseEvent.getX();
        dragDelta.y = getCenterY() - mouseEvent.getY();
        getScene().setCursor(Cursor.MOVE);
      }
    });
    setOnMouseReleased(new EventHandler<MouseEvent>() {
      @Override public void handle(MouseEvent mouseEvent) {
        getScene().setCursor(Cursor.HAND);

      }
    });
    setOnMouseDragged(new EventHandler<MouseEvent>() {
      @Override public void handle(MouseEvent mouseEvent) {
        double newX = mouseEvent.getX() + dragDelta.x;
        if (newX > 0 && newX < getScene().getWidth()) {
          setCenterX(newX);
        }
        double newY = mouseEvent.getY() + dragDelta.y;
        if (newY > 0 && newY < getScene().getHeight()) {
          setCenterY(newY);
        }

        //Recompute screen;
        group.getChildren().add(rectangle);
        scene = new Scene(group,800,800);;
        primaryStage.setScene(scene);
      }
    });
    setOnMouseEntered(new EventHandler<MouseEvent>() {
      @Override public void handle(MouseEvent mouseEvent) {
        if (!mouseEvent.isPrimaryButtonDown()) {
          getScene().setCursor(Cursor.HAND);
        }
      }
    });
    setOnMouseExited(new EventHandler<MouseEvent>() {
      @Override public void handle(MouseEvent mouseEvent) {
        if (!mouseEvent.isPrimaryButtonDown()) {
          getScene().setCursor(Cursor.DEFAULT);
        }
      }
    });
  }
//records relative x and y co-ordinates.
  private class Delta { double x, y; }

}
}

Any idea, what and where I should add something ?

Answer

James_D picture James_D · Oct 10, 2014

Since the "handles" are always in the same position relative to the rectangle, I would bind their position to the position of the rectangle. You can achieve this with

circle.centerXProperty().bind(...);
circle.centerYProperty().bind(...);

where the argument is some ObservableValue<Number>.

Then in the dragging handlers, just move the Rectangle as required (the computations are slightly complex but not too bad). Since the positions of the circles are bound, they will follow the rectangle.

Here's one possible implementation that uses this strategy:

import java.util.Arrays;

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class DraggingRectangle extends Application {

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

    @Override
    public void start(Stage primaryStage) {

        Pane root = new Pane();

        Rectangle rect = createDraggableRectangle(200, 200, 400, 300);
        rect.setFill(Color.NAVY);

        root.getChildren().add(rect);


        Scene scene = new Scene(root, 800, 800);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Rectangle createDraggableRectangle(double x, double y, double width, double height) {
        final double handleRadius = 10 ;

        Rectangle rect = new Rectangle(x, y, width, height);

        // top left resize handle:
        Circle resizeHandleNW = new Circle(handleRadius, Color.GOLD);
        // bind to top left corner of Rectangle:
        resizeHandleNW.centerXProperty().bind(rect.xProperty());
        resizeHandleNW.centerYProperty().bind(rect.yProperty());

        // bottom right resize handle:
        Circle resizeHandleSE = new Circle(handleRadius, Color.GOLD);
        // bind to bottom right corner of Rectangle:
        resizeHandleSE.centerXProperty().bind(rect.xProperty().add(rect.widthProperty()));
        resizeHandleSE.centerYProperty().bind(rect.yProperty().add(rect.heightProperty()));

        // move handle:
        Circle moveHandle = new Circle(handleRadius, Color.GOLD);
        // bind to bottom center of Rectangle:
        moveHandle.centerXProperty().bind(rect.xProperty().add(rect.widthProperty().divide(2)));
        moveHandle.centerYProperty().bind(rect.yProperty().add(rect.heightProperty()));

        // force circles to live in same parent as rectangle:
        rect.parentProperty().addListener((obs, oldParent, newParent) -> {
            for (Circle c : Arrays.asList(resizeHandleNW, resizeHandleSE, moveHandle)) {
                Pane currentParent = (Pane)c.getParent();
                if (currentParent != null) {
                    currentParent.getChildren().remove(c);
                }
                ((Pane)newParent).getChildren().add(c);
            }
        });

        Wrapper<Point2D> mouseLocation = new Wrapper<>();

        setUpDragging(resizeHandleNW, mouseLocation) ;
        setUpDragging(resizeHandleSE, mouseLocation) ;
        setUpDragging(moveHandle, mouseLocation) ;

        resizeHandleNW.setOnMouseDragged(event -> {
            if (mouseLocation.value != null) {
                double deltaX = event.getSceneX() - mouseLocation.value.getX();
                double deltaY = event.getSceneY() - mouseLocation.value.getY();
                double newX = rect.getX() + deltaX ;
                if (newX >= handleRadius 
                        && newX <= rect.getX() + rect.getWidth() - handleRadius) {
                    rect.setX(newX);
                    rect.setWidth(rect.getWidth() - deltaX);
                }
                double newY = rect.getY() + deltaY ;
                if (newY >= handleRadius 
                        && newY <= rect.getY() + rect.getHeight() - handleRadius) {
                    rect.setY(newY);
                    rect.setHeight(rect.getHeight() - deltaY);
                }
                mouseLocation.value = new Point2D(event.getSceneX(), event.getSceneY());
            }
        });

        resizeHandleSE.setOnMouseDragged(event -> {
            if (mouseLocation.value != null) {
                double deltaX = event.getSceneX() - mouseLocation.value.getX();
                double deltaY = event.getSceneY() - mouseLocation.value.getY();
                double newMaxX = rect.getX() + rect.getWidth() + deltaX ;
                if (newMaxX >= rect.getX() 
                        && newMaxX <= rect.getParent().getBoundsInLocal().getWidth() - handleRadius) {
                    rect.setWidth(rect.getWidth() + deltaX);
                }
                double newMaxY = rect.getY() + rect.getHeight() + deltaY ;
                if (newMaxY >= rect.getY() 
                        && newMaxY <= rect.getParent().getBoundsInLocal().getHeight() - handleRadius) {
                    rect.setHeight(rect.getHeight() + deltaY);
                }
                mouseLocation.value = new Point2D(event.getSceneX(), event.getSceneY());
            }
        });

        moveHandle.setOnMouseDragged(event -> {
            if (mouseLocation.value != null) {
                double deltaX = event.getSceneX() - mouseLocation.value.getX();
                double deltaY = event.getSceneY() - mouseLocation.value.getY();
                double newX = rect.getX() + deltaX ;
                double newMaxX = newX + rect.getWidth();
                if (newX >= handleRadius 
                        && newMaxX <= rect.getParent().getBoundsInLocal().getWidth() - handleRadius) {
                    rect.setX(newX);
                }
                double newY = rect.getY() + deltaY ;
                double newMaxY = newY + rect.getHeight();
                if (newY >= handleRadius 
                        && newMaxY <= rect.getParent().getBoundsInLocal().getHeight() - handleRadius) {
                    rect.setY(newY);
                }
                mouseLocation.value = new Point2D(event.getSceneX(), event.getSceneY());
            }

        });

        return rect ;
    }

    private void setUpDragging(Circle circle, Wrapper<Point2D> mouseLocation) {

        circle.setOnDragDetected(event -> {
            circle.getParent().setCursor(Cursor.CLOSED_HAND);
            mouseLocation.value = new Point2D(event.getSceneX(), event.getSceneY());
        });

        circle.setOnMouseReleased(event -> {
            circle.getParent().setCursor(Cursor.DEFAULT);
            mouseLocation.value = null ;
        });
    }

    static class Wrapper<T> { T value ; }


}