FXML is an XML-based language for designing JavaFX user interfaces, enabling a clean separation between UI layout and application logic. This comprehensive guide covers FXML usage, best practices, and advanced techniques.
Basic FXML Structure
1. Simple FXML Document
hello-view.fxml:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.Label?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.text.Font?> <VBox xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" spacing="20.0" alignment="CENTER" style="-fx-padding: 30; -fx-background-color: #f5f5f5;" fx:controller="com.example.controller.HelloController"> <Label text="Welcome to JavaFX!" style="-fx-text-fill: #2c3e50; -fx-font-weight: bold;"> <font> <Font size="24.0" /> </font> </Label> <Button text="Click Me!" onAction="#handleButtonClick" style="-fx-background-color: #3498db; -fx-text-fill: white; -fx-font-size: 14px;" prefWidth="120.0" prefHeight="40.0"/> <Label fx:id="messageLabel" text="Click the button above" style="-fx-text-fill: #7f8c8d;"/> </VBox>
2. Corresponding Controller
HelloController.java:
package com.example.controller;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.event.ActionEvent;
public class HelloController {
@FXML
private Label messageLabel;
@FXML
private Button clickButton;
private int clickCount = 0;
// Initialize method called after FXML loading
@FXML
private void initialize() {
System.out.println("Controller initialized!");
updateMessage();
}
@FXML
private void handleButtonClick(ActionEvent event) {
clickCount++;
updateMessage();
// Change button style after clicks
if (clickCount >= 5) {
clickButton.setStyle("-fx-background-color: #e74c3c; -fx-text-fill: white;");
}
}
private void updateMessage() {
String message = String.format("Button clicked %d time%s",
clickCount, clickCount == 1 ? "" : "s");
messageLabel.setText(message);
}
// Public method that can be called from main application
public void resetCounter() {
clickCount = 0;
updateMessage();
clickButton.setStyle("-fx-background-color: #3498db; -fx-text-fill: white;");
}
}
3. Main Application Class
MainApplication.java:
package com.example;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class MainApplication extends Application {
private static Stage primaryStage;
@Override
public void start(Stage stage) throws IOException {
primaryStage = stage;
showHelloView();
}
public void showHelloView() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/hello-view.fxml"));
Parent root = loader.load();
// Get controller instance if needed
HelloController controller = loader.getController();
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add(getClass().getResource("/css/styles.css").toExternalForm());
primaryStage.setTitle("JavaFX FXML Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Advanced FXML Layouts
4. Complex Layout with Multiple Panes
dashboard-view.fxml:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.scene.chart.*?> <?import javafx.collections.FXCollections?> <?import javafx.geometry.Insets?> <BorderPane xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.controller.DashboardController"> <!-- Top Menu Bar --> <top> <MenuBar> <menus> <Menu text="File"> <items> <MenuItem text="New" onAction="#handleNew"/> <MenuItem text="Open" onAction="#handleOpen"/> <SeparatorMenuItem/> <MenuItem text="Exit" onAction="#handleExit"/> </items> </Menu> <Menu text="View"> <items> <CheckMenuItem text="Show Charts" selected="true" onAction="#toggleChartsVisibility"/> <CheckMenuItem text="Dark Mode" onAction="#toggleDarkMode"/> </items> </Menu> </menus> </MenuBar> </top> <!-- Left Navigation --> <left> <VBox spacing="10" style="-fx-background-color: #34495e; -fx-padding: 15;"> <Label text="NAVIGATION" style="-fx-text-fill: #ecf0f1; -fx-font-weight: bold; -fx-padding: 0 0 10 0;"/> <Button text="Dashboard" onAction="#showDashboard" style="-fx-background-color: #3498db; -fx-text-fill: white;" maxWidth="Infinity"/> <Button text="Users" onAction="#showUsers" style="-fx-background-color: transparent; -fx-text-fill: #bdc3c7;" maxWidth="Infinity"/> <Button text="Settings" onAction="#showSettings" style="-fx-background-color: transparent; -fx-text-fill: #bdc3c7;" maxWidth="Infinity"/> </VBox> </left> <!-- Center Content --> <center> <TabPane fx:id="mainTabPane" tabClosingPolicy="UNAVAILABLE"> <tabs> <Tab text="Dashboard"> <ScrollPane fitToWidth="true" fitToHeight="true"> <VBox spacing="20" style="-fx-padding: 20;"> <!-- Statistics Cards --> <HBox spacing="20" alignment="CENTER"> <VBox style="-fx-background-color: white; -fx-padding: 20; -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 10, 0, 0, 0);" prefWidth="200"> <Label text="Total Users" style="-fx-text-fill: #7f8c8d;"/> <Label fx:id="totalUsersLabel" text="0" style="-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #2c3e50;"/> </VBox> <VBox style="-fx-background-color: white; -fx-padding: 20; -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 10, 0, 0, 0);" prefWidth="200"> <Label text="Revenue" style="-fx-text-fill: #7f8c8d;"/> <Label fx:id="revenueLabel" text="$0.00" style="-fx-font-size: 24; -fx-font-weight: bold; -fx-text-fill: #27ae60;"/> </VBox> </HBox> <!-- Chart --> <LineChart fx:id="salesChart" prefHeight="300"> <xAxis> <CategoryAxis label="Month"/> </xAxis> <yAxis> <NumberAxis label="Sales"/> </yAxis> </LineChart> </VBox> </ScrollPane> </Tab> <Tab text="User Management"> <VBox spacing="15" style="-fx-padding: 20;"> <HBox spacing="10" alignment="CENTER_LEFT"> <TextField fx:id="searchField" promptText="Search users..." prefWidth="300"/> <Button text="Search" onAction="#searchUsers"/> <Region HBox.hgrow="ALWAYS"/> <Button text="Add User" onAction="#addUser" style="-fx-background-color: #27ae60; -fx-text-fill: white;"/> </HBox> <TableView fx:id="usersTableView" prefHeight="400"> <columns> <TableColumn text="ID" prefWidth="80"> <cellValueFactory> <PropertyValueFactory property="id"/> </cellValueFactory> </TableColumn> <TableColumn text="Name" prefWidth="150"> <cellValueFactory> <PropertyValueFactory property="name"/> </cellValueFactory> </TableColumn> <TableColumn text="Email" prefWidth="200"> <cellValueFactory> <PropertyValueFactory property="email"/> </cellValueFactory> </TableColumn> <TableColumn text="Role" prefWidth="100"> <cellValueFactory> <PropertyValueFactory property="role"/> </cellValueFactory> </TableColumn> </columns> </TableView> </VBox> </Tab> </tabs> </TabPane> </center> <!-- Bottom Status Bar --> <bottom> <BorderPane style="-fx-background-color: #ecf0f1; -fx-padding: 5;"> <left> <Label fx:id="statusLabel" text="Ready"/> </left> <right> <Label fx:id="timeLabel" text="00:00:00"/> </right> </BorderPane> </bottom> </BorderPane>
5. Advanced Controller
DashboardController.java:
package com.example.controller;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.XYChart;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.util.Duration;
import java.net.URL;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ResourceBundle;
public class DashboardController implements Initializable {
@FXML private Label totalUsersLabel;
@FXML private Label revenueLabel;
@FXML private Label statusLabel;
@FXML private Label timeLabel;
@FXML private LineChart<String, Number> salesChart;
@FXML private TableView<User> usersTableView;
@FXML private TextField searchField;
@FXML private TabPane mainTabPane;
private ObservableList<User> users = FXCollections.observableArrayList();
private Timeline clockTimeline;
@Override
public void initialize(URL location, ResourceBundle resources) {
setupClock();
initializeData();
setupChart();
setupTable();
}
private void setupClock() {
clockTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> {
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
timeLabel.setText(time);
}));
clockTimeline.setCycleCount(Animation.INDEFINITE);
clockTimeline.play();
}
private void initializeData() {
// Sample data
totalUsersLabel.setText("1,247");
revenueLabel.setText("$45,678.90");
users.addAll(
new User(1, "John Doe", "[email protected]", "Admin"),
new User(2, "Jane Smith", "[email protected]", "User"),
new User(3, "Bob Johnson", "[email protected]", "Editor")
);
}
private void setupChart() {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName("Monthly Sales");
series.getData().add(new XYChart.Data<>("Jan", 1000));
series.getData().add(new XYChart.Data<>("Feb", 1500));
series.getData().add(new XYChart.Data<>("Mar", 1200));
series.getData().add(new XYChart.Data<>("Apr", 1800));
series.getData().add(new XYChart.Data<>("May", 2000));
salesChart.getData().add(series);
}
private void setupTable() {
usersTableView.setItems(users);
}
// Event handlers
@FXML
private void handleNew(ActionEvent event) {
statusLabel.setText("Creating new file...");
}
@FXML
private void handleOpen(ActionEvent event) {
FileChooser fileChooser = new FileChooser();
fileChooser.showOpenDialog(null);
}
@FXML
private void handleExit(ActionEvent event) {
System.exit(0);
}
@FXML
private void toggleChartsVisibility(ActionEvent event) {
CheckMenuItem menuItem = (CheckMenuItem) event.getSource();
salesChart.setVisible(menuItem.isSelected());
}
@FXML
private void toggleDarkMode(ActionEvent event) {
// Implement dark mode toggle
}
@FXML
private void showDashboard(ActionEvent event) {
mainTabPane.getSelectionModel().select(0);
}
@FXML
private void showUsers(ActionEvent event) {
mainTabPane.getSelectionModel().select(1);
}
@FXML
private void showSettings(ActionEvent event) {
// Show settings tab
}
@FXML
private void searchUsers(ActionEvent event) {
String searchText = searchField.getText().toLowerCase();
if (searchText.isEmpty()) {
usersTableView.setItems(users);
} else {
ObservableList<User> filtered = users.filtered(user ->
user.getName().toLowerCase().contains(searchText) ||
user.getEmail().toLowerCase().contains(searchText)
);
usersTableView.setItems(filtered);
}
}
@FXML
private void addUser(ActionEvent event) {
// Open add user dialog
statusLabel.setText("Add user dialog opened");
}
public void cleanup() {
if (clockTimeline != null) {
clockTimeline.stop();
}
}
}
// User model class
class User {
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
private final StringProperty role = new SimpleStringProperty();
public User(int id, String name, String email, String role) {
setId(id);
setName(name);
setEmail(email);
setRole(role);
}
// Getters and setters for properties
public int getId() { return id.get(); }
public void setId(int value) { id.set(value); }
public IntegerProperty idProperty() { return id; }
public String getName() { return name.get(); }
public void setName(String value) { name.set(value); }
public StringProperty nameProperty() { return name; }
public String getEmail() { return email.get(); }
public void setEmail(String value) { email.set(value); }
public StringProperty emailProperty() { return name; }
public String getRole() { return role.get(); }
public void setRole(String value) { role.set(value); }
public StringProperty roleProperty() { return role; }
}
CSS Styling with FXML
6. External CSS Stylesheet
styles.css:
/* Root styles */
.root {
-fx-font-family: "Segoe UI", Arial, sans-serif;
-fx-font-size: 14px;
}
/* Button styles */
.button {
-fx-background-radius: 4px;
-fx-border-radius: 4px;
-fx-cursor: hand;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 2);
}
.button:hover {
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 3);
}
.button:pressed {
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 2, 0, 0, 1);
}
/* Primary button */
.button-primary {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
.button-primary:hover {
-fx-background-color: #2980b9;
}
/* Success button */
.button-success {
-fx-background-color: #27ae60;
-fx-text-fill: white;
}
.button-success:hover {
-fx-background-color: #229954;
}
/* Text field styles */
.text-field {
-fx-background-radius: 4px;
-fx-border-radius: 4px;
-fx-border-color: #bdc3c7;
-fx-border-width: 1px;
-fx-padding: 8px;
}
.text-field:focused {
-fx-border-color: #3498db;
-fx-effect: dropshadow(three-pass-box, rgba(52, 152, 219, 0.3), 4, 0, 0, 2);
}
/* Table styles */
.table-view {
-fx-border-color: #bdc3c7;
-fx-border-width: 1px;
}
.table-view .column-header {
-fx-background-color: #34495e;
-fx-text-fill: white;
-fx-font-weight: bold;
}
.table-view .table-row-cell:even {
-fx-background-color: #f8f9fa;
}
.table-view .table-row-cell:odd {
-fx-background-color: white;
}
.table-view .table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
/* Card styles */
.card {
-fx-background-color: white;
-fx-background-radius: 8px;
-fx-border-radius: 8px;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 10, 0, 0, 0);
-fx-padding: 20px;
}
/* Label styles */
.label-heading {
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-text-fill: #2c3e50;
}
.label-subheading {
-fx-font-size: 14px;
-fx-text-fill: #7f8c8d;
}
/* Progress indicator */
.progress-indicator {
-fx-progress-color: #3498db;
}
/* Tab pane styles */
.tab-pane .tab-header-area .tab-header-background {
-fx-background-color: #ecf0f1;
}
.tab-pane .tab {
-fx-background-color: #bdc3c7;
-fx-background-radius: 4px 4px 0 0;
}
.tab-pane .tab:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
Custom Components and Reusability
7. Custom FXML Components
user-card.fxml:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.Label?> <?import javafx.scene.control.Button?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.image.ImageView?> <?import javafx.scene.image.Image?> <VBox xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" style="-fx-background-color: white; -fx-padding: 15; -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 0); -fx-spacing: 10;" fx:controller="com.example.controller.UserCardController"> <HBox spacing="10" alignment="CENTER_LEFT"> <ImageView fx:id="avatarImage" fitWidth="40" fitHeight="40" style="-fx-background-radius: 20;"/> <VBox spacing="2"> <Label fx:id="nameLabel" style="-fx-font-weight: bold; -fx-font-size: 14;"/> <Label fx:id="emailLabel" style="-fx-text-fill: #7f8c8d; -fx-font-size: 12;"/> </VBox> </HBox> <HBox spacing="10" alignment="CENTER_RIGHT"> <Button text="Message" onAction="#handleMessage" style="-fx-background-color: #3498db; -fx-text-fill: white; -fx-font-size: 12;"/> <Button text="Profile" onAction="#handleProfile" style="-fx-background-color: transparent; -fx-border-color: #3498db; -fx-text-fill: #3498db; -fx-font-size: 12;"/> </HBox> </VBox>
UserCardController.java:
package com.example.controller;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.net.URL;
import java.util.ResourceBundle;
public class UserCardController implements Initializable {
@FXML private Label nameLabel;
@FXML private Label emailLabel;
@FXML private ImageView avatarImage;
private User user;
@Override
public void initialize(URL location, ResourceBundle resources) {
// Default initialization
}
public void setUser(User user) {
this.user = user;
updateView();
}
private void updateView() {
if (user != null) {
nameLabel.setText(user.getName());
emailLabel.setText(user.getEmail());
// Set avatar image (you can use a default image)
try {
Image image = new Image(getClass().getResourceAsStream("/images/avatar.png"));
avatarImage.setImage(image);
} catch (Exception e) {
// Use default image or leave blank
}
}
}
@FXML
private void handleMessage() {
System.out.println("Message user: " + user.getName());
}
@FXML
private void handleProfile() {
System.out.println("View profile: " + user.getName());
}
}
8. Using Custom Components in Main FXML
main-view.fxml (with custom components):
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import com.example.component.UserCard?> <ScrollPane xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fitToWidth="true" fx:controller="com.example.controller.MainController"> <VBox spacing="15" style="-fx-padding: 20;"> <Label text="User Directory" style="-fx-font-size: 24; -fx-font-weight: bold;"/> <FlowPane hgap="15" vgap="15" prefWrapLength="800"> <!-- Custom UserCard components will be added dynamically --> </FlowPane> </VBox> </ScrollPane>
Dynamic FXML Loading
9. Dynamic UI Management
DynamicViewController.java:
package com.example.controller;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DynamicViewController {
@FXML private FlowPane userCardsContainer;
private List<UserCardController> userCardControllers = new ArrayList<>();
@FXML
private void initialize() {
loadUserCards();
}
private void loadUserCards() {
try {
List<User> users = getUserData(); // Get users from service
for (User user : users) {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/user-card.fxml")
);
Node userCard = loader.load();
UserCardController controller = loader.getController();
controller.setUser(user);
userCardsContainer.getChildren().add(userCard);
userCardControllers.add(controller);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void refreshUserCards() {
userCardsContainer.getChildren().clear();
userCardControllers.clear();
loadUserCards();
}
private List<User> getUserData() {
// Sample data - in real app, this would come from a service
return List.of(
new User(1, "John Doe", "[email protected]", "Admin"),
new User(2, "Jane Smith", "[email protected]", "User"),
new User(3, "Bob Johnson", "[email protected]", "Editor"),
new User(4, "Alice Brown", "[email protected]", "User")
);
}
}
Best Practices and Patterns
10. FXML Best Practices
Controller Factory Pattern:
public class ControllerFactory {
public static <T> T loadFXML(String fxmlPath) throws IOException {
FXMLLoader loader = new FXMLLoader(
MainApplication.class.getResource(fxmlPath)
);
Parent root = loader.load();
return loader.getController();
}
public static <T> T loadFXMLWithRoot(String fxmlPath, Parent root) throws IOException {
FXMLLoader loader = new FXMLLoader(
MainApplication.class.getResource(fxmlPath)
);
loader.setRoot(root);
loader.load();
return loader.getController();
}
}
// Usage
public class MainController {
public void showUserProfile(User user) throws IOException {
UserProfileController controller = ControllerFactory.loadFXML("/fxml/user-profile.fxml");
controller.setUser(user);
// Show in dialog or new window
}
}
Event Bus for Communication:
public class EventBus {
private static final Map<Class<?>, List<Consumer<?>>> subscribers = new HashMap<>();
public static <T> void subscribe(Class<T> eventType, Consumer<T> handler) {
subscribers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
}
public static <T> void publish(T event) {
List<Consumer<?>> handlers = subscribers.get(event.getClass());
if (handlers != null) {
for (Consumer<?> handler : handlers) {
((Consumer<T>) handler).accept(event);
}
}
}
}
// Event classes
class UserUpdatedEvent {
private final User user;
public UserUpdatedEvent(User user) {
this.user = user;
}
public User getUser() { return user; }
}
// Usage in controllers
public class UserListController {
@FXML
private void initialize() {
EventBus.subscribe(UserUpdatedEvent.class, this::onUserUpdated);
}
private void onUserUpdated(UserUpdatedEvent event) {
refreshUserList();
}
}
Key Benefits of FXML
- Separation of Concerns: UI layout separated from application logic
- Type Safety: Compile-time checking of FXML references
- Tool Support: Visual editors like Scene Builder
- Reusability: Custom components and templates
- Maintainability: Clear structure and organization
- Internationalization: Built-in resource bundle support
- CSS Integration: Easy styling and theming
FXML provides a robust, maintainable approach to JavaFX UI development, especially for complex applications with multiple screens and reusable components.