Skip to content

Instantly share code, notes, and snippets.

@jewelsea
Last active December 8, 2020 09:14
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jewelsea/5375786 to your computer and use it in GitHub Desktop.
Save jewelsea/5375786 to your computer and use it in GitHub Desktop.
Demonstrates modifying a polygon's shape in JavaFX by allowing the user to drag around control points attached to the corners of the Polygon.
import javafx.scene.Scene;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
/** Drag the anchors around to change a polygon's points. */
public class TriangleManipulator extends Application {
public static void main(String[] args) throws Exception { launch(args); }
// main application layout logic.
@Override public void start(final Stage stage) throws Exception {
Polygon triangle = createStartingTriangle();
Group root = new Group();
root.getChildren().add(triangle);
root.getChildren().addAll(createControlAnchorsFor(triangle.getPoints()));
stage.setTitle("Triangle Manipulation Sample");
stage.setScene(
new Scene(
root,
400, 400, Color.ALICEBLUE
)
);
stage.show();
}
// creates a triangle.
private Polygon createStartingTriangle() {
Polygon triangle = new Polygon();
triangle.getPoints().setAll(
100d, 100d,
150d, 50d,
250d, 150d
);
triangle.setStroke(Color.FORESTGREEN);
triangle.setStrokeWidth(4);
triangle.setStrokeLineCap(StrokeLineCap.ROUND);
triangle.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
return triangle;
}
// @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();
for (int i = 0; i < points.size(); i+=2) {
final int idx = i;
DoubleProperty xProperty = new SimpleDoubleProperty(points.get(i));
DoubleProperty yProperty = new SimpleDoubleProperty(points.get(i + 1));
xProperty.addListener(new ChangeListener<Number>() {
@Override public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
points.set(idx, (double) x);
}
});
yProperty.addListener(new ChangeListener<Number>() {
@Override public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) {
points.set(idx + 1, (double) y);
}
});
anchors.add(new Anchor(Color.GOLD, xProperty, yProperty));
}
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(), 10);
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);
}
}
});
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; }
}
}
@jewelsea
Copy link
Author

Answer to StackOverflow question: JavaFX Polygon modify

@gustavson
Copy link

Hi jewelsea,

I've tried it with Java8, but when I drag the anchors, the triangle leaves at the same position and size.
I couldn't find the problem.

Nice greatings,
gustavson

@jewelsea
Copy link
Author

@gustavson,

Thanks for your observation. I updated the gist code so that (I think) it should work on early Java 8 builds.

It looks like JavaFX 8 initial release through build 11 aggressively garbage collects the xProperty, yProperty references which attach listeners to point positions in the polygons. I updated the gist to explicitly store property references in the Anchor class. This ensures that the properties will not be garbage collected until the anchor point of the polygon vertex is no longer need. The change should work with these Java 8 initial builds through Java build 11 (I no longer have these installed to adequately test it). You can check the revision history of the gist to better understand the change.

I tried the application unmodified with Java8u20 on Windows 7 x64 and it worked fine and it also used to work with Java 7, so it looks like a temporary modification was made to the way references are stored for properties with attached change listeners. I think this modification is no longer in effect with 8u20. I'm guessing the modification to the JavaFX listener reference handling was put in place to try to make it less likely for JavaFX programs to leak memory, however it was probably reverted because it could potentially break stuff (like this application).

The documentation comment for ChangeListener states "The ObservableValue stores a strong reference to the listener which will prevent the listener from being garbage collected and may result in a memory leak. It is recommended to either unregister a listener by calling removeListener after use or to use an instance of WeakChangeListener avoid this situation.". What was happening (I think) in the early Java 8 builds is that this contract was not being respected and the ObservableValue was just storing a weak reference, allowing the listener to be prematurely garbage collected.

@nickbbishop
Copy link

@jewelsea Thanks very much for this (and all the many other great sample projects), they're very very helpful for beginners!

The only thing I couldn't find was a nice sample project of how a bigger project might be managed with some of these methods & calls split into a MVC format but I appreciate all your efforts, thanks again.

@thiloilg
Copy link

Wow thank you very helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment