FXML for UI Design in Java

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

  1. Separation of Concerns: UI layout separated from application logic
  2. Type Safety: Compile-time checking of FXML references
  3. Tool Support: Visual editors like Scene Builder
  4. Reusability: Custom components and templates
  5. Maintainability: Clear structure and organization
  6. Internationalization: Built-in resource bundle support
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper