Reputation: 452
I would like to automatically adjust the width/height of a javafx.stage.Stage
whenever the preferred width/height of the Scene
's root node changes.
It's a small utility window (not resizable by the user) that contains multiple javafx.scene.control.TitledPane
s and the window's height should increase when one of these TitledPanes is expanded (otherwise the TitlePane's content may be out of bounds).
Unfortunately I could only find javafx.stage.Window.sizeToScene()
, which initially sets the window's size to the root's preferred size.
Is there a way to permanently bind the Stage size to the root node size?
Upvotes: 1
Views: 10245
Reputation: 21799
Class Region
has a prefHeightProperty and a prefWidthProperty, and class Stage
has a setWidth method and a setHeight method.
You can listen to the property change of the root Region of the scene of your stage, and in the listener you can call the corresponding mutator method of your stage:
root.prefHeightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(newVal.doubleValue()));
root.prefWidthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(newVal.doubleValue()));
An example that I used for testing:
public class Main extends Application {
@Override
public void start(final Stage primaryStage) {
try {
// Add some controls to set the pref size of the root VBox
final VBox par = new VBox();
final TextField tf1 = new TextField();
final TextField tf2 = new TextField();
Button b1 = new Button();
b1.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
// On button press set the pref size of the VBox to the values of the TextFields
par.setPrefSize(Double.parseDouble(tf1.getText()), Double.parseDouble(tf2.getText()));
}
});
par.getChildren().addAll(tf1, tf2, b1);
Scene scene = new Scene(par,400,400);
// Attach the listeners
par.prefHeightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(newVal.doubleValue()));
par.prefWidthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(newVal.doubleValue()));
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Layouts query the preferred size of their nodes by invoking the prefWidth(height) and prefHeight(width) methods. By default, UI controls compute default values for their preferred size that is based on the content of the control. For example, the computed size of a Button object is determined by the length of the text and the size of the font used for the label, plus the size of any image. Typically, the computed size is just big enough for the control and the label to be fully visible.
UI controls also provide default minimum and maximum sizes that are based on the typical usage of the control. For example, the maximum size of a Button object defaults to its preferred size because you don't usually want buttons to grow arbitrarily large. However, the maximum size of a ScrollPane object is unbounded because typically you do want them to grow to fill their spaces.
Based on this, the idea is to have a ScrollPane
as root, with hidden scrollbars, as this control allows the content to "freely" grow.
As the content can grow, you can listen to the heightProperty
and widthProperty
of the content of the ScrollPane, and set the size of the Stage
.
Example:
public class Main extends Application {
@Override
public void start(final Stage primaryStage) {
try {
// ScrollPane will be the root
ScrollPane sp = new ScrollPane();
// Don't show the ScrollBars
sp.setHbarPolicy(ScrollBarPolicy.NEVER);
sp.setVbarPolicy(ScrollBarPolicy.NEVER);
// TitledPane will be the content of the root
TitledPane tp = new TitledPane();
sp.setContent(tp);
// Set the layout bounds of the TitledPane
tp.setMinHeight(400);
tp.setMinWidth(400);
tp.setMaxHeight(900);
tp.setMaxWidth(900);
// Fill the TitledPane with some Buttons and containers to test the growing and shrinking progress
final VBox vboxTPContentVertical = new VBox();
final VBox vboxTexts = new VBox();
final HBox hboxTexts = new HBox();
HBox hboxButtons = new HBox();
vboxTPContentVertical.getChildren().addAll(hboxButtons, hboxTexts, vboxTexts);
Button b1 = new Button("Add row");
b1.setOnAction((event) -> vboxTexts.getChildren().addAll(new Text("Row1"), new Text("Row2")));
Button b2 = new Button("Add column");
b2.setOnAction((event) -> hboxTexts.getChildren().addAll(new Text("Col1"), new Text("Col2")));
Button b3 = new Button("Remove row");
b3.setOnAction((event) -> {
vboxTexts.getChildren().remove(0);
vboxTexts.getChildren().remove(0);
});
Button b4 = new Button("Remove column");
b4.setOnAction((event) -> {
hboxTexts.getChildren().remove(0);
hboxTexts.getChildren().remove(0);
});
hboxButtons.getChildren().addAll(b1, b2, b3, b4);
tp.setContent(vboxTPContentVertical);
// Set the ScrollPane as root
Scene scene = new Scene(sp, 400, 400);
// Now just listen to the heightProperty and widthProperty of the TitledPane
tp.heightProperty().addListener((obs, oldVal, newVal) -> primaryStage.setHeight(to.doubleValue()));
tp.widthProperty().addListener((obs, oldVal, newVal) -> primaryStage.setWidth(to.doubleValue()));
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Upvotes: 1
Reputation: 452
Based on DVarga's example I crafted the following "solution":
A InvalidationListener
is installed on the heightProperty
of every child node that may shrink/grow in height (two TitlePanes
in this example).
When a heightProperty
is invalidated, the height of the containing window gets recalculated (also honering window decorations).
Example:
public class SampleApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
final Label lbl1 = new Label("content");
final TitledPane tp1 = new TitledPane("First TP", lbl1);
final Label lbl2 = new Label("more content");
final TitledPane tp2 = new TitledPane("Second TP", lbl2);
final VBox rootPane = new VBox(tp1, tp2);
tp1.heightProperty().addListener((InvalidationListener) observable -> {
updateWindowHeight(rootPane);
});
tp2.heightProperty().addListener((InvalidationListener) observable -> {
updateWindowHeight(rootPane);
});
final Scene scene = new Scene(rootPane);
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.show();
}
private void updateWindowHeight(final VBox rootPane) {
final Scene scene = rootPane.getScene();
if (scene == null)
return;
final Window window = scene.getWindow();
if (window == null)
return;
final double rootPrefHeight = rootPane.prefHeight(-1);
final double decorationHeight = window.getHeight() - scene.getHeight(); // window decorations
window.setHeight(rootPrefHeight + decorationHeight);
}
public static void main(String[] args) {
launch(args);
}
}
Although it works as intended, there are some major drawbacks with this solution:
I couldn't get a cleaner solution to work. javafx.stage.Stage
and javafx.scene.Scene
are just too obstructed (possible extension points are final or package-private) to implement this feature where it belongs to.
Update
Using just window.sizeToScene()
instead of
final double rootPrefHeight = rootNode.prefHeight(-1);
final double decorationHeight = window.getHeight() - scene.getHeight();
window.setHeight(rootPrefHeight + decorationHeight);
generates a lot less "stutter" when adjusting the window size!
Upvotes: 4