AutoScalePane in JavaFX with layoutChildren() - javafx

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

Related

JavaFX: Deferred coordinate computation

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.

How to put marks in a scrollbar

I'm implementing a search feature and I would like to highlight the positions of the matches in the scrollbar of my table view.
Is there any way to show color marks in a scrollbar in JavaFX?
If you get access to the ScrollBar after it has been layouted for the first time, you can add the marks to the track:
public class ScrollBarMark {
private final Rectangle rect;
private final DoubleProperty position = new SimpleDoubleProperty();
public ScrollBarMark() {
rect = new Rectangle(5, 5, Color.RED.deriveColor(0, 1, 1, 0.5));
rect.setManaged(false);
}
public void attach(ScrollBar scrollBar) {
StackPane sp = (StackPane) scrollBar.lookup(".track");
rect.widthProperty().bind(sp.widthProperty());
sp.getChildren().add(rect);
rect.layoutYProperty().bind(Bindings.createDoubleBinding(() -> {
double height = sp.getLayoutBounds().getHeight();
double visibleAmout = scrollBar.getVisibleAmount();
double max = scrollBar.getMax();
double min = scrollBar.getMin();
double pos = position.get();
double delta = max - min;
height *= 1 - visibleAmout / delta;
return height * (pos - min) / delta;
},
position,
sp.layoutBoundsProperty(),
scrollBar.visibleAmountProperty(),
scrollBar.minProperty(),
scrollBar.maxProperty()));
}
public final double getPosition() {
return this.position.get();
}
public final void setPosition(double value) {
this.position.set(value);
}
public final DoubleProperty positionProperty() {
return this.position;
}
public void detach() {
StackPane parent = (StackPane) rect.getParent();
if (parent != null) {
parent.getChildren().remove(rect);
rect.layoutYProperty().unbind();
rect.widthProperty().unbind();
}
}
}
Right now this only works with vertical ScrollBars.
#Override
public void start(Stage primaryStage) {
ScrollBar scrollBar = new ScrollBar();
scrollBar.setOrientation(Orientation.VERTICAL);
scrollBar.setMax(100);
scrollBar.setVisibleAmount(50);
scrollBar.valueProperty().addListener((a,b,c) -> System.out.println(c));
StackPane root = new StackPane();
root.getChildren().add(scrollBar);
Scene scene = new Scene(root, 200, 500);
// do layout
root.applyCss();
root.layout();
ScrollBarMark mark1 = new ScrollBarMark();
ScrollBarMark mark2 = new ScrollBarMark();
mark1.attach(scrollBar);
mark2.attach(scrollBar);
mark1.setPosition(50);
mark2.setPosition(75);
primaryStage.setScene(scene);
primaryStage.show();
}

Scaling Group of Text and ImageView inside StackPane

I need to get image and text above. I create StackPane and put ImageView and Group with Text elements inside. I want to resize my StackPane and keep ImageView proportions and position of Group of Texts over this ImageView. Code below works, but when i resize StackPane i cant fix Group of Texts position.
Small window. Top of text on the top of image
Big window. Top of text lower than top of image
public class SlideFromText extends Slide {
private StackPane sp = new StackPane();
private ImageView iv = new ImageView();
private Group text_group = new Group();
public SlideFromText(Image img, String text_string) {
iv.setImage(img);
iv.setPreserveRatio(true);
iv.fitWidthProperty().bind(sp.widthProperty());
iv.fitHeightProperty().bind(sp.heightProperty());
sp.setAlignment(Pos.TOP_CENTER);
sp.setMinSize(0, 0);
sp.getChildren().add(iv);
String lines[] = text_string.split("\\r?\\n");
double height = 0;
double maxWidth = 0;
String font_name = (String) Options.getOption("font");
Font f = new Font(font_name, 20);
ArrayList<Text> text_array = new ArrayList<>();
for (String line : lines) {
Text text = new Text(line);
text.setFont(f);
text.setFill(Color.WHITE);
text.setTextOrigin(VPos.BASELINE);
text.setTextAlignment(TextAlignment.CENTER);
DropShadow sh = new DropShadow(3, Color.BLACK);
sh.setSpread(0.25);
text.setEffect(sh);
height += text.getBoundsInParent().getHeight();
text.setY(height);
text.setX(sh.getRadius() / 2);
maxWidth = Math.max(maxWidth, text.getBoundsInParent().getWidth());
text_array.add(text);
}
for (Text t : text_array) {
t.setWrappingWidth(maxWidth);
}
text_group.getChildren().addAll(text_array);
sp.getChildren().add(text_group);
iv.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
Node node = sp.getChildren().get(1);
if (node.getClass() != Group.class) {
return;
}
Group group_node = (Group) node;
group_node.setLayoutX(-1.0 * group_node.layoutBoundsProperty().get().getWidth() / 2.0);
group_node.getTransforms().clear();
double scale_x = (iv.getBoundsInParent().getWidth() * 0.95) / group_node.getBoundsInParent().getWidth();
double scale_y = (iv.getBoundsInParent().getHeight() * 0.95) / group_node.getBoundsInParent().getHeight();
double scale_factor = Math.min(scale_x, scale_y);
double pivot_x = (group_node.getBoundsInLocal().getMaxX() - group_node.getBoundsInLocal().getMinX()) / 2;
Scale scale = new Scale(scale_factor, scale_factor, pivot_x, 0);
group_node.getTransforms().add(scale);
}
});
}
#Override
public Pane getPane() {
return sp;
}
}

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