Lesson 78Elective: Desktop apps with JavaFX
Nodes, controls, and layout panes
The Node class family, choosing a root node, and the controls and panes this module's programs are built from.
By BasithUpdated 60 min
- Node
- Parent
- Group
- Region
- Pane
- Control
Every JavaFX program in this course is assembled from the same small set of parts. The registration form, the four calculator programs and the two database screens share about a dozen classes, and nearly all of them hang from one family tree that starts at Node. This lesson draws that tree, explains the choice of root that decides how a window behaves when it is resized, and then takes each part the programs use in turn. When those parts turn up in a hundred-line listing, you will already know what every line is for.
- Draw the Node class hierarchy and say what each class adds to the one above it.
- Choose a root for a scene (a
Group, aRegionor aWebView) and predict what the window does when it is resized. - Say how each common pane arranges its children, and pick the right one for a layout.
- Create, read and change every control the JavaFX programs here use, from
LabeltoAlert. - Place nodes in a
GridPaneby column and row, with spans, gaps, column constraints and margins. - Read
Insets,PosandHPosvalues at a glance.
start method from your first JavaFX application (build a scene graph, put it in a Scene, put the scene on the Stage) and you have everything this lesson needs.The Node family tree#
Two words carry this whole lesson. A scene graph is the tree of everything in one window. A node is one object in that tree: a shape, a control, a pane, an image. The class Node, in the package javafx.scene, is the superclass of all of them, and the diagram below draws the family.
Read each arrow as “extends”. Canvas extends Node. Group extends Parent. Control extends Region, which extends Parent, which extends Node. Two boxes carry informal names: there is no class called Shape2D (the two-dimensional shapes extend Shape, in javafx.scene.shape), and the three-dimensional box is the class Shape3D, written without a space.
| Box | What it adds to the class above it | Examples |
|---|---|---|
Node, in javafx.scene | The root of everything visible. A node has a position, can be hidden, faded, rotated or scaled, given an effect such as a shadow, styled with CSS, and can react to the mouse and the keyboard. Abstract. | None directly: you always create a subclass. |
Shape2D, really Shape, in javafx.scene.shape | A geometric figure with a fill colour and an outline (its stroke). Abstract. | Circle, Rectangle, Ellipse, Line, Polygon, and Text |
Canvas, in javafx.scene.canvas | A blank picture of fixed size that you draw on with commands such as “fill this rectangle”. However much you draw, it stays one node. | A drawing area, a simple game board |
ImageView, in javafx.scene.image | Shows an Image loaded from a file or a web address. | A photo, a logo |
Shape 3D, really Shape3D, in javafx.scene.shape | A solid in three dimensions, lit and shaded. Abstract. | Box, Sphere, Cylinder |
Parent, in javafx.scene | A list of child nodes, and the job of laying them out. Abstract. | None directly |
Group, in javafx.scene | Lets you add children through getChildren(). Draws them in order and applies its own effects and transforms to all of them, but never moves them or stretches them (a resizable child simply gets its preferred size), and cannot be resized itself. | The root in your first JavaFX programs |
Region, in javafx.scene.layout | A size that can change (a minimum, preferred and maximum width and height), plus padding, a background and a border, all styleable with CSS. | new Region() as an empty spacer |
WebView, in javafx.scene.web | A web browser engine inside the scene, showing pages written in HTML, CSS and JavaScript. | A help page inside an application |
Chart, in javafx.scene.chart | Draws data, with a title and a legend. Abstract. | PieChart, BarChart, LineChart |
Pane, in javafx.scene.layout | Lets you add children through getChildren(), which Region keeps to itself. Each subclass adds one rule for arranging them. | VBox, HBox, GridPane, StackPane, BorderPane |
Control, in javafx.scene.control | Something the user works with. It takes keyboard focus (Tab moves between controls), can show a tooltip, and gets its look from a skin that CSS can restyle. Abstract. | Button, Label, TextField, PasswordField, TextArea |
The word “tree” gets used for two different things in JavaFX, and it pays to keep them apart. The diagram above is a tree of classes: it says that a Button is a kind of Control. A scene graph is a tree of objects: it says that this particular VBox holds this particular Button. The three types of node, root, branch and leaf, are positions in the second tree, and the first tree decides who may stand where. Only a Parent has a list of children, so only a Group, a Region or a WebView can be a root or a branch. Nodes such as a Rectangle, an Ellipse, a Box, an ImageView and a MediaView can only ever be leaves. The root is the first node of the scene graph, the one at the top.
One row deserves a second look. The Textclass that drew “Welcome to Ooty” in your first JavaFX application lives in javafx.scene.text, but it extends Shape, so in the hierarchy it belongs under the Shape2D box beside Circle and Rectangle. That is why a Text is placed with setX and setY like any shape, and why it has a fill colour. A Label looks much the same on screen but is a Control, and the difference shows as soon as either one goes into a layout pane. The two are compared side by side further down.
Six of the boxes are abstract classes: Node, Parent, Shape, Shape3D, Chart and Control. Writing new Node() or new Control() does not compile. They exist to hold what their subclasses share, exactly like the abstract classes you wrote yourself. Group, Region, Pane, Canvas, ImageView and WebView are concrete, so you can create them directly.
The tree is also why JavaFX code reads so evenly. A method whose parameter type is Node accepts an object of any class on it, because of upcasting: a Button is a Control, which is a Region, which is a Parent, which is a Node. So root.getChildren().add(...), new VBox(10, ...), grid.add(..., 0, 1) and borderPane.setCenter(...) all take a button, a text, an image, or a whole GridPane full of controls, through the same parameter. The Scene constructor is stricter. Its first parameter is a Parent, so only the Parent branch can be a root, and that branch holds exactly three kinds of box.
Key ideaEverything you can see is a Node, so anything that accepts a Node accepts all of it. A scene's root must be a Parent, which leaves three kinds: a Group, a Region, or a WebView.
Choosing a root: Group, Region or WebView#
Every scene has exactly one root, the node at the top of its scene graph, and it is not optional: Scene has no constructor without a root, and new Scene(null) throws a NullPointerException. The root can be a Group, a Region or a WebView. Your first JavaFX application used a Group. Which one you pick decides what happens when the user drags the edge of the window.
A Group root leaves everything where you put it#
The rule for a Group root has two halves: every node is clipped to the scene, and changing the size of the scene does not change the layout.
- Clipped to the scene.The window shows the rectangle from (0, 0) to the scene's width and height, and nothing outside it. A
Textplaced at x = 550 in a 600-wide scene shows its first letter or so and is cut off at the right edge. There is no scroll bar: the rest is simply not drawn. - Resizing does not change the layout. A
Groupnever moves its children and never stretches them. Each one stays at the coordinates it was given, or at (0, 0) if it was given none, at its preferred size. TheGroupcannot be resized either, because its size is just the box around its children. Make the window bigger and the new space stays empty; make it smaller and the window edge cuts into your nodes.
A Region root follows the window#
If the root is a Region (any pane, any control, a chart), the scene resizes it to fill the window every time the window changes size, and the region then re-arranges its children by its own rule. A StackPane re-centres them, a VBox stretches them across the new width, and a GridPane hands the extra width to a column that is allowed to grow. That is why every program after your first one uses a pane as its root: a GridPane for the registration form and the calculators, a VBox for the database screens.
The third choice, a WebView root, turns the whole window into a web page. A WebView resizes with the window too, and the page reflows to the new width.
One experiment shows the difference#
Here is a button in a Group root, in a window the user can resize.
import javafx.application.Application;import javafx.scene.Group;import javafx.scene.Scene;import javafx.scene.control.Button;import javafx.stage.Stage; public class GroupRoot extends Application { @Override public void start(Stage stage) { Button button = new Button("Click me"); Group root = new Group(button); Scene scene = new Scene(root, 300, 200); stage.setTitle("Group root"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); }}A 300 by 200 window titled "Group root". The button sits in the top-left corner at its natural size. Drag the window out to twice the size: the button stays exactly where it was, and the new space is empty white.
Now change the two lines that mention Group, and nothing else.
import javafx.scene.layout.StackPane; // instead of javafx.scene.Group StackPane root = new StackPane(button); // instead of new Group(button)The button sits in the middle of the window. Drag the window out and the button moves with it, staying in the middle. It keeps its natural size, because a Button does not grow past its preferred size unless you raise its maximum.
Key ideaA Group root keeps the layout you built, whatever the window does. A Region root rebuilds the layout every time the window changes size, which is what a form needs.
The Region family: panes, controls and charts#
Region is the base class for the resizable parts of a window: almost every layout pane and control inherits from it. The properties it gives them are these, and every pane and control you use has them because it inherits them.
- Three sizes in each direction: a minimum, a preferred and a maximum width (
setMinWidth,setPrefWidth,setMaxWidth), and the same for height. A pane asks each child for these and chooses its size between them. The form'ssetPrefHeight(40)on its text fields is this. - Padding: empty space between the region's edge and its contents, set with
setPadding(new Insets(...)). - A background and a border, which you can set from Java or with CSS, for example
pane.setStyle("-fx-background-color: white; -fx-border-color: gray;").
Region is concrete, so new Region() works. On its own it is an empty box, and its one everyday use is as an invisible spacer that soaks up spare room in a row.
Pane and the layout panes#
Pane, in javafx.scene.layout, is the base class of the layout panes: AnchorPane, BorderPane, GridPane, FlowPane, StackPane, TilePane and more. A plain Pane arranges nothing: like a Group, it leaves each child at its own coordinates, although unlike a Group it can be resized and styled. Each subclass adds one rule for arranging children, so choosing a pane means choosing the rule.
| Pane | How it arranges its children | Typical use |
|---|---|---|
AnchorPane | Pins a child a fixed distance from the edges you choose, with calls such as AnchorPane.setTopAnchor(node, 10.0). An anchored side follows its edge when the window is resized. | A button that must stay in the bottom-right corner. Scene Builder's default root. |
BorderPane | Five regions: top, bottom, left, right and centre. The centre gets all the space the other four do not use. | The outline of a whole application: toolbar on top, status line at the bottom, content in the middle. |
GridPane | Rows and columns. Each child goes in a cell given as (column, row) and may span several cells. | Forms, with labels in one column and fields in the next: the registration form and every calculator. |
FlowPane | Left to right in a row, wrapping onto a new row when it reaches the edge, like words in a paragraph. | A set of tags or buttons that should wrap when the window narrows. |
StackPane | All children in the same space, one on top of another, centred by default. Later children are drawn over earlier ones. | Centring one node in the window, or a caption over a picture. |
TilePane | Like FlowPane, but every cell (tile) is the same size, the size of the largest child. | A keypad, a grid of thumbnails. |
VBox | One column, top to bottom, with a fixed gap between neighbours. | StudentApp's column of buttons above its output area. |
HBox | One row, left to right, with a fixed gap between neighbours. | A row of buttons, such as Save beside Cancel. |
Here is code for one of these panes. A StackPane puts its children one on top of another, like a stack of cards.
//Creating a Stack PaneStackPane pane = new StackPane(); //Adding text area to the paneObservableList list = pane.getChildren();list.add(NodeObject);Nothing to see on its own: NodeObject stands for whichever node you add, and the pane still has to become the root of a Scene.
pane.getChildren() hands back the pane's live list of children, an ObservableList, and adding a node to that list puts the node on the pane. These are the same two lines your first JavaFX application used with a Group. The comment mentions a text area, but NodeObject is a placeholder for any node. (Because the list is written without <Node> after it, javac prints a harmless note about unchecked operations, the same note DisplayingText produced.) Here is a complete program with two children, so you can see the stacking.
import javafx.application.Application;import javafx.scene.Scene;import javafx.scene.layout.StackPane;import javafx.scene.paint.Color;import javafx.scene.shape.Rectangle;import javafx.scene.text.Font;import javafx.scene.text.Text;import javafx.stage.Stage; public class StackDemo extends Application { @Override public void start(Stage stage) { Rectangle card = new Rectangle(220, 100, Color.LIGHTBLUE); Text label = new Text("On top"); label.setFont(new Font(24)); StackPane pane = new StackPane(); pane.getChildren().addAll(card, label); // card first, so it is underneath stage.setTitle("StackPane"); stage.setScene(new Scene(pane, 400, 250)); stage.show(); } public static void main(String[] args) { launch(args); }}A 400 by 250 window titled "StackPane". A light blue 220 by 100 rectangle sits in the centre, with the words "On top" centred on it. Resize the window and both stay in the centre.
Order matters: the rectangle is first in the list, so it is drawn first and the text lands on top of it. Swap the two in addAll and the rectangle hides the text. With a text area as the only child, the area would fill the whole window, because a StackPane stretches each resizable child to its own size, as far as the child's maximum allows, and centres whatever it cannot stretch. A Rectangle and a Text are not resizable, so they keep their size and sit in the middle.
Control and the common controls#
The other big branch under Region is Control, in javafx.scene.control: the base class of everything the user clicks, types into or picks from, such as Button, Label, TextField, CheckBox, ComboBox, ChoiceBox and Accordion.
| Control | What the user sees and does | How your code reads it |
|---|---|---|
Button | A push button with a caption. Clicking it fires an action. | Nothing to read: setOnAction(...) runs your code when it is pressed. |
Label | A piece of text the user cannot edit, usually naming the field beside it. | getText() |
TextField | One line of text the user can type into. | getText() returns a String |
CheckBox | A small box with a caption, ticked or not. | isSelected() returns a boolean |
ComboBox | A drop-down list. It can also be made editable, so the user may type a value instead. | getValue() returns the chosen item, or null if none |
ChoiceBox | A simpler drop-down for a short, fixed list. | getValue(), or null if none |
Accordion | A stack of titled panels. Opening one closes the panel that was open. | getExpandedPane() returns the open panel |
Button save = new Button("Save");Label nameLabel = new Label("Name:");TextField nameField = new TextField();CheckBox agree = new CheckBox("I agree to the terms"); ComboBox<String> branch = new ComboBox<>();branch.getItems().addAll("CSE", "IT", "ECE"); ChoiceBox<String> year = new ChoiceBox<>();year.getItems().addAll("1", "2", "3", "4"); Accordion help = new Accordion( new TitledPane("Login", new Label("Use your school email.")), new TitledPane("Fees", new Label("Pay before the 10th.")));Placed in a VBox, top to bottom: a Save button, the text "Name:", an empty field, an unticked box captioned "I agree to the terms", a drop-down that lists CSE, IT and ECE when opened, a smaller one listing 1 to 4, and two closed panels titled Login and Fees.
The <String> after ComboBox and ChoiceBox says what kind of item the list holds, so getValue() hands back a String. getItems() is the list of choices, and addAll fills it. Of these seven, the programs in this course use Button, Label and TextField, and the next section covers those three properly.
Chart: PieChart and XYChart#
Chart, in javafx.scene.chart, is the base class of every chart. It has two subclasses, PieChart and XYChart. The charts drawn on x and y axes, AreaChart, BarChart, BubbleChart and the rest, all extend XYChart. PieChart has no subclasses: a pie has no axes.
AreaChart: values joined by a line, with the area underneath filled in.BarChart: one bar per category, its height showing the value.BubbleChart: points drawn as circles, whose size shows a third value.LineChart: points joined by straight lines, good for change over time.ScatterChart: points with no lines between them.
The XY charts need two axes, such as a CategoryAxis for names along the bottom and a NumberAxis for values up the side (new BarChart<>(new CategoryAxis(), new NumberAxis())), and they take their points as XYChart.Data objects grouped into an XYChart.Series. A PieChart needs none of that, which makes it the quickest chart to try.
import javafx.application.Application;import javafx.scene.Scene;import javafx.scene.chart.PieChart;import javafx.stage.Stage; public class BranchChart extends Application { @Override public void start(Stage stage) { PieChart chart = new PieChart(); chart.setTitle("Students per branch"); chart.getData().addAll( new PieChart.Data("CSE", 60), new PieChart.Data("IT", 30), new PieChart.Data("ECE", 30)); stage.setTitle("PieChart"); stage.setScene(new Scene(chart, 500, 400)); stage.show(); } public static void main(String[] args) { launch(args); }}A 500 by 400 window. The title "Students per branch" runs across the top. Below it, a circle is cut into three coloured slices: CSE takes half the circle, IT and ECE a quarter each. Each slice is labelled with its name, and a legend underneath matches colours to names.
The values are relative. The chart adds them up (120) and gives each slice its share of the circle, so CSE's 60 becomes half the pie. Because a Chart is a Region, the chart can be the scene's root, as here, or sit inside any pane, and it redraws itself at the new size when the window is resized. PieChart.Data is a class nested inside PieChart, the same arrangement as Alert.AlertType later in this lesson.
WebView: a browser inside the scene#
A WebView shows a web page inside your window. It is a Parent but not a Region, which is why the hierarchy draws it beside Group and Region rather than under either.
WebView browser = new WebView();browser.getEngine().load("https://openjfx.io"); stage.setTitle("Browser");stage.setScene(new Scene(browser, 900, 600));stage.show();A 900 by 600 window showing the openjfx.io home page, given an internet connection. Links work, and the page reflows when the window is resized.
The work is split in two. getEngine() returns a WebEngine, which is not a node: it loads the page and runs its JavaScript. The WebView is the node that shows what the engine produces. The engine underneath is WebKit, the WebView box in the architecture diagram of the JavaFX overview. It lives in its own module, so both commands need it added: --add-modules javafx.controls,javafx.web.
The controls the programs use#
From the registration form to the factorial program, the JavaFX programs in this course put just seven kinds of thing on screen: Label, Text, TextField, PasswordField, TextArea, Button and Alert, with Font to size the text. All of them are in javafx.scene.control, except Text, Font and FontWeight, which are in javafx.scene.text.
Label, Text and Font#
// Label: a control that shows text the user cannot editLabel nameLabel = new Label("Full Name : ");nameLabel.setText("Name : "); // change it laterString shown = nameLabel.getText(); // "Name : " // Text: a shape whose outline happens to be lettersText text = new Text("Welcome to Ooty");text.setFont(new Font(45)); // size 45, default font familytext.setX(50); // where the text startstext.setY(150); // the baseline the letters sit on // The registration form's heading: bold Arial, size 24Label headerLabel = new Label("Registration Form");headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24));Once these are in a scene: nameLabel reads "Name : " in the normal font. The Text draws "Welcome to Ooty" in large letters, starting 50 pixels from the left with its baseline 150 pixels down (when its parent is a Group). The heading is bold Arial at size 24.
new Font(45)uses the constructor that takes only a size, so the text keeps the system's default font family. DisplayingText used exactly this.Font.font("Arial", FontWeight.BOLD, 24)is a static factory method. The plain constructors have no weight parameter, so this is how you ask for bold: family Arial, weight bold, size 24. If Arial is not installed, JavaFX picks the closest font it has instead of failing.FontWeightis an enum with nine weights, fromTHINtoBLACK.NORMALandBOLDare the two you will use.
Shape, in javafx.scene.text. Always a leaf.Control, in javafx.scene.control, and so a Region.setX and setY, the start of its baseline. A layout pane overrides both, because the pane does the placing.setWrappingWidth.Region, and an optional picture (a graphic) beside the text.Group.The rule of thumb follows from the table: inside a layout pane, use a Label; to draw words at exact coordinates in a Group, use a Text.
TextField, PasswordField and TextArea#
TextField nameField = new TextField();nameField.setPrefHeight(40); // taller than the defaultString name = nameField.getText(); // what the user typed, "" if nothingnameField.setText("Asha Verma"); // replace the contents TextField resultField = new TextField();resultField.setPromptText("Result"); // grey hint while the field is emptyresultField.setEditable(false); // the program writes here, the user cannot PasswordField passwordField = new PasswordField();String password = passwordField.getText(); // the real characters, not the dots TextArea output = new TextArea();output.setText("Students:\n"); // many lines are allowedoutput.appendText("1 - John - 20\n"); // add to the endnameField is 40 pixels tall. resultField shows a grey "Result" until the program writes into it, and ignores the keyboard. passwordField shows one dot for each character typed. output shows two lines: "Students:" and "1 - John - 20".
getText()always returns aString, and an empty one ("") when nothing has been typed. The registration form'sisEmpty()checks rely on exactly that.setPromptText("Result")shows a grey hint while the field is empty. It is not the field's text:getText()still returns"".setEditable(false)turns a field into a display. The user can select and copy its text but not change it, while the program can still callsetText. The calculators show their answers this way.PasswordFieldextendsTextField, so every method above works on it. It draws a dot for each character and refuses to copy or cut its contents to the clipboard, butgetText()returns the real characters, because your program needs them.TextAreaholds many lines and shows scroll bars when the text outgrows it. StudentApp and EmployeeManagerApp use one, calledoutput, to show query results and error messages.setWrapText(true)wraps long lines instead of scrolling sideways.
Button#
Button submitButton = new Button("Submit");submitButton.setPrefHeight(40);submitButton.setPrefWidth(100);submitButton.setDefaultButton(true); // Enter in this window presses it submitButton.setOnAction(e -> { // runs every time the button is pressed System.out.println("Submit pressed");});A 100 by 40 button labelled Submit, tinted blue because it is the default button. Each click, and each press of Enter in that window, prints "Submit pressed" in the console.
setOnAction(...)registers the code to run when the button fires: a click, or the Space key while the button has focus. Thee -> { ... }is a lambda from enums and lambdas. The next lesson shows the longer form, an anonymous class, and explains both.setDefaultButton(true)makes Enter press this button from anywhere in its window, unless the control with focus uses Enter itself (aTextAreauses it to start a new line). The default stylesheet tints the default button blue, which is why the Submit button on the registration form is blue.setPrefWidth(100)andsetPrefHeight(40)set its preferred size. A button does not stretch beyond its preferred size unless you raise its maximum withsetMaxWidth(Double.MAX_VALUE).
Alert#
An Alert is a small ready-made dialog window, not a node: you never add it to a pane, you show it. The registration form builds every one of its messages like this.
Alert alert = new Alert(Alert.AlertType.ERROR);alert.setTitle("Form Error!"); // text in the dialog's title baralert.setHeaderText(null); // no header band above the messagealert.setContentText("Please enter your name"); // the message itselfalert.initOwner(stage); // this dialog belongs to that windowalert.show(); // appear, and carry on at onceA small dialog titled "Form Error!" over the main window, with an error icon beside the message "Please enter your name" and a single OK button. Until it is closed, the main window ignores clicks.
| Alert.AlertType | Icon and buttons | Fits a message like |
|---|---|---|
NONE | No icon and no buttons. Add one yourself, because a dialog with no buttons cannot be closed by the user. | A fully custom dialog |
INFORMATION | Information icon, OK | “Record inserted!” |
WARNING | Warning icon, OK | “The table already exists.” |
CONFIRMATION | Question icon, OK and Cancel | “Delete John?” |
ERROR | Error icon, OK | “Please enter your name” |
setTitlesets the text in the dialog's title bar, andsetContentTextthe message. Each type also fills in a default title and a default header text, which these calls replace.setHeaderText(null)removes the header band that normally sits above the message, and the icon moves down beside the message instead.initOwner(stage)ties the dialog to a window: it opens centred over that window and stays in front of it. It must be called before the dialog is shown.show()puts the dialog on screen and returns immediately, so the next line of your code runs while the dialog is still up.showAndWait()returns only after the user closes the dialog, and hands back which button they pressed. Either way the user cannot click the main window while an alert is open, because alerts are modal. The difference is only whether your code waits.
That last difference matters for a question. To act on OK or Cancel, you need the answer, so you wait for it.
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);confirm.setHeaderText(null);confirm.setContentText("Delete John?"); Optional<ButtonType> answer = confirm.showAndWait(); // waits hereif (answer.isPresent() && answer.get() == ButtonType.OK) { // delete the record}A dialog with a question icon, the text "Delete John?" and the buttons OK and Cancel. The code after showAndWait() runs only once the user has pressed one of them, and deletes only on OK.
Optional<ButtonType> is a small wrapper that may or may not hold a value (here, the button pressed); isPresent() checks that it does and get() takes it out. It lives in java.util, and ButtonType in javafx.scene.control.
Reading and writing each control#
| Control | Your code writes it with | Your code reads it with |
|---|---|---|
Label | setText(...), setFont(...) | getText() |
Text | setText(...), setFont(...), setX, setY | getText() |
TextField | setText(...), clear(), setPromptText(...), setEditable(false) | getText(), a String, empty if nothing was typed |
PasswordField | The same as TextField | getText(), the real characters |
TextArea | setText(...), appendText(...), clear() | getText(), every line in one String with \n between lines |
Button | setText(...), setDefaultButton(true), setPrefWidth(...) | No value to read: setOnAction(...) runs your code when it is pressed |
Alert | setTitle, setHeaderText, setContentText, initOwner | showAndWait() returns the button pressed |
The layouts the programs use#
Two panes do all the arranging in these programs: a VBox in the two database screens and a GridPane everywhere else. Three small value classes tune them: Insets, Pos and HPos.
VBox and HBox#
StudentApp, the database program, builds its whole window in one line:
VBox root = new VBox(10, createBtn, insertBtn, updateBtn, deleteBtn, selectBtn, output);Scene scene = new Scene(root, 400, 300);A 400 by 300 window: five buttons stacked down the left edge, 10 pixels apart, and the output text area below them, stretched across the full width of the window.
The constructor VBox(double spacing, Node... children) takes the gap first and then any number of children (the ... makes it a varargs parameter). Each child goes below the one before, 10 pixels apart. A VBox stretches resizable children to its own width when it can, so the TextAreafills the window's width, while the buttons keep their natural widths because a button's maximum width is its preferred width.
Spacing and padding are different settings, and confusing them is why layouts look almost right. Spacing is the gap betweenneighbouring children, so it appears only between them. Padding is the gap between the pane's edge and its children, so it appears only around the outside. StudentApp has spacing 10 and no padding, which is why its buttons touch the window edge. One more line would fix that:
VBox root = new VBox(10, createBtn, insertBtn, updateBtn, deleteBtn, selectBtn, output);root.setPadding(new Insets(10)); // 10 pixels inside each of the four edgesThe same window with a 10 pixel margin of empty space inside every edge. The 10 pixel gaps between the buttons are unchanged.
HBox is the same idea turned sideways: new HBox(10, saveButton, cancelButton) lines the buttons up left to right, 10 pixels apart, and setAlignment(Pos.CENTER_RIGHT) pushes the whole row to the right-hand side. To let one child take the spare width of the row, give it permission with HBox.setHgrow(searchField, Priority.ALWAYS). An empty new Region() given the same permission makes an invisible spacer that pushes everything after it to the far end of the row.
GridPane: placing nodes by column and row#
A GridPane places each child in a cell of an invisible table. Columns and rows are numbered from 0, and the grid has as many of each as its children use. There are two ways to place a child:
grid.add(node, columnIndex, rowIndex)puts the node in one cell.grid.add(node, columnIndex, rowIndex, columnSpan, rowSpan)lets it cover several:columnSpancells across androwSpancells down, starting from the given cell.
GridPane grid = new GridPane();grid.setHgap(10); // 10 pixels between columnsgrid.setVgap(10); // 10 pixels between rowsgrid.setPadding(new Insets(20)); // 20 pixels inside every edge TextField nameField = new TextField();TextField emailField = new TextField();Button saveButton = new Button("Save"); grid.add(new Label("Name:"), 0, 0); // column 0, row 0grid.add(nameField, 1, 0); // column 1, row 0grid.add(new Label("Email:"), 0, 1); // column 0, row 1grid.add(emailField, 1, 1); // column 1, row 1grid.add(saveButton, 0, 2, 2, 1); // column 0, row 2, 2 columns wide, 1 row tallGridPane.setHalignment(saveButton, HPos.CENTER);Two rows, each a label with a field beside it: the labels line up in one column and the fields in the next. A Save button sits centred underneath, across both columns.
grid.add(node, 1, 2) means column 1, row 2. That is the opposite of a two-dimensional array from two-dimensional arrays, where grid[1][2] means row 1, column 2. Think of x before y on a graph: the first number moves you across, the second moves you down. Mix them up and the form comes out transposed with no error message, because both orders are legal calls.The console program below models the difference with an ordinary array. Its add method takes the column first, the way GridPane.add does, and stores into grid[row][column], the way an array is indexed. Step through it and watch which cell each call fills.
public class Main { // A console model of GridPane.add(node, columnIndex, rowIndex). // The method takes the column first. The array is indexed row first. static void add(String[][] grid, String node, int column, int row) { grid[row][column] = node; } public static void main(String[] args) { String[][] grid = { {".", "."}, // row 0 {".", "."}, // row 1 {".", "."} // row 2 }; add(grid, "Name:", 0, 0); // column 0, row 0 add(grid, "[name]", 1, 0); // column 1, row 0 add(grid, "Email:", 0, 1); // column 0, row 1 add(grid, "[email]", 1, 1); // column 1, row 1 add(grid, "[Save]", 1, 2); // column 1, row 2 for (int r = 0; r < grid.length; r++) { System.out.println("row " + r + ": " + grid[r][0] + " " + grid[r][1]); } }}It prints row 0: Name: [name], then row 1: Email: [email], then row 2: . [Save]. Now change the third call to add(grid, "Email:", 1, 0), the classic slip, and run it again: the label lands in row 0, column 1, and replaces [name]. In a real GridPane nothing is replaced. The two nodes share the cell and are drawn on top of each other, which looks just as broken.
The calculators' way: setConstraints, then addAll#
SimpleCalculator and PerfectNumberChecker place their nodes in two steps instead of one. First a static method stamps a cell on each node; then a single call adds them all.
GridPane grid = new GridPane();grid.setPadding(new Insets(10, 10, 10, 10));grid.setVgap(8);grid.setHgap(10); GridPane.setConstraints(inputLabel, 0, 0); // column 0, row 0GridPane.setConstraints(inputField, 1, 0); // column 1, row 0GridPane.setConstraints(checkButton, 1, 1); // column 1, row 1GridPane.setConstraints(resultField, 1, 2); // column 1, row 2 grid.getChildren().addAll(inputLabel, inputField, checkButton, resultField);"Enter an integer:" with a field beside it, the Check button under the field, and the result field under that, all in the top-left corner of a 300 by 200 window.
GridPane.setConstraints(node, column, row) takes the column first, like add. It adds nothing: it stores the column and row on the node itself, and the grid reads them once the node is one of its children. That is all add does internally too (store the position, then add the node to getChildren()), so the two styles build identical windows. The two-step style has one extra way to fail. A node added without its setConstraints call keeps the default position, column 0 and row 0, and lands on top of whatever is already there.
The other calls in that listing appear in almost every grid you will write.
setHgap(10)is the horizontal gap between neighbouring columns, andsetVgap(8)the vertical gap between rows. Gaps go only between cells, never around the outside.setPadding(new Insets(10, 10, 10, 10))is the empty space between the grid's edge and its cells.Insetsis explained below.setAlignment(Pos.CENTER), which this grid does not call, decides where the block of cells sits when the grid is bigger than its content. The default isPos.TOP_LEFT, which is why the calculators hug the top-left corner, while the registration form, which setsPos.CENTER, sits in the middle of its window.
A grid that needs more room than the window gives it is cut off at the edge. SimpleCalculator stacks nine rows in a 400 by 300 scene, and its result field ends up clipped at the bottom: the rows and their gaps need a little more than 300 pixels, and a GridPane has no scroll bar.
Column constraints, alignment and margins#
So far each column has been as wide as its widest child, and every child has sat at the left of its cell. The registration form changes both with one ColumnConstraints object per column.
ColumnConstraints labels = new ColumnConstraints(100, 100, Double.MAX_VALUE);labels.setHalignment(HPos.RIGHT); // every node in column 0 sits at the right of its cell ColumnConstraints fields = new ColumnConstraints(200, 200, Double.MAX_VALUE);fields.setHgrow(Priority.ALWAYS); // column 1 takes any spare width grid.getColumnConstraints().addAll(labels, fields); // first for column 0, second for column 1Column 0 is 100 pixels wide, its labels pushed against the right side of the column, next to the fields. Column 1 is at least 200 pixels wide and takes all the extra width as the window grows.
- The constructor takes three widths: minimum, preferred and maximum. The column is laid out at its preferred width and never narrower than its minimum.
Double.MAX_VALUEis the largest number adoublecan hold, about 1.8 times 10 to the power 308. As a maximum width it means “no limit”. It allows a column to grow, but it does not make it grow.setHgrow(Priority.ALWAYS)is what makes it grow: when the grid is wider than its columns need, the spare width goes to this column.Priorityhas three values:ALWAYS,SOMETIMES(grow only if noALWAYScolumn wants the space) andNEVER.setHalignment(HPos.RIGHT)sets where every node in the column sits across its cell. The default isHPos.LEFT.getColumnConstraints()is the grid's list of constraints, matched to columns by position: the first object governs column 0, the second column 1.
Two more static methods adjust a single node rather than a whole column.
GridPane.setHalignment(saveButton, HPos.CENTER); // this node only; beats its column's settingGridPane.setMargin(headerLabel, new Insets(20, 0, 20, 0)); // 20 pixels above and below this nodeThe button is centred across the cells it spans. The heading gets 20 pixels of space above it and 20 below it, inside its row.
Notice which calls start with the class name. grid.add(...)is an instance method: it changes this grid's list of children. GridPane.setConstraints, GridPane.setHalignment and GridPane.setMargin are static. They belong to no particular grid; they only store a value on the node, and whichever GridPane holds the node reads that value when it lays out. That is the static idea again, in a library you did not write.
Insets, Pos and HPos#
All three live in javafx.geometry. They are small values that other calls take as arguments.
Insets(top, right, bottom, left)holds four distances in the order a clock hand meets them, starting at twelve: top, right, bottom, left.new Insets(20)gives all four sides the same value. You pass anInsetstosetPaddingand toGridPane.setMargin.Poscombines a vertical and a horizontal position in one value:TOP_LEFT,TOP_CENTER,TOP_RIGHT,CENTER_LEFT,CENTER,CENTER_RIGHT,BOTTOM_LEFT,BOTTOM_CENTER,BOTTOM_RIGHT, and threeBASELINE_values that line up the bottoms of letters. A pane'ssetAlignmenttakes one.HPosis horizontal only:LEFT,CENTER,RIGHT. It is used for a whole column (ColumnConstraints.setHalignment) or for one node in a grid (GridPane.setHalignment). Its vertical partner,VPos, hasTOP,CENTER,BASELINEandBOTTOM.
Pos, HPos and VPos are enums, the fixed sets of named values from enums and lambdas. Every Insets in these programs is one of these four:
| Written | Top | Right | Bottom | Left | Where |
|---|---|---|---|---|---|
new Insets(40, 40, 40, 40) | 40 | 40 | 40 | 40 | The registration form's padding |
new Insets(20) | 20 | 20 | 20 | 20 | InterestCalculator and ArmstrongFactorial |
new Insets(10, 10, 10, 10) | 10 | 10 | 10 | 10 | SimpleCalculator and PerfectNumberChecker |
new Insets(20, 0, 20, 0) | 20 | 0 | 20 | 0 | The margin round the form's heading and its Submit button |
BorderPane, briefly#
A BorderPane has five named regions, filled with setTop, setBottom, setLeft, setRight and setCenter. Top and bottom stretch across the full width, left and right take their preferred width, and the centre gets everything that is left, growing and shrinking with the window. A region you never set takes no space at all.
Button insertBtn = new Button("Insert Record");Button selectBtn = new Button("Select Records");VBox buttons = new VBox(10, insertBtn, selectBtn);TextArea output = new TextArea(); BorderPane root = new BorderPane();root.setTop(new Label("Student records"));root.setLeft(buttons);root.setCenter(output);root.setBottom(new Label("Connected to studentdb"));A heading across the top, two buttons down the left, the text area filling the middle and growing with the window, and a status line along the bottom. No right region was set, so it takes no space.
GridPane root, both ColumnConstraints, a Label heading in bold Arial, two text fields, a password field, a default button and two kinds of Alert. Every line of it is now familiar. Continue with form creation using JavaFX.Try it
Answer each one in your head or on paper first, then open the answer.
void addTo(Pane parent, Node child). Can you pass a Button as the child? A VBox as the child? A Label as the parent? Explain each answer from the class hierarchy.new Scene(..., 400, 300): a Group, a GridPane, a Button, a Text, an ImageView, a WebView? Say why for each.Group root holds one Text with setX(500) and setY(100), in a scene 400 pixels wide. What do you see? What changes when the user drags the window to 600 wide? And what if the root were a StackPane instead?grid.add(new Label("Email:"), 1, 0); and grid.add(emailField, 0, 1);. Where does each node end up, and what is the fix?VBox root = new VBox(10, a, b, c); followed by root.setPadding(new Insets(5, 10, 15, 20));, how far is a from the top edge, how far is b below a, and how far is c from the left edge?AlertType do you use, and why does show() not work here?Practice
Work through these on your own. Each one builds on this page.
GridPane.setConstraints and getChildren().addAll, once with grid.add. Confirm the two windows are identical. Then delete the setConstraints line for the Check button and describe exactly what you see, and why.BorderPane root: the five buttons in a FlowPane along the top, and the TextArea in the centre. Drag the window narrower and wider, and write a paragraph comparing it with the VBox version.PieChart of how you spend a typical day, in five slices that add up to 24 hours. Then show the same five numbers in a BarChart with a CategoryAxis and a NumberAxis, and say what each chart makes easier to see.Group root, with a StackPane root, and with the button itself as the root (new Scene(button, 300, 200)). Before each run, write down what you expect to see when the window is resized. Afterwards, explain every result from the Node hierarchy.Spotted a mistake, or a part that did not make sense? Report it by email