I am encountering an issue with SplitPane dividers if the content has multiline FlowPane. There is no issue if the FlowPane rendered in one row. If the FlowPane has more than one row then there is a shift in the content part.
The more the no of rows, the greater the shift is.
To demonstrate the issue, below is quick a demo. The demo contains three vertical splitPanes, where each SplitPane has FlowPane with different no. of rows. (1st splitPane - 1row, 2nd SplitPane - 2rows, 3rd SplitPane - 3rows)
When resizing the splitPane with 1 FlowPane row, there is no issue, everything works fine. Whereas if I resize the second splitPane, the content is shifting from its desired place leaving a void space in SplitPane. When resizing the third splitPane, the space is even much bigger.
I believe this should be some issue in SplitPane-FlowPane calculations (Or I might be wrong as well). But at this stage rather than trying to figure the root cause (which will be somewhere inside JavaFX source code), I am more desperate in fixing this with some work around.
I tried few ways by binding the heights, setting some Region constants, etc. But none worked. All the height calculations of FlowPane are indeed correct.
Do any of you have any suggestions on how I can fix this.
Note: The issue can be reproduced in all versions of JavaFX
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
#SuppressWarnings("javadoc")
public class SplitPaneDividerIssueDemo extends Application {
/**
* FlowPane for debugging purpose.
*/
class SimpleFlowPane extends FlowPane {
#Override
protected double computeMaxHeight(final double width) {
final double height = super.computeMaxHeight(width);
// Debugging the first FlowPane in each SplitPane
if (isFirst()) {
System.out.println("Computed max height for " + getId() + " :: " + height);
}
return height;
}
#Override
protected double computeMinHeight(final double width) {
final double height = super.computeMinHeight(width);
if (isFirst()) {
System.out.println("Computed min height for " + getId() + " :: " + height);
}
return height;
}
#Override
protected double computePrefHeight(final double width) {
final double height = super.computePrefHeight(width);
if (isFirst()) {
System.out.println("Computed pref height for " + getId() + " :: " + height);
}
return height;
}
private boolean isFirst() {
return getId().endsWith("-1");
}
}
private int splitId = 1;
private int flowId = 1;
public static void main(final String... a) {
Application.launch(a);
}
#Override
public void start(final Stage primaryStage) throws Exception {
final HBox root = new HBox(buildSplitPane(10), buildSplitPane(20), buildSplitPane(30));
root.setSpacing(10);
final Scene scene = new Scene(root, 1250, 700);
primaryStage.setScene(scene);
primaryStage.setTitle("SplitPane Divider Issue");
primaryStage.show();
}
private VBox buildContent(final int count) {
final Button button = new Button("Button");
final FlowPane flowPane = new SimpleFlowPane();
flowPane.setId("flow-" + splitId + "-" + flowId);
flowPane.setVgap(5);
flowPane.setHgap(5);
for (int i = 0; i < count; i++) {
flowPane.getChildren().add(new Button("" + i));
}
final ScrollPane scroll = new ScrollPane();
VBox.setVgrow(scroll, Priority.ALWAYS);
final ToolBar toolBar = new ToolBar();
toolBar.getItems().add(new Button("Test"));
final VBox pane = new VBox();
pane.setPadding(new Insets(5));
pane.setSpacing(5);
pane.setStyle("-fx-background-color:yellow;-fx-border-width:1px;-fx-border-color:red;");
pane.getChildren().addAll(button, flowPane, scroll, toolBar);
pane.parentProperty().addListener((obs,old,content)->{
if(content!=null){
content.layoutYProperty().addListener((obs1,old1,layoutY)->{
System.out.println("LayoutY of content having "+flowPane.getId()+" :: "+layoutY);
});
}
});
flowId++;
return pane;
}
private SplitPane buildSplitPane(final int count) {
final SplitPane splitPane = new SplitPane();
splitPane.setStyle("-fx-background-color:green;");
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.setDividerPositions(.36, .70);
splitPane.getItems().addAll(buildContent(count), buildContent(count), buildContent(count));
HBox.setHgrow(splitPane, Priority.ALWAYS);
splitId++;
flowId = 1;
return splitPane;
}
}
The problem is within the minHeight of a FlowPane since it is oriented horizontally making that minHeight very dynamic. It appears to be designed where the minHeight is changed as it grows and shrinks in width. When you condense the parent vertically, the VBox calculates its minHeight as the "top/bottom insets plus the sum of each child's min height plus spacing between each child" according to the docs. Apparently there is some problem where a FlowPane's parent cannot account for its minHeight.
An HBox calculates its minHeight as the "top/bottom insets plus the largest of the children's min heights." So, if you wrap the FlowPane in an HBox, that HBox minHeight will be bound to the height of the FlowPane, and then place that HBox in the VBox where the FlowPane should be.
HBox flowPaneContainer = new HBox();
flowPaneContainer.getChildren().add(flowPane);
pane.getChildren().addAll(button, flowPaneContainer, scroll, toolBar);
EDIT: This is fine if your stage size is fixed. If your application is resizable, then more will have to be done because the flowPane minHeight will change, changing the HBox minHeight, and will then result in the same problem because there won't be enough room for everything inside every VBox.
With resizable apps, I normally handle this by wrapping each section of a SplitPane in a ScrollPane.
Related
I have a ScrollPane as below:
ScrollPane scroller = new ScrollPane();
scroller.getStyleClass().add("scroller");
scroller.setPrefWidth(width);
scroller.setFocusTraversable(Boolean.FALSE);
scroller.setPannable(Boolean.TRUE);
scroller.setFitToWidth(Boolean.TRUE);
scroller.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroller.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
this.setCenter(scroller);
scroller.contentProperty().addListener((observableValue, last, now) ->
{
ScrollBar scrollBar = (ScrollBar) scroller.lookup(".scroll-bar:vertical");
if (scrollBar != null)
{
if (scrollBar.isVisible())
{
log.info("Scrollbar visible, setting lower card width..");
}
else
{
log.info("Scrollbar not visible, setting default card width..");
}
}
});
As you can see I've attached a listener to the content property to know when the content is set. I am trying to see if the scrollbar is visible when the content is updated. Even though I can see the scroll bar on the UI, it always goes to else part - "Scrollbar not visible".
Not sure if there is any other way to do this? Checked a lot on StackOverflow and Oracle docs - nothing solid found to suggest otherwise.
-- Adding context to the problem to better understand:
Just trying to explain what the problem is not sure if I should put it as a reply comment or edit the question, please advise and will change it:
So I have this view that brings up records from Firebase that need to be loaded on the TilePane that is hosted in ScrollPane which goes into the Center of the BorderPane.
The time by which I get the response from the Firebase is unpredictable as its async. So the UI gets loaded up with the empty TilePane and then the async call goes to fetch data. When the data is available, I need to prepare Cards (which is HBox) but the number of columns is fixed. So have to adjust the width of the cards to keep the gap (16px) and padding (16px) consistent on the TilePane at the same time maintain 5 columns. The width of each card needs to be recalculated based on the fact that whether or not there is a scrollbar on the display. Because if the scrollbar is displayed it takes some space and the TilePane will down it to 4 columns leaving a lot of empty space. Happy to explain further if this is not clear.
I strongly suggest to follow the suggestions given in the comments. It is all about choosing the correct layout.
The purpose of me answering this question is, in future, if someone comes across this question for dealing with scroll bar visibility, they will atleast know a way to get that (in JavaFX 8).
One way to check for the scrollbar visiblity is to register the appropriate scrollbar on layoutChildren and add a listener to its visilble property. Something like...
ScrollPane scrollPane = new ScrollPane() {
ScrollBar vertical;
#Override
protected void layoutChildren() {
super.layoutChildren();
if (vertical == null) {
vertical = (ScrollBar) lookup(".scroll-bar:vertical");
vertical.visibleProperty().addListener((obs, old, val) -> updateContent(val));
updateContent(vertical.isVisible());
}
}
};
The updateContent(visible) method is stuff you want to do when the visibility gets updated.
A complete working demo is as below.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ScrollPaneScrollBarVisibility_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
BorderPane borderPane = new BorderPane();
Scene sc = new Scene(borderPane, 300, 300);
stage.setScene(sc);
stage.setTitle("ScrollBar visibility");
stage.show();
ScrollPane scrollPane = new ScrollPane() {
ScrollBar vertical;
#Override
protected void layoutChildren() {
super.layoutChildren();
if (vertical == null) {
vertical = (ScrollBar) lookup(".scroll-bar:vertical");
vertical.visibleProperty().addListener((obs, old, val) -> updateContent(val));
updateContent(vertical.isVisible());
}
}
};
scrollPane.setContent(getContent());
borderPane.setCenter(scrollPane);
}
private void updateContent(boolean scrollBarVisible) {
System.out.println("Vertical scroll bar visible :: " + scrollBarVisible);
}
private VBox getContent() {
VBox labels = new VBox();
labels.setSpacing(5);
for (int i = 0; i < 10; i++) {
labels.getChildren().add(new Label("X " + i));
}
Button add = new Button("Add");
add.setOnAction(e -> labels.getChildren().add(new Label("Text")));
Button remove = new Button("Remove");
remove.setOnAction(e -> {
if (!labels.getChildren().isEmpty()) {
labels.getChildren().remove(labels.getChildren().size() - 1);
}
});
HBox buttons = new HBox(add, remove);
buttons.setSpacing(15);
VBox content = new VBox(buttons, labels);
content.setPadding(new Insets(15));
content.setSpacing(15);
return content;
}
public static void main(String[] args) {
Application.launch(args);
}
}
As #James_D said, used GridPane and it worked without any listeners:
GridPane cards = new GridPane();
cards.setVgap(16);
cards.setHgap(16);
cards.setAlignment(Pos.CENTER);
cards.setPadding(new Insets(16));
ColumnConstraints constraints = new ColumnConstraints();
constraints.setPercentWidth(20);
constraints.setHgrow(Priority.ALWAYS);
constraints.setFillWidth(Boolean.TRUE);
cards.getColumnConstraints().addAll(constraints, constraints, constraints, constraints, constraints);
I have 5 columns, so 5 times constraints. Worked just fine.
I have a BorderPane inside a Tabpane inside a ScrollPane. The ScrollPane.ScrollBarPolicy.AS_NEEDED does work if i remove the TabPane and put the BorderPane as Content of the ScrollPane. How do i get this to work with the TabPane?
Somehow the BorderPane is able to tell the ScrollPane when to display Scrollbars and the TabPane unable to do so. I looked through the avaible Methods for the Tabpane but couldn't find any for this resizing.
Working Example:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class FXApplication extends Application {
private BorderPane border;
private GridPane inner;
private TabPane tabPane;
#Override
public void start(Stage primaryStage) {
tabPane = new TabPane();
Tab tab = new Tab("test");
tabPane.getTabs().add(tab);
border = new BorderPane();
border.setCenter(innerGrid());
tab.setContent(border);
ScrollPane scp = new ScrollPane();
scp.setFitToHeight(true);
scp.setFitToWidth(true);
scp.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scp.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
// scp.setContent(border); // this works
scp.setContent(tabPane); // this doesnt
Scene s = new Scene(scp);
primaryStage.setScene(s);
primaryStage.show();
}
private GridPane innerGrid() {
inner = new GridPane();
for(int i=0; i<11 ;i++) {
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setHgrow(Priority.SOMETIMES);
inner.getColumnConstraints().add(columnConstraints);
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setVgrow(Priority.SOMETIMES);
inner.getRowConstraints().add(rowConstraints);
}
for(int i=0; i<100 ;i++) {
inner.add(new Button("Button " + i), i/10, i%10);
}
return inner;
}
public static void main(String[] args) {
FXApplication.launch(args);
}
}
Astonishingly, the exact behavior of AS_NEEDED is unspecified. All we have is the ScrollPaneSkin to look at. The decision whether or not to show the (f.i.) horizontal bar happens in its private method determineHorizontalSBVisible()
private boolean determineHorizontalSBVisible() {
final ScrollPane sp = getSkinnable();
if (Properties.IS_TOUCH_SUPPORTED) {
return (tempVisibility && (nodeWidth > contentWidth));
}
else {
// RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
(nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
}
}
Here nodeWidth is the actual width of the content node - has been calculated previously, respecting the node's min/max widths - and contentWidth is the width available for laying out the content.
Unreadable code (for me ;) In the case of resizable content and fitting into scrollPane's content area boils down to returning true if both content's actual and min width are greater than the available width.
The minWidth makes the difference in your context: BorderPane has a min > 0, TabPane has a min == 0, so the method above always returns false.
The other way round: to allow the hbar being visible with the TabPane it needs a min, f.i. by relating it to its pref:
tabPane.setMinWidth(Region.USE_PREF_SIZE);
I am trying to wrap my head around Scroll- and Tilepanes atm, and I have come upon an issue I just cant solve without a dirty hack.
I have a horizontal TilePane that has 8 Tiles, and I set it to have 4 columns, resulting in 2 rows with 4 tiles.
That TilePane I put in an HBox, since if I put it in a StackPane it would stretch the size of the tilepane making my colum setting void. A bit weird that setting the prefColumns/Rows recalculates the size of the TilePane, rather than trying to set the actual amounts of columns/rows, feels more like a dirty hack.
Anyway, putting the HBox directly into the ScrollPane would not work either, since the Scrollbars would not appear even after the 2nd row of tiles would get cut off. Setting that HBox again in a Stackpane which I then put in a ScrollPane does the trick. Atleast until I resize the width of the window to be so small the tilepane has to align the tiles anew and a 3rd or more rows appear.
Here is the basic programm:
public class Main extends Application {
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
StackPane stack = new StackPane();
stack.getChildren().add(hbox);
ScrollPane sp = new ScrollPane();
sp.setFitToHeight(true);
sp.setFitToWidth(true);
sp.setContent(stack);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
I managed to achieve my wanted behaviour, but its more of a really dirty hack. I added a listener to the height and width of my HBox containing the TilePane and assumed that when the height changes its because the width got so small that a column was removed and a new row added. To be able to do that I put the HBox in a VBox so that it would not grow withe the height of the ScrollPane. For the width I simply calculated if there is space to display another colum (up to 4), to do it.
Here are the changes:
public class Main extends Application {
private boolean notFirstPassHeight;
private boolean notFirstPassWidth;
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
// I took the value from ScenicView
tilePane.prefTileWidthProperty().set(182);
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
ScrollPane sp = new ScrollPane();
sp.setFitToHeight(true);
sp.setFitToWidth(true);
StackPane stack = new StackPane();
VBox vbox = new VBox();
vbox.setStyle("-fx-background-color: red");
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
notFirstPassHeight = false;
notFirstPassWidth = false;
hbox.heightProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassHeight) {
tilePane.setPrefColumns(tilePane.getPrefColumns() - 1);
stack.requestLayout();
}
notFirstPassHeight = true;
});
hbox.widthProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassWidth && tilePane.getPrefColumns() <= 3
&& (newValue.doubleValue() / (tilePane.getPrefColumns() + 1)) > tilePane.getPrefTileWidth()) {
tilePane.setPrefColumns(tilePane.getPrefColumns() + 1);
stack.requestLayout();
}
notFirstPassWidth = true;
});
vbox.getChildren().add(hbox);
stack.getChildren().add(vbox);
sp.setContent(stack);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
However this approach requires me to
1.Know the Width of the Tiles in the Tilepane.
2.Consider Padding and Gap between tiles for my calculation to be accurate, which I dont do in my example.
And its just not a good approach at any rate if you ask me. Too complicated a process for such a basic thing. There has to be a way better and simple way to accomplish complete resizability and the wanted behaviour with TilePanes in a ScrollPane.
Setting the preferred number of columns and/or rows in the TilePane determines the calculation for the prefWidth and prefHeight values for that tile pane. If you want to force a maximum number of columns, you just need to make the maxWidth equal to the computed prefWidth: you can do this with
tilePane.setMaxWidth(Region.USE_PREF_SIZE);
This means that (as long as the tile pane is placed in something that manages layout), it will never be wider than the pref width, which is computed to allow the preferred number of columns. It may, of course, be smaller than that. (Note you could use the same trick with setMinWidth if you needed a minimum number of columns, rather than a maximum number of columns.)
The scroll pane's fitToHeight and fitToWidth properties will, when true, attempt to resize the height (respectively width) of the content to be equal to the height (width) of the scroll pane's viewport. These operations will take precedence over the preferred height (width) of the content, but will attempt to respect the minimum height (width).
Consequently, it's usually a mistake to call both setFitToWidth(true) and setFitToHeight(true), as this will almost always turn off scrolling completely (just forcing the content to be the same size as the scroll pane's viewport).
So here you want to make the max width of the tile pane respect the pref width, and fix the width of the tile pane to be the width of the scroll pane's viewport (so that when you shrink the width of the window, it shrinks the width of the viewport and creates more columns). This will add a vertical scrollbar if the number of rows grows large enough, and only add a horizontal scrollbar if the viewport shrinks horizontally below the minimum width of the tile pane (which is computed as the minimum of the preferred widths of all the nodes it contains).
I think the following version of your original code does essentially what you are looking for:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;
public class ScrollingTilePane extends Application {
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
// dont grow more than the preferred number of columns:
tilePane.setMaxWidth(Region.USE_PREF_SIZE);
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
// StackPane stack = new StackPane();
// stack.getChildren().add(tilePane);
// stack.setStyle("-fx-background-color: blue;");
ScrollPane sp = new ScrollPane();
// sp.setFitToHeight(true);
sp.setFitToWidth(true);
sp.setContent(hbox);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Note that if you need to change the background color of the space outside the scroll pane's content, you can use the following in an external style sheet:
.scroll-pane .viewport {
-fx-background-color: red ;
}
I have two Canvas instances in my SplitPane. When I drag the divider bar, one Canvas grows. When I drag the divider back, the Canvas doesn't really shrink. It holds the same maximum dimension as it was ever given by the resize. So I only see a part of that Canvas, however much space the SplitPane can show. Similar behavior occurs for the other Canvas. I need them to shrink back to fit into their respective sections when the divider is dragged, not just have them clipped.
The Canvases are wrapped in a one-element GridPane subclass before being added to the SplitPane. This wrapper class resizes the Canvas whenever the wrapper resizes.
The problem is that the SplitPane is giving the wrong sizes: whatever the maximum size ever was for that Canvas. Why would the SplitPane think this is the right thing to do?
package experimental;
import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class SplitPaneProblem extends Application {
static public void main(String[] args) {launch(args);}
public SplitPaneProblem() { }
#Override
public void start(Stage primaryStage) throws Exception {
SplitPane splitPane = new SplitPane();
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.getItems().add(new TestPane("0",new Canvas()));
splitPane.getItems().add(new TestPane("1",new Canvas()));
primaryStage.setScene(new Scene(splitPane));
primaryStage.setHeight(400);
primaryStage.setWidth(400);
primaryStage.show();
}
public class TestPane extends AnchorPane {
String name;
Canvas canvas;
public TestPane(String name,Canvas canvas) {
super();
this.canvas = canvas;
getChildren().add(canvas);
//1 canvas.setManaged(false);
this.name = name;
}
#Override
public void resize(double width,double height) {
System.out.println(name+" input "+width+' '+height);
super.resize(width, height);
System.out.println(name+" panel "+getWidth()+' '+getHeight());
//0 canvas.setWidth (width);
//0 canvas.setHeight(height);
System.out.println(name+" canvas "+canvas.getWidth()+' '+canvas.getHeight());
}
}
}
Canvas doesn't resize by itself. Run the code as shown, and drag the divider up and down. Read the console output. The size of the Canvas instances never changes from (0,0).
Uncomment the lines beginning with //0 and run it again. Now the Canvas instances are forcibly resized to the size of their containing panes. Drag the divider up and down. Note that the Canvas instances grow vertically, but they never shrink, even though the code tells them to. Imagine we were drawing an X across the corners of the Canvases. These X drawings would grow with the divider. But when you drag the divider the other direction, the image is clipped, but it never shrinks.
Uncomment the line beginning with //1 and run it again. Now it works.
I don't know about you, but I think this is bizarre.
Based on this post from Jasper Potts, you can create a resizable canvas, and use it in your split pane.
class ResizableCanvas extends Pane {
private final int id;
private final Canvas canvas = new Canvas();
public ResizableCanvas(int id) {
this.id=id;
getChildren().add(canvas);
}
#Override
protected void layoutChildren() {
final int top = (int)snappedTopInset();
final int right = (int)snappedRightInset();
final int bottom = (int)snappedBottomInset();
final int left = (int)snappedLeftInset();
final int w = (int)getWidth() - left - right;
final int h = (int)getHeight() - top - bottom;
canvas.setLayoutX(left);
canvas.setLayoutY(top);
if (w != canvas.getWidth() || h != canvas.getHeight()) {
canvas.setWidth(w);
canvas.setHeight(h);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, w, h);
if(id==1){
gc.setFill(Color.BLUE);
gc.fillOval(5, 5, w-10, h-10);
} else {
gc.setFill(Color.RED);
gc.fillRect(10, 10, w-20, h-20);
}
}
}
}
#Override
public void start(Stage stage) throws Exception {
ResizableCanvas canvas1 = new ResizableCanvas(1);
ResizableCanvas canvas2 = new ResizableCanvas(2);
SplitPane split = new SplitPane();
split.getItems().addAll(canvas1,canvas2);
stage.setScene(new Scene(split, 600, 400));
stage.show();
}
Note that everytime the scene is resized or the split content divider is moved, both canvases will be redrawn, but always with the exact size of their respective panes. As he also suggests, you may consider snapshot your canvas contents, and use an image to improve performance.
I want to set the same width for the buttons of a pane, but before displaying the Stage there is no button width. How can I get the width without displaying the Stage?
Example of code that does not work because width is not defined:
public static HBox createHorizontalButtonBox(final List<Button> buttons, final Pos alignment, final double spacing, final boolean sameWidth) {
HBox box = new HBox(spacing);
box.setAlignment(alignment);
box.getChildren().addAll(buttons);
if (sameWidth && buttons.size() > 1) {
double max = maxWidth(buttons);
for (Button b : buttons) {
b.setPrefWidth(max);
}
}
return box;
}
Let the layout pane do the layout for you.
If the layout pane you are using isn't giving you the exact layout you want then either:
Adjust constraints on the layout pane and its child nodes.
Compose multiple different types of layout panes.
Use a different built-in layout pane type.
Create your own layout pane type.
For your method you have a parameter indicating whether to size all of the child nodes the same size. A pane which sizes all child nodes the same size is a TilePane, so you could choose that to layout your elements. A GridPane will also work because it has configurable constraints to size elements the same size. A stock HBox won't work directly because it doesn't have a property to size all child elements the same size. You could subclass HBox to do this if you wished (by overriding layoutChildren()).
import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SameSizeButtons extends Application {
#Override
public void start(Stage stage) {
VBox layout = new VBox(
10,
createHorizontalButtonBox(
Arrays.stream("All buttons the same width".split(" "))
.map(Button::new)
.collect(Collectors.toList()),
Pos.CENTER,
10,
true
),
createHorizontalButtonBox(
Arrays.stream("All buttons different widths".split(" "))
.map(Button::new)
.collect(Collectors.toList()),
Pos.CENTER_RIGHT,
10,
false
)
);
layout.setPadding(new Insets(10));
layout.getChildren().forEach(node ->
node.setStyle("-fx-border-color: red; -fx-border-width: 1px;")
);
stage.setScene(new Scene(layout));
stage.show();
}
public static Pane createHorizontalButtonBox(
final List<Button> buttons,
final Pos alignment,
final double spacing,
final boolean sameWidth) {
return sameWidth
? createSameWidthHorizontalButtonBox(
buttons,
alignment,
spacing
)
: createDifferentWidthHorizontalButtonBox(
buttons,
alignment,
spacing
);
}
private static Pane createSameWidthHorizontalButtonBox(
List<Button> buttons,
Pos alignment,
double spacing)
{
TilePane tiles = new TilePane(
Orientation.HORIZONTAL,
spacing,
0,
buttons.toArray(
new Button[buttons.size()]
)
);
tiles.setMinWidth(TilePane.USE_PREF_SIZE);
tiles.setPrefRows(1);
tiles.setAlignment(alignment);
buttons.forEach(b -> {
b.setMinWidth(Button.USE_PREF_SIZE);
b.setMaxWidth(Double.MAX_VALUE);
});
return tiles;
}
private static Pane createDifferentWidthHorizontalButtonBox(
List<Button> buttons,
Pos alignment,
double spacing)
{
HBox hBox = new HBox(
spacing,
buttons.toArray(
new Button[buttons.size()]
)
);
hBox.setAlignment(alignment);
buttons.forEach(b ->
b.setMinWidth(Button.USE_PREF_SIZE)
);
return hBox;
}
public static void main(String[] args) {
launch(args);
}
}