JavaFX split space evenly between two side panels in an HBox - java

I have an HBox that contains a square VBox in the center. The HBox width can be made larger than it's height, leaving extra space on the sides. I want to fill this extra space on the left and right with separate VBoxes where I will put buttons, info, etc. I want these two VBoxes, the "side panels," to always be of equal width. However, as you can see from the image below, when the right panel has a button and the left does not, it is wider than the left panel:
I thought
rightPanel.minWidthProperty().bindBidirectional(leftPanel.minWidthProperty());
rightPanel.prefWidthProperty().bindBidirectional(leftPanel.prefWidthProperty());
would do the trick, but it didn't.
Here's my code:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class FX010 extends Application{
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
GamePanel gp = new GamePanel();
Scene scene = new Scene(gp, 800, 600);
primaryStage.setTitle("testing");
primaryStage.minWidthProperty().bind(primaryStage.heightProperty());
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}
public class GamePanel extends HBox{
public GamePanel() {
this.setMinHeight(400);
final VBox boardBox = new VBox();
boardBox.alignmentProperty().set(Pos.CENTER);
this.alignmentProperty().set(Pos.CENTER);
VBox leftPanel, rightPanel;
leftPanel = new VBox();
rightPanel = new VBox();
leftPanel.setBorder(new Border(new BorderStroke(Color.DARKRED, BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,new BorderWidths(1))));
rightPanel.setBorder(new Border(new BorderStroke(Color.DARKRED, BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,new BorderWidths(1))));
rightPanel.minWidthProperty().bindBidirectional(leftPanel.minWidthProperty());
rightPanel.prefWidthProperty().bindBidirectional(leftPanel.prefWidthProperty());
rightPanel.getChildren().add(new Button("testtesttest"));
HBox.setHgrow(leftPanel, Priority.ALWAYS);
HBox.setHgrow(rightPanel, Priority.ALWAYS);
StackPane board = new StackPane(new Rectangle(100,100,Color.RED));
board.setBorder(new Border(new BorderStroke(Color.DARKBLUE, BorderStrokeStyle.DASHED,
CornerRadii.EMPTY,new BorderWidths(1))));
final NumberBinding binding = Bindings.min(widthProperty(), heightProperty());
boardBox.prefWidthProperty().bind(binding);
boardBox.prefHeightProperty().bind(binding);
boardBox.setMaxSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
VBox.setVgrow(board, Priority.ALWAYS);
boardBox.getChildren().add(board);
getChildren().addAll(leftPanel, boardBox, rightPanel);
//HBox.setHgrow(this, Priority.ALWAYS);
}
}
}
How can I force leftPanel to always have the same width as rightPanel, while still ensuring that boardBox is always a square?
Thanks!

It works when you add e. g.:
leftPanel.setPrefWidth(100d);
rightPanel.setPrefWidth(100d);
Edit: Maybe it would be better if you use a GridPane as a root layout where you can have three columns with a fixed percentage width like e. g.: 15 % (left-hand side), 70 % (center) and 15 % (right-hand side). But only you can know and decide what fits best for your project. :-P

After a lot of testing, I got this to work:
public class GamePanel extends HBox{
Pane leftPanel, rightPanel, iLeft, iRight;
VBox boardBox;
public GamePanel() {
this.setMinHeight(400);
iLeft = new Pane();
boardBox = new VBox();
iRight = new Pane();
boardBox.alignmentProperty().set(Pos.CENTER);
this.alignmentProperty().set(Pos.CENTER);
leftPanel = new Pane();
rightPanel = new Pane();
leftPanel.setBorder(new Border(new BorderStroke(Color.DARKRED, BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,new BorderWidths(1))));
rightPanel.setBorder(new Border(new BorderStroke(Color.DARKRED, BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,new BorderWidths(1))));
rightPanel.setPrefWidth(0);
leftPanel.setPrefWidth(0);
iRight.getChildren().add(new Button("testtesttest"));
rightPanel.getChildren().add(iRight);
HBox.setHgrow(leftPanel, Priority.ALWAYS);
HBox.setHgrow(rightPanel, Priority.ALWAYS);
StackPane board = new StackPane(new Rectangle(100,100,Color.RED));
board.setBorder(new Border(new BorderStroke(Color.DARKBLUE, BorderStrokeStyle.DASHED,
CornerRadii.EMPTY,new BorderWidths(1))));
final NumberBinding binding = Bindings.min(widthProperty(), heightProperty());
boardBox.prefWidthProperty().bind(binding);
boardBox.prefHeightProperty().bind(binding);
boardBox.setMaxSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
VBox.setVgrow(board, Priority.ALWAYS);
boardBox.getChildren().add(board);
getChildren().addAll(leftPanel, boardBox, rightPanel);
//HBox.setHgrow(this, Priority.ALWAYS);
}
}
This solution involves setting the left and right panel's prefWidth to zero, then adding internal panes into each panel, which the content is then added to. I think the reason that this works is because space is allocated for the prefWidth before the panel's size is expanded by the Hgrow constraint. Since equal amounts of space are added to each panel by the HBox, the panel with a greater prefWidth ends up bigger. By setting both to zero, they both start with no prefWidth and thus end up at the same size. I believe that's also why anko's answer seemed to work - because when they start with equal prefWidths and the same amount of space is added to each, they end up with the same size. However, setting a prefWidth of 100 messes with the boardBox's size (makes it not a square) when the stage's width is shrunk as small as possible.
I'm sure there is a better way to do this, and I'd be happy to accept a better answer.

Related

How to replicate this 2-1 layout in JavaFX?

I've looked into all of the layout options in the docs, and I am not allowed to use FXML. I don't understand where to start with configuring the 3 containers in the scene.
There are many ways you can do this; probably the most immediate is to put the two panes on the left in a VBox, and then use a BorderPane, with the VBox on the left and the "CCTV" pane in the center. This looks like:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class LayoutExample extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Pane lighting = new Pane();
lighting.getChildren().add(new Label("Lighting"));
Pane heating = new Pane();
heating.getChildren().add(new Label("Heating"));
Pane cctv = new Pane();
cctv.getChildren().add(new Label("CCTV"));
lighting.getStyleClass().add("control-pane");
heating.getStyleClass().add("control-pane");
cctv.getStyleClass().add("control-pane");
BorderPane root = new BorderPane() ;
VBox left = new VBox();
left.getChildren().add(lighting);
left.getChildren().add(heating);
// expand both panes in left to full width of vbox:
left.setFillWidth(true);
// add vertical space between panes:
left.setSpacing(5);
// allocate extra vertical space equally to both panes in left:
VBox.setVgrow(lighting, Priority.ALWAYS);
VBox.setVgrow(heating, Priority.ALWAYS);
root.setLeft(left);
root.setCenter(cctv);
// Add a left margin to the center pane to give it some space:
BorderPane.setMargin(cctv, new Insets(0, 0, 0, 10));
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("layout-style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setWidth(800);
primaryStage.setHeight(640);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
This CSS isn't quite right, but gives you an idea how to add the borders:
.control-pane {
-fx-background-color: -fx-body-color, -fx-outer-border, -fx-body-color ;
-fx-background-insets: 0, 1, 2 ;
-fx-background-radius: 0, 1, 0 ;
-fx-padding: 3 ;
/** Just to demo empty boxes: **/
-fx-min-width: 300px ;
-fx-min-height: 300px ;
}
This looks like:
You could also use a HBox as the root, and set the hgrow on each to prioritize extra horizontal space on the CCTV pane. Or use a GridPane, and set the row span of the CCTV pane to 2. There are probably many other ways to do this.

GridPane doesn't align in center

I am making a Login Screen with a number pad, and I can't seem to center align a GridPane of buttons in a Pane. What am I doing wrong?
Main.java
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCombination;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args){
launch(args);
}
public void start(Stage primaryStage) throws Exception{
Rectangle2D bounds = Screen.getPrimary().getBounds();
LoginScreen loginScreen = new LoginScreen(bounds.getWidth(), bounds.getHeight());
Scene scene = new Scene(loginScreen.get());
primaryStage.setScene(scene);
primaryStage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
primaryStage.show();
primaryStage.setFullScreen(true);
}
}
LoginScreen.java
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
public class LoginScreen {
private Pane root;
private GridPane numberPad;
public LoginScreen(double screenWidth, double screenHeight){
root = new Pane();
root.setPrefSize(screenWidth, screenHeight);
root.setBackground(new Background(new BackgroundFill(Color.AQUA, CornerRadii.EMPTY, Insets.EMPTY)));
numberPad = new GridPane();
Button button01 = new Button("1");
Button button02 = new Button("2");
Button button03 = new Button("3");
Button button04 = new Button("4");
Button button05 = new Button("5");
Button button06 = new Button("6");
Button button07 = new Button("7");
Button button08 = new Button("8");
Button button09 = new Button("9");
numberPad.setAlignment(Pos.CENTER);
numberPad.add(button01, 0, 0);
numberPad.add(button02, 1, 0);
numberPad.add(button03, 2, 0);
numberPad.add(button04, 0, 1);
numberPad.add(button05, 1, 1);
numberPad.add(button06, 2, 1);
numberPad.add(button07, 0, 2);
numberPad.add(button08, 1, 2);
numberPad.add(button09, 2, 2);
root.getChildren().addAll(numberPad);
}
public Pane get(){
return root;
}
}
GUI code is verbose, and this post editor isn't letting me post my question as is, so I need these extra lines to get it to accept my question. If I thought I could cut down my code to just the numberPad.setAlignment(Pos.Center) and still make it clear how I am attempting to center my GridPane I most certainly would. I do humbly thank those who might lend me their time to help me solve this issue I have.
Edit 01:
My issue is that the GridPane itself is drawn in the top left corner of the screen rather than in the center of the screen.
You need to actually set the alignment for the parent container. A Pane is not a valid container for doing this, however.
If you were to use a VBox instead, you could simply set its alignment as so:
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
That will cause all of the children of the VBox to be placed in the center.
The Pos enum also provides other methods of positioning, including TOP_CENTER, TOP_LEFT, and BOTTOM_RIGHT, for example.

Divide stage into 2 gridpanes JavaFX

So Im trying to have text on the left and buttons on the right, text should have constant size and buttons should resize to fill the rest of the window.
Here is my result so far:
I dont want my text over buttons, I want them to share the whole window.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
GridPane buttons = new GridPane();
GridPane textGrid = new GridPane();
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Button button1 = new Button();
Button button2 = new Button();
Button button3 = new Button();
Button button4 = new Button();
Button button5 = new Button();
button1.setText("Button1");
button2.setText("Button4");
button3.setText("Button3");
button4.setText("Button4");
button5.setText("Button5");
TextArea text1 = new TextArea();
text1.setText("Test");
text1.setPrefSize(100, 100);
button1.prefWidthProperty().bind(buttons.widthProperty());
button2.prefWidthProperty().bind(buttons.widthProperty());
button3.prefWidthProperty().bind(buttons.widthProperty());
button4.prefWidthProperty().bind(buttons.widthProperty());
button5.prefWidthProperty().bind(buttons.widthProperty());
button1.prefHeightProperty().bind(buttons.heightProperty());
button2.prefHeightProperty().bind(buttons.heightProperty());
button3.prefHeightProperty().bind(buttons.heightProperty());
button4.prefHeightProperty().bind(buttons.heightProperty());
button5.prefHeightProperty().bind(buttons.heightProperty());
buttons.addColumn(0, button1, button2, button3, button4, button5);
textGrid.addColumn(0, text1);
Scene scene = new Scene(root, 280, 180);
root.getChildren().addAll(buttons, textGrid);
buttons.setAlignment(Pos.TOP_RIGHT);
textGrid.setAlignment(Pos.TOP_LEFT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
It is usually better to let the layout panes handle the layout management rather than trying to manage the layout through bindings.
Here is a sample:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.stream.IntStream;
public class Main extends Application {
private static final int N_BUTTONS = 5;
#Override
public void start(Stage stage) {
VBox buttonLayout = new VBox(
10,
IntStream.range(0, N_BUTTONS)
.mapToObj(this::createButton)
.toArray(Button[]::new)
);
HBox.setHgrow(buttonLayout, Priority.ALWAYS);
TextArea textArea = new TextArea("Test");
textArea.setPrefWidth(100);
textArea.setMaxWidth(TextArea.USE_PREF_SIZE);
textArea.setMinWidth(TextArea.USE_PREF_SIZE);
HBox layout = new HBox(10, textArea, buttonLayout);
layout.setPadding(new Insets(10));
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private Button createButton(int i) {
Button button = new Button("Button " + i);
// button.setMaxWidth(Double.MAX_VALUE);
button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
VBox.setVgrow(button, Priority.ALWAYS);
return button;
}
public static void main(String[] args) {
launch(args);
}
}
Here are a couple of things I would point out based upon the sample:
As the buttons are so similar, create the buttons in a loop rather than individually in code. I use an IntStream range with a map and a toArray, but you could do the same thing with a standard for loop (which may be easier to understand).
Use combinations of standard layout panes to achieve your layout. For example the buttons are vertically spaced, so put them in a VBox, the text and the buttons are horizontal to each other, so use a HBox.
Use constraints on the layouts to massage them into performing the layout you like, for example, HBox.setHgrow(buttonLayout, Priority.ALWAYS); tells the Box to always assign any extra additional space in the Box to the buttonLayout so that the buttons will fill any remaining area.
Set constraints on the individual nodes to size them how you wish, for example the following code establishes a fixed width for the textArea, which will not vary (you could similar code to establish a fixed height if you wished):
textArea.setPrefWidth(100);
textArea.setMaxWidth(TextArea.USE_PREF_SIZE);
textArea.setMinWidth(TextArea.USE_PREF_SIZE);
Some controls will automatically expand themselves beyond their max size, buttons do not by default, to enable this behavior use the following code (if you only wanted the width to expand and not the height then you would only set the maxWidth rather than the maxSize):
button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
Rather than defining layouts in code as in this example, instead use a tool such as SceneBuilder to create the scene visually and save the layout as an FXML file, so that the layout is separated from your code (similarly place any styling in an external CSS file).

JavaFX ScrollPane [ setPrefSize, setMinSize, setMaxSize ] not working

I'm writing a GUI application with a ScrollPane, but had some issues with resizing. I extracted the essential code in the following example:
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import javafx.scene.layout.VBox;
import javafx.scene.control.ScrollPane;
public class JavaFXExample extends Application {
final int width = 300;
final int height = 300;
#Override
public void start(Stage primaryStage) {
Button b = new Button("This should be at the bottom!");
//this vbox goes inside the scrollpane
VBox boxInScrollPane = new VBox(10);
boxInScrollPane.setAlignment(Pos.BOTTOM_CENTER);
boxInScrollPane.getChildren().add(b);
//main content
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(boxInScrollPane);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
//Doesn't do anything!
scrollPane.setPrefSize(100, 100);
scrollPane.setMaxSize(100, 100);
scrollPane.setMinSize(100, 100);
Scene scene = new Scene(scrollPane, width, height);
primaryStage.setScene(scene);
primaryStage.show();
//set size of boxInScrollPane to be equal to the viewport
Bounds viewportBounds = scrollPane.getViewportBounds();
double innerWidth = viewportBounds.getMaxX() - viewportBounds.getMinX();
double innerHeight = viewportBounds.getMaxY() - viewportBounds.getMinY();
System.out.println(innerWidth + " " + innerHeight);
boxInScrollPane.setPrefSize(innerWidth, innerHeight);
}
public static void main(String[] args) {
launch(args);
}
}
So I have a window, which contains a ScrollPane, which contains a VBox, which contains a button. The example here, where I resize the scrollpane to be 100x100px in a 300x300px window, is arbitrary. What's important is that when I run this code, I get a scrollpane that fills the entire window! Here's my output:
What's going on here?
The root of the scene is sized to fill the entire scene, irrespective of its min/pref/max size. If you want the ScrollPane to remain 100 pixels wide and 100 pixels high, wrap it in another container (pretty much any container will do); the container will then be resized, but the ScrollPane will respect its layout sizes:
Scene scene = new Scene(new StackPane(scrollPane), width, height);
By default a StackPane centers its content, so this results in
Solution:
Pane pane = new Pane(scrollPane);
Scene scene = new Scene(pane, width, height);
primaryStage.setScene(scene);
primaryStage.show();
From Scene constuctor doc:
Creates a Scene for a specific root Node with a specific size.
Setting ScrollPane as root node will make it resize to given size in constructor so the previous settings will not work.
Solution will be to make a simple pane that will be resized so ScrollPane will be on his own rules.

JavaFX 8: Stage insets (window decoration thickness)?

How can i determine the stage/window insets in JavaFX? In Swing i could simple write:
JFrame frame = new JFrame();
Insets insets = frame.getInsets();
What would be the equivalent in JavaFX to get the size of the border and the titlebar of the window?
You can determine these by looking at the bounds of the scene relative to the width and height of the window.
Given a Scene scene;, scene.getX() and scene.getY() give the x and y coordinates of the Scene within the window. These are equivalent to the left and top insets, respectively.
The right and bottom are slightly trickier, but
scene.getWindow().getWidth()-scene.getWidth()-scene.getX()
gives the right insets, and similarly
scene.getWindow().getHeight()-scene.getHeight()-scene.getY()
gives the bottom insets.
These values will of course only make sense once the scene is placed in a window and the window is visible on the screen.
If you really want an Insets object you can do something like the following (which would even stay valid if the border or title bar changed size after the window was displayed):
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class WindowInsetsDemo extends Application {
#Override
public void start(Stage primaryStage) {
Label topLabel = new Label();
Label leftLabel = new Label();
Label rightLabel = new Label();
Label bottomLabel = new Label();
VBox root = new VBox(10, topLabel, leftLabel, bottomLabel, rightLabel);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 600, 400);
ObjectBinding<Insets> insets = Bindings.createObjectBinding(() ->
new Insets(scene.getY(),
primaryStage.getWidth()-scene.getWidth() - scene.getX(),
primaryStage.getHeight()-scene.getHeight() - scene.getY(),
scene.getX()),
scene.xProperty(),
scene.yProperty(),
scene.widthProperty(),
scene.heightProperty(),
primaryStage.widthProperty(),
primaryStage.heightProperty()
);
topLabel.textProperty().bind(Bindings.createStringBinding(() -> "Top: "+insets.get().getTop(), insets));
leftLabel.textProperty().bind(Bindings.createStringBinding(() -> "Left: "+insets.get().getLeft(), insets));
rightLabel.textProperty().bind(Bindings.createStringBinding(() -> "Right: "+insets.get().getRight(), insets));
bottomLabel.textProperty().bind(Bindings.createStringBinding(() -> "Bottom: "+insets.get().getBottom(), insets));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Categories