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.