Scaling Group of Text and ImageView inside StackPane - javafx

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

Related

How to make an pane stay over line connecting draggablenode in javafx

I am designing a UI of a graph structure with draggable nodes. In the graph I have a component called relation(it is a pane) which shows the link between two nodes.
I want relation to stay and move along with line at mid of line.
Current UI design is as shown below
And the expected one is like:
You need to refresh the position of the node when the line's end coordinates are modified. To avoid triggering the calculation multiple times per layout pass, I recommend doing this from the layoutChildren method of the parent, but you could also do this from a listener to the startX, endY, ... properties. This will lead to some unnecessary computations though.
As for calcualting the position of the node: The center of the node needs to align with the midpoint of the line, so you need to solve the following equation for markTopLeft:
markTopLeft + (markWidth, markHeight) / 2 = (lineStart + lineEnd) / 2
markTopLeft = (lineStart + lineEnd - (markWidth, markHeight)) / 2
Example
Pane allowing for custom layout calculations
public class PostProcessPane extends Pane {
private final Set<Node> modifiedChildren = new HashSet<>();
private final Set<Node> modifiedChildrenUnmodifiable = Collections.unmodifiableSet(modifiedChildren);
private final List<Consumer<Set<Node>>> postProcessors = new ArrayList<>();
public List<Consumer<Set<Node>>> getPostProcessors() {
return postProcessors;
}
private final ChangeListener listener = (o, oldValue, newValue) -> modifiedChildren.add((Node) ((ReadOnlyProperty) o).getBean());
private void initListener() {
getChildren().addListener((ListChangeListener.Change<? extends Node> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
for (Node n : c.getRemoved()) {
n.boundsInParentProperty().removeListener(listener);
}
}
if (c.wasAdded()) {
for (Node n : c.getAddedSubList()) {
n.boundsInParentProperty().addListener(listener);
}
}
}
});
}
public PostProcessPane() {
initListener();
}
public PostProcessPane(Node... children) {
super(children);
initListener();
for (Node n : children) {
n.boundsInParentProperty().addListener(listener);
}
}
#Override
protected void layoutChildren() {
super.layoutChildren();
if (!modifiedChildren.isEmpty()) {
for (Consumer<Set<Node>> processor : postProcessors) {
processor.accept(modifiedChildrenUnmodifiable);
}
modifiedChildren.clear();
}
}
}
Usage
#Override
public void start(Stage primaryStage) throws Exception {
Rectangle r1 = new Rectangle(200, 50, Color.BLUE);
Rectangle r2 = new Rectangle(200, 50, Color.RED);
Rectangle mark = new Rectangle(200, 50, Color.YELLOW);
Line line = new Line();
r1.setX(20);
r2.setX(380);
r2.setY(450);
PostProcessPane root = new PostProcessPane(line, r1, r2, mark);
root.getPostProcessors().add(changedNodes -> {
if (changedNodes.contains(r1) || changedNodes.contains(r2) || changedNodes.contains(mark)) {
Bounds bounds1 = r1.getBoundsInParent();
Bounds bounds2 = r2.getBoundsInParent();
// refresh line ends
line.setStartX(bounds1.getMinX() + bounds1.getWidth() / 2);
line.setStartY(bounds1.getMaxY());
line.setEndX(bounds2.getMinX() + bounds2.getWidth() / 2);
line.setEndY(bounds2.getMinY());
// recalculate mark position
mark.setX((line.getStartX() + line.getEndX() - mark.getWidth()) / 2);
mark.setY((line.getStartY() + line.getEndY() - mark.getHeight()) / 2);
}
});
// add some movement for the nodes
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(r1.xProperty(), r1.getX()),
new KeyValue(r1.yProperty(), r1.getY()),
new KeyValue(r2.xProperty(), r2.getX())),
new KeyFrame(Duration.seconds(1),
new KeyValue(r2.xProperty(), r1.getX())),
new KeyFrame(Duration.seconds(2),
new KeyValue(r1.xProperty(), r2.getX()),
new KeyValue(r1.yProperty(), r2.getY() / 2))
);
timeline.setAutoReverse(true);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}

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 to add an ImagePattern to a Rectangle while still maintaining the Rectangles background color

I have a chess board and I am trying to add pieces to the board. Every spot on the board is a Rectangle so I thought the best way to add pieces would be to add an ImagePattern to each Rectangle that gets a piece. The problem I encountered was when I added an ImagePattern to a Rectangle it would make the background of that Rectangle white despite what the color was before the ImagePattern was added. So my question is, is there a way for me to preserve the background color of a Rectangle after an ImagePattern is added?
For demo purposes my code only adds one piece to the board.
public class ChessBoard extends Application {
GridPane root = new GridPane();
final int size = 8;
public void start(Stage primaryStage) {
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
Rectangle square = new Rectangle();
Color color;
if ((row + col) % 2 == 0)
color = Color.WHITE;
else
color = Color.BLACK;
square.setFill(color);
root.add(square, col, row);
if(row == 4 && col == 3){
Image p = new Image("Peices/Black/0.png");
ImagePattern pat = new ImagePattern(p);
square.setFill(pat);
}
square.widthProperty().bind(root.widthProperty().divide(size));
square.heightProperty().bind(root.heightProperty().divide(size));
square.setOnMouseClicked(e->{
square.setFill(Color.BLUE);
});
}
}
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I think you are searching for blending which is done with the BlendMode. For example:
Image p = new Image("Peices/Black/0.png");
ImagePattern pat = new ImagePattern(p);
Rectangle r1 = new Rectangle();
r1.setX(50);
r1.setY(50);
r1.setWidth(50);
r1.setHeight(50);
r1.setFill(pat);
Rectangle r = new Rectangle();
r.setX(50);
r.setY(50);
r.setWidth(50);
r.setHeight(50);
r.setFill(Color.BLUE);
r.setBlendMode(BlendMode.ADD);
As far as I know there is no direct way to accomplish this.
Source: https://docs.oracle.com/javase/8/javafx/api/javafx/scene/effect/BlendMode.html
No, you cannot use more than a single fill with a Rectangle. Theoretically you could use a Region with multiple backgrounds, but this is probably a bad idea. Most likely you'll want at least some of the following functionality for the pieces, which will not work, if you do not make pieces their own nodes:
Dragging a piece from one field to another
animating moves
I recommend using a StackPane and put the Board in the background and put a Pane on top of it to place the pieces. Simply use ImageViews for the pieces.
Example:
#Override
public void start(Stage primaryStage) {
GridPane board = new GridPane();
Region[][] fields = new Region[8][8];
for (int i = 0; i < fields.length; i++) {
Region[] flds = fields[i];
for (int j = 0; j < flds.length; j++) {
Region field = new Region();
flds[j] = field;
field.setBackground(new Background(new BackgroundFill((i + j) % 2 == 0 ? Color.WHITE : Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
}
board.addRow(i, flds);
}
// use 1/8 of the size of the Grid for each field
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setPercentHeight(100d / 8);
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100d / 8);
for (int i = 0; i < 8; i++) {
board.getColumnConstraints().add(columnConstraints);
board.getRowConstraints().add(rowConstraints);
}
Pane piecePane = new Pane();
StackPane root = new StackPane(board, piecePane);
NumberBinding boardSize = Bindings.min(root.widthProperty(), root.heightProperty());
ImageView queen = new ImageView("https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Chess_qdt45.svg/480px-Chess_qdt45.svg.png");
DropShadow shadow = new DropShadow(BlurType.GAUSSIAN, Color.WHITE, 2, 1, 0, 0);
// drop shadow for black piece on black field
queen.setEffect(shadow);
// trigger move to topleft field on mouse click
queen.setOnMouseClicked(evt -> {
Node source = (Node) evt.getSource();
TranslateTransition animation = new TranslateTransition(Duration.seconds(0.5), source);
Region targetRegion = fields[0][0];
final PositionChangeListener listener = (PositionChangeListener) source.getUserData();
listener.setField(null);
animation.setByX(targetRegion.getLayoutX() - source.getLayoutX());
animation.setByY(targetRegion.getLayoutY() - source.getLayoutY());
animation.setOnFinished(e -> {
source.setTranslateX(0);
source.setTranslateY(0);
listener.setField(targetRegion);
});
animation.play();
});
PositionChangeListener changeListener = new PositionChangeListener(queen);
queen.setUserData(changeListener);
changeListener.setField(fields[4][3]);
// board size should be as large as possible but at most the min of the parent sizes
board.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
board.maxWidthProperty().bind(boardSize);
board.maxHeightProperty().bind(boardSize);
// same size for piecePane
piecePane.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
piecePane.maxWidthProperty().bind(boardSize);
piecePane.maxHeightProperty().bind(boardSize);
// add piece to piecePane
piecePane.getChildren().add(queen);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private static class PositionChangeListener implements ChangeListener<Bounds> {
private final ImageView piece;
public PositionChangeListener(ImageView piece) {
this.piece = piece;
}
private Region currentField;
public void setField(Region newRegion) {
// register/unregister listeners to bounds changes of associated field
if (currentField != null) {
currentField.boundsInParentProperty().removeListener(this);
}
currentField = newRegion;
if (newRegion != null) {
newRegion.boundsInParentProperty().addListener(this);
changed(null, null, newRegion.getBoundsInParent());
}
}
#Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
// align piece with field
piece.setLayoutX(newValue.getMinX());
piece.setLayoutY(newValue.getMinY());
piece.setFitHeight(newValue.getHeight());
piece.setFitWidth(newValue.getWidth());
}
}

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

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