resizable and movable rectangle
Asked Answered
S

2

5

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 ?

Sommersommers answered 10/10, 2014 at 11:56 Comment(2)
As a general strategy, I would bind the positions of the circles to the positions of the rectangle - that way the circles will always be in the correct position relative to the rectangle. In the dragging handlers, change the properties of the rectangle only, and the circles will follow due to the binding. You should really clean up the code that you've posted - there are redundant variables in there and it throws exceptions when you drag. Recomputing the scene is unnecessary.Pomology
For the first thing, I don't know how to bind the circles to the rectangle, that's why I posted here. And for the errors, no idea, how to clean it. Thanks for your adviceUnderbody
P
19

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 ; }


}
Pomology answered 10/10, 2014 at 13:38 Comment(5)
You're welcome. I recommend you read it carefully and make sure you understand how it works.Pomology
I'm just about doing that. Very interesting. I've to understang it, because I want now the rectangle to be transparent, in order to see the desktop behind (not the stage/scene)Underbody
@James_D, what is the purpose of the Wrapper class?Forbidding
@PaulrBear When using local variables inside (declared outside, used inside) lambdas or anonymous classes the local variable must be effectively final (i.e. it cannot be changed). By wrapping the local variable inside another class (in this case, Wrapper) you can use the "local" variable inside lambdas/anonymous classes while still allowing the "local" variable to be mutable. This is because the real local variable is now a Wrapper; which is effectively final. But you can still manipulate the state of Wrapper because technically the state is instance scoped and not locally scoped.Differential
I'm trying to understand this snippet, a question. in setOnMouseDragged you check this: newX >= handleRadius, why? is this so that if you drag your mouse out of the component that the resizable rectangle is in, you stop resizing?Sideshow
P
0

TLDR: Wrap that for Loop in a newParent != null check.

I'm unable to comment because of low reputation points, but I'd like to point out something that needs to be added to James_D's code above to avoid a problem. I ran into issues when I attempted to use the clear() method on a Pane with multiple resizable rectangles. I was able to fix this by changing his code from...

This

// 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);
    }
});

To This.

// force circles to live in same parent as rectangle:
rect.parentProperty().addListener((obs, oldParent, newParent) -> {
    if (newParent != null) {
        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);
        }
    }
});
Premillennialism answered 13/5, 2019 at 8:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.