What is the difference between a Pane and a Group? - javafx

In JavaFX, what is the difference between a Pane and a Group? I can't make out any difference.

A Group is not resizable (meaning that its size is not managed by its parent in the scene graph), and takes on the union of the bounds of its child nodes. (So, in other words, the local bounds of a Group will be the smallest rectangle containing the bounds of all the child nodes). If it is larger than the space it is allocated in its parent, it will be clipped.
By contrast, a Pane is resizable, so its size is set by its parent, which essentially determine its bounds.
Here is a quick demo. The Group is on top and the Pane below. Both contain a fixed blue square at (100,100) and a green square which is moved by pressing the left/right arrow keys. Note how at the beginning, the blue square appears in the top left corner of the group, because the local bounds of the group start at the top-leftmost point of all its child nodes (i.e. the local bounds of the group extend from (100, 100) right and down). As you move the green rectangles "off screen", the group adjusts its bounds to incorporate the changes, wherever possible, whereas the pane remains fixed.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GroupVsPaneDemo extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Group group = new Group();
VBox.setVgrow(group, Priority.NEVER);
VBox.setVgrow(pane, Priority.NEVER);
VBox vbox = new VBox(group, pane);
Rectangle rect1 = new Rectangle(100, 100, 100, 100);
Rectangle rect2 = new Rectangle(100, 100, 100, 100);
Rectangle rect3 = new Rectangle(200, 200, 100, 100);
Rectangle rect4 = new Rectangle(200, 200, 100, 100);
rect1.setFill(Color.BLUE);
rect2.setFill(Color.BLUE);
rect3.setFill(Color.GREEN);
rect4.setFill(Color.GREEN);
group.getChildren().addAll(rect1, rect3);
pane.getChildren().addAll(rect2, rect4);
Scene scene = new Scene(vbox, 800, 800);
scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
double deltaX ;
switch(e.getCode()) {
case LEFT:
deltaX = -10 ;
break ;
case RIGHT:
deltaX = 10 ;
break ;
default:
deltaX = 0 ;
}
rect3.setX(rect3.getX() + deltaX);
rect4.setX(rect4.getX() + deltaX);
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

The few important difference between Pane and Group is that :
Pane can have its own size, where as a Group will take on the collective bounds of its children and is not directly resizable.
Pane can be used when you want to position its nodes at absolute position.

Also, note that Group was designed to be very lightweight and doesn't support a lot of styles. For example, you can't set border or background color for the group.
See this answer for more details.

Related

JavaFX AnchorPane maxHeight ignored with clipped Circle

I have an AnchorPane which contains a clipped circle. I set a maximum height to the anchorpane, so that if the circle's y position is high, the circle won't be displayed. The problem is that when the circle goes to the lower part of the anchorpane, it increases it's height. This should not be happening.
This happens even before the clipped element reaches the lower part of the anchorpane. Once the "invisible" part of the circle reaches the lower part, it starts increasing it's height.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Init extends Application {
private AnchorPane canvasContainer;
private AnchorPane mainPane;
#Override
public void start(Stage stage) throws Exception {
canvasContainer = new AnchorPane();
mainPane = new AnchorPane();
Scene scene = new Scene(mainPane, 800, 700);
stage.setScene(scene);
canvasContainer.setPrefWidth(600.0d);
canvasContainer.setPrefHeight(500.0d);
//IGNORED
canvasContainer.setMaxHeight(canvasContainer.getPrefHeight());
canvasContainer.setLayoutX(14.0d);
canvasContainer.setLayoutY(14.0d);
canvasContainer.setStyle("-fx-border-color: black; -fx-border-width: 1 1 1 1;");
RadialGradient gradient = new RadialGradient(0, 0, 0.5, 0.5, 1, true, CycleMethod.NO_CYCLE, new Stop[] {
new Stop(0, Color.ORANGE),
new Stop(0.2, Color.YELLOW),
new Stop(0.5, Color.TRANSPARENT)
});
//I AM MODIFYING THIS VALUE
int y = 500;
Circle circleGradient = new Circle(200, y, 50);
circleGradient.setFill(gradient);
Rectangle rect = new Rectangle(200 - 50, y - 50, 1000, 50/2);
circleGradient.setClip(rect);
canvasContainer.getChildren().addAll(circleGradient);
mainPane.getChildren().add(canvasContainer);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I had the same issue when I was trying to make a circle grow so it would fill a rectangle which was a small area of my Scene. The filling animation worked great but the maxHeight and maxWidth of your canvasContainer are ignored. In my case that ended up with the area growing and destroying everything nearby.
Solution : add the circle to the mainPane and not canvasContainer

JavaFX | changing Line anchor?

I tried drawing a line and changing its end coordinate when dragging.
The problem is, it changes both of the lines' ends, with respect to the middle, supposedly the anchor.
Is there a way to move the anchor to the start of the line?
My code is:
Line path = new Line(30,30, 70 , 75);
path.setStrokeWidth(5);
Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e ->{
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
Group shapes = new Group();
shapes.getChildren().addAll(path, point);
BorderPane root = new BorderPane(shapes);
Scene scene = new Scene(root,600,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
the result:
The ideal is that the pivot point will be at the start of the line, and not at the middle.
What you're seeing is not the line growing or shrinking in both directions; instead, what's happening is, as the line changes length, the BorderPane repositions it in order to keep it centered. The same thing is happening with the "rotation". When you move an end in such a way as to change the angle of the line, the bounding box changes in a way that, when the BorderPane repositions the line, causes the other end to move in the opposite direction.
More specifically, the BorderPane is repositioning the Group—effectively the same thing since Group doesn't position its children. One fix for this is to make the Group unmanaged. This will stop the BorderPane from repositioning the Group as its bounds change. Note, however, that this will also stop the Group from contributing to the size and layout calculations of the BorderPane.
Here's an example:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
private static void installDragHandlers(Circle circle) {
circle.setOnMousePressed(event -> {
Point2D offset = new Point2D(
event.getX() - circle.getCenterX(),
event.getY() - circle.getCenterY()
);
circle.setUserData(offset);
event.consume();
});
circle.setOnMouseDragged(event -> {
Point2D offset = (Point2D) circle.getUserData();
circle.setCenterX(event.getX() - offset.getX());
circle.setCenterY(event.getY() - offset.getY());
event.consume();
});
circle.setOnMouseReleased(event -> {
circle.setUserData(null);
event.consume();
});
}
#Override
public void start(Stage primaryStage) {
Line line = new Line(200, 200, 400, 200);
line.setStrokeWidth(2);
Circle start = new Circle(5, Color.GREEN);
start.centerXProperty().bindBidirectional(line.startXProperty());
start.centerYProperty().bindBidirectional(line.startYProperty());
installDragHandlers(start);
Circle end = new Circle(5, Color.RED);
end.centerXProperty().bindBidirectional(line.endXProperty());
end.centerYProperty().bindBidirectional(line.endYProperty());
installDragHandlers(end);
Group group = new Group(line, start, end);
group.setManaged(false);
primaryStage.setScene(new Scene(new BorderPane(group), 600, 400));
primaryStage.setTitle("SO-55196882");
primaryStage.show();
}
}
The Line is initialized with hard coded start and end points so that it is initially centered in the scene (whose initial dimensions are also hard coded).
Put shapes in group.
public void start(final Stage primaryStage) throws AWTException {
final Line path = new Line(30, 30, 70, 75);
path.setStrokeWidth(5);
final Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e -> {
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
final Group root = new Group(path, point);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}

JavaFX ScrollPane and Scaling of the Content

I would like to show a photo as an ImageView in a ScrollPane with an ZoomIn and ZoomOut Function. But if I reduce by means of scale the imageview, an undesirable empty edge is created in the ScrollPane. How can you make sure that the ScrollPane is always the size of the scaled ImageView?
See the following example. For simplicity, I replaced the ImageView with a rectangle.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ScrollPaneDemo extends Application {
double scale;
Pane contPane = new Pane();
#Override
public void start(Stage primaryStage) {
BorderPane pane = new BorderPane();
ScrollPane sp = new ScrollPane();
sp.setContent(contPane);
sp.setVvalue(0.5);
sp.setHvalue(0.5);
Rectangle rec = new Rectangle(2820, 1240,Color.RED);
scale = 0.2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
contPane.getChildren().add(rec);
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
scale*=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
scale/=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
contPane scaled by using transform don't change its layoutBounds automatically. If you want not to make empty space in contPane, you'd better wrap the node in Group.
See this post. Layout using the transformed bounds
sp.setContent(new Group(contPane));
In addition, if you don't want to make empty space in ScrollPane, limit minimum scale to rate which width or height of the content fits viewport's one.
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
updateScale(scale * 2.0d);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
updateScale(scale / 2.0d);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
updateScale(0.2d);
private void updateScale(double newScale) {
scale = Math.max(newScale, Math.max(sp.getViewportBounds().getWidth() / rec.getWidth(), sp.getViewportBounds().getHeight() / rec.getHeight()));
contPane.setScaleX(scale);
contPane.setScaleY(scale);
}
Consider a case of the image is smaller than ScrollPane's viewport. Because for showing no empty space, this code will stretch contents when it doesn't have enough size.
In a case of huge images, TravisF's comment helps you.

How can I rotate a label

In JavaFX 8 I would like to specify the css to rotate a Label so that instead of the text going from left to right, it goes from bottom to top.
How can I do that?
Any node can have it's rotation styled via CSS using the -fx-rotate css attribute.
This is the angle of the rotation in degrees. Zero degrees is at 3 o'clock (directly to the right). Angle values are positive clockwise. Rotation is about the center.
So in your code or FXML you can have:
label.setStyle("vertical");
And in your css stylesheet you can define:
.vertical { -fx-rotate: -90; }
Also note James_D's answer suggestion of wrapping the label in a Group to account for the rotation when performing layout bounds calculations.
Call setRotate on the label to rotate it about its center.
To allow layout panes to properly measure the bounds of the label after rotation, wrap it in a Group:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class RotatedLabelTest extends Application {
#Override
public void start(Stage primaryStage) {
Label label1 = new Label("Hello");
Label label2 = new Label("World");
label1.setRotate(-90);
Group labelHolder = new Group(label1);
HBox root = new HBox(5, labelHolder, label2);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 250, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can easily set a label's rotation by specifying the degrees of rotation as per the code below:
label.setRotate(45)
for 45 degrees, for example.
if you want to set it after some operations, that is, after the label has been displayed in the User Interface, you can use
Platform.runLater() lambda expression.
i.e
Platform.runLater(()->
label.setRotate(45);
)

adding path transition to Group in JavaFX

I'm writing some javaFX code and I have added some Paths to a Group, this Group has been added to another Group which has been added to the root and is displaying on the screen. Now at a certain point I want to use a PathTransition to animate this lowest level group to a new location. I'm having trouble working out the correct coordinates for the transition. I have read that the PathTransition will animate the node from its center and not the upper left hand corner so I have tried adding
Group.getLayoutX()/2 to the starting x and Group.getLayoutY()/2 to the starting y coord but it still seems to make the group jump to a new starting location before the animation begins.
The final destination seems a bit off as well.
Is there a better way to animate a Group that contains several Paths?
Adjust the layoutX and layoutY of the nodes following paths based upon half the layout bounds of the nodes being animated.
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
Try the modification of Uluk's circle path transition code from JavaFX 2 circle path for animation.
The code will have the upper left corner of the nodes original layout follow the path.
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ArcToDemo extends Application {
private PathTransition pathTransitionEllipse;
private PathTransition pathTransitionCircle;
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, 600, 460));
// Ellipse path example
Rectangle rect = new Rectangle(0, 0, 40, 40);
rect.setArcHeight(10);
rect.setArcWidth(10);
rect.setFill(Color.ORANGE);
root.getChildren().add(rect);
Path path = createEllipsePath(200, 200, 50, 100, 45);
root.getChildren().add(path);
pathTransitionEllipse = PathTransitionBuilder.create()
.duration(Duration.seconds(4))
.path(path)
.node(rect)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
// Circle path example
Rectangle rect2 = new Rectangle(0, 0, 20, 20);
rect2.setArcHeight(10);
rect2.setArcWidth(10);
rect2.setFill(Color.GREEN);
root.getChildren().add(rect2);
Path path2 = createEllipsePath(400, 200, 150, 150, 0);
root.getChildren().add(path2);
pathTransitionCircle = PathTransitionBuilder.create()
.duration(Duration.seconds(2))
.path(path2)
.node(rect2)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect2.setLayoutX(rect2.getLayoutX() + rect2.getLayoutBounds().getWidth() / 2);
rect2.setLayoutY(rect2.getLayoutY() + rect2.getLayoutBounds().getHeight() / 2);
}
private Path createEllipsePath(double centerX, double centerY, double radiusX, double radiusY, double rotate) {
ArcTo arcTo = new ArcTo();
arcTo.setX(centerX - radiusX + 1); // to simulate a full 360 degree celcius circle.
arcTo.setY(centerY - radiusY);
arcTo.setSweepFlag(false);
arcTo.setLargeArcFlag(true);
arcTo.setRadiusX(radiusX);
arcTo.setRadiusY(radiusY);
arcTo.setXAxisRotation(rotate);
Path path = PathBuilder.create()
.elements(
new MoveTo(centerX - radiusX, centerY - radiusY),
arcTo,
new ClosePath()) // close 1 px gap.
.build();
path.setStroke(Color.DODGERBLUE);
path.getStrokeDashArray().setAll(5d, 5d);
return path;
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
pathTransitionEllipse.play();
pathTransitionCircle.play();
}
public static void main(String[] args) {
launch(args);
}
}
Implementation Notes
The code assumes that the original layout position of the nodes being animated is 0,0 (if it is not then you will need to adjust accordingly).
For non-square nodes, there is a slight gap between the node and the path (because the calculations are based on the layout bounds rectangle and not the visible shape of the node).
Also the path transition is using an orientation of NONE rather than ORTHOGONAL_TO_TANGENT (otherwise the layout calculations give a strange effect as the node starts to rotate and it seems as though the node is traveling divergent to the path).
Instead of the above approach, you could define a custom subclass of Transition which modifies the TranslateX and TranslateY properties of the node so that the corner of the node follows the path rather than the center and no modifications to the node's layout are required, however that would be significantly more work.

Resources