JavaFX Controls, Layouts & FXML

Project: A Form-Based JavaFX App with FXML

18 min Lesson 10 of 12

Project: A Form-Based JavaFX App with FXML

This final lesson puts everything from the tutorial together into a single, self-contained project: a Contact Registration Form. The application collects a first name, last name, email address, gender, and a short bio, validates the input when the user clicks Submit, then displays the submitted record in a read-only summary panel. The entire UI is defined in FXML and wired to a Java controller — no layout code lives in Java.

Why this project? Forms are the most common GUI pattern. Building one end-to-end forces you to combine every skill covered in this tutorial: layout panes, controls, FXML, controllers, event handling, and validation feedback.

Project Structure

Keep the source tree clean. A typical Maven layout for a JavaFX project looks like this:

src/ main/ java/ com/example/contactapp/ MainApp.java ContactController.java Contact.java resources/ com/example/contactapp/ contact-form.fxml styles.css

The FXML file and the CSS file live inside resources at the same package path as the Java classes. JavaFX resolves them via getClass().getResource("contact-form.fxml") when loading the scene.

The Model: Contact.java

A minimal plain-Java model carries the form data between the controller and the rest of the application. No JavaFX types here — keeping the model free of UI dependencies means you can test it without a running toolkit.

package com.example.contactapp; public class Contact { private final String firstName; private final String lastName; private final String email; private final String gender; private final String bio; public Contact(String firstName, String lastName, String email, String gender, String bio) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.gender = gender; this.bio = bio; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public String getGender() { return gender; } public String getBio() { return bio; } }

The FXML File: contact-form.fxml

The root element is a BorderPane. The center holds a GridPane with the form fields; the bottom holds the action buttons; the right holds the summary panel that appears after submission.

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.geometry.Insets?> <BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.contactapp.ContactController" prefWidth="720" prefHeight="480" stylesheets="@styles.css"> <!-- ===== Centre: input form ===== --> <center> <GridPane hgap="12" vgap="14"> <padding><Insets top="24" right="24" bottom="24" left="24"/></padding> <!-- Column constraints --> <columnConstraints> <ColumnConstraints minWidth="110" halignment="RIGHT"/> <ColumnConstraints hgrow="ALWAYS"/> </columnConstraints> <Label text="First Name *" GridPane.columnIndex="0" GridPane.rowIndex="0"/> <TextField fx:id="firstNameField" promptText="Enter first name" GridPane.columnIndex="1" GridPane.rowIndex="0"/> <Label text="Last Name *" GridPane.columnIndex="0" GridPane.rowIndex="1"/> <TextField fx:id="lastNameField" promptText="Enter last name" GridPane.columnIndex="1" GridPane.rowIndex="1"/> <Label text="Email *" GridPane.columnIndex="0" GridPane.rowIndex="2"/> <TextField fx:id="emailField" promptText="user@example.com" GridPane.columnIndex="1" GridPane.rowIndex="2"/> <Label text="Gender" GridPane.columnIndex="0" GridPane.rowIndex="3"/> <ComboBox fx:id="genderCombo" promptText="Select…" GridPane.columnIndex="1" GridPane.rowIndex="3"/> <Label text="Bio" GridPane.columnIndex="0" GridPane.rowIndex="4" GridPane.valignment="TOP" style="-fx-padding: 4 0 0 0;"/> <TextArea fx:id="bioArea" promptText="A short introduction…" prefRowCount="4" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="4"/> <Label fx:id="errorLabel" styleClass="error-label" GridPane.columnIndex="1" GridPane.rowIndex="5"/> </GridPane> </center> <!-- ===== Bottom: action buttons ===== --> <bottom> <HBox spacing="12" alignment="CENTER_RIGHT"> <padding><Insets bottom="16" right="24"/></padding> <Button text="Clear" onAction="#handleClear"/> <Button text="Submit" defaultButton="true" styleClass="primary-btn" onAction="#handleSubmit"/> </HBox> </bottom> <!-- ===== Right: summary panel (hidden until submitted) ===== --> <right> <VBox fx:id="summaryPane" spacing="8" visible="false" managed="false" styleClass="summary-pane"> <padding><Insets top="24" right="20" bottom="24" left="20"/></padding> <Label text="Submitted Record" styleClass="summary-title"/> <Label fx:id="summaryName"/> <Label fx:id="summaryEmail"/> <Label fx:id="summaryGender"/> <Label fx:id="summaryBio" wrapText="true" maxWidth="200"/> </VBox> </right> </BorderPane>
Set managed="false" together with visible="false" on any pane you want to hide initially. visible="false" alone makes the pane invisible but keeps its space reserved, leaving an ugly blank gap. Setting both properties removes it from layout flow entirely until you need it.

The Controller: ContactController.java

The controller is where all behavior lives. It is instantiated by the FXMLLoader — never by your own code — so every field annotated with @FXML is injected automatically before initialize() runs.

package com.example.contactapp; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.layout.VBox; import java.net.URL; import java.util.ResourceBundle; import java.util.regex.Pattern; public class ContactController implements Initializable { // ---- injected controls ---- @FXML private TextField firstNameField; @FXML private TextField lastNameField; @FXML private TextField emailField; @FXML private ComboBox<String> genderCombo; @FXML private TextArea bioArea; @FXML private Label errorLabel; // ---- summary panel ---- @FXML private VBox summaryPane; @FXML private Label summaryName; @FXML private Label summaryEmail; @FXML private Label summaryGender; @FXML private Label summaryBio; private static final Pattern EMAIL_RE = Pattern.compile("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$"); @Override public void initialize(URL location, ResourceBundle resources) { genderCombo.setItems( FXCollections.observableArrayList("Male", "Female", "Prefer not to say") ); } @FXML private void handleSubmit() { String firstName = firstNameField.getText().trim(); String lastName = lastNameField.getText().trim(); String email = emailField.getText().trim(); String gender = genderCombo.getValue(); String bio = bioArea.getText().trim(); // ---- validation ---- if (firstName.isEmpty() || lastName.isEmpty()) { showError("First name and last name are required."); return; } if (!EMAIL_RE.matcher(email).matches()) { showError("Please enter a valid email address."); return; } clearError(); Contact c = new Contact(firstName, lastName, email, gender != null ? gender : "—", bio); // ---- populate summary ---- summaryName.setText(c.getFirstName() + " " + c.getLastName()); summaryEmail.setText(c.getEmail()); summaryGender.setText("Gender: " + c.getGender()); summaryBio.setText(bio.isEmpty() ? "(no bio)" : bio); summaryPane.setVisible(true); summaryPane.setManaged(true); } @FXML private void handleClear() { firstNameField.clear(); lastNameField.clear(); emailField.clear(); genderCombo.setValue(null); bioArea.clear(); clearError(); summaryPane.setVisible(false); summaryPane.setManaged(false); } private void showError(String message) { errorLabel.setText(message); } private void clearError() { errorLabel.setText(""); } }

The Entry Point: MainApp.java

The application class is lean. Its only job is to load the FXML, attach the scene, and show the stage.

package com.example.contactapp; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class MainApp extends Application { @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader( getClass().getResource("contact-form.fxml") ); Parent root = loader.load(); primaryStage.setTitle("Contact Registration"); primaryStage.setScene(new Scene(root)); primaryStage.setResizable(true); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
The resource path must match the package. If the FXML file is at resources/com/example/contactapp/contact-form.fxml, then getClass().getResource("contact-form.fxml") resolves correctly because MainApp lives in the same package. Using an absolute path like "/contact-form.fxml" would fail at runtime unless the file is in the root of the classpath.

Minimal CSS: styles.css

JavaFX CSS is a subset of CSS 2 with JavaFX-specific properties prefixed with -fx-. A small stylesheet makes the form look polished without cluttering the FXML or the controller.

.primary-btn { -fx-background-color: #2563eb; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 6 18 6 18; } .primary-btn:hover { -fx-background-color: #1d4ed8; } .error-label { -fx-text-fill: #dc2626; -fx-font-size: 12px; } .summary-pane { -fx-background-color: #f0f9ff; -fx-border-color: #bae6fd; -fx-border-width: 1; } .summary-title { -fx-font-size: 14px; -fx-font-weight: bold; }

Key Design Decisions

  • Separation of concerns: layout in FXML, behavior in the controller, data in the model. None of these three files knows the internal details of the others.
  • Validation before model construction: the Contact object is only built after all checks pass. This keeps the model always valid — a principle that scales well when you later add database persistence.
  • managed + visible toggling: the summary pane uses both flags so the window does not waste space before submission, and expands naturally to reveal the panel afterward.
  • Regex compiled once: the email pattern is a static final constant, not recompiled on every submit click.

Summary

You have now built a complete form-driven JavaFX application: a clean FXML layout combining BorderPane, GridPane, HBox, and VBox; a controller that handles initialization, validation, submission, and clearing; a plain-Java model; and a CSS stylesheet that applies branding without touching Java code. This architecture — FXML for structure, controller for behavior, model for data — is the standard pattern you will use and see in every professional JavaFX codebase.