I have a problem with zooming and panning image in ScrollPane. So far I have code like this:
Image image = imageView.getImage();
scrollPane.setPrefViewportWidth(0d);
scrollPane.setPrefViewportHeight(0d);
imageView.setFitWidth(0d);
imageView.setFitHeight(0d);
Bounds viewportBounds = scrollPane.getViewportBounds();
boolean vertical = image.getWidth() > image.getHeight();
if (imageView.getRotate() == 90 || imageView.getRotate() == 270d) {
vertical = !vertical;
}
imageView.setPreserveRatio(true);
double propX = viewportBounds.getWidth() / image.getWidth();
double propY = viewportBounds.getHeight() / image.getHeight();
boolean xLead = !(propX > propY);
imageView.setScaleX((xLead) ? propX : propY);
imageView.setScaleY((xLead) ? propX : propY);
scrollPane.setContent(imageView);
scrollPane.setPannable(true);
scrollPane.setHvalue(scrollPane.getHmin() + (scrollPane.getHmax() - scrollPane.getHmin()) / 2); // center the scroll contents.
scrollPane.setVvalue(scrollPane.getVmin() + (scrollPane.getVmax() - scrollPane.getVmin()) / 2);
zoom(imageView);
private void zoom(ImageView imagePannable) {
imagePannable.setOnScroll(
new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.20;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 0.80;
}
imagePannable.setScaleX(imagePannable.getScaleX() * zoomFactor);
imagePannable.setScaleY(imagePannable.getScaleY() * zoomFactor);
event.consume();
}
});
}
What I want to do is align image relative to mouse pointer not to center of image everytime.
I also have a problem with large images (like maps which are 8*A4 size for example). When I zooming this maps pannable function stop working. What is wrong with this code? Thanks for helps!
Several people (including me) have had this same question. I got my answer here.
In the interest of clarity, here is a working example of a panning & zooming pane using a rectangle as the zoomed node. I have implemented this in a slightly more complex way with an ImageView.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.NAVY);
rect.setStrokeType(StrokeType.INSIDE);
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = scale - oldScale;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}
Related
I have a class "Vertex" that contains double x, y.
Another class "Face" holds a list of "Vertex" objects. Neighboring faces share the same vertices.
At the moment I'm creating a javafx.scene.shape.Polygon for every Face and add them all to my scene, which looks like this:
Screenshot
Now I'm planning to modify the polygons, similar to this: JavaFX modify polygons
The problem is that the polygons don't save references to my Vertex objects but double values. When I change the position of one point, the same point in the neighboring polygons is still at the old position. How can I link those points to each other? And also how to save the changes back to my "Face" object?
Code example as requested: pastebin.com/C3JHb2nM
Here you go. Calculated common anchor positions then adjusted all of the common ones when one was moved.
Anchor contains addCommon function which adds an anchor that is common to it. The common variable stores all the common anchors. Then when handle is called, all of the common ones x and y positions change as well.
Also, I will suggest that you hold the common points in Faces. I've created a simple method that will calculate all the Faces that share a vertex in said class. But following a MVC guideline, you need to have a model that provides all of necessary data to create a GUI. I would suggest Mesh, Face, and Vertex should provide all necessary information to create the GUI, and not as a two way street. Basically, Anchor should not alter your models in any way.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.Polygon;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
public class Main extends Application {
public class Vertex {
private double x, y;
public Vertex(double x, double y) {
this.x = x;
this.y = y;
}
public Double[] getPoint() {
return new Double[]{x, y};
}
}
public class Face {
private List<Vertex> verts;
public Face(Vertex... verts) {
this.verts = new ArrayList<>(Arrays.asList(verts));
}
public Polygon getPolygon() {
Polygon polygon = new Polygon();
polygon.setFill(Color.GRAY);
polygon.setStroke(Color.BLACK);
for (Vertex vertex : verts) {
polygon.getPoints().addAll(vertex.getPoint());
}
return polygon;
}
public boolean containsVertex(Vertex ver) {
for (Vertex v : this.verts) {
if (v.x == ver.x && v.y == ver.y) {
return true;
}
}
return false;
}
}
public class Mesh {
private List<Vertex> verts = new ArrayList<Vertex>();
private List<Face> faces = new ArrayList<Face>();
private Map<Vertex, List<Face>> commonVertices = new HashMap<>();
public List<Polygon> getPolygons() {
List<Polygon> polygons = new ArrayList<Polygon>();
for (Face face : faces) {
polygons.add(face.getPolygon());
}
return polygons;
}
public Mesh() {
verts.add(new Vertex(50, 50));
verts.add(new Vertex(300, 50));
verts.add(new Vertex(500, 50));
verts.add(new Vertex(50, 300));
verts.add(new Vertex(250, 300));
verts.add(new Vertex(500, 300));
verts.add(new Vertex(50, 600));
verts.add(new Vertex(300, 700));
verts.add(new Vertex(500, 700));
faces.add(new Face(verts.get(0), verts.get(1), verts.get(4), verts.get(3)));
faces.add(new Face(verts.get(4), verts.get(1), verts.get(2), verts.get(5)));
faces.add(new Face(verts.get(3), verts.get(4), verts.get(7), verts.get(6)));
faces.add(new Face(verts.get(7), verts.get(4), verts.get(5)));
faces.add(new Face(verts.get(7), verts.get(5), verts.get(8)));
findCommonVertices();
}
private void findCommonVertices() {
for (Vertex ver : this.verts) {
List<Face> share = new ArrayList<>();
for (Face face : this.faces) {
if (face.containsVertex(ver)) {
share.add(face);
}
}
commonVertices.put(ver, share);
}
}
public Map<Vertex, List<Face>> getCommonVertices() {
return this.commonVertices;
}
}
#Override
public void start(Stage stage) {
Group root = new Group();
Scene scene = new Scene(root, 1024, 768);
stage.setScene(scene);
Group g = new Group();
Mesh mesh = new Mesh();
List<Polygon> polygons = mesh.getPolygons();
g.getChildren().addAll(polygons);
List<Anchor> anchors = new ArrayList<>();
for (Polygon p : polygons) {
ObservableList<Anchor> temp = createControlAnchorsFor(p.getPoints());
g.getChildren().addAll(temp);
for (Anchor kk : temp) {
anchors.add(kk);
}
}
for (int i = 0; i < anchors.size(); i++) {
List<Anchor> common = new ArrayList<>();
for (int j = 0; j < anchors.size(); j++) {
if (i != j) {
if (anchors.get(i).x.doubleValue() == anchors.get(j).x.doubleValue() && anchors.get(i).y.doubleValue() == anchors.get(j).y.doubleValue()) {
anchors.get(i).addCommon(anchors.get(j));
System.out.println("COMMON " + i + " " + j);
}
}
}
// anchors.get(i).setCommon(common);
}
scene.setRoot(g);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
// Everything below was copied from here: https://gist.github.com/jewelsea/5375786
// #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;
List<Anchor> common = new ArrayList<>();
public void setCommon(List<Anchor> common) {
this.common = common;
}
public void addCommon(Anchor com) {
common.add(com);
enableDrag();
}
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);
if (common != null) {
for (Anchor an : common) {
an.setCenterX(newX);
System.out.println("CALLED");
}
}
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
if (common != null) {
for (Anchor an : common) {
an.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;
}
}
}
Make the x and y coordinates of the vertices properties. You could then add listeners to those properties that modify the points of the Polygons:
public class Vertex {
public DoubleProperty xProperty() {
...
}
public DoubleProperty yProperty() {
...
}
}
public class VertexListener implements InvalidationListerner {
private final Vertex vertex;
private final int firstIndex;
private final List<Double> points;
public VertexListener(Vertex vertex, Polygon polygon, int pointIndex) {
this.firstIndex = 2 * pointIndex;
this.vertex = vertex;
this.points = polygon.getPoints();
vertex.xProperty().addListener(this);
vertex.yProperty().addListener(this);
}
public void dispose() {
vertex.xProperty().removeListener(this);
vertex.yProperty().removeListener(this);
}
#Override
public void invalidated(Observable observable) {
double x = vertex.getX();
double y = vertex.getY();
points.set(firstIndex, x);
points.set(firstIndex+1, y);
}
}
This way you only need to adjust the property values in Vertex and the listeners will add all incident faces...
Need to make a sphere in JavaFX "blink" over time, like fade in and fade out. Is it possible? Or at least change shades of color. I managed to make it blink by changing SelfIlluminationMap property of the PhongMaterial I am using, with this code
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class JavaFXApplication15 extends Application {
Image im = new Image("bump.jpg");
PhongMaterial ph = new PhongMaterial(Color.GREEN);
int nums = 0;
UpdateTimer timer;
private class UpdateTimer extends AnimationTimer {
int counter = 0;
#Override
public void handle(long now) {
if (counter == 20) {
update();
counter = 0;
}
counter++;
}
}
private Parent createContent() throws Exception {
timer = new UpdateTimer();
ph = new PhongMaterial(Color.YELLOW, null, null, null, im);
Box box = new Box(5, 5, 5);
box.setMaterial(ph);
// Create and position camera
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.getTransforms().addAll(
new Rotate(-20, Rotate.X_AXIS),
new Translate(0, 0, -50)
);
// Build the Scene Graph
Group root = new Group();
root.getChildren().add(camera);
root.getChildren().add(box);
// Use a SubScene
SubScene subScene = new SubScene(
root,
300, 300,
true,
SceneAntialiasing.BALANCED
);
subScene.setFill(Color.ALICEBLUE);
subScene.setCamera(camera);
Group group = new Group();
group.getChildren().add(subScene);
return group;
}
void update() {
if (ph.getSelfIlluminationMap() != null) {
ph.setSelfIlluminationMap(null);
} else {
ph.setSelfIlluminationMap(im);
}
}
#Override
public void start(Stage stage) throws Exception {
stage.setResizable(false);
Scene scene = new Scene(createContent());
stage.setScene(scene);
stage.show();
timer.start();
}
public static void main(String[] args) {
launch(args);
}
}
but is it possible to make it fade in and out? With some kind of transition possibly?
Just to see the effect, I tried animating the specularColor of a sphere's PhongMaterial. Starting from this example, I followed the approach shown here to get a color lookup table of equally spaced brightness values of a given hue.
private final Queue<Color> clut = new LinkedList<>();
The implementation of handle() simply cycles through the table.
#Override
public void handle(long now) {
redMaterial.setSpecularColor(clut.peek());
clut.add(clut.remove());
}
If the result is appealing, a more flexible approach might accrue from using a concrete subclass of Transition.
private final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
setAutoReverse(true);
setCycleCount(INDEFINITE);
}
#Override
protected void interpolate(double d) {
redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
}
};
import java.util.LinkedList;
import java.util.Queue;
import javafx.animation.AnimationTimer;
import javafx.animation.Animation;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* #see https://stackoverflow.com/a/44447913/230513
* #see https://stackoverflow.com/a/37755149/230513
* #see https://stackoverflow.com/a/37743539/230513
* #see https://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
private static final class Content {
private static final double WIDTH = 3;
private static final Color LITE = Color.RED;
private static final Color DARK = Color.RED.darker().darker();
private final Xform group = new Xform();
private final Group cube = new Group();
private final Group axes = new Group();
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private final Sphere sphere;
private final PhongMaterial redMaterial = new PhongMaterial();
private final UpdateTimer timer = new UpdateTimer();
private class UpdateTimer extends AnimationTimer {
private static final double N = 32d;
private final Queue<Color> clut = new LinkedList<>();
public UpdateTimer() {
for (int i = 0; i < N; i++) {
clut.add(Color.hsb(LITE.getHue(), 1, 1 - (i / N)));
}
for (int i = 0; i < N; i++) {
clut.add(Color.hsb(LITE.getHue(), 1, i / N));
}
}
#Override
public void handle(long now) {
redMaterial.setSpecularColor(clut.peek());
clut.add(clut.remove());
}
}
private final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
setAutoReverse(true);
setCycleCount(INDEFINITE);
}
#Override
protected void interpolate(double d) {
redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
}
};
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().addAll(c.box, c.sphere);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
double edge = 3 * size / 4;
xAxis = createBox(edge, WIDTH, WIDTH, edge);
yAxis = createBox(WIDTH, edge / 2, WIDTH, edge);
zAxis = createBox(WIDTH, WIDTH, edge / 4, edge);
box = new Box(edge, edge / 2, edge / 4);
box.setDrawMode(DrawMode.LINE);
sphere = new Sphere(8);
redMaterial.setDiffuseColor(DARK);
redMaterial.setSpecularColor(LITE);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(edge / 2);
sphere.setTranslateY(-edge / 4);
sphere.setTranslateZ(-edge / 8);
}
private Box createBox(double w, double h, double d, double edge) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
b.setTranslateX(-edge / 2 + w / 2);
b.setTranslateY(edge / 4 - h / 2);
b.setTranslateZ(edge / 8 - d / 2);
return b;
}
}
private static class Xform extends Group {
private final Point3D px = new Point3D(1.0, 0.0, 0.0);
private final Point3D py = new Point3D(0.0, 1.0, 0.0);
private Rotate r;
private Transform t = new Rotate();
public void rx(double angle) {
r = new Rotate(angle, px);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void rz(double angle) {
r = new Rotate(angle);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-2 * SIZE);
scene.setCamera(camera);
scene.setOnMousePressed((MouseEvent e) -> {
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseOldX = e.getSceneX();
mouseOldY = e.getSceneY();
});
scene.setOnMouseDragged((MouseEvent e) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (e.isShiftDown()) {
content.group.rz(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isPrimaryButtonDown()) {
content.group.rx(+mouseDeltaY * 180.0 / scene.getHeight());
content.group.ry(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isSecondaryButtonDown()) {
camera.setTranslateX(camera.getTranslateX() - mouseDeltaX * 0.1);
camera.setTranslateY(camera.getTranslateY() - mouseDeltaY * 0.1);
}
});
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
//content.timer.start();
content.animation.play();
}
public static void main(String[] args) {
launch(args);
}
}
I have a class which extends javafx.scene.Node, say DraggableNode.
I have written the event-handlers for dragging for any such DraggableNode .
Class DraggableNode extends Node
{
...
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
offsetX = dragStartPoitionX - event.getSceneX();
offsetY = dragStartPoitionY - event.getSceneY();
setLayoutX (event.getSceneX());
setLayoutY (event.getSceneY());
...
}
}
}
This event-handler works fine for dragging this node individually.
Next, I require to select multiple such "nodes" and, dragging of one of the selected node should change the (x,y) co-ordinates of all the selected nodes by "offsetX" & "offsetY".
Selection algorithm is also implemented(in a Class extending Pane in which these nodes are added as children). But, what I need is to somehow trigger the drag event handlers of the other selected nodes so that the final output would look like a multi-drag.
First you create a selection model, say a Set<Node>. Whenever you add a Node to your selection, you add it to the selection model.
When you drag a node, you simply change the position of all of the other nodes of the selection model in your event handler as well.
As simple as that.
Here's code which also supports rubberband selection, shift & ctrl keypress during selection, etc:
NodeSelection.java
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class NodeSelection extends Application {
public static Image image = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg");
// public Image image = new Image( getClass().getResource( "tiger.jpg").toExternalForm());
SelectionModel selectionModel = new SelectionModel();
DragMouseGestures dragMouseGestures = new DragMouseGestures();
static Random rnd = new Random();
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setStyle("-fx-background-color:white");
new RubberBandSelection( pane);
double width = 200;
double height = 160;
double padding = 20;
for( int row=0; row < 4; row++) {
for( int col=0; col < 4; col++) {
Selectable selectable = new Selectable( width, height);
selectable.relocate( padding * (col+1) + width * col, padding * (row + 1) + height * row);
pane.getChildren().add(selectable);
dragMouseGestures.makeDraggable(selectable);
}
}
Label infoLabel = new Label( "Drag on scene for Rubberband Selection. Shift+Click to add to selection, CTRL+Click to toggle selection. Drag selected nodes for multi-dragging.");
pane.getChildren().add( infoLabel);
Scene scene = new Scene( pane, 1600, 900);
scene.getStylesheets().add( getClass().getResource("application.css").toExternalForm());
primaryStage.setScene( scene);
primaryStage.show();
}
private class Selectable extends Region {
ImageView view;
public Selectable( double width, double height) {
view = new ImageView( image);
view.setFitWidth(width);
view.setFitHeight(height);
getChildren().add( view);
this.setPrefSize(width, height);
}
}
private class SelectionModel {
Set<Node> selection = new HashSet<>();
public void add( Node node) {
if( !node.getStyleClass().contains("highlight")) {
node.getStyleClass().add( "highlight");
}
selection.add( node);
}
public void remove( Node node) {
node.getStyleClass().remove( "highlight");
selection.remove( node);
}
public void clear() {
while( !selection.isEmpty()) {
remove( selection.iterator().next());
}
}
public boolean contains( Node node) {
return selection.contains(node);
}
public int size() {
return selection.size();
}
public void log() {
System.out.println( "Items in model: " + Arrays.asList( selection.toArray()));
}
}
private class DragMouseGestures {
final DragContext dragContext = new DragContext();
private boolean enabled = false;
public void makeDraggable(final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// don't do anything if the user is in the process of adding to the selection model
if( event.isControlDown() || event.isShiftDown())
return;
Node node = (Node) event.getSource();
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
// clear the model if the current node isn't in the selection => new selection
if( !selectionModel.contains(node)) {
selectionModel.clear();
selectionModel.add( node);
}
// flag that the mouse released handler should consume the event, so it won't bubble up to the pane which has a rubberband selection mouse released handler
enabled = true;
// prevent rubberband selection handler
event.consume();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !enabled)
return;
// all in selection
for( Node node: selectionModel.selection) {
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// prevent rubberband selection handler
if( enabled) {
// set node's layout position to current position,remove translate coordinates
for( Node node: selectionModel.selection) {
fixPosition(node);
}
enabled = false;
event.consume();
}
}
};
/**
* Set node's layout position to current position, remove translate coordinates.
* #param node
*/
private void fixPosition( Node node) {
double x = node.getTranslateX();
double y = node.getTranslateY();
node.relocate(node.getLayoutX() + x, node.getLayoutY() + y);
node.setTranslateX(0);
node.setTranslateY(0);
}
class DragContext {
double x;
double y;
}
}
private class RubberBandSelection {
final DragContext dragContext = new DragContext();
Rectangle rect;
Pane group;
boolean enabled = false;
public RubberBandSelection( Pane group) {
this.group = group;
rect = new Rectangle( 0,0,0,0);
rect.setStroke(Color.BLUE);
rect.setStrokeWidth(1);
rect.setStrokeLineCap(StrokeLineCap.ROUND);
rect.setFill(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.6));
group.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_DRAGGED, onMouseDraggedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_RELEASED, onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// simple flag to prevent multiple handling of this event or we'd get an exception because rect is already on the scene
// eg if you drag with left mouse button and while doing that click the right mouse button
if( enabled)
return;
dragContext.mouseAnchorX = event.getSceneX();
dragContext.mouseAnchorY = event.getSceneY();
rect.setX(dragContext.mouseAnchorX);
rect.setY(dragContext.mouseAnchorY);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().add( rect);
event.consume();
enabled = true;
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !event.isShiftDown() && !event.isControlDown()) {
selectionModel.clear();
}
for( Node node: group.getChildren()) {
if( node instanceof Selectable) {
if( node.getBoundsInParent().intersects( rect.getBoundsInParent())) {
if( event.isShiftDown()) {
selectionModel.add( node);
} else if( event.isControlDown()) {
if( selectionModel.contains( node)) {
selectionModel.remove( node);
} else {
selectionModel.add( node);
}
} else {
selectionModel.add( node);
}
}
}
}
selectionModel.log();
rect.setX(0);
rect.setY(0);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().remove( rect);
event.consume();
enabled = false;
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double offsetX = event.getSceneX() - dragContext.mouseAnchorX;
double offsetY = event.getSceneY() - dragContext.mouseAnchorY;
if( offsetX > 0)
rect.setWidth( offsetX);
else {
rect.setX(event.getSceneX());
rect.setWidth(dragContext.mouseAnchorX - rect.getX());
}
if( offsetY > 0) {
rect.setHeight( offsetY);
} else {
rect.setY(event.getSceneY());
rect.setHeight(dragContext.mouseAnchorY - rect.getY());
}
event.consume();
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}
public static void main(String[] args) {
launch(args);
}
}
application.css
.highlight {
-fx-effect: dropshadow(three-pass-box, red, 4, 4, 0, 0);
}
Screenshot:
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 ?
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 ; }
}
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);
}
}
});
Is there a way to modify a polygon in JavaFX? For example if I have a triangle and I press and then drag a point from that triangle, the triangle will modify with the new coordinates of the point.
Layer some control nodes over the corners of the polygon.
Attach appropriate event handlers to the control nodes so that they can be dragged around.
Modify the polygon's points as the control node is moved (using change listeners attached to each of the control node's location properties).
Here is a sample solution:
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; }
}
}
I derived this solution from: CubicCurve JavaFX