This discussion is archived
5 Replies Latest reply: Dec 18, 2012 10:09 AM by 935521 RSS

Rectangle custom property on lower right corner

935521 Newbie
Currently Being Moderated
Is it possible in Rectangle class to create a doubleproperty similar to DoubleProperty xProperty() upper-left corner but that defines the X coordinates of the lower-right corner?

Same problem for Y coordinate.

These new properties should be able to be passed as parameter to method bindDirectional.

Thanks
  • 1. Re: Rectangle custom property on lower right corner
    James_D Guru
    Currently Being Moderated
    Two ways:

    My preferred way is to create a wrapper class for the rectangle which exposes properties for the x and y coordinates of the bottom right of the rectangle, and the width and height, and also exposes the rectangle itself as a Shape. The actual x and y properties of the rectangle itself are bound to custom bindings on the properties. Exposing the rectangle as a Shape prevents access to the x and y properties for the rectangle but allows easy access to other properties, such as fill and stroke.

    The wrapper class looks like
    import javafx.beans.binding.DoubleBinding;
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.shape.Shape;
    
    
    public class BottomRightRectangle {
      private final Rectangle rectangle ;
      private final DoubleProperty maxXProperty ;
      private final DoubleProperty maxYProperty ;
      private final DoubleProperty widthProperty ;
      private final DoubleProperty heightProperty ;
      
      public BottomRightRectangle(double maxX, double maxY, double width, double height) {
        this.rectangle = new Rectangle(maxX-width, maxY-height, width, height);
        this.maxXProperty = new SimpleDoubleProperty(maxX);
        this.maxYProperty = new SimpleDoubleProperty(maxY);
        this.widthProperty = new SimpleDoubleProperty(width);
        this.heightProperty = new SimpleDoubleProperty(height);
        rectangle.xProperty().bind(new DoubleBinding() {
          { super.bind(maxXProperty, widthProperty); }
          @Override
          protected double computeValue() {
            return maxXProperty.get() - widthProperty.get();
          }
        });
        rectangle.yProperty().bind(new DoubleBinding() {
          { super.bind(maxYProperty, heightProperty); }
          @Override
          protected double computeValue() {
            return maxYProperty.get() - heightProperty.get();
          }
        });
        rectangle.widthProperty().bind(this.widthProperty);
        rectangle.heightProperty().bind(this.heightProperty);
      }
      
      public BottomRightRectangle() {
        this(0, 0, 0, 0);
      }
      
      public Shape rectangle() { return rectangle ; }
      
      public DoubleProperty widthProperty() { return widthProperty ; }
      public DoubleProperty heightProperty() { return heightProperty ; }
      public DoubleProperty maxXProperty() { return maxXProperty ; }
      public DoubleProperty maxYProperty() { return maxYProperty ; }
      public double getMaxX() { return maxXProperty.get(); }
      public double getMaxY() { return maxYProperty.get(); }
      public double getWidth() { return widthProperty.get(); }
      public double getHeight() { return heightProperty.get(); }
      public void setMaxX(double x) { maxXProperty.set(x); }
      public void setMaxY(double y) { maxYProperty.set(y); }
      public void setWidth(double width) { widthProperty.set(width); }
      public void setHeight(double height) { heightProperty.set(height); }
    
    }
    And a simple test case:
    import javafx.application.Application;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.shape.Shape;
    import javafx.stage.Stage;
    
    public class BottomRightRectangleTest extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        final Pane pane = new Pane();
        final BottomRightRectangle brr = new BottomRightRectangle(400, 400, 200,
            200);
        final Shape shape = brr.rectangle();
        shape.setFill(Color.CORNFLOWERBLUE);
        shape.setStroke(Color.BLUE);
        final Circle bottomRightHandle = new Circle(10, Color.BLUE);
        bottomRightHandle.centerXProperty().bindBidirectional(brr.maxXProperty());
        bottomRightHandle.centerYProperty().bindBidirectional(brr.maxYProperty());
    
        bottomRightHandle.setOnMouseDragged(new EventHandler<MouseEvent>() {
          @Override
          public void handle(MouseEvent event) {
            bottomRightHandle.setCenterX(event.getX());
            bottomRightHandle.setCenterY(event.getY());
          }
        });
    
        pane.getChildren().addAll(shape, bottomRightHandle);
        BorderPane root = new BorderPane();
        root.setCenter(pane);
        Button resetButton = new Button("Reset");
        resetButton.setOnAction(new EventHandler<ActionEvent>() {
          @Override
          public void handle(ActionEvent event) {
            brr.setMaxX(400);
            brr.setMaxY(400);
          }
        });
        root.setBottom(resetButton);
        Scene scene = new Scene(root, 600, 620);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }
    If you prefer a more direct approach, I think you need to use listeners to bi-directionally update the x and y properties of the rectangle if the properties you define change, and vice versa. Checking that a real change occurs before doing an update prevents any infinite recursion. This is the same test with this approach:
    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.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.BorderPane;
    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 BottomRightRectangleTest2 extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        final Pane pane = new Pane();
        final Rectangle shape = new Rectangle(200, 200, 200, 200);
        shape.setFill(Color.CORNFLOWERBLUE);
        shape.setStroke(Color.BLUE);
        final Circle bottomRightHandle = new Circle(10, Color.BLUE);
    
        final DoubleProperty maxXProperty = createMaxXProperty(shape);
        final DoubleProperty maxYProperty = createMaxYProperty(shape);
        bottomRightHandle.centerXProperty().bindBidirectional(maxXProperty);
        bottomRightHandle.centerYProperty().bindBidirectional(maxYProperty);
    
        bottomRightHandle.setOnMouseDragged(new EventHandler<MouseEvent>() {
          @Override
          public void handle(MouseEvent event) {
            bottomRightHandle.setCenterX(event.getX());
            bottomRightHandle.setCenterY(event.getY());
          }
        });
    
        pane.getChildren().addAll(shape, bottomRightHandle);
        BorderPane root = new BorderPane();
        root.setCenter(pane);
        Button resetButton = new Button("Reset");
        resetButton.setOnAction(new EventHandler<ActionEvent>() {
          @Override
          public void handle(ActionEvent event) {
            maxXProperty.set(400);
            maxYProperty.set(400);
          }
        });
        root.setBottom(resetButton);
        Scene scene = new Scene(root, 600, 620);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      private DoubleProperty createMaxXProperty(final Rectangle rect) {
        final DoubleProperty maxX = new SimpleDoubleProperty(rect.getX()
            + rect.getWidth());
        maxX.addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMinX = maxX.get() - rect.getWidth();
            if (rect.getX() != newMinX) {
              rect.setX(newMinX);
            }
          }
        });
        rect.xProperty().addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMaxX = rect.getX() + rect.getWidth();
            if (maxX.get() != newMaxX) {
              maxX.set(newMaxX);
            }
          }
        });
        rect.widthProperty().addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMaxX = rect.getX() + rect.getWidth();
            if (maxX.get() != newMaxX) {
              maxX.set(newMaxX);
            }
          }
        });
        return maxX;
      }
    
      private DoubleProperty createMaxYProperty(final Rectangle rect) {
        final DoubleProperty maxY = new SimpleDoubleProperty(rect.getY()
            + rect.getHeight());
        maxY.addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMinY = maxY.get() - rect.getWidth();
            if (rect.getY() != newMinY) {
              rect.setY(newMinY);
            }
          }
        });
        rect.yProperty().addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMaxY = rect.getY() + rect.getHeight();
            if (maxY.get() != newMaxY) {
              maxY.set(newMaxY);
            }
          }
        });
        rect.heightProperty().addListener(new ChangeListener<Number>() {
          @Override
          public void changed(ObservableValue<? extends Number> observable,
              Number oldValue, Number newValue) {
            double newMaxY = rect.getY() + rect.getHeight();
            if (maxY.get() != newMaxY) {
              maxY.set(newMaxY);
            }
          }
        });
        return maxY;
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }
  • 2. Re: Rectangle custom property on lower right corner
    935521 Newbie
    Currently Being Moderated
    HI James D,

    thank you for your very detailed reply.

    I already use xProperty and yProperty binded to a circle (anchor) in the upper left corner, while with your method you overwrite it and bind it to the lower right corner.

    Is it possible to use your methods without overwriting x and y property I currently use to bind to the upper left corner?
  • 3. Re: Rectangle custom property on lower right corner
    James_D Guru
    Currently Being Moderated
    There are six potential properties you could use (say, minX, minY, maxX, maxY, width, and height). I'd recommend exposing no more than four of these as writable properties, as it will lead to confusion (since the properties are interdependent; if you change maxX for example, are you expecting minX to change or width to change?).

    The Rectangle provided by the API exposes minX, minY, width and height; my example exposes maxX, maxY width and height. You could easily rewrite my example to expose minX, minY, maxX and maxY (simply define minX and minY properties instead of width and height; bind the rectangle's x and y properties directly to minX and minY, and bind the rectangle's width and height properties to a the computed value of maxX-minX and maxY-minY).

    You could also expose read only properties for the two properties you don't want to manipulate directly; in my example you could add the methods
    public ReadOnlyDoubleProperty minXProperty() { return rectangle.xProperty(); }
    public ReadOnlyDoubleProperty minYProperty() { return rectangle.yProperty(); }
    (or similarly with width and height if you choose to refactor as above).

    You could expose these as writable properties by changing the return types of these methods to DoubleProperty, but I don't advise it. Calling set(...) or bind(...) on these properties will cause a RuntimeException as they are bound (see this discussion: Useful design patterns to reduce risk of "bound value cannot be set" error
  • 4. Re: Rectangle custom property on lower right corner
    James_D Guru
    Currently Being Moderated
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.shape.Shape;
    
    
    public class BottomRightRectangle {
      private final Rectangle rectangle ;
      private final DoubleProperty maxXProperty ;
      private final DoubleProperty maxYProperty ;
      private final DoubleProperty minXProperty ;
      private final DoubleProperty minYProperty ;
      
      public BottomRightRectangle(double minX, double minY, double maxX, double maxY) {
        this.rectangle = new Rectangle(minX, minY, maxX-minX, maxY-minY);
        this.maxXProperty = new SimpleDoubleProperty(maxX);
        this.maxYProperty = new SimpleDoubleProperty(maxY);
        this.minXProperty = new SimpleDoubleProperty(minX);
        this.minYProperty = new SimpleDoubleProperty(minY);
        rectangle.widthProperty().bind(maxXProperty.subtract(minXProperty));
        rectangle.heightProperty().bind(maxYProperty.subtract(minYProperty));
        rectangle.xProperty().bind(this.minXProperty);
        rectangle.yProperty().bind(this.minYProperty);
      }
      
      public BottomRightRectangle() {
        this(0, 0, 0, 0);
      }
      
      public Shape rectangle() { return rectangle ; }
      
      public DoubleProperty minXProperty() { return minXProperty ; }
      public DoubleProperty minYProperty() { return minYProperty ; }
      public DoubleProperty maxXProperty() { return maxXProperty ; }
      public DoubleProperty maxYProperty() { return maxYProperty ; }
      public double getMaxX() { return maxXProperty.get(); }
      public double getMaxY() { return maxYProperty.get(); }
      public double getMinX() { return minXProperty.get(); }
      public double getMinY() { return minYProperty.get(); }
      public void setMaxX(double x) { maxXProperty.set(x); }
      public void setMaxY(double y) { maxYProperty.set(y); }
      public void setMinX(double minX) { minXProperty.set(minX); }
      public void setminY(double minY) { minYProperty.set(minY); }
    
    }
    And the test:
    import javafx.application.Application;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.shape.Shape;
    import javafx.stage.Stage;
    
    public class BottomRightRectangleTest extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        final Pane pane = new Pane();
        final BottomRightRectangle brr = new BottomRightRectangle(200, 200, 400, 400);
        final Shape shape = brr.rectangle();
        shape.setFill(Color.CORNFLOWERBLUE);
        shape.setStroke(Color.BLUE);
        final Circle bottomRightHandle = new Circle(10, Color.BLUE);
        bottomRightHandle.centerXProperty().bindBidirectional(brr.maxXProperty());
        bottomRightHandle.centerYProperty().bindBidirectional(brr.maxYProperty());
    
        final Circle topLeftHandle = new Circle(10, Color.BLUE);
        topLeftHandle.centerXProperty().bindBidirectional(brr.minXProperty());
        topLeftHandle.centerYProperty().bindBidirectional(brr.minYProperty());
       
        bottomRightHandle.setOnMouseDragged(new EventHandler<MouseEvent>() {
          @Override
          public void handle(MouseEvent event) {
            if (event.getX() >= topLeftHandle.getCenterX()) {
              bottomRightHandle.setCenterX(event.getX());
            }
            if (event.getY() >= topLeftHandle.getCenterY()) {
              bottomRightHandle.setCenterY(event.getY());
            }
          }
        });
        
        topLeftHandle.setOnMouseDragged(new EventHandler<MouseEvent>() {
          @Override
          public void handle(MouseEvent event) {
            if (event.getX() <= bottomRightHandle.getCenterX()) {
              topLeftHandle.setCenterX(event.getX());
            }
            if (event.getY() <= bottomRightHandle.getCenterY()) {
              topLeftHandle.setCenterY(event.getY());
            }
          }
        });
    
        pane.getChildren().addAll(shape, bottomRightHandle, topLeftHandle);
        BorderPane root = new BorderPane();
        root.setCenter(pane);
        Button resetButton = new Button("Reset");
        resetButton.setOnAction(new EventHandler<ActionEvent>() {
          @Override
          public void handle(ActionEvent event) {
            brr.setMinX(200);
            brr.setminY(200);
            brr.setMaxX(400);
            brr.setMaxY(400);
          }
        });
        root.setBottom(resetButton);
        Scene scene = new Scene(root, 600, 620);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    }
  • 5. Re: Rectangle custom property on lower right corner
    935521 Newbie
    Currently Being Moderated
    Hi James D

    I want to thank very very much for your quick reply: your solution is perfect, exactly what I needed.

    Thanks once again!

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points