Learn Java

Lesson 79Elective: Desktop apps with JavaFX

Form creation using JavaFX

A registration form built in five steps: a GridPane layout, the controls, and a submit handler that checks every field and answers with an alert.

By Updated 60 min

  • registration form
  • GridPane
  • ColumnConstraints
  • HPos
  • Priority
  • column span

This lesson builds one complete program, a registration form, in five steps. It is the first JavaFX program in the course that answers the user: they type into three fields, press Submit, and get back either a complaint or a welcome. This lesson follows the five steps line by line, traces the finished program from launch to the dialog it shows, and ends with the mistakes the form still lets through and how to catch them.

By the end of this lesson
  • Build a registration form in five steps, and say what each step adds to the window.
  • Explain every line of createRegistrationFormPane, including what the two ColumnConstraints do to an 800-pixel-wide window.
  • Read every gridPane.add call in addUIControls as a column, a row and a span.
  • Explain the anonymous EventHandler class: what it is, when its handle method runs, and why it can use nameField.
  • Show error and confirmation alerts with a showAlert helper method, and say what initOwner, setHeaderText(null) and show() each do.
  • List what the first version fails to check, and write a handler that catches it.
From before
This lesson uses almost everything in controls and layout panes: GridPane, ColumnConstraints, Label, TextField, PasswordField, Button, Alert, Insets, Pos and HPos. The submit handler is an anonymous class, the one-off class with no name from inner classes, and the class it writes implements an interface, as in interfaces. The checks call isEmpty and trim from strings, and the skeleton of the program (extend Application, override start, call launch) is the one from your first JavaFX application.

The form, and the five steps that build it#

Here is the finished window, before anyone has typed into it.

Registration Form JavaFX Application
Registration Form
Full Name :
 
Email ID :
 
Password :
 
Submit
The finished form: a bold heading centred over two columns, three right-aligned labels with wide fields beside them, and a Submit button centred underneath. Submit is the default button, which the default style tints blue (drawn dark here).

It is built in five steps, and each section below is one of them.

  1. 1Create the main class, which extends Application and overrides start.
  2. 2Create the layout: a GridPane with its alignment, padding, gaps and column constraints.
  3. 3Create a scene with the grid as its root.
  4. 4Add the controls to the grid: the heading, three labels, three fields and the button.
  5. 5Add the handler that runs when the form is submitted, with a helper method that shows alerts.

The complete program is these five pieces put together.

Step 1: the main Application class#

First, create the main class. It extends javafx.application.Application and overrides its start() method.

RegistrationFormApplication.java (step 1)
read along
1import javafx.application.Application;
2import javafx.stage.Stage;
3
4public class RegistrationFormApplication extends Application {
5
6 @Override
7 public void start(Stage primaryStage) throws Exception {
8 primaryStage.setTitle("Registration Form JavaFX Application");
9 primaryStage.show();
10 }
11
12 public static void main(String[] args) {
13 launch(args);
14 }
15}
Output
A window with "Registration Form JavaFX Application" in its title
bar and nothing inside it. With no scene to measure, its size is
left to the operating system.

This is the skeleton from your first JavaFX application, with the form's own names. RegistrationFormApplication extends Application, an abstract class whose one abstract method is start(Stage). JavaFX calls start once and passes in primaryStage, the window it has created for you; setTitle puts the text in its title bar, and show() makes it appear. main only calls launch(args), which starts the JavaFX runtime, creates the RegistrationFormApplication object and calls start on it.

throws Exception is allowed because Application.start itself declares it, and an overriding method may declare the same checked exceptions (or fewer). Nothing in this program throws a checked exception, so the clause could be deleted without changing anything.

Compiling and running (Command Prompt)
read along
1javac --module-path %PATH_TO_FX% --add-modules javafx.controls RegistrationFormApplication.java
2java --enable-native-access=javafx.graphics --module-path %PATH_TO_FX% --add-modules javafx.controls RegistrationFormApplication
Output
The window from step 1. On macOS or Linux write $PATH_TO_FX instead
of %PATH_TO_FX%. The --enable-native-access flag silences the
"restricted method" warnings that JDK 24 and later print for JavaFX.

Run the program after every step. Each step changes the window in a way you can see, and a mistake is far easier to find in the five lines you just added than in the finished hundred.

Step 2: the layout, a GridPane#

JavaFX arranges controls with layout containers called panes. This form uses a GridPane: a flexible grid of rows and columns, with one control in each cell.

A form is a table in disguise. Each row is one question, the left column holds the questions and the right column the answers, and the answers should all start at the same x position so the eye can run straight down them. A GridPane gives exactly that: every node in column 1 starts where column 1 starts, however long the labels in column 0 are. Building each row as its own HBox would not, because each row would size itself and a longer label would push its field further right than the others.

createRegistrationFormPane
read along
1private GridPane createRegistrationFormPane() {
2 // Instantiate a new Grid Pane
3 GridPane gridPane = new GridPane();
4
5 // Position the pane at the center of the screen, both vertically and horizontally
6 gridPane.setAlignment(Pos.CENTER);
7
8 // Set a padding of 20px on each side
9 gridPane.setPadding(new Insets(40, 40, 40, 40));
10
11 // Set the horizontal gap between columns
12 gridPane.setHgap(10);
13
14 // Set the vertical gap between rows
15 gridPane.setVgap(10);
16
17 // Add Column Constraints
18
19 // columnOneConstraints will be applied to all the nodes placed in column one.
20 ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE);
21 columnOneConstraints.setHalignment(HPos.RIGHT);
22
23 // columnTwoConstraints will be applied to all the nodes placed in column two.
24 ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE);
25 columnTwoConstrains.setHgrow(Priority.ALWAYS);
26
27 gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains);
28
29 return gridPane;
30}
Output
Nothing visible yet: the method returns an empty grid with its
settings in place. Step 3 puts it on screen and step 4 fills it.

It needs javafx.geometry.Pos, Insets and HPos, and javafx.scene.layout.* for GridPane, ColumnConstraints and Priority. The method creates a GridPane, sets its alignment and padding, and adds gaps between its columns and rows. Line by line:

  • new GridPane() creates an empty grid. No rows or columns exist yet: a grid has as many as its children use.
  • setAlignment(Pos.CENTER)decides where the block of rows and columns sits inside the grid. The comment says it positions “the pane at the center of the screen”, which is loose: the grid will be the root, so it always fills the window, and what gets centred is its content. Horizontally there is nothing to centre, because column 1 grows to use the whole width (see below). Vertically, the five rows need less than the window's 500 pixels, so the spare height is split equally above and below them. Without this line the form would sit at the top of the window.
  • setPadding(new Insets(40, 40, 40, 40)) keeps 40 pixels clear inside every edge: top, right, bottom, left, clockwise from the top. new Insets(40) says the same thing more briefly.
  • setHgap(10) puts 10 pixels between column 0 and column 1, and setVgap(10) puts 10 pixels between neighbouring rows.
  • new ColumnConstraints(100, 100, Double.MAX_VALUE) sizes the first column: minimum 100, preferred 100, maximum unlimited. A ColumnConstraints says how much space a column gets, and its constructor takes the minimum, preferred and maximum width, in that order. Strictly the constraints size the column, not each node in it: a label stays as wide as its text and is placed inside the 100-pixel column.
  • setHalignment(HPos.RIGHT) pushes every node in the first column to the right side of its cell, against the fields.
  • new ColumnConstraints(200,200, Double.MAX_VALUE) sizes the second column: never narrower than 200, preferred 200, no maximum.
  • setHgrow(Priority.ALWAYS) makes the second column take any spare width: when the window gets wider, this column grows to fill it. Double.MAX_VALUE, the largest double (about 1.8 times 10 to the power 308), only allows a column to grow; Priority.ALWAYS is what makes it grow. The first column has the same maximum but no grow setting, so it stays at 100.
  • getColumnConstraints().addAll(...) attaches the two objects. The list is matched to columns by position: the first object governs column 0, the second column 1.
The comment says 20px
The comment above setPaddingreads “Set a padding of 20px on each side”, but the code passes 40 four times. The comment was left behind when the numbers changed; the code is what runs, so the padding is 40 pixels on each side. When a comment and the code disagree, believe the code.
Column one is column 0
The comments in the code count columns from one (“column one”, “column two”), while the code counts from zero, as gridPane.add will in step 4. columnOneConstraints governs column index 0, the labels; columnTwoConstrains governs column index 1, the fields. The second variable name is also missing a t. Java does not mind, as long as every use is spelled the same way; if you correct it in one place, correct all three.

Put real numbers on it. In the 800-pixel-wide window of step 3, the widths across are 40 of padding, 100 for column 0, a 10-pixel gap, then column 1, then 40 of padding. Column 1 gets everything left: 800 minus 190 is 610 pixels, its preferred 200 plus all 410 spare pixels. Drag the window out to 1000 pixels and column 1 becomes 810, while column 0 stays at 100.

column 0min 100, pref 100, right-alignedcolumn 1min 200, pref 200, growsrow 02 colsrow 1row 2row 3row 42 colsRegistration FormFull Name :Email ID :Password :Submit
The grid that steps 2 and 4 build together: two columns and five rows. Column 0 holds the labels, pushed right; column 1 holds the fields and takes all the extra width. The heading in row 0 and the Submit button in row 4 each span both columns.

Step 3: a scene with the grid as its root#

Next, start() creates a scene with the grid as its root node.

start
read along
1@Override
2public void start(Stage primaryStage) throws Exception {
3 primaryStage.setTitle("Registration Form JavaFX Application");
4
5 // Create the registration form pane
6 GridPane gridPane = createRegistrationFormPane();
7 // Create a scene with the registration form gridPane as the root node.
8 Scene scene = new Scene(gridPane, 800, 500);
9 // Set the scene in primary stage
10 primaryStage.setScene(scene);
11
12 primaryStage.show();
13}
Output
An 800 by 500 window with the title, light grey and still empty:
the grid has no children yet.

createRegistrationFormPane() hands back the configured grid, and new Scene(gridPane, 800, 500) makes it the root of an 800-wide, 500-tall scene (width first, then height). A GridPane may be a root because it is a Region, and so a Parent. Being a Region also means it follows the window: as the Group and StackPane experiment in controls and layout panes showed, the scene resizes a region root to fill the window and the region lays its children out again. That is what lets column 1 grow when the user widens the window. The light grey is the background the default stylesheet gives a root region.

Step 4: adding the controls#

JavaFX comes with ready-made controls such as TextField, PasswordField and Button. A second helper method creates the form's controls and adds them to the grid.

addUIControls
read along
1private void addUIControls(GridPane gridPane) {
2 // Add Header
3 Label headerLabel = new Label("Registration Form");
4 headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24));
5 gridPane.add(headerLabel, 0,0,2,1);
6 GridPane.setHalignment(headerLabel, HPos.CENTER);
7 GridPane.setMargin(headerLabel, new Insets(20, 0,20,0));
8
9 // Add Name Label
10 Label nameLabel = new Label("Full Name : ");
11 gridPane.add(nameLabel, 0,1);
12
13 // Add Name Text Field
14 TextField nameField = new TextField();
15 nameField.setPrefHeight(40);
16 gridPane.add(nameField, 1,1);
17
18 // Add Email Label
19 Label emailLabel = new Label("Email ID : ");
20 gridPane.add(emailLabel, 0, 2);
21
22 // Add Email Text Field
23 TextField emailField = new TextField();
24 emailField.setPrefHeight(40);
25 gridPane.add(emailField, 1, 2);
26
27 // Add Password Label
28 Label passwordLabel = new Label("Password : ");
29 gridPane.add(passwordLabel, 0, 3);
30
31 // Add Password Field
32 PasswordField passwordField = new PasswordField();
33 passwordField.setPrefHeight(40);
34 gridPane.add(passwordField, 1, 3);
35
36 // Add Submit Button
37 Button submitButton = new Button("Submit");
38 submitButton.setPrefHeight(40);
39 submitButton.setDefaultButton(true);
40 submitButton.setPrefWidth(100);
41 gridPane.add(submitButton, 0, 4, 2, 1);
42 GridPane.setHalignment(submitButton, HPos.CENTER);
43 GridPane.setMargin(submitButton, new Insets(20, 0,20,0));
44}
Output
Once start calls this method, the window shows the whole form.
Submit does nothing yet: step 5 gives it a handler.

A method that is never called never runs, so start gains one line, between creating the grid and creating the scene. This is how the complete program has it:

start (complete version)
read along
1@Override
2public void start(Stage primaryStage) throws Exception {
3 primaryStage.setTitle("Registration Form JavaFX Application");
4
5 // Create the registration form grid pane
6 GridPane gridPane = createRegistrationFormPane();
7 // Add UI controls to the registration form grid pane
8 addUIControls(gridPane);
9 // Create a scene with registration form grid pane as the root node
10 Scene scene = new Scene(gridPane, 800, 500);
11 // Set the scene in primary stage
12 primaryStage.setScene(scene);
13
14 primaryStage.show();
15}
Output
The full form, in an 800 by 500 window.

gridPane.add takes the column index and the row index, both counting from zero, and optionally how many columns and rows the node spans. Read every call as column first, then row:

CallColumnRowSpanLands in
gridPane.add(headerLabel, 0,0,2,1)002 columns, 1 rowRow 0, across both columns
gridPane.add(nameLabel, 0,1)01noneRow 1, left
gridPane.add(nameField, 1,1)11noneRow 1, right
gridPane.add(emailLabel, 0, 2)02noneRow 2, left
gridPane.add(emailField, 1, 2)12noneRow 2, right
gridPane.add(passwordLabel, 0, 3)03noneRow 3, left
gridPane.add(passwordField, 1, 3)13noneRow 3, right
gridPane.add(submitButton, 0, 4, 2, 1)042 columns, 1 rowRow 4, across both columns
Every gridPane.add call in addUIControls. The two five-argument calls are the two nodes that cross both columns.

So the name, email and password rows follow one another: rows 1, 2 and 3, with the labels in column 0 and the fields in column 1. The other calls each do one job.

  • headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24)) makes the heading bold Arial at size 24.
  • GridPane.setHalignment(headerLabel, HPos.CENTER)centres the heading across the two columns it spans. A setting on the node itself beats the column's setting, so column 0's HPos.RIGHT does not apply to it. The Submit button gets the same treatment.
  • GridPane.setMargin(headerLabel, new Insets(20, 0,20,0))gives the heading 20 pixels of space above and 20 below (top, right, bottom, left), and none at the sides. The margin is inside the heading's row, so the row grows to hold it.
  • setPrefHeight(40) makes each field 40 pixels tall, a little taller than the default and easier to click. No width is set, because column 1 stretches its fields across its whole width, 610 pixels in an 800-pixel window.
  • PasswordField draws a dot for each character typed, while its getText() still returns the real characters for the program to check.
  • submitButton.setDefaultButton(true) makes Enter press Submit from anywhere in the window. It is also why the button is tinted blue.
  • setPrefWidth(100) and setPrefHeight(40) fix the button at 100 by 40. A button does not stretch past its preferred size, so HPos.CENTER places it in the middle of row 4, and its margin of 20 above and below separates it from the password field.
GridPane.setMargin, with a capital G
setMargin is a static method of GridPane, and its Insets run top, right, bottom, left. You may see it written gridPane.setMargin, with a small g. Java accepts a static method called through an object, but GridPane.setMargin, with the class name as in the code, is the right way to write it, because the method belongs to the class rather than to any one grid.

Step 5: the handler that runs on submit#

Last, the submit button needs a handler: code that runs whenever the user clicks Submit or presses Enter. The code goes at the end of addUIControls, after the button and the fields exist.

The submit handler
read along
1submitButton.setOnAction(new EventHandler<ActionEvent>() {
2 @Override
3 public void handle(ActionEvent event) {
4 if(nameField.getText().isEmpty()) {
5 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
6 "Form Error!", "Please enter your name");
7 return;
8 }
9 if(emailField.getText().isEmpty()) {
10 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
11 "Form Error!", "Please enter your email id");
12 return;
13 }
14 if(passwordField.getText().isEmpty()) {
15 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
16 "Form Error!", "Please enter a password");
17 return;
18 }
19
20 showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(),
21 "Registration Successful!", "Welcome " + nameField.getText());
22 }
23});
Output
Submit with the name field empty: a dialog titled "Form Error!"
saying "Please enter your name". Submit with all three fields filled:
a dialog titled "Registration Successful!" saying "Welcome " and the
name.

In short, setOnAction registers the handler on the button, and the handler shows an error alert if a field is empty, or a confirmation alert if all three are filled. Each piece is worth taking apart.

What new EventHandler<ActionEvent>() { ... } means#

This is the hardest-looking expression in the JavaFX lessons, and it is built entirely from pieces you know. Start with the interface. EventHandler, in javafx.event, declares one abstract method:

javafx.event.EventHandler (as declared in JavaFX)
read along
1public interface EventHandler<T extends Event> extends EventListener {
2 void handle(T event);
3}
Output
An interface with a single abstract method, handle. T is filled in
when the interface is used: EventHandler<ActionEvent> has
handle(ActionEvent event).

The <T> is a placeholder for a type of event, filled in where the interface is used. EventHandler<ActionEvent>means “a handler for action events”, so its method becomes void handle(ActionEvent event). An ActionEvent is what a button produces when it is pressed. setOnAction takes an EventHandler<ActionEvent>: any object whose class implements that interface.

You could write such a class with a name, class SubmitHandler implements EventHandler<ActionEvent>, give it a handle method, and pass new SubmitHandler(). The anonymous class does the same in one expression. new EventHandler<ActionEvent>() { ... } defines a class with no name that implements EventHandler<ActionEvent>, writes its handle method inside the braces, and creates one object of it, all at once. It looks as if it creates an object of an interface, which is impossible; the braces are what make it legal, because they are the body of a brand new class. The anonymous class in inner classes extended a class, and this one implements an interface, but the shape is the same.

Now the timing. setOnAction only stores that object in the button: when addUIControls runs, nothing inside handle runs. Later, every time the button fires (a click, or Enter because it is the default button), JavaFX creates an ActionEvent and calls handle(event) on the stored object. The event parameter describes what happened, and event.getSource() would return the button. This handler never looks at it, which is allowed.

Key idea

setOnAction does not run your code. It stores an object, and JavaFX calls that object's handle method later, once for every press of the button.

Why nameField still works inside handle#

nameField, emailField and passwordField are local variables of addUIControls, and gridPane is its parameter. Local variables disappear when their method returns, and addUIControls returns long before anyone clicks Submit. Yet handle, running minutes later, uses all four.

It works because the anonymous object takes copies. When new EventHandler<ActionEvent>() { ... } runs, Java copies the current value of every local variable the class body uses into the new object. Those values are references, so the copy of nameField points at the very TextField the user is typing into, and nameField.getText() inside handle reads whatever is in the field at the moment of the click. The variable is gone; the text field it referred to is not, because the grid and the handler both still refer to it.

Copying is only safe if the variable can never change afterwards, or the copy and the original could disagree. So Java allows it only for local variables that are final or effectively final: assigned once and never reassigned. nameField is assigned once, to new TextField(), and never again, so it qualifies without the word final. One extra line and it no longer does:

Breaking the rule
read along
1TextField nameField = new TextField();
2// ... the rest of addUIControls, including the handler that uses nameField ...
3nameField = new TextField(); // a second assignment: no longer effectively final
Output
error: local variables referenced from an inner class must be final or effectively final

Before Java 8 such variables had to be declared final by hand; since Java 8 the compiler works it out. The handler can also call showAlert, an instance method, because an anonymous class created inside an instance method keeps a reference to the object that created it, here the RegistrationFormApplication that JavaFX made in launch.

The three checks, and why each one returns#

Inside handle, the checks run top to bottom in the order of the fields on screen. nameField.getText() returns what is in the field now, and isEmpty() is true when that string has no characters at all. If the name is empty, the handler shows an error and return; ends handle on the spot: the email check, the password check and the welcome never run. So the user hears about one problem at a time, the first one, and the success message is reached only when all three checks have passed.

Delete the three returns and an empty form shows four dialogs at once: three “Form Error!” alerts and a “Registration Successful!” alert that says “Welcome” followed by nothing. show() does not wait for one dialog to close before the next line runs, so nothing stops the handler from reaching the end.

The showAlert helper#

The alerts come from JavaFX's built-in Alert class, wrapped in a small helper:

showAlert
read along
1private void showAlert(Alert.AlertType alertType, Window owner, String title, String message) {
2 Alert alert = new Alert(alertType);
3 alert.setTitle(title);
4 alert.setHeaderText(null);
5 alert.setContentText(message);
6 alert.initOwner(owner);
7 alert.show();
8}
Output
Builds and shows one alert. Called four times by the handler, with
four different titles and messages.
  • Four parameters, because four things change from one alert to the next. Without the helper, each of the four calls in the handler would be six lines long.
  • Alert.AlertType alertType: AlertType is an enum nested inside Alert. The handler passes ERROR (an error icon and an OK button) or CONFIRMATION (a question icon, OK and Cancel).
  • Window owner: the handler passes gridPane.getScene().getWindow(). From the grid, ask for the Scene it is in; from the scene, ask for the window showing it. That window is primaryStage, but getWindow() returns it as a Window, the superclass of Stage, which is why the parameter has that type and the program imports javafx.stage.Window. It is upcasting again.
  • setTitle(title)puts “Form Error!” or “Registration Successful!” in the dialog's title bar, and setContentText(message) sets the message.
  • setHeaderText(null) removes the header band an alert normally shows above its message, so the icon sits beside the message instead.
  • initOwner(owner)makes the dialog belong to the form's window: it opens centred over the form and stays in front of it. It must come before show().
  • show() puts the dialog up and returns at once. handle has nothing left to do after showAlert, so not waiting costs nothing. The user still cannot use the form until the dialog is closed, because alerts are modal.
Form Error!
Please enter your name
OK
Submit with the name field empty. The error icon sits beside the message (there is no header, because of setHeaderText(null)), and OK is the only button.
Registration Successful!
Welcome Asha Verma
OKCancel
Submit with all three fields filled. Every CONFIRMATION alert has OK and Cancel, and this program treats them the same. Windows puts OK first, as here; macOS and Linux put Cancel first.

A CONFIRMATION alert normally asks a question, but this program never reads the answer: OK and Cancel both just close the dialog. To act on the answer, a program calls showAndWait() and checks which button came back, as the controls and layout panes lesson showed.

The same handler as a lambda#

EventHandler has exactly one abstract method, which makes it a functional interface, so a lambda from enums and lambdas can stand in for the anonymous class. The lambda is just the parameter and the body of handle:

The submit handler, written as a lambda
read along
1submitButton.setOnAction(event -> {
2 if (nameField.getText().isEmpty()) {
3 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
4 "Form Error!", "Please enter your name");
5 return;
6 }
7 if (emailField.getText().isEmpty()) {
8 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
9 "Form Error!", "Please enter your email id");
10 return;
11 }
12 if (passwordField.getText().isEmpty()) {
13 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
14 "Form Error!", "Please enter a password");
15 return;
16 }
17
18 showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(),
19 "Registration Successful!", "Welcome " + nameField.getText());
20});
Output
Exactly the same behaviour as the anonymous class version.

Everything else is unchanged. return; still ends the handler, and the lambda captures nameFieldand the others under the same effectively-final rule (the compiler's message then says “a lambda expression” instead of “an inner class”). The imports of EventHandler and ActionEvent are no longer needed, since neither name appears. Every later JavaFX program in the course, from the calculators to StudentApp, writes its handlers this way.

The complete program#

Here are the five steps put together. It is the program to be able to write from memory and explain line by line. (It is adapted from a well-known registration form tutorial on the CalliCoder website.)

RegistrationFormApplication.java
read along
1import javafx.application.Application;
2import javafx.event.ActionEvent;
3import javafx.event.EventHandler;
4import javafx.geometry.HPos;
5import javafx.geometry.Insets;
6import javafx.geometry.Pos;
7import javafx.scene.Scene;
8import javafx.scene.control.*;
9import javafx.scene.layout.*;
10import javafx.scene.text.Font;
11import javafx.scene.text.FontWeight;
12import javafx.stage.Stage;
13import javafx.stage.Window;
14
15public class RegistrationFormApplication extends Application {
16
17 @Override
18 public void start(Stage primaryStage) throws Exception {
19 primaryStage.setTitle("Registration Form JavaFX Application");
20
21 // Create the registration form grid pane
22 GridPane gridPane = createRegistrationFormPane();
23 // Add UI controls to the registration form grid pane
24 addUIControls(gridPane);
25 // Create a scene with registration form grid pane as the root node
26 Scene scene = new Scene(gridPane, 800, 500);
27 // Set the scene in primary stage
28 primaryStage.setScene(scene);
29
30 primaryStage.show();
31 }
32
33 private GridPane createRegistrationFormPane() {
34 // Instantiate a new Grid Pane
35 GridPane gridPane = new GridPane();
36
37 // Position the pane at the center of the screen, both vertically and horizontally
38 gridPane.setAlignment(Pos.CENTER);
39
40 // Set a padding of 20px on each side
41 gridPane.setPadding(new Insets(40, 40, 40, 40));
42
43 // Set the horizontal gap between columns
44 gridPane.setHgap(10);
45
46 // Set the vertical gap between rows
47 gridPane.setVgap(10);
48
49 // Add Column Constraints
50
51 // columnOneConstraints will be applied to all the nodes placed in column one.
52 ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE);
53 columnOneConstraints.setHalignment(HPos.RIGHT);
54
55 // columnTwoConstraints will be applied to all the nodes placed in column two.
56 ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE);
57 columnTwoConstrains.setHgrow(Priority.ALWAYS);
58
59 gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains);
60
61 return gridPane;
62 }
63
64 private void addUIControls(GridPane gridPane) {
65 // Add Header
66 Label headerLabel = new Label("Registration Form");
67 headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24));
68 gridPane.add(headerLabel, 0,0,2,1);
69 GridPane.setHalignment(headerLabel, HPos.CENTER);
70 GridPane.setMargin(headerLabel, new Insets(20, 0,20,0));
71
72 // Add Name Label
73 Label nameLabel = new Label("Full Name : ");
74 gridPane.add(nameLabel, 0,1);
75
76 // Add Name Text Field
77 TextField nameField = new TextField();
78 nameField.setPrefHeight(40);
79 gridPane.add(nameField, 1,1);
80
81
82 // Add Email Label
83 Label emailLabel = new Label("Email ID : ");
84 gridPane.add(emailLabel, 0, 2);
85
86 // Add Email Text Field
87 TextField emailField = new TextField();
88 emailField.setPrefHeight(40);
89 gridPane.add(emailField, 1, 2);
90
91 // Add Password Label
92 Label passwordLabel = new Label("Password : ");
93 gridPane.add(passwordLabel, 0, 3);
94
95 // Add Password Field
96 PasswordField passwordField = new PasswordField();
97 passwordField.setPrefHeight(40);
98 gridPane.add(passwordField, 1, 3);
99
100 // Add Submit Button
101 Button submitButton = new Button("Submit");
102 submitButton.setPrefHeight(40);
103 submitButton.setDefaultButton(true);
104 submitButton.setPrefWidth(100);
105 gridPane.add(submitButton, 0, 4, 2, 1);
106 GridPane.setHalignment(submitButton, HPos.CENTER);
107 GridPane.setMargin(submitButton, new Insets(20, 0,20,0));
108
109 submitButton.setOnAction(new EventHandler<ActionEvent>() {
110 @Override
111 public void handle(ActionEvent event) {
112 if(nameField.getText().isEmpty()) {
113 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
114 "Form Error!", "Please enter your name");
115 return;
116 }
117 if(emailField.getText().isEmpty()) {
118 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
119 "Form Error!", "Please enter your email id");
120 return;
121 }
122 if(passwordField.getText().isEmpty()) {
123 showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(),
124 "Form Error!", "Please enter a password");
125 return;
126 }
127
128 showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(),
129 "Registration Successful!", "Welcome " + nameField.getText());
130 }
131 });
132 }
133
134 private void showAlert(Alert.AlertType alertType, Window owner, String title, String message) {
135 Alert alert = new Alert(alertType);
136 alert.setTitle(title);
137 alert.setHeaderText(null);
138 alert.setContentText(message);
139 alert.initOwner(owner);
140 alert.show();
141 }
142
143 public static void main(String[] args) {
144 launch(args);
145 }
146}
Output
An 800 by 500 window titled "Registration Form JavaFX Application":
the bold heading "Registration Form" centred above three
right-aligned labels with wide fields beside them, and a Submit
button centred underneath. Submitting with a field empty shows
"Form Error!" with the message for the first empty field. Submitting
with all three filled shows "Registration Successful!" with
"Welcome " and the name.

Here it is in use.

Registration Form JavaFX Application
Registration Form
Full Name :
Asha Verma
Email ID :
asha@example.com
Password :
●●●●●●●●●●
Submit
The form filled in with Asha Verma, asha@example.com and a ten-character password. Pressing Enter in the password field, or clicking Submit, brings up the Registration Successful! dialog.

What happens when you press Enter#

Here is the whole run, from the command to the welcome, in the order things actually happen. Being able to tell this story is the best test that you understand the program.

  1. 1You run the java command. The JVM calls main, and main calls launch(args).
  2. 2launch starts the JavaFX runtime, creates a RegistrationFormApplication object, makes a window, and calls start(primaryStage) on the JavaFX Application Thread, the one thread allowed to touch the window.
  3. 3start sets the title and calls createRegistrationFormPane(), which builds an empty GridPane with its alignment, padding, gaps and two column constraints, and returns it.
  4. 4start calls addUIControls(gridPane). The heading, three labels, two text fields, the password field and the button go into their cells, and setOnAction stores the handler object in the button. handle does not run.
  5. 5start wraps the grid in an 800 by 500 Scene, puts it on the stage and calls show(). Then start returns, but the program does not end: the runtime keeps the window open and waits for the user.
  6. 6You type Asha Verma, asha@example.com and a password. Each field keeps its own text as you type, and none of your code runs.
  7. 7With the cursor still in the password field, you press Enter. The password field has no action handler of its own, so it passes the key on, and the window's default button, Submit, fires an ActionEvent.
  8. 8JavaFX calls handle(event) on the stored handler. The name, email and password checks each find text, so none of them returns early.
  9. 9The last line calls showAlert with CONFIRMATION, the window, “Registration Successful!” and “Welcome Asha Verma”. showAlert builds the alert, sets its title, removes its header, sets the message, makes the form its owner and calls show(), which returns at once. handle ends.
  10. 10The dialog stays over the form until you press OK or Cancel. The fields still hold everything you typed, because nothing in the program clears them.

What the form lets through, and a stricter handler#

The first version checks one thing per field: that it is not empty. Plenty gets past that.

  • A name of only spaces. " " has three characters, so isEmpty()is false, and the welcome reads “Welcome” followed by spaces.
  • An email with no @. asha passes, and so does any word at all.
  • A one-letter password. a is not empty, so it passes.
  • Nothing is cleared after success. The values stay in the fields, and pressing Enter again registers the same person a second time.
  • A question that asks nothing. The success message is a CONFIRMATION alert, whose Cancel button does exactly what OK does. An INFORMATION alert, with only OK, says what the program means.

Here is a handler that closes those gaps. It keeps the showAlert method unchanged and still checks the fields in the order they appear on screen.

A stricter submit handler
read along
1submitButton.setOnAction(event -> {
2 Window owner = gridPane.getScene().getWindow();
3 String name = nameField.getText().trim();
4 String email = emailField.getText().trim();
5 String password = passwordField.getText(); // not trimmed: spaces may be deliberate
6
7 if (name.isEmpty()) {
8 showAlert(Alert.AlertType.ERROR, owner, "Form Error!", "Please enter your name");
9 return;
10 }
11 int at = email.indexOf("@");
12 if (at < 1 || !email.substring(at + 1).contains(".")) {
13 showAlert(Alert.AlertType.ERROR, owner, "Form Error!", "Please enter a valid email id");
14 return;
15 }
16 if (password.length() < 8) {
17 showAlert(Alert.AlertType.ERROR, owner, "Form Error!",
18 "Password must be at least 8 characters");
19 return;
20 }
21
22 showAlert(Alert.AlertType.INFORMATION, owner, "Registration Successful!", "Welcome " + name);
23 nameField.clear();
24 emailField.clear();
25 passwordField.clear();
26 nameField.requestFocus();
27});
Output
"   " as the name: "Please enter your name".
"asha" as the email: "Please enter a valid email id".
"a" as the password: "Password must be at least 8 characters".
All three good: "Registration Successful!" with "Welcome " and the
trimmed name; then the fields empty and the cursor returns to Full
Name.
  • trim() removes spaces from both ends, so " " becomes an empty string and fails the name check, and an email typed with a stray space becomes a clean address. The password is deliberately not trimmed: a space can be part of a password, and quietly changing what the user typed would lock them out next time.
  • The email rule is simple on purpose: at least one character before the @, and a dot somewhere after it. indexOf("@") returns the position of the first @, or -1 if there is none, so at < 1 catches both a missing @ and an @ in the very first position. Because || stops at the first true condition, substring(at + 1) runs only when an @ was found; it is everything after the @, and it must contain a dot. An empty email has no @, so it fails here too. Real address checking is far more involved, but this catches the usual slips.
  • password.length() < 8 rejects short passwords, including an empty one.
  • After success, clear() empties each field and requestFocus() puts the cursor back in the name field, ready for the next person.

The rules themselves are plain string code, so they run in the course's visualizer. This console version returns the message the form would show for each set of inputs. Step through it and watch at and the substring during the second test.

FormRules.java
1public class Main {
2 // The stricter form's three rules, without the window.
3 // Returns the message the form would show, or "OK".
4 static String check(String name, String email, String password) {
5 name = name.trim();
6 email = email.trim();
7 if (name.isEmpty()) {
8 return "Please enter your name";
9 }
10 int at = email.indexOf("@");
11 if (at < 1 || !email.substring(at + 1).contains(".")) {
12 return "Please enter a valid email id";
13 }
14 if (password.length() < 8) {
15 return "Password must be at least 8 characters";
16 }
17 return "OK";
18 }
19
20 public static void main(String[] args) {
21 String[] names = {" ", "Asha Verma", "Asha Verma", "Asha Verma"};
22 String[] emails = {"asha@example.com", "asha", "asha@example.com", " asha@example.com "};
23 String[] passwords = {"secret123", "secret123", "a", "secret123"};
24
25 for (int i = 0; i < names.length; i++) {
26 System.out.println("Test " + (i + 1) + ": " + check(names[i], emails[i], passwords[i]));
27 }
28 }
29}
Prints
›Test 1: Please enter your name
›Test 2: Please enter a valid email id
›Test 3: Password must be at least 8 characters
›Test 4: OK

It prints Test 1: Please enter your name, Test 2: Please enter a valid email id, Test 3: Password must be at least 8 characters and Test 4: OK. Test 4's email has a space at each end, and trim() removes them before the check, so it passes.

Next: events in general
The form handles one kind of event, a button press, with one handler. Event handling looks at events properly, uses the lambda form throughout, and works through a set of small calculator programs.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Read gridPane.add(headerLabel, 0, 0, 2, 1) aloud: what does each number mean, and which cells does the heading occupy? Which other call in addUIControls uses five arguments, and why?
2
A user fills in all three fields and presses Enter while the cursor is still in the password field. Why does the form submit, and what single line would stop that from happening?
3
What changes on screen if you delete columnOneConstraints.setHalignment(HPos.RIGHT); and nothing else? Does the heading move?
4
What does new Insets(20, 0, 20, 0) mean in GridPane.setMargin(headerLabel, ...)? What would new Insets(0, 0, 40, 0) change?
5
Why does each of the three checks end with return;? What would the user see if all three fields were empty and the three returns had been deleted?
6
nameField is a local variable of addUIControls, which has finished long before anyone clicks Submit. Why can handle still use it, and what single added line would make the program stop compiling?

Practice

Work through these on your own. Each one builds on this page.

1
Add a Confirm Password row under Password, with a second PasswordField, and move Submit down a row. Reject the form with “Passwords do not match” when the two differ.
2
Add a Gender row with two radio buttons, Male and Female, side by side in column 1, of which only one can be selected. Reject the form with “Please select a gender” when neither is chosen.
3
Add a Clear button beside Submit that empties all three fields and puts the cursor back in the Full Name field. Submit must stay the default button.
4
Add a Phone row whose value must be exactly 10 digits, with the message “Please enter a 10-digit phone number”. Write the check first as a static boolean method in a console program, and test it on 9876543210, 98765 and 98765abcde before you put it in the form.

Spotted a mistake, or a part that did not make sense? Report it by email