JavaFX: Deferred coordinate computation - javafx

I'm trying to draw a curve between two nodes, which reside in different parent container. The container can be arbitrary deeply nested. I am able to transform the local coordinates into the coordinate space of the common ancestor, however the node bounds are not available, at the time the line is drawn.
In my application I create my nodes in a background thread and add them later to the scene. Therefore the computation of the line coordinates should just be triggered, when the two nodes are fully layouted (they should have a height and width).
I tried to solve the issue with a property change listener, but I cannot find a property which changes enough frequently.
Is there an event I can listen to, which gets triggered if the needed properties are set?
Thank you in advance!
I added a simplified example, which should show the problem. The line is correctly drawn, as soon as you click the "redraw"-Button.
Application Class:
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Showcase");
Scene scene = new Scene(root, 300, 300);
primaryStage.setScene(scene);
primaryStage.setResizable(false);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
sample.fxml
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.*?>
<AnchorPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml">
<ToolBar AnchorPane.rightAnchor="0" AnchorPane.leftAnchor="0" prefHeight="50">
<Button onAction="#redrawEdge" text="Redraw Line"/>
</ToolBar>
<AnchorPane fx:id="contentPane" AnchorPane.topAnchor="50"
AnchorPane.leftAnchor="0"
AnchorPane.rightAnchor="0"/>
</AnchorPane>
Controller:
public class Controller {
private final Rectangle node1 = new Rectangle();
private final Ellipse node2 = new Ellipse();
private final HBox parent = new HBox();
private final HBox parent2 = new HBox();
private final VBox parentParent = new VBox();
private final Line edge = new Line();
#FXML
private AnchorPane contentPane;
#FXML
public void initialize() {
// not updated as expected
node1.boundsInParentProperty().addListener((o) -> drawLine());
node2.boundsInParentProperty().addListener((o) -> drawLine());
node1.setHeight(20);
node1.setWidth(20);
node1.setFill(Color.BURLYWOOD);
node2.setRadiusX(20);
node2.setRadiusY(20);
node2.setFill(Color.DEEPSKYBLUE);
parent.setStyle("-fx-border-color: #DC143C");
parent.setPadding(new Insets(10));
parent.getChildren().add(node1);
parent2.setStyle("-fx-border-color: #5CD3A9");
parent2.setPadding(new Insets(10));
parent2.getChildren().add(node2);
parentParent.setStyle("-fx-border-color: #0336FF");
parentParent.setLayoutX(200);
parentParent.setLayoutY(50);
parentParent.setPadding(new Insets(10));
parentParent.getChildren().add(parent);
contentPane.setStyle("-fx-border-color: #4B0082;");
contentPane.setPadding(new Insets(10));
contentPane.getChildren().addAll(parentParent, parent2, edge);
}
#FXML
public void redrawEdge(ActionEvent actionEvent) {
drawLine();
}
private void drawLine() {
Bounds n1InCommonAncestor = getRelativeBounds(node1, contentPane);
Bounds n2InCommonAncestor = getRelativeBounds(node2, contentPane);
Point2D n1Center = getCenter(n1InCommonAncestor);
Point2D n2Center = getCenter(n2InCommonAncestor);
Point2D startIntersection = findIntersectionPoint(n1InCommonAncestor,
n1Center, n2Center);
Point2D endIntersection = findIntersectionPoint(n2InCommonAncestor,
n2Center, n1Center);
edge.setStartX(startIntersection.getX());
edge.setStartY(startIntersection.getY());
edge.setEndX(endIntersection.getX());
edge.setEndY(endIntersection.getY());
}
private Bounds getRelativeBounds(Node node, Node relativeTo) {
Bounds nodeBoundsInScene = node.localToScene(node.getBoundsInLocal());
return relativeTo.sceneToLocal(nodeBoundsInScene);
}
private Point2D getCenter(Bounds bounds) {
return new Point2D(bounds.getMinX() + bounds.getWidth() / 2,
bounds.getMinY() + bounds.getHeight() / 2);
}
private Point2D findIntersectionPoint(Bounds nodeBounds,
Point2D inside, Point2D outside) {
Point2D middle = outside.midpoint(inside);
double deltaX = outside.getX() - inside.getX();
double deltaY = outside.getY() - inside.getY();
if (Math.hypot(deltaX, deltaY) < 1.) {
return middle;
} else {
if (nodeBounds.contains(middle)) {
return findIntersectionPoint(nodeBounds, middle, outside);
} else {
return findIntersectionPoint(nodeBounds, inside, middle);
}
}
}
}

Creating the following should work, though you may need to consider consequences on performance in some circumstances:
Node node = ... ;
ObjectBinding<Bounds> boundsInScene = new ObjectBinding<Bounds>() {
{
bind(node.boundsInLocalProperty(), node.localToSceneTransformProperty());
}
#Override
protected Bounds computeValue() {
return node.localToScene(node.getBoundsInLocal());
}
};
You could also do something similar for the relative bounds:
Node node1 = ... ;
Node node2 = ... ;
ObjectBinding<Bounds> relativeBounds = new ObjectBinding<Bounds>() {
{
bind(
node1.boundsInLocalProperty(),
node1.localToSceneTransformProperty(),
node2.boundsInLocalProperty(),
node2.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
}
};
Adding listeners to these, and redrawing when they change, should give you the control you need over when to redraw.

Related

AutoScalePane in JavaFX with layoutChildren()

I am trying to created a custom pane, which scales it's content to the available space of the pane.
I created a demo application, which splits the Stage with a SplitPane. Each split contains one AutoScalePane (see FMXL). I would expect the AutoScalePane to shrink/grow its content according to the available space (please play with the split bar)
The content of the AutoScalePane is grouped in a Group, which should be scaled, as the AutoScalePane boundaries change.
Even though, I receive the correct bounds and can compute the right zoom ratio (check debug log), the Circle nodes are not scaled..
I assume that I made a mistake in the layoutChildren() method, but I can't see an obvious issue.
It would be great if somebody with more JavaFX experience could help me :)
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("AutoScalePane Test");
primaryStage.setScene(new Scene(root, 700, 200));
primaryStage.show();
}
}
View Controller:
public class Controller {
#FXML
public AutoScalePane scalePaneLeft;
#FXML
public AutoScalePane scalePaneRight;
#FXML
public void initialize() {
fillLeftContent();
fillRightContent();
}
private void fillLeftContent() {
Circle circle1 = new Circle(100, 300, 10);
Circle circle2 = new Circle(150, 300, 10);
Circle circle3 = new Circle(200, 300, 10);
Circle circle4 = new Circle(250, 300, 10);
scalePaneLeft.addChildren(new Node[] {circle1, circle2, circle3,
circle4});
}
private void fillRightContent() {
Circle circle1 = new Circle(100, 200, 20);
Circle circle2 = new Circle(150, 200, 20);
Circle circle3 = new Circle(200, 200, 20);
Circle circle4 = new Circle(250, 200, 20);
scalePaneRight.addChildren(new Node[] {circle1, circle2, circle3,
circle4});
}
}
FXML View:
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import sample.AutoScalePane?>
<AnchorPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml">
<SplitPane dividerPositions="0.3" orientation="HORIZONTAL" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"
AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" style="-fx-background-color: #2c5069;">
<AutoScalePane fx:id="scalePaneLeft"
style="-fx-background-color: #943736;"/>
<AutoScalePane fx:id="scalePaneRight"
style="-fx-background-color: #d27452;"/>
</SplitPane>
</AnchorPane>
Auto-scale Pane:
/**
* Auto-scales its content according to the available space of the Pane.
* The content is always centered
*
*/
public class AutoScalePane extends Pane {
private Group content = new Group();
private Scale zoom = new Scale(1, 1);
public AutoScalePane() {
layoutBoundsProperty().addListener((o) -> {
autoScale();
});
content.scaleXProperty().bind(zoom.xProperty());
content.scaleYProperty().bind(zoom.yProperty());
getChildren().add(content);
}
/**
* Adds nodes to the AutoScalePane
*
* #param children nodes
*/
public void addChildren(Node... children) {
content.getChildren().addAll(children);
requestLayout();
}
private void autoScale() {
if (getHeight() > 0
&& getWidth() > 0
&& content.getBoundsInParent().getWidth() > 0
&& content.getBoundsInParent().getHeight() > 0) {
// scale
double scaleX = getWidth() / content.getBoundsInParent().getWidth();
double scaleY = getHeight() / content.getBoundsInParent()
.getHeight();
System.out.println("*************** DEBUG ****************");
System.out.println("Pane Width: " + getWidth());
System.out.println("Content Bounds Width: " + content
.getBoundsInParent()
.getWidth());
System.out.println("Pane Height: " + getHeight());
System.out.println("Content Bounds Height: " + content
.getBoundsInParent()
.getHeight());
System.out.println("ScaleX: " + scaleX);
System.out.println("ScaleY: " + scaleY);
double zoomFactor = Math.min(scaleX, scaleY);
zoom.setX(zoomFactor);
zoom.setY(zoomFactor);
requestLayout();
}
}
#Override
protected void layoutChildren() {
final double paneWidth = getWidth();
final double paneHeight = getHeight();
final double insetTop = getInsets().getTop();
final double insetRight = getInsets().getRight();
final double insetLeft = getInsets().getLeft();
final double insertBottom = getInsets().getBottom();
final double contentWidth = (paneWidth - insetLeft - insetRight) *
zoom.getX();
final double contentHeight = (paneHeight - insetTop - insertBottom) *
zoom.getY();
layoutInArea(content, 0, 0, contentWidth, contentHeight,
getBaselineOffset(), HPos.CENTER, VPos.CENTER);
}
}
layoutChildren is invoked on a change of the size of the node. You don't need to register a listener if you adjust the scale from the layoutChildren method.
As for the zoom: You never really modify the scale properties. You don't update the Scale anywhere but in this snippet:
double zoomFactor = Math.min(zoom.getX(), zoom.getY());
zoom.setX(zoomFactor);
zoom.setY(zoomFactor);
so zoom.getX() and zoom.getY() always return 1 which is equal to the initial scale factor.
Note that you can apply the Scale matrix to the transforms of the content node directly, but this wouldn't use the center as a pivot point of the zoom.
BTW: By extending Region instead of Pane you restrict the access to the children list to protected which prevents users from modifying it.
public class AutoScalePane extends Region {
private final Group content = new Group();
public AutoScalePane() {
content.setManaged(false); // avoid constraining the size by content
getChildren().add(content);
}
/**
* Adds nodes to the AutoScalePane
*
* #param children nodes
*/
public void addChildren(Node... children) {
content.getChildren().addAll(children);
requestLayout();
}
#Override
protected void layoutChildren() {
final Bounds groupBounds = content.getBoundsInLocal();
final double paneWidth = getWidth();
final double paneHeight = getHeight();
final double insetTop = getInsets().getTop();
final double insetRight = getInsets().getRight();
final double insetLeft = getInsets().getLeft();
final double insertBottom = getInsets().getBottom();
final double contentWidth = (paneWidth - insetLeft - insetRight);
final double contentHeight = (paneHeight - insetTop - insertBottom);
// zoom
double factorX = contentWidth / groupBounds.getWidth();
double factorY = contentHeight / groupBounds.getHeight();
double factor = Math.min(factorX, factorY);
content.setScaleX(factor);
content.setScaleY(factor);
layoutInArea(content, insetLeft, insetTop, contentWidth, contentHeight,
getBaselineOffset(), HPos.CENTER, VPos.CENTER);
}
}

How can I assign an EventListener to a Path in javafx?

I have this piece of code which doesn't work correctly.
I want to set a listener for when a user clicks inside the square, yet
neither the pop-up nor the message "clicked" are displayed when I click
inside the square.
What am I missing?
This method is inside the Coords class.
public static void drawMyShape(final GraphicsContext ctx) {
Path path = new Path();
MoveTo mT = new MoveTo();
LineTo lT[] = new LineTo[4];
mT.setX(200.0);
mT.setY(200.0);
lT[0] = new LineTo(400.0, 200.0);
lT[1] = new LineTo(400.0, 400.0);
lT[2] = new LineTo(200.0, 400.0);
lT[3] = new LineTo(200.0, 200.0);
path.setStroke(Color.BEIGE);
path.getElements().addAll(mT, lT[0], lT[1], lT[2], lT[3]);
path.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
final Stage dialog = new Stage();
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.initOwner(Main.prim_stage);
VBox box = new VBox(20);
box.getChildren().add(new Text("Hey"));
Scene s = new Scene(box, 300, 200);
dialog.setScene(s);
dialog.show();
System.out.println("Clicked");
}
});
ctx.setLineWidth(4.0);
ctx.beginPath();
ctx.moveTo(mT.getX(), mT.getY());
for (int i = 0; i < lT.length; i++) {
ctx.lineTo(lT[i].getX(), lT[i].getY());
}
ctx.closePath();
ctx.stroke();
}
EDITED ON SUGGESTION by users.
So his is the main program:
public class Main extends Application {
public static Pane root;
private static Canvas main_canvas;
private static GraphicsContext ctx;
private static Rectangle2D bounds;
private static Scene scene;
public static Stage prim_stage;
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Switzerland Advertising");
initElements(primaryStage);
Coords.drawMyShape(ctx);
primaryStage.show();
System.out.println("Launched");
}
public static void main(String[] args) {
launch(args);
}
}
Everything is instanciated inside the following function, which works correctly and displays a full screen application with a canvas and a square drawn into it (image at the bottom).
private void initElements(final Stage primaryStage) {
prim_stage = primaryStage;
// ----------------------------------------
bounds = Screen.getPrimary().getVisualBounds();
double w = bounds.getWidth();
double h = bounds.getHeight();
// ----------------------------------------
// init elements of scene
root = new Pane();
main_canvas = new Canvas(w, h);
// ----------------------------------------
// init scene elements
scene = new Scene(root, w, h);
primaryStage.setX(bounds.getMinX());
primaryStage.setY(bounds.getMinY());
primaryStage.setWidth(w);
primaryStage.setHeight(h);
primaryStage.setScene(scene);
// ----------------------------------------
ctx = main_canvas.getGraphicsContext2D();
// set elements in main pane
root.getChildren().add(main_canvas);
// ----------------------------------------
}
So how can I make the pop-up window appear whenever I click inside the region drawn on the canvas?
This is the program
Your path is just a local variabl within your method. It has to be attached to the scene graph in order to get events. But when you attach it to the scene graph, drawing the same path on a canvas also does not make much sense.

How to create StackPane on the drawn rectangle area

I'm creating UI editor and for that I need to draw UI components on mouse events. I'm stuck on drawing button with caption inside of it. As a result of my searches over stackoverflow I tried to use StackPane for creating Rectangle with caption.
For layout I'm using Group element. The problem is, when I add StackPane to the Group it's being displayed on the top left corner of the Group. However, if I draw just Rectangle itself, it's being displayed on that place, where I'm releasing the mouse.
How to achieve the same effect for StackPane?
Here is my code:
public class Main extends Application {
double startingPointX, startingPointY;
Group rectanglesGroup = new Group();
Rectangle newRectangle = null;
boolean newRectangleIsBeingDrawn = false;
// the following method adjusts coordinates so that the rectangle
// is shown "in a correct way" in relation to the mouse event
void adjustRectanglePRoperties(double startingPointX,
double startingPointY, double endingPointX, double endingPointY,
Rectangle givenRectangle) {
givenRectangle.setX(startingPointX);
givenRectangle.setY(startingPointY);
givenRectangle.setWidth(endingPointX - startingPointX);
givenRectangle.setHeight(endingPointY - startingPointY);
if (givenRectangle.getWidth() < 0) {
givenRectangle.setWidth(-givenRectangle.getWidth());
givenRectangle.setX(givenRectangle.getX()
- givenRectangle.getWidth());
}
if (givenRectangle.getHeight() < 0) {
givenRectangle.setHeight(-givenRectangle.getHeight());
givenRectangle.setY(givenRectangle.getY()
- givenRectangle.getHeight());
}
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Drawing rectangles");
Scene scene = new Scene(rectanglesGroup, 800, 600);
scene.setFill(Color.BEIGE);
scene.setOnMousePressed(e -> {
if (newRectangleIsBeingDrawn == false) {
startingPointX = e.getSceneX();
startingPointY = e.getSceneY();
newRectangle = new Rectangle();
// a non finished rectangle has always the same color
newRectangle.setFill(Color.SNOW); // almost white color
//Line line = new Line(20,120,270,120);
newRectangle.setStroke(Color.BLACK);
newRectangle.setStrokeWidth(1);
newRectangle.getStrokeDashArray().addAll(3.0, 7.0, 3.0, 7.0);
rectanglesGroup.getChildren().add(newRectangle);
newRectangleIsBeingDrawn = true;
}
});
scene.setOnMouseDragged(e -> {
if (newRectangleIsBeingDrawn == true) {
double currentEndingPointX = e.getSceneX();
double currentEndingPointY = e.getSceneY();
adjustRectanglePRoperties(startingPointX, startingPointY,
currentEndingPointX, currentEndingPointY, newRectangle);
}
});
scene.setOnMouseReleased(e->{
if(newRectangleIsBeingDrawn == true){
//now the drawing of the new rectangle is finished
//let's set the final color for the rectangle
/******************Drawing textbox*******************************/
//newRectangle.setFill(Color.WHITE);
//newRectangle.getStrokeDashArray().removeAll(3.0, 7.0, 3.0, 7.0);
/****************************************************************/
/*****************Drawing button*********************************/
Image image = new Image("file:button.png");
ImagePattern buttonImagePattern = new ImagePattern(image);
newRectangle.setFill(buttonImagePattern);
newRectangle.setStroke(Color.WHITE);
newRectangle.getStrokeDashArray().removeAll(3.0,7.0,3.0,7.0);
Text text = new Text("Button");
rectanglesGroup.getChildren().remove(newRectangle);
StackPane stack = new StackPane();
stack.getChildren().addAll(newRectangle, text);
rectanglesGroup.getChildren().add(stack);
/****************************************************************/
colorIndex++; //index for the next color to use
//if all colors have been used we'll start re-using colors
//from the beginning of the array
if(colorIndex>=rectangleColors.length){
colorIndex=0;
}
newRectangle=null;
newRectangleIsBeingDrawn=false;
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I'm using OnMouseReleased event to create components.
I looked for the setX, setPosition or something like this methods, but couldn't find them in StackPane's methods.
And I don't know how translate methods work. So I didn't try them to achieve my goal.
You should read the documentation about a JavaFX Node.
You can position the nodes absolutely via setLayoutX (and Y) or relative via setTranslateX (and Y), which adds to the current layout position.
A StackPane is just a container and in your case no different to any other Node you want to place on your Scene. Just create it, set the dimensions and location and put it on the Scene.
Your code doesn't work, so I created my own. Here's example code about how to approach this matter:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class RubberBandSelectionDemo extends Application {
CheckBox drawButtonCheckBox;
public static void main(String[] args) {
launch(args);
}
Pane root;
#Override
public void start(Stage primaryStage) {
root = new Pane();
root.setStyle("-fx-background-color:white");
root.setPrefSize(1024, 768);
drawButtonCheckBox = new CheckBox( "Draw Button");
root.getChildren().add( drawButtonCheckBox);
primaryStage.setScene(new Scene(root, root.getWidth(), root.getHeight()));
primaryStage.show();
new RubberBandSelection(root);
}
public class RubberBandSelection {
final DragContext dragContext = new DragContext();
Rectangle rect;
Pane group;
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) {
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);
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// get coordinates
double x = rect.getX();
double y = rect.getY();
double w = rect.getWidth();
double h = rect.getHeight();
if( drawButtonCheckBox.isSelected()) {
// create button
Button node = new Button();
node.setDefaultButton(false);
node.setPrefSize(w, h);
node.setText("Button");
node.setLayoutX(x);
node.setLayoutY(y);
root.getChildren().add( node);
} else {
// create rectangle
Rectangle node = new Rectangle( 0, 0, w, h);
node.setStroke( Color.BLACK);
node.setFill( Color.BLACK.deriveColor(0, 0, 0, 0.3));
node.setLayoutX( x);
node.setLayoutY( y);
root.getChildren().add( node);
}
// remove rubberband
rect.setX(0);
rect.setY(0);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().remove( rect);
}
};
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());
}
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}
}
And here's an image:
The demo shows a rubberband selection which allows you to draw a selection rectangle. Upon release of the mouse button either a rectangle or a button is drawn, depending on the "Draw Button" checkbox selection in the top left corner. If you'd like to draw a StackPane, just change the code accordingly in the mouse released handler.
And of course, if you want to draw the components directly instead of the rubberband, just exchange the Rectangle in the rubberband selection code with e. g. a Button. Here's the Button drawing code only, just replace it in the above example.
public class RubberBandSelection {
final DragContext dragContext = new DragContext();
Button button;
Pane group;
public RubberBandSelection( Pane group) {
this.group = group;
button = new Button();
button.setPrefSize(0, 0);
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) {
dragContext.mouseAnchorX = event.getSceneX();
dragContext.mouseAnchorY = event.getSceneY();
button.setLayoutX(dragContext.mouseAnchorX);
button.setLayoutY(dragContext.mouseAnchorY);
button.setPrefWidth(0);
button.setPrefHeight(0);
group.getChildren().add( button);
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// get coordinates
double x = button.getLayoutX();
double y = button.getLayoutY();
double w = button.getWidth();
double h = button.getHeight();
// create button
Button node = new Button();
node.setDefaultButton(false);
node.setPrefSize(w, h);
node.setText("Button");
node.setLayoutX(x);
node.setLayoutY(y);
root.getChildren().add( node);
// remove rubberband
button.setLayoutX(0);
button.setLayoutY(0);
button.setPrefWidth(0);
button.setPrefHeight(0);
group.getChildren().remove( button);
}
};
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)
button.setPrefWidth( offsetX);
else {
button.setLayoutX(event.getSceneX());
button.setPrefWidth(dragContext.mouseAnchorX - button.getLayoutX());
}
if( offsetY > 0) {
button.setPrefHeight( offsetY);
} else {
button.setLayoutY(event.getSceneY());
button.setPrefHeight(dragContext.mouseAnchorY - button.getLayoutY());
}
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}

Create a path transition with absolute coordinates for a StackPane object

OrangeBlock is an orange block with text inside. It is implemented as a StackPane that contains text on top of a rectangle. (This approach is demonstrated in the documentation for StackPane.)
I've placed an OrangeBlock at coordinates (100, 80) and am now trying to make it travel smoothly to some target coordinates. Unfortunately I get a nasty bump in my path:
For some reason the coordinates in the PathElements are interpreted relative to the orange block.
Why is this? And how can I make my OrangeBlock travel along a path with absolute coordinates? Minimal working example below.
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PathTransitionExample extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
PathTransition transition = newPathTransitionTo(block, 460, 320);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block,
double toX, double toY) {
double fromX = block.getLayoutX();
double fromY = block.getLayoutY();
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
}
I debugged the JavaFX code out of curiosity. Seems like you are out of luck with a proper solution. Here's what happens:
The PathTransition code has a method interpolate(double frac) which includes:
cachedNode.setTranslateX(x - cachedNode.impl_getPivotX());
cachedNode.setTranslateY(y - cachedNode.impl_getPivotY());
The impl_getPivotX() and impl_getPivotY() methods contain this:
public final double impl_getPivotX() {
final Bounds bounds = getLayoutBounds();
return bounds.getMinX() + bounds.getWidth()/2;
}
public final double impl_getPivotY() {
final Bounds bounds = getLayoutBounds();
return bounds.getMinY() + bounds.getHeight()/2;
}
So the PathTransition always uses the center of your node for the calculation. In other words this works with e. g. a Circle node, but not with e. g. a Rectangle node. Moreover you need the layoutBounds, so the PathTransition must be created after the bounds were made available.
You can see in the PathTransition code that the calculations are all relative and already involve the layout position. So in your lineTo you have to consider this.
Worth noting is that the LineTo class has a method setAbsolut(boolean). However it doesn't solve your problem.
So my solution to your problem would be
creating the PathTransition after the primary stage was made visible
modification of the moveTo and lineTo parameters
This works for me (I added a Rectangle shape to identify the proper bounds visually):
public class PathTransitionExampleWorking2 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
Rectangle rect = new Rectangle( 100, 80, 460-100+60, 320-80+40);
root.getChildren().add(rect);
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate( 100, 80);
root.getChildren().add(block);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
// layout bounds are used in path transition => PathTransition creation must happen when they are available
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {
double fromX = block.getLayoutBounds().getWidth() / 2;
double fromY = block.getLayoutBounds().getHeight() / 2;
toX -= block.getLayoutX() - block.getLayoutBounds().getWidth() / 2;
toY -= block.getLayoutY() - block.getLayoutBounds().getHeight() / 2;
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
}
edit: another solution would be to use this instead of MoveTo and LineTo:
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node) {
super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
Note: You still have to create the PathTransition after the primaryStage was created.
edit: here's another example with the block moving to the position of the mouse-click:
public class PathTransitionExample extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
Label label = new Label( "Click on scene to set destination");
label.relocate(0, 0);
root.getChildren().add(label);
Scene scene = new Scene(root, 600, 400);
scene.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<Event>() {
PathTransition transition;
{
transition = new PathTransition();
transition.setNode(block);
transition.setDuration(Duration.seconds(2));
}
#Override
public void handle(Event event) {
transition.stop();
setPositionFixed(block.getLayoutX() + block.getTranslateX(), block.getLayoutY() + block.getTranslateY());
double x = ((MouseEvent) event).getX();
double y = ((MouseEvent) event).getY();
Path path = new Path();
path.getElements().add(new MoveToAbs( block));
path.getElements().add(new LineToAbs( block, x, y));
transition.setPath(path);
transition.play();
}
private void setPositionFixed( double x, double y) {
block.relocate(x, y);
block.setTranslateX(0);
block.setTranslateY(0);
}
});
primaryStage.setScene( scene);
primaryStage.show();
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {
Path path = new Path();
path.getElements().add(new MoveToAbs( block));
path.getElements().add(new LineToAbs( block, toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node) {
super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
}
The solution I'm using now is to simply offset layoutX and layoutY of the Path in the opposite direction.
private static void offsetPathForAbsoluteCoords(Path path, OrangeBlock block) {
Node rectangle = block.getChildren().iterator().next();
double width = rectangle.getLayoutBounds().getWidth();
double height = rectangle.getLayoutBounds().getHeight();
path.setLayoutX(-block.getLayoutX() + width / 2);
path.setLayoutY(-block.getLayoutY() + height / 2);
}
Inserting a call to this method immediately after the Path instantiation fixes the problem.
I'm not really satisfied with this solution. I don't understand why layoutX and layoutY need to be involved at all. Is there a neater way?
So Basically,
the relocate(x,y) method sets the layout x/y values...
the Transition uses the translateX/Y values...
I could be wrong but I believe that when either value gets invalidated the scene runs a "layout" pass on the nodes in scene.
When this happens it tries to set the node to it's known layoutX/Y values, which are set when you call relocate(x,y) (layout values are 0,0 by default).
This causes the node to be drawn in "layoutPosition" then "pathPosition" within the transition at each step, causing jitters and the node to be offset from where it should be.
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
block.relocate(100, 80);
//block.setTranslateX(100);
//block.setTranslateY(80);
System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
root.getChildren().add(block);
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.currentTimeProperty().addListener(e->{
System.out.println("\nLayout Values: " + block.getLayoutX() + " : " + block.getLayoutY()
+"\nTranslate Values:" + block.getTranslateX() + " : " + block.getTranslateY()
);});
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block,
double toX, double toY) {
double fromX = block.getLayoutX();//getTranslateX();
double fromY = block.getLayoutY();//getTranslateY();
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
Not going to post pics, But my first result was like your first post, changing it to the above code gave me the second post result.
Usually it is always best to avoid "layout" values on a dynamic (moving) object for these reasons. If you like the convenience of the relocate method, I'd implement your own setting the translate values instead.
Cheers :)
EDIT:
I edited some code to print what happens as the transition is running so you can see what happens in your original version..
Both #jdub1581 and #Lorand have given valid points:
Transition is applied modifying block's translateXProperty() and translateYProperty().
Transition is applied on the center of the block.
I'll add one more thing:
We are mixing two different things: the global path we want the block to follow, and the local path we have to apply to the transition, so the block follows the first one.
Let's add a pathScene to the group:
Path pathScene = new Path();
pathScene.getElements().add(new MoveTo( block.getLayoutX(), block.getLayoutY()));
pathScene.getElements().add(new LineTo(460, 320));
root.getChildren().add(pathScene);
This will be our scene now (I've added two labels with the coordinates of the origin and end of the path for clarity):
Now we need to determine the local path, so we'll change pathScene elements to local coordinates of the block, and translate it to its center:
Path pathLocal = new Path();
pathScene.getElements().forEach(elem->{
if(elem instanceof MoveTo){
Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
} else if(elem instanceof LineTo){
Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
}
});
As #Lorand also mentioned, this should be done after the stage is shown to compute the size of the block.
Now we can create the transition and play it.
PathTransition transition = new PathTransition();
transition.setPath(pathLocal);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
transition.play();
Finally, this is all the code we need to make the block follow the desired path:
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
// Path in scene coordinates, added to group
// in order to visualize the transition path for the block to follow
Path pathScene = new Path();
pathScene.getElements().add(new MoveTo(block.getLayoutX(), block.getLayoutY()));
pathScene.getElements().add(new LineTo(460, 320));
root.getChildren().add(pathScene);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
PathTransition transition = newPathTransitionTo(pathScene, block);
transition.play();
}
private PathTransition newPathTransitionTo(Path pathScene, OrangeBlock block) {
// Calculate the path in local coordinates of the block
// so transition is applied to the block without bumps
Path pathLocal = new Path();
pathScene.getElements().forEach(elem->{
if(elem instanceof MoveTo){
Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
} else if(elem instanceof LineTo){
Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
}
});
PathTransition transition = new PathTransition();
transition.setPath(pathLocal);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
Note that this solution is equivalent to the one given by #Lorand.
If we monitorize the X, Y translate properties of the block, these go from (0,0) to (360,240), which are just the relative ones on the global path.
You're using relocate function to locate your Block. relocate function makes computation on x and y for locating your object. If you used setLayoutX to locate Block and then use getLayoutX, this problem might not be happened. Same explanations is valid for y property.
You can find some informations about your problem in here: http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#layoutXProperty

JavaFX correct scaling

I want to scale all nodes in a Pane on a scroll event.
What I have tried so far:
When I do scaleX or scaleY, border of pane
scales respectively (seen when set Pane style -fx-border-color: black;). So not every event would start if I'm not from the borders
of pane, so I need it all.
Next step I tried to scale each node and it turned out really bad,
something like this - (lines stretched through the points). Or if
scrolling in other side, it would be less
Another method I tried was to scale points of Node. It's better, but
I don't like it. It looks like
point.setScaleX(point.getScaleX()+scaleX) and for y and other nodes
appropriately.
I created a sample app to demonstrate one approach to performing scaling of a node in a viewport on a scroll event (e.g. scroll in and out by rolling the mouse wheel).
The key logic to the sample for scaling a group placed within a StackPane:
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor =
(event.getDeltaY() > 0)
? SCALE_DELTA
: 1/SCALE_DELTA;
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
}
});
The scroll event handler is set on the enclosing StackPane which is a resizable pane so it expands to fill any empty space, keeping the zoomed content centered in the pane. If you move the mouse wheel anywhere inside the StackPane it will zoom in or out the enclosed group of nodes.
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
public static void main(String[] args) { launch(args); }
#Override public void start(final Stage stage) {
final Group group = new Group(
createStar(),
createCurve()
);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(
createMenuBar(stage, group),
zoomPane
);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(
layout
);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor =
(event.getDeltaY() > 0)
? SCALE_DELTA
: 1/SCALE_DELTA;
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
}
});
zoomPane.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) {
zoomPane.setClip(new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()));
}
});
return zoomPane;
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent(
"M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120"
);
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent(
"M100,10 L100,10 40,180 190,60 10,60 160,180 z"
);
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(
exitMenuItem
);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1/1.5);
group.setScaleY(group.getScaleY() * 1/1.5);
}
});
zoomMenu.getItems().setAll(
zoomResetMenuItem,
zoomInMenuItem,
zoomOutMenuItem
);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(
fileMenu,
zoomMenu
);
return menuBar;
}
// icons source from: http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =? http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
Update for a zoomed node in a ScrollPane
The above implementation works well as far as it goes, but it is useful to be able to place the zoomed node inside a scroll pane, so that when you zoom in making the zoomed node larger than your available viewport, you can still pan around the zoomed node within the scroll pane to view parts of the node.
I found achieving the behavior of zooming in a scroll pane difficult, so I asked for help on an Oracle JavaFX Forum thread.
Oracle JavaFX forum user James_D came up with the following solution which solves the zooming within a ScrollPane problem quite well.
His comments and code were as below:
A couple of minor changes first: I wrapped the StackPane in a Group so that the ScrollPane would be aware of the changes to the transforms, as per the ScrollPane Javadocs. And then I bound the minimum size of the StackPane to the viewport size (keeping the content centered when smaller than the viewport).
Initially I thought I should use a Scale transform to zoom around the displayed center (i.e. the point on the content that is at the center of the viewport). But I found I still needed to fix the scroll position afterwards to keep the same displayed center, so I abandoned that and reverted to using setScaleX() and setScaleY().
The trick is to fix the scroll position after scaling. I computed the scroll offset in local coordinates of the scroll content, and then computed the new scroll values needed after the scale. This was a little tricky. The basic observation is that
(hValue-hMin)/(hMax-hMin) = x / (contentWidth - viewportWidth), where x is the horizontal offset of the left edge of the viewport from the left edge of the content.
Then you have centerX = x + viewportWidth/2.
After scaling, the x coordinate of the old centerX is now centerX*scaleFactor. So we just have to set the new hValue to make that the new center. There's a bit of algebra to figure that out.
After that, panning by dragging was pretty easy :).
A corresponding feature request to add high level APIs to support zooming and scaling functionality in a ScrollPane is Add scaleContent functionality to ScrollPane. Vote for or comment on the feature request if you would like to see it implemented.
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Group group = new Group(createStar(), createCurve());
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
}
});
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
}
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
}
});
scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
}
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2 ;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2 ;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
}
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu);
return menuBar;
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
The answer from jewelsea has one issue, if the size of original content in the zoomPane is already larger than View Port. Then the following code will not work.
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
The result is when we zoom out, the content is not centered any more.
To resolve this issue, you need to create another StackPane in between the zoomPane and ScrollPane.
// Create a zoom pane for zoom in/out
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final Group zoomContent = new Group(zoomPane);
// Create a pane for holding the content, when the content is smaller than the view port,
// it will stay the view port size, make sure the content is centered
final StackPane canvasPane = new StackPane();
canvasPane.getChildren().add(zoomContent);
final Group scrollContent = new Group(canvasPane);
// Scroll pane for scrolling
scroller = new ScrollPane();
scroller.setContent(scrollContent);
And in the viewportBoundsProperty listener, Change zoomPane to canvasPane
// Set the minimum canvas size
canvasPane.setMinSize(newValue.getWidth(), newValue.getHeight());
JavaFx is too complicated for zoom in/out. To achieve the same effect, WPF is much easier.

Resources