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 BasithUpdated 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.
- Build a registration form in five steps, and say what each step adds to the window.
- Explain every line of
createRegistrationFormPane, including what the twoColumnConstraintsdo to an 800-pixel-wide window. - Read every
gridPane.addcall inaddUIControlsas a column, a row and a span. - Explain the anonymous
EventHandlerclass: what it is, when itshandlemethod runs, and why it can usenameField. - Show error and confirmation alerts with a
showAlerthelper method, and say whatinitOwner,setHeaderText(null)andshow()each do. - List what the first version fails to check, and write a handler that catches it.
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.
It is built in five steps, and each section below is one of them.
- 1Create the main class, which extends
Applicationand overridesstart. - 2Create the layout: a
GridPanewith its alignment, padding, gaps and column constraints. - 3Create a scene with the grid as its root.
- 4Add the controls to the grid: the heading, three labels, three fields and the button.
- 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.
import javafx.application.Application;import javafx.stage.Stage; public class RegistrationFormApplication extends Application { @Override public void start(Stage primaryStage) throws Exception { primaryStage.setTitle("Registration Form JavaFX Application"); primaryStage.show(); } public static void main(String[] args) { launch(args); }}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.
javac --module-path %PATH_TO_FX% --add-modules javafx.controls RegistrationFormApplication.javajava --enable-native-access=javafx.graphics --module-path %PATH_TO_FX% --add-modules javafx.controls RegistrationFormApplicationThe 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.
private GridPane createRegistrationFormPane() { // Instantiate a new Grid Pane GridPane gridPane = new GridPane(); // Position the pane at the center of the screen, both vertically and horizontally gridPane.setAlignment(Pos.CENTER); // Set a padding of 20px on each side gridPane.setPadding(new Insets(40, 40, 40, 40)); // Set the horizontal gap between columns gridPane.setHgap(10); // Set the vertical gap between rows gridPane.setVgap(10); // Add Column Constraints // columnOneConstraints will be applied to all the nodes placed in column one. ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE); columnOneConstraints.setHalignment(HPos.RIGHT); // columnTwoConstraints will be applied to all the nodes placed in column two. ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE); columnTwoConstrains.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains); return gridPane;}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, andsetVgap(10)puts 10 pixels between neighbouring rows.new ColumnConstraints(100, 100, Double.MAX_VALUE)sizes the first column: minimum 100, preferred 100, maximum unlimited. AColumnConstraintssays 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 largestdouble(about 1.8 times 10 to the power 308), only allows a column to grow;Priority.ALWAYSis 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.
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.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.
Step 3: a scene with the grid as its root#
Next, start() creates a scene with the grid as its root node.
@Overridepublic void start(Stage primaryStage) throws Exception { primaryStage.setTitle("Registration Form JavaFX Application"); // Create the registration form pane GridPane gridPane = createRegistrationFormPane(); // Create a scene with the registration form gridPane as the root node. Scene scene = new Scene(gridPane, 800, 500); // Set the scene in primary stage primaryStage.setScene(scene); primaryStage.show();}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.
private void addUIControls(GridPane gridPane) { // Add Header Label headerLabel = new Label("Registration Form"); headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24)); gridPane.add(headerLabel, 0,0,2,1); GridPane.setHalignment(headerLabel, HPos.CENTER); GridPane.setMargin(headerLabel, new Insets(20, 0,20,0)); // Add Name Label Label nameLabel = new Label("Full Name : "); gridPane.add(nameLabel, 0,1); // Add Name Text Field TextField nameField = new TextField(); nameField.setPrefHeight(40); gridPane.add(nameField, 1,1); // Add Email Label Label emailLabel = new Label("Email ID : "); gridPane.add(emailLabel, 0, 2); // Add Email Text Field TextField emailField = new TextField(); emailField.setPrefHeight(40); gridPane.add(emailField, 1, 2); // Add Password Label Label passwordLabel = new Label("Password : "); gridPane.add(passwordLabel, 0, 3); // Add Password Field PasswordField passwordField = new PasswordField(); passwordField.setPrefHeight(40); gridPane.add(passwordField, 1, 3); // Add Submit Button Button submitButton = new Button("Submit"); submitButton.setPrefHeight(40); submitButton.setDefaultButton(true); submitButton.setPrefWidth(100); gridPane.add(submitButton, 0, 4, 2, 1); GridPane.setHalignment(submitButton, HPos.CENTER); GridPane.setMargin(submitButton, new Insets(20, 0,20,0));}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:
@Overridepublic void start(Stage primaryStage) throws Exception { primaryStage.setTitle("Registration Form JavaFX Application"); // Create the registration form grid pane GridPane gridPane = createRegistrationFormPane(); // Add UI controls to the registration form grid pane addUIControls(gridPane); // Create a scene with registration form grid pane as the root node Scene scene = new Scene(gridPane, 800, 500); // Set the scene in primary stage primaryStage.setScene(scene); primaryStage.show();}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:
| Call | Column | Row | Span | Lands in |
|---|---|---|---|---|
gridPane.add(headerLabel, 0,0,2,1) | 0 | 0 | 2 columns, 1 row | Row 0, across both columns |
gridPane.add(nameLabel, 0,1) | 0 | 1 | none | Row 1, left |
gridPane.add(nameField, 1,1) | 1 | 1 | none | Row 1, right |
gridPane.add(emailLabel, 0, 2) | 0 | 2 | none | Row 2, left |
gridPane.add(emailField, 1, 2) | 1 | 2 | none | Row 2, right |
gridPane.add(passwordLabel, 0, 3) | 0 | 3 | none | Row 3, left |
gridPane.add(passwordField, 1, 3) | 1 | 3 | none | Row 3, right |
gridPane.add(submitButton, 0, 4, 2, 1) | 0 | 4 | 2 columns, 1 row | Row 4, across 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'sHPos.RIGHTdoes 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.PasswordFielddraws a dot for each character typed, while itsgetText()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)andsetPrefHeight(40)fix the button at 100 by 40. A button does not stretch past its preferred size, soHPos.CENTERplaces it in the middle of row 4, and its margin of 20 above and below separates it from the password field.
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.
submitButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { if(nameField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your name"); return; } if(emailField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your email id"); return; } if(passwordField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter a password"); return; } showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(), "Registration Successful!", "Welcome " + nameField.getText()); }});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:
public interface EventHandler<T extends Event> extends EventListener { void handle(T event);}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 ideasetOnAction 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:
TextField nameField = new TextField();// ... the rest of addUIControls, including the handler that uses nameField ...nameField = new TextField(); // a second assignment: no longer effectively finalerror: 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:
private void showAlert(Alert.AlertType alertType, Window owner, String title, String message) { Alert alert = new Alert(alertType); alert.setTitle(title); alert.setHeaderText(null); alert.setContentText(message); alert.initOwner(owner); alert.show();}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:AlertTypeis an enum nested insideAlert. The handler passesERROR(an error icon and an OK button) orCONFIRMATION(a question icon, OK and Cancel).Window owner: the handler passesgridPane.getScene().getWindow(). From the grid, ask for theSceneit is in; from the scene, ask for the window showing it. That window isprimaryStage, butgetWindow()returns it as aWindow, the superclass ofStage, which is why the parameter has that type and the program importsjavafx.stage.Window. It is upcasting again.setTitle(title)puts “Form Error!” or “Registration Successful!” in the dialog's title bar, andsetContentText(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 beforeshow().show()puts the dialog up and returns at once.handlehas nothing left to do aftershowAlert, so not waiting costs nothing. The user still cannot use the form until the dialog is closed, because alerts are modal.
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:
submitButton.setOnAction(event -> { if (nameField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your name"); return; } if (emailField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your email id"); return; } if (passwordField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter a password"); return; } showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(), "Registration Successful!", "Welcome " + nameField.getText());});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.)
import javafx.application.Application;import javafx.event.ActionEvent;import javafx.event.EventHandler;import javafx.geometry.HPos;import javafx.geometry.Insets;import javafx.geometry.Pos;import javafx.scene.Scene;import javafx.scene.control.*;import javafx.scene.layout.*;import javafx.scene.text.Font;import javafx.scene.text.FontWeight;import javafx.stage.Stage;import javafx.stage.Window; public class RegistrationFormApplication extends Application { @Override public void start(Stage primaryStage) throws Exception { primaryStage.setTitle("Registration Form JavaFX Application"); // Create the registration form grid pane GridPane gridPane = createRegistrationFormPane(); // Add UI controls to the registration form grid pane addUIControls(gridPane); // Create a scene with registration form grid pane as the root node Scene scene = new Scene(gridPane, 800, 500); // Set the scene in primary stage primaryStage.setScene(scene); primaryStage.show(); } private GridPane createRegistrationFormPane() { // Instantiate a new Grid Pane GridPane gridPane = new GridPane(); // Position the pane at the center of the screen, both vertically and horizontally gridPane.setAlignment(Pos.CENTER); // Set a padding of 20px on each side gridPane.setPadding(new Insets(40, 40, 40, 40)); // Set the horizontal gap between columns gridPane.setHgap(10); // Set the vertical gap between rows gridPane.setVgap(10); // Add Column Constraints // columnOneConstraints will be applied to all the nodes placed in column one. ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE); columnOneConstraints.setHalignment(HPos.RIGHT); // columnTwoConstraints will be applied to all the nodes placed in column two. ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE); columnTwoConstrains.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains); return gridPane; } private void addUIControls(GridPane gridPane) { // Add Header Label headerLabel = new Label("Registration Form"); headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24)); gridPane.add(headerLabel, 0,0,2,1); GridPane.setHalignment(headerLabel, HPos.CENTER); GridPane.setMargin(headerLabel, new Insets(20, 0,20,0)); // Add Name Label Label nameLabel = new Label("Full Name : "); gridPane.add(nameLabel, 0,1); // Add Name Text Field TextField nameField = new TextField(); nameField.setPrefHeight(40); gridPane.add(nameField, 1,1); // Add Email Label Label emailLabel = new Label("Email ID : "); gridPane.add(emailLabel, 0, 2); // Add Email Text Field TextField emailField = new TextField(); emailField.setPrefHeight(40); gridPane.add(emailField, 1, 2); // Add Password Label Label passwordLabel = new Label("Password : "); gridPane.add(passwordLabel, 0, 3); // Add Password Field PasswordField passwordField = new PasswordField(); passwordField.setPrefHeight(40); gridPane.add(passwordField, 1, 3); // Add Submit Button Button submitButton = new Button("Submit"); submitButton.setPrefHeight(40); submitButton.setDefaultButton(true); submitButton.setPrefWidth(100); gridPane.add(submitButton, 0, 4, 2, 1); GridPane.setHalignment(submitButton, HPos.CENTER); GridPane.setMargin(submitButton, new Insets(20, 0,20,0)); submitButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { if(nameField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your name"); return; } if(emailField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your email id"); return; } if(passwordField.getText().isEmpty()) { showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter a password"); return; } showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(), "Registration Successful!", "Welcome " + nameField.getText()); } }); } private void showAlert(Alert.AlertType alertType, Window owner, String title, String message) { Alert alert = new Alert(alertType); alert.setTitle(title); alert.setHeaderText(null); alert.setContentText(message); alert.initOwner(owner); alert.show(); } public static void main(String[] args) { launch(args); }}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.
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.
- 1You run the
javacommand. The JVM callsmain, andmaincallslaunch(args). - 2
launchstarts the JavaFX runtime, creates aRegistrationFormApplicationobject, makes a window, and callsstart(primaryStage)on the JavaFX Application Thread, the one thread allowed to touch the window. - 3
startsets the title and callscreateRegistrationFormPane(), which builds an emptyGridPanewith its alignment, padding, gaps and two column constraints, and returns it. - 4
startcallsaddUIControls(gridPane). The heading, three labels, two text fields, the password field and the button go into their cells, andsetOnActionstores the handler object in the button.handledoes not run. - 5
startwraps the grid in an 800 by 500Scene, puts it on the stage and callsshow(). Thenstartreturns, but the program does not end: the runtime keeps the window open and waits for the user. - 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.
- 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. - 8JavaFX calls
handle(event)on the stored handler. The name, email and password checks each find text, so none of them returns early. - 9The last line calls
showAlertwithCONFIRMATION, the window, “Registration Successful!” and “Welcome Asha Verma”.showAlertbuilds the alert, sets its title, removes its header, sets the message, makes the form its owner and callsshow(), which returns at once.handleends. - 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, soisEmpty()is false, and the welcome reads “Welcome” followed by spaces. - An email with no @.
ashapasses, and so does any word at all. - A one-letter password.
ais 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
CONFIRMATIONalert, whose Cancel button does exactly what OK does. AnINFORMATIONalert, 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.
submitButton.setOnAction(event -> { Window owner = gridPane.getScene().getWindow(); String name = nameField.getText().trim(); String email = emailField.getText().trim(); String password = passwordField.getText(); // not trimmed: spaces may be deliberate if (name.isEmpty()) { showAlert(Alert.AlertType.ERROR, owner, "Form Error!", "Please enter your name"); return; } int at = email.indexOf("@"); if (at < 1 || !email.substring(at + 1).contains(".")) { showAlert(Alert.AlertType.ERROR, owner, "Form Error!", "Please enter a valid email id"); return; } if (password.length() < 8) { showAlert(Alert.AlertType.ERROR, owner, "Form Error!", "Password must be at least 8 characters"); return; } showAlert(Alert.AlertType.INFORMATION, owner, "Registration Successful!", "Welcome " + name); nameField.clear(); emailField.clear(); passwordField.clear(); nameField.requestFocus();});" " 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, soat < 1catches 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() < 8rejects short passwords, including an empty one.- After success,
clear()empties each field andrequestFocus()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.
public class Main { // The stricter form's three rules, without the window. // Returns the message the form would show, or "OK". static String check(String name, String email, String password) { name = name.trim(); email = email.trim(); if (name.isEmpty()) { return "Please enter your name"; } int at = email.indexOf("@"); if (at < 1 || !email.substring(at + 1).contains(".")) { return "Please enter a valid email id"; } if (password.length() < 8) { return "Password must be at least 8 characters"; } return "OK"; } public static void main(String[] args) { String[] names = {" ", "Asha Verma", "Asha Verma", "Asha Verma"}; String[] emails = {"asha@example.com", "asha", "asha@example.com", " asha@example.com "}; String[] passwords = {"secret123", "secret123", "a", "secret123"}; for (int i = 0; i < names.length; i++) { System.out.println("Test " + (i + 1) + ": " + check(names[i], emails[i], passwords[i])); } }}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.
Try it
Answer each one in your head or on paper first, then open the answer.
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?columnOneConstraints.setHalignment(HPos.RIGHT); and nothing else? Does the heading move?new Insets(20, 0, 20, 0) mean in GridPane.setMargin(headerLabel, ...)? What would new Insets(0, 0, 40, 0) change?return;? What would the user see if all three fields were empty and the three returns had been deleted?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.
PasswordField, and move Submit down a row. Reject the form with “Passwords do not match” when the two differ.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