JavaFX accordion with multiple open panes
I had slightly different requirements
- The accordion either expands or manages the view space for the embedded views
- The entire view can be put into a scroll view
- Every box fully expands to the size of the whole view, if the accordion is fixed size, or it expands to the size of the content, if it is not fixed view.
Although in my case, I was not able to fulfill all of 3. and test 2., I was able to come up with the following fix:
1) Use a ScrollPane, with a VBox inside, with TitledWindows inside. 2) Make sure your TitledPanes are set to VBox.grow="SOMETIMES" . 3) Add a VBox as the last element and set VBox.vgrow="ALWAYS" - this pushes the TitledPanes up to their minimum size. Everybody else has provided Code examples, if you want to use fxml, or don't want to use Java, just using the elements directly works just as well (generated with SceneBuilder):
<ScrollPane fitToHeight="true" fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<content>
<VBox fx:id="leftVBox" maxHeight="1.7976931348623157E308" prefHeight="200.0" prefWidth="100.0">
<children>
<TitledPane fx:id="titledPanelOne" animated="false" expanded="false" style="-fx-background-color: red;" text="Pane One" VBox.vgrow="SOMETIMES">
<content>
<ListView fx:id="listViewOne" maxHeight="1.7976931348623157E308" prefHeight="200.0" prefWidth="200.0" />
</content>
</TitledPane>
<TitledPane fx:id="titledPanelTwo" animated="false" expanded="false" style="-fx-background-color: green;" text="Pane Two" VBox.vgrow="SOMETIMES">
<content>
<ListView fx:id="listViewTwo" maxHeight="1.7976931348623157E308" prefHeight="200.0" prefWidth="200.0" />
</content>
</TitledPane>
<VBox prefHeight="0.0" prefWidth="0.0" VBox.vgrow="ALWAYS" />
</children>
</VBox>
</content>
</ScrollPane>
4) Although this does get you stacked boxes which expand/contract independently of one another, this doesn't fix the issue where you have boxes which do not resize properly to their contents (if for example you have a List View embedded as in the above example), and so you now have to scroll quite a bit when there is plenty of screen real-estate left. The solution? A bit of Java is required.
To implement this fix, we first bind the TitledPane's maxHeightProperty()
to the outer VBox's heightProperty()
:
public class Controller implements Initializable {
//... controller code
@Override
public void initialize(URL location, ResourceBundle resources) {
//...
variablesViewPane.maxHeightProperty().bind(leftVBox.heightProperty());
historyViewPane.maxHeightProperty().bind(leftVBox.heightProperty());
}
}
The we bind to each pane's expandedProperty()
, and dynamically bind and unbind the prefHeighProperty()
:
private static void bindExpanded(TitledPane pane, ReadOnlyDoubleProperty prop) {
pane.expandedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
pane.prefHeightProperty().bind(prop);
} else {
pane.prefHeightProperty().unbind();
pane.prefHeightProperty().set(0);
}
});
}
If we are shown, we ask to be as large as the VBox, if we aren't shown, we ask to be as small as possible. The benefit of doing things this way is that the layout then automatically calculated the available height based on the number of currently shown TitledPanes - which leads to exactly the behavior that we want.
I go into more detail here:
http://sebastianaudet.com/blog/playing-with-javafx/
No, a JavaFX 2.2 Accordion can only have one open pane at a time.
I created an enhancement request (JDK-8090554 StackedTitledPanes control) for a feature which allows you to open more than one pane in the accordion at a time, however the feature request has currently not been implemented.
In the meantime, you can construct a similar control yourself quite easily by creating multiple TitledPane instances and placing these in a VBox.
private VBox createStackedTitledPanes() {
final VBox stackedTitledPanes = new VBox();
stackedTitledPanes.getChildren().setAll(
new TitledPane("Pane 1", contentNode1),
new TitledPane("Pane 2", contentNode2),
new TitledPane("Pane 3", contentNode3)
);
((TitledPane) stackedTitledPanes.getChildren().get(0)).setExpanded(true);
return stackedTitledPanes;
}
If necessary, you can wrap the VBox
containing your panes in a ScrollPane, so that the contents of all of your expanded panes can be usable if their area overflows the available area.
I created a sample solution (icons are linkware from: http://www.fasticon.com).
Update
Modernized and inlined the previously externally linked example solution for a scrollable stack of TitledPanes.
Also, note that in a modern JavaFX environment the default styling is a bit different (fewer gradients in by default in things like the TitledPane content background), so it will look slightly different than the prior image in this answer, but otherwise behavior is similar.
import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class StackedPanes extends Application {
// image license: linkware - backlink to http://www.fasticon.com
private static final Image BLUE_FISH = new Image("http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Blue-Fish-icon.png");
private static final Image RED_FISH = new Image("http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Red-Fish-icon.png");
private static final Image YELLOW_FISH = new Image("http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Yellow-Fish-icon.png");
private static final Image GREEN_FISH = new Image("http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Green-Fish-icon.png");
@Override
public void start(Stage stage) {
VBox stackedTitledPanes = createStackedTitledPanes();
ScrollPane scroll = new ScrollPane(stackedTitledPanes);
scroll.setFitToWidth(true);
scroll.setFitToHeight(true);
scroll.setPrefWidth(410);
scroll.setStyle("-fx-base: cadetblue;");
stage.setTitle("Fishy, fishy");
Scene scene = new Scene(scroll);
stage.setScene(scene);
stage.show();
}
private VBox createStackedTitledPanes() {
final VBox stackedTitledPanes = new VBox();
stackedTitledPanes.getChildren().setAll(
createTitledPane("One Fish", GREEN_FISH),
createTitledPane("Two Fish", YELLOW_FISH, GREEN_FISH),
createTitledPane("Red Fish", RED_FISH),
createTitledPane("Blue Fish", BLUE_FISH)
);
((TitledPane) stackedTitledPanes.getChildren().get(0)).setExpanded(true);
return stackedTitledPanes;
}
public TitledPane createTitledPane(String title, Image... images) {
FlowPane content = new FlowPane();
for (Image image : images) {
ImageView imageView = new ImageView(image);
content.getChildren().add(imageView);
FlowPane.setMargin(imageView, new Insets(10));
}
content.setAlignment(Pos.TOP_CENTER);
TitledPane pane = new TitledPane(title, content);
pane.setExpanded(false);
return pane;
}
public static void main(String[] args) {
Application.launch(args);
}
}
Combining the best of the existing answers with a simplification, you can replicate a multiple-open accordion by creating several titled panels in a ScrollPane VBox, then binding the max-height property with a listener (to do it on the XML means that the panes would reserve space even when collapsed)
<ScrollPane fitToHeight="true" fitToWidth="true">
<AnchorPane id="Content">
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<!-- Setting maxHeight="Infinity" to all makes them all grow as we want, but then they get spaces between, when collapsed (so we do it in code) -->
<TitledPane fx:id="pane1" animated="false" VBox.vgrow="ALWAYS" expanded="false" text="Pane 1">...</TitledPane>
<TitledPane fx:id="pane2" animated="false" VBox.vgrow="ALWAYS" maxHeight="Infinity" text="Pane 2 (starts expanded)">...</TitledPane>
<TitledPane fx:id="pane3" animated="false" VBox.vgrow="ALWAYS" expanded="false" text="Pane 3">...</TitledPane>
</VBox>
</AnchorPane>
</ScrollPane>
Note: You'll need to callthis method for each pane:
pane1.expandedProperty().addListener((observable, oldValue, newValue) -> {
//make it fill space when expanded but not reserve space when collapsed
if (newValue) {
pane1.maxHeightProperty().set(Double.POSITIVE_INFINITY);
} else {
pane1.maxHeightProperty().set(Double.NEGATIVE_INFINITY);
}
});