Automatic resizing of LineChart fails if embedded in a pane - layout

The following program fails to resize the line chart horizontally when embedded in a Pane (or borderpane of anchorpane for the matter)
If the line chart is directly parented to the VBox instead, then everything works as expected.
I found I needed to bind the chart size to the parent pane, which I assume must be done automatically by VBox and HBox.
After trying different combination of enclosing in HBox/VBox, setting growing and alignment policies, I am quite confused about how layouts work.
I observe that there are differences in how ui components behave wrt resizing.
Any clarification (or digest insight on javadoc unclear documentation) is appreciated.
Best regards.
Source edited and clarified
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.chart.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("x = f(y)");
XYChart.Series data = new XYChart.Series();
data.setName("Serie 1");
for (int i = 0; i < 10; i++) {
data.getData().add(new XYChart.Data(i, i * i));
}
lineChart.getData().add(data);
VBox vb = new VBox();
vb.setFillWidth(true);
HBox hb = new HBox();
hb.getChildren().add(lineChart);
hb.setFillHeight(true);
vb.getChildren().add(hb);
HBox.setHgrow(lineChart, Priority.ALWAYS);
VBox.setVgrow(hb, Priority.ALWAYS);
Scene scene = new Scene(vb);
stage.setScene(scene);
stage.centerOnScreen();
stage.setResizable(true);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

For the record, the problem of the provided example is solved after modifying this typo:
HBox.setHgrow(hb, Priority.ALWAYS);
to:
HBox.setHgrow(lineChart, Priority.ALWAYS);
This fixes resizing horizontally.
When embedded directly in VBox, the chart's size is recomputed on resize, as it is anchored to the VBox, which boundaries change.
When embedded in a HBox, we have to provide a hint for the HBox to grow horizontally, and vertically.
Vertically it's done with:
VBox.setVgrow(hb, Priority.ALWAYS);
Horizontally it's done by requesting its content to occupy all available space, which the fix above is about.

Related

How to dynamically resize an svg in javafx according to the size of the region

I'm keen on svg and would like to put many of them in my User Interface. But I have a problem with the size of svg. I would like to load any svg I retrieve as a parameter and resize it dynamically to the size of the control.
All the examples I found are resize thanks to the "rescale" method (as found in the following article JavaFX: How to resize button containing svg image.
But since I have no idea of the size of the original svg I don't know what factor to apply in the rescale method.
So, my question is how do I generify the following code:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.shape.SVGPath;
import javafx.stage.Stage;
public class Main extends Application{
private final int MIN_BUTTON_SIZE = 10;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
HBox root = new HBox();
root.setAlignment(Pos.CENTER);
SVGPath svg = new SVGPath();
svg.setContent("M87.5,50.002C87.5,29.293,70.712,12.5,50,12.5c-20.712,0-37.5,16.793-37.5,37.502C12.5,70.712,29.288,87.5,50,87.5" +
"c6.668,0,12.918-1.756,18.342-4.809c0.61-0.22,1.049-0.799,1.049-1.486c0-0.622-0.361-1.153-0.882-1.413l0.003-0.004l-6.529-4.002" +
"L61.98,75.79c-0.274-0.227-0.621-0.369-1.005-0.369c-0.238,0-0.461,0.056-0.663,0.149l-0.014-0.012" +
"C57.115,76.847,53.64,77.561,50,77.561c-15.199,0-27.56-12.362-27.56-27.559c0-15.195,12.362-27.562,27.56-27.562" +
"c14.322,0,26.121,10.984,27.434,24.967C77.428,57.419,73.059,63,69.631,63c-1.847,0-3.254-1.23-3.254-3.957" +
"c0-0.527,0.176-1.672,0.264-2.111l4.163-19.918l-0.018,0c0.012-0.071,0.042-0.136,0.042-0.21c0-0.734-0.596-1.33-1.33-1.33h-7.23" +
"c-0.657,0-1.178,0.485-1.286,1.112l-0.025-0.001l-0.737,3.549c-1.847-3.342-5.629-5.893-10.994-5.893" +
"c-10.202,0-19.877,9.764-19.877,21.549c0,8.531,5.101,14.775,13.632,14.775c4.75,0,9.587-2.727,12.665-7.035l0.088,0.527" +
"c0.615,3.342,9.843,7.576,15.121,7.576c7.651,0,16.617-5.156,16.617-19.932l-0.022-0.009C87.477,51.13,87.5,50.569,87.5,50.002z" +
"M56.615,56.844c-1.935,2.727-5.101,5.805-9.763,5.805c-4.486,0-7.212-3.166-7.212-7.738c0-6.422,5.013-12.754,12.049-12.754" +
"c3.958,0,6.245,2.551,7.124,4.486L56.615,56.844z");
Button buttonWithGraphics = new Button();
buttonWithGraphics.setGraphic(svg);
// Bind the Image scale property to the buttons size
svg.scaleXProperty().bind(buttonWithGraphics.widthProperty().divide(100));
svg.scaleYProperty().bind(buttonWithGraphics.heightProperty().divide(100));
// Declare a minimum size for the button
buttonWithGraphics.setMinSize(MIN_BUTTON_SIZE, MIN_BUTTON_SIZE);
root.getChildren().addAll(buttonWithGraphics);
root.layoutBoundsProperty().addListener((observableValue, oldBounds, newBounds) -> {
double size = Math.max(MIN_BUTTON_SIZE, Math.min(newBounds.getWidth(), newBounds.getHeight()));
buttonWithGraphics.setPrefSize(size, size);
}
);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
I guess this is linked with the following lines:
svg.scaleXProperty().bind(buttonWithGraphics.widthProperty().divide(100));
create a resizable canvas, you can find details here
canvas can be scaled, and also graphic context can also be scale
var gc = canvas.getGraphicsContext2D();
gc.scale(0.1, 0.1);
use gc to draw the svg path, line, circle, etc.
sample code here:
var canvas = new ResizableCanvas() {
#Override
public void draw() {
var gc = getGraphicsContext2D();
gc.save();//make sure you save the status here and restore after all operations are finished
//System.out.print(getWidth()+" ");
//System.out.println(getHeight());
var width = getWidth();
var height = getHeight();
gc.clearRect(0,0,width, height);
gc.scale(width/512.002, height/512.002);
gc.beginPath();
gc.setFill(Color.web("#2D4961"));
gc.appendSVGPath("""
M399.994,0H112.008C94.337,0,80.009,14.327,80.009,31.999v342.624
c0.08,16.159,8.288,31.191,21.839,39.998l145.433,94.796c5.304,3.448,12.135,3.448,17.439,0l145.433-94.556
c13.551-8.808,21.759-23.839,21.839-39.998V31.999C431.993,14.327,417.665,0,399.994,0z M399.994,68.477
c-6.872-6.24-15.399-10.352-24.559-11.839c-1.496-9.2-5.64-17.759-11.919-24.639h36.478V68.477z M148.486,31.999
c-6.264,6.864-10.408,15.391-11.919,24.559c-9.168,1.512-17.695,5.656-24.559,11.919V31.999H148.486z M256.001,476.858
l-26.479-17.199c11.047-4.6,20.327-12.623,26.479-22.879c6.152,10.256,15.431,18.279,26.479,22.879L256.001,476.858z
M399.994,374.622c0.008,5.424-2.728,10.48-7.28,13.439l-91.756,59.917c-20.895-1.592-37.022-19.039-36.958-39.998
c0-4.416-3.584-8-8-8s-8,3.584-8,8c0.064,20.959-16.063,38.406-36.958,39.998l-91.756-59.917c-4.552-2.96-7.288-8.016-7.28-13.439
V103.995c0-17.671,14.327-31.998,31.998-31.998c4.416,0,8-3.584,8-8c0-17.671,14.327-31.999,31.999-31.999h143.993
c17.671,0,31.999,14.327,31.999,31.999c0,4.416,3.584,8,8,8c17.671,0,31.999,14.327,31.999,31.998L399.994,374.622L399.994,374.622z""");
gc.fill();
gc.setFill(Color.web("#44637F"));
gc.beginPath();
gc.appendSVGPath("""
M80.009,31.999v271.987l0,0c8.84,0,15.999-7.16,15.999-15.999V31.999
c0-8.84,7.16-15.999,15.999-15.999h271.987c8.84,0,15.999-7.16,15.999-15.999H112.008C94.329,0,80.009,14.327,80.009,31.999z""");
gc.fill();
gc.setFill(Color.web("#123247"));
gc.beginPath();
gc.appendSVGPath("""
M410.154,414.861c13.551-8.808,21.759-23.839,21.839-39.998V55.997l0,0
c-8.84,0-15.999,7.16-15.999,15.999v304.466c0,8.04-4.024,15.551-10.719,19.999L269.28,487.097
c-8.304,5.56-13.287,14.887-13.279,24.879l0,0c3.096,0.008,6.12-0.88,8.72-2.56L410.154,414.861z""");
gc.fill();
gc.restore();
}
};
canvas.widthProperty().bind(canvas.heightProperty());
canvas.heightProperty().bind(stage.getScene().heightProperty().multiply(0.1));
The solution above tries to do it dynamically, although (as you said) fails to do it in respect to the original SvgPath size.
I would recommend doing it like that:
double size = 30;
svg.setScaleX(size / svg.boundsInLocalProperty().get().getWidth());
svg.setScaleY(size / svg.boundsInLocalProperty().get().getHeight());
This will scale your SvgPath to size.

Scene Graph behaviour when adding node to multiple scenes//panes

I am currently experimenting with JavaFX's scene graph. I came across a strange problem that i can't really explain. Have a look at the following code:
final BorderPane bp1 = new BorderPane();
final Button button = new Button("CLICK ME");
Scene sc1 = new Scene(bp1,100,100);
bp1.setCenter(button);
stage1.setScene(sc1);
stage1.show();
Stage stage2 = new Stage();
final BorderPane bp2 = new BorderPane();
Scene sc2 = new Scene(bp2,100,100);
stage2.setScene(sc2);
stage2.setX(250);
stage2.show();
bp2.setCenter(button);
bp1.setCenter(button);
What i am trying to do here is to add a node (Button) to a Borderpane which lies within a scene on stage1. At the end of the code i try to add the node to a different scene(pane) on stage2. This actually works. But the last line does not. The button remains on stage2. The strange thing is that if i replace the borderpane with a gridpane, the code works as intended. Why does the borderpane behave different from the gridpane in this situation?
Another question which came to my mind in this case: I assume that there is exactly 1 scene graph per scene (or stage). Is that correct?
I guess you found a bug in the BorderPane implementation. The BorderPane has a bit of peculiar way how it manages its children. You can find the BorderPane implementation here. I suggest you file a bug report at the javafx jira
My testcase:
final BorderPane bp1 = new BorderPane();
final BorderPane bp2 = new BorderPane();
System.out.println("bp1 "+bp1);
System.out.println("bp2 "+bp2);
final Button button = new Button("CLICK ME");
button.parentProperty().addListener(new ChangeListener<Parent>() {
#Override
public void changed(ObservableValue<? extends Parent> observableValue, Parent parent, Parent parent2) {
System.out.println("changed");
System.out.println(button.getParent());
}
});
bp1.centerProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
System.out.println("bp1 center invalidated ");
}
});
bp2.centerProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
System.out.println("bp2 center invalidated ");
}
});
// bp1.centerProperty().addListener(new ChangeListener<Node>() {
// #Override
// public void changed(ObservableValue<? extends Node> observableValue, Node node, Node node2) {
// System.out.println("bp1 center changed "+node+" -> "+node2);
// }
// });
// bp2.centerProperty().addListener(new ChangeListener<Node>() {
// #Override
// public void changed(ObservableValue<? extends Node> observableValue, Node node, Node node2) {
// System.out.println("bp2 center changed "+node+" -> "+node2);
// }
// });
Scene sc1 = new Scene(bp1,100,100);
bp1.setCenter(button);
primaryStage.setScene(sc1);
primaryStage.show();
Stage stage2 = new Stage();
Scene sc2 = new Scene(bp2,100,100);
stage2.setScene(sc2);
stage2.setX(250);
stage2.show();
bp2.setCenter(button);
bp1.setCenter(button);
output:
bp1 BorderPane#48a80c67
bp2 BorderPane#10c66375
changed
BorderPane#48a80c67[styleClass=root]
bp1 center invalidated
changed
null
changed
BorderPane#10c66375[styleClass=root]
bp2 center invalidated
Explanation:
I found that bp1's center property is not invalidated and thus not updated when the button is added to the second scene. Adding a node to another scene should normally cause it to be removed from the previous scene. However, since BorderPane uses a peculiar way to hold its values (object properties for center, left and so on), after the button is added to the second scene, the button is not removed from the center property of the BorderPane. Setting the button a second time on the same BorderPane doesn't add the button to its children because it thinks that it is already set as the center node.
To your second question:
yes, only one scene graph per stage/scene.

How can I make a TextArea stretch to fill the content, expanding the parent in the process?

So I have a TextArea and as the user pastes paragraphs into it, or just writes in it, I want it to expand vertically to reveal all the available text. I.e. not to use a scrollbar in the text field itself... much like what happens on many web pages. Many users, myself included, don't like to be forced to edit in a small window. Exactly how Facebook status updates box works.
I've tried
myTextArea.autoSize()
wrapped in an
myTextArea.textProperty().addListener(new ChangeListener()....);
but that doesn't work. I think it's happy autosizing to its current size.
The left, right & top anchors are set to it's parent AnchorPane. I've tried it with the bottom attached and not attached. Ideally I'd like to grow the anchor pane as the textarea grows.
I don't mind reading the TextProperty and calculating a trigger size which I set myself... but this seems a hacky approach IF there is already a best practise. The number of properties and sub objects of javafx is sufficiently daunting that it seems like a good point to ask the question here, rather than trying to figure out how many pixels the font/paragraphs etc are taking up.
Update:
So I thought maybe I was overthinking it, and all I needed to do was to switch the scrollbars off and the rest would happen. Alas, looking for available fields and methods for "scroll", "vertical", "vbar" comes up with nothing I can use. ScrollTopProperty looks like it's for something else.
The problem; the height of textArea is wanted to be grown or shrunk while its text is changing by either user's typing or copy-pasting. Here is another approach:
public class TextAreaDemo extends Application {
private Text textHolder = new Text();
private double oldHeight = 0;
#Override
public void start(Stage primaryStage) {
final TextArea textArea = new TextArea();
textArea.setPrefSize(200, 40);
textArea.setWrapText(true);
textHolder.textProperty().bind(textArea.textProperty());
textHolder.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
if (oldHeight != newValue.getHeight()) {
System.out.println("newValue = " + newValue.getHeight());
oldHeight = newValue.getHeight();
textArea.setPrefHeight(textHolder.getLayoutBounds().getHeight() + 20); // +20 is for paddings
}
}
});
Group root = new Group(textArea);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
// See the explanation below of the following line.
// textHolder.setWrappingWidth(textArea.getWidth() - 10); // -10 for left-right padding. Exact value can be obtained from caspian.css
}
public static void main(String[] args) {
launch(args);
}
}
But it has a drawback; the textarea's height is changing only if there are line breaks (ie Enter keys) between multiple lines, if the user types long enough the text gets wrapped to multiple line but the height is not changing.
To workaround this drawback I added this line
textHolder.setWrappingWidth(textArea.getWidth() - 10);
after primaryStage.show();. It works well for long typings where user does not linebreaks. However this generates another problem. This problem occurs when the user is deleting the text by hitting "backspace". The problem occurs exactly when the textHolder height is changed and where the textArea's height is set to new value. IMO it maybe a bug, didn't observe deeper.
In both case the copy-pasting is handling properly.
Awaiting a better, i use this hacky solution.
lookup the vertical scrollbar of the textarea.
make it transparent
listen to its visible property
when the scrollbar become visible i add a row to the textarea.
The code:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class GrowGrowTextArea extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
AnchorPane root = new AnchorPane();
root.setStyle("-fx-padding:20;-fx-background-color:dodgerblue;");
final TextArea textArea = new TextArea();
AnchorPane.setTopAnchor(textArea, 10.0);
AnchorPane.setLeftAnchor(textArea, 10.0);
AnchorPane.setRightAnchor(textArea, 10.0);
root.getChildren().add(textArea);
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
ScrollBar scrollBar = lookupVerticalScrollBar(textArea);
scrollBar.setOpacity(0.0);
scrollBar.visibleProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> source,
Boolean wasVisible,
Boolean isVisible) {
if (isVisible) {
textArea.setPrefRowCount(textArea.getPrefRowCount() + 1);
textArea.requestLayout();
}
}
});
}
private ScrollBar lookupVerticalScrollBar(Node node) {
if (node instanceof ScrollBar && ((ScrollBar)node).getOrientation() == Orientation.VERTICAL) {
return (ScrollBar) node;
}
if (node instanceof Parent) {
ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
for (Node child : children) {
ScrollBar scrollBar = lookupVerticalScrollBar(child);
if (scrollBar != null) {
return scrollBar;
}
}
}
return null;
}
}
I had a similar problem with creating expanding TextArea. I was creating TextArea that looks like TextField and expand vertically every time when there is no more space in line.
I have tested all solutions that I could find on this topic on stack and other sources available. I found few good solutions but neither was good enough.
After many hours of fighting, I figured out this approach.
I extended TextArea class, override layoutChildren() method and add a listener on text height.
#Override
protected void layoutChildren() {
super.layoutChildren();
setWrapText(true);
addListenerToTextHeight();
}
private void addListenerToTextHeight() {
ScrollPane scrollPane = (ScrollPane) lookup(".scroll-pane");
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
StackPane viewport = (StackPane) scrollPane.lookup(".viewport");
Region content = (Region) viewport.lookup(".content");
Text text = (Text) content.lookup(".text");
text.textProperty().addListener(textHeightListener(text));
}
private InvalidationListener textHeightListener(Text text) {
return (property) -> {
// + 1 for little margin
double textHeight = text.getBoundsInLocal().getHeight() + 1;
//To prevent that our TextArena will be smaller than our TextField
//I used DEFAULT_HEIGHT = 18.0
if (textHeight < DEFAULT_HEIGHT) {
textHeight = DEFAULT_HEIGHT;
}
setMinHeight(textHeight);
setPrefHeight(textHeight);
setMaxHeight(textHeight);
};
}
I used some of the code found in the previous answers.
The growTextAreaIfNecessary method will increase the height of textArea until the scrollbar is not visible (limited to 20 lines in this example).
The problem with this approach is that the window needs to be redrawn several times until the perfect height is found.
private ScrollBar lookupVerticalScrollBar(Node node) {
if (node instanceof ScrollBar && ((ScrollBar) node).getOrientation() == Orientation.VERTICAL) {
return (ScrollBar) node;
}
if (node instanceof Parent) {
ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
for (Node child : children) {
ScrollBar scrollBar = lookupVerticalScrollBar(child);
if (scrollBar != null) {
return scrollBar;
}
}
}
return null;
}
private void growTextAreaIfNecessary(TextArea textArea) {
Platform.runLater(() -> {
ScrollBar lookupVerticalScrollBar = lookupVerticalScrollBar(textArea);
int prefRowCount = textArea.getPrefRowCount();
if (lookupVerticalScrollBar.isVisible() && prefRowCount < 20) {
textArea.setPrefRowCount(prefRowCount + 1);
System.out.println("increasing height to: " + (prefRowCount + 1));
growTextAreaIfNecessary(textArea);
}
});
}
I have tried many hacks, most of them had jitters while typing, this to me was the perfect result:
textArea.textProperty().addListener((obs,old,niu)->{
Text t = new Text(old+niu);
t.setFont(textArea.getFont());
StackPane pane = new StackPane(t);
pane.layout();
double height = t.getLayoutBounds().getHeight();
double padding = 20 ;
textArea.setMinHeight(height+padding);
});

Adding space between buttons in VBox

I have a collection of buttons:
VBox menuButtons = new VBox();
menuButtons.getChildren().addAll(addButton, editButton, exitButton);
I want to add some spacing between these buttons, without using a CSS style sheet. I think there should be a way to do this.
setPadding(); is for the Buttons in the VBox.
setMargin(); should be for the VBox itself. But I didn't find a way for the spacing between the buttons.
I'm glad for any ideas. :)
VBox supports spacing out of the box:
VBox menuButtons = new VBox(5);
or
menuButtons.setSpacing(5);
Just call setSpacing method and pass some value.
Example with HBox (it's same for VBox):
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.HBoxBuilder;
import javafx.stage.Stage;
public class SpacingDemo extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Spacing demo");
Button btnSave = new Button("Save");
Button btnDelete = new Button("Delete");
HBox hBox = HBoxBuilder.create()
.spacing(30.0) //In case you are using HBoxBuilder
.padding(new Insets(5, 5, 5, 5))
.children(btnSave, btnDelete)
.build();
hBox.setSpacing(30.0); //In your case
stage.setScene(new Scene(hBox, 320, 240));
stage.show();
}
}
And this is how it looks:
Without of spacing:
With spacing:
If you're using FXML, use the spacing attribute:
<VBox spacing="5" />
As others have mentioned you can use setSpacing().
However, you can also use setMargin(), it is not for the pane (or box in your words), it is for individual Nodes. setPadding() method is for the pane itself. In fact, setMargin() takes a node as a parameter so you can guess what it's for.
For example:
HBox pane = new HBox();
Button buttonOK = new Button("OK");
Button buttonCancel = new Button("Cancel");
/************************************************/
pane.setMargin(buttonOK, new Insets(0, 10, 0, 0)); //This is where you should be looking at.
/************************************************/
pane.setPadding(new Insets(25));
pane.getChildren().addAll(buttonOK, buttonCancel);
Scene scene = new Scene(pane);
primaryStage.setTitle("Stage Title");
primaryStage.setScene(scene);
primaryStage.show();
You could get the same result if you replaced that line with
pane.setSpacing(10);
If you have several nodes that should be spaced, setSpacing() method is far more convenient because you need to call setMargin() for each individual node and that would be ridiculous. However, setMargin() is what you need if you need margins(duh) around a node that you can determine how much to each side because setSpacing() methods places spaces only in between nodes, not between the node and the edges of the window.
The same effect as the setSpacing method can also be achieved via css:
VBox {
-fx-spacing: 8;
}

Scaling in JavaFX and ScrollPanes

I've been trying to work with the scaling transform in JavaFX, but haven't quite been able to wrap my head around it. Basically, I have a Pane containing a complex graph and would like to be able to rescale it. The scaling part itself works fine, however, the enclosing scroll pane will not adapt to the graph.
For simplicity's sake, i'll post a short example in which my graph is replaced by a label:
public class TestApp extends Application {
#Override public void start(final Stage stage) throws Exception {
final Label label = new Label("Hello World");
label.getTransforms().setAll(new Scale(0.5, 0.5));
label.setStyle("-fx-background-color:blue");
label.setFont(new Font(200));
final ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(label);
stage.setScene(new Scene(scrollPane));
stage.setWidth(200);
stage.setHeight(100);
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
The label will scale correctly, but the enclosing scroll pane's bars will still accomodate a component of the original size.
I've tried so far:
Playing around with the labels min and pref size
wrapping the label inside a Group (no scrollbars will appear whatsoever)
scaling the enclosing Group rather than the label
What am I missing? What can I do to make the ScrollPane adapt to the content view?
Thanks for your help.
According to the ScrollPane document you might try to wrap a Pane in a Group so the ScrollPane is scroll by visual bound not the actual layout bound.
ScrollPane layout calculations are based on the layoutBounds rather than the
boundsInParent (visual bounds) of the scroll node. If an application wants the
scrolling to be based on the visual bounds of the node (for scaled content etc.),
they need to wrap the scroll node in a Group.
I implemented scaling in a ScrollPane for Graphs and other nodes in
this example of scrollpane viewports, transforms and layout bounds in JavaFX.
The code was implemented when I was first learning JavaFX, so certainly the code could be cleaner and perhaps there are simpler ways to accomplish this (e.g. using a Group as the container for the scaled node as suggested in the ScrollPane documentation).
One key to getting the solution I wanted (ScrollBars only appearing when you are zoomed in and the node is larger than the visible viewport), was this code:
// create a container for the viewable node.
final StackPane nodeContainer = new StackPane();
nodeContainer.getChildren().add(node);
// place the container in the scrollpane and adjust the pane's viewports as required.
final ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(nodeContainer);
scrollPane.viewportBoundsProperty().addListener(
new ChangeListener<Bounds>() {
#Override public void changed(ObservableValue<? extends Bounds> observableValue, Bounds oldBounds, Bounds newBounds) {
nodeContainer.setPrefSize(
Math.max(node.getBoundsInParent().getMaxX(), newBounds.getWidth()),
Math.max(node.getBoundsInParent().getMaxY(), newBounds.getHeight())
);
}
});
...
// adjust the view layout based on the node scalefactor.
final ToggleButton scale = new ToggleButton("Scale");
scale.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent actionEvent) {
if (scale.isSelected()) {
node.setScaleX(3); node.setScaleY(3);
} else {
node.setScaleX(1); node.setScaleY(1);
}
// runlater as we want to size the container after a layout pass has been performed on the scaled node.
Platform.runLater(new Runnable() {
#Override public void run() {
nodeContainer.setPrefSize(
Math.max(nodeContainer.getBoundsInParent().getMaxX(), scrollPane.getViewportBounds().getWidth()),
Math.max(nodeContainer.getBoundsInParent().getMaxY(), scrollPane.getViewportBounds().getHeight())
);
}
});
}
});

Resources