FXML et Contrôleurs JavaFX

FXML est un langage de balisage basé sur XML qui permet de définir l'interface utilisateur JavaFX de manière déclarative, en séparant la présentation (FXML) de la logique métier (contrôleurs Java).

Pourquoi utiliser FXML ?

FXML est un langage de balisage basé sur XML qui permet de définir l'interface utilisateur JavaFX de manière déclarative, en séparant la présentation (FXML) de la logique métier (contrôleurs Java).

Prenons un exemple simple pour illustrer la différence entre les deux approches :

Interface créée en Java procédural

// Création de l'interface en Java
VBox root = new VBox(10);
root.setPadding(new Insets(20));

Label titleLabel = new Label("Connexion");
titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");

Label usernameLabel = new Label("Nom d'utilisateur :");
TextField usernameField = new TextField();

Label passwordLabel = new Label("Mot de passe :");
PasswordField passwordField = new PasswordField();

Button loginButton = new Button("Se connecter");
loginButton.setOnAction(e -> handleLogin(usernameField.getText(), passwordField.getText()));

root.getChildren().addAll(
    titleLabel,
    usernameLabel, usernameField,
    passwordLabel, passwordField,
    loginButton
);

Même interface en FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>

<VBox spacing="10.0" xmlns:fx="http://javafx.com/fxml" 
      fx:controller="com.example.javafxdemo.LoginController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
    </padding>
    <children>
        <Label text="Connexion" style="-fx-font-size: 20px; -fx-font-weight: bold;" />
        <Label text="Nom d'utilisateur :" />
        <TextField fx:id="usernameField" />
        <Label text="Mot de passe :" />
        <PasswordField fx:id="passwordField" />
        <Button text="Se connecter" onAction="#handleLogin" />
    </children>
</VBox>

Structure d'un fichier FXML

Un fichier FXML typique contient les éléments suivants :

1. Entête XML et déclarations d'importation

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.geometry.Insets?>

Les instructions <?import?> importent les classes JavaFX nécessaires, similaires aux imports en Java.

2. Élément racine

<VBox xmlns:fx="http://javafx.com/fxml" 
      fx:controller="com.example.javafxdemo.MyController"
      spacing="10" alignment="CENTER">
    <!-- Contenu -->
</VBox>

Chaque fichier FXML a un élément racine (ici VBox) qui définit le conteneur principal. L'attribut xmlns:fx définit l'espace de noms FXML, et fx:controller spécifie la classe contrôleur associée.

3. Propriétés

Les propriétés peuvent être définies de plusieurs façons :

<!-- Comme attributs XML -->
<Button text="Cliquez-moi" textFill="blue" />

<!-- Comme éléments imbriqués -->
<Button>
    <text>Cliquez-moi</text>
    <textFill>
        <Color blue="1.0" />
    </textFill>
</Button>

<!-- Avec une balise <padding> pour les objets spéciaux -->
<VBox>
    <padding>
        <Insets top="10" right="10" bottom="10" left="10" />
    </padding>
</VBox>

4. Enfants et contenu

<VBox>
    <children>
        <Label text="Étiquette 1" />
        <Button text="Bouton 1" />
        <TextField promptText="Entrez du texte" />
    </children>
</VBox>

<!-- Version plus concise sans la balise <children> -->
<VBox>
    <Label text="Étiquette 1" />
    <Button text="Bouton 2" />
    <TextField promptText="Entrez du texte" />
</VBox>

La balise <children> est facultative dans de nombreux cas, et le contenu peut être ajouté directement comme enfants de l'élément parent.

5. Attributs spéciaux avec préfixe fx:

<TextField fx:id="nameField" />
<ComboBox>
    <value>
        <fx:reference source="defaultItem" />
    </value>
</ComboBox>
<Label alignment="fx:constant javafx.geometry.Pos.CENTER" />

Association avec un contrôleur

Le contrôleur contient la logique métier associée à l'interface FXML. Voici comment lier un contrôleur à une vue FXML :

1. Spécifier le contrôleur dans le FXML

<VBox xmlns:fx="http://javafx.com/fxml" 
      fx:controller="com.example.javafxdemo.HelloController">
    <!-- Contenu -->
</VBox>

2. Définir les champs annotés @FXML dans le contrôleur

package com.example.javafxdemo;

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;

public class HelloController {
    // L'annotation @FXML permet d'injecter les composants définis dans le FXML
    @FXML
    private TextField nameField;
    
    @FXML
    private Button helloButton;
    
    // Méthode d'initialisation appelée après le chargement du FXML
    @FXML
    private void initialize() {
        // Code d'initialisation
        nameField.setPromptText("Entrez votre nom");
    }
    
    // Gestionnaire d'événement pour l'action du bouton
    @FXML
    private void handleButtonAction() {
        String name = nameField.getText();
        System.out.println("Bonjour, " + name + " !");
    }
}

L'annotation @FXML est utilisée pour marquer les champs et méthodes qui doivent être accessibles depuis le fichier FXML.

3. Créer des gestionnaires d'événements dans le contrôleur

// Dans le contrôleur
@FXML
private void handleButtonClick() {
    String name = nameField.getText();
    if (name.trim().isEmpty()) {
        resultLabel.setText("Veuillez entrer un nom !");
    } else {
        resultLabel.setText("Bonjour, " + name + " !");
    }
}

// Dans le FXML
<Button text="Saluer" onAction="#handleButtonClick" />

La méthode de gestion d'événement est annotée avec @FXML et référencée dans le FXML avec le préfixe #.

4. Méthode initialize()

@FXML
private void initialize() {
    // Code exécuté après l'injection de tous les champs
    resultLabel.setText("Prêt !");
    nameField.setPromptText("Entrez votre nom");
}

La méthode initialize() est appelée automatiquement par JavaFX après l'injection de tous les champs annotés @FXML. Elle permet d'initialiser l'état de l'interface.

Chargement d'un fichier FXML

Le chargement d'un fichier FXML se fait généralement avec la classe FXMLLoader :

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class HelloApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        // Chargement du fichier FXML
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/example/javafxdemo/hello-view.fxml"));
        Parent root = loader.load();
        
        // Accès au contrôleur (facultatif)
        HelloController controller = loader.getController();
        
        // Configuration et affichage de la scène
        Scene scene = new Scene(root, 400, 300);
        primaryStage.setTitle("Hello FXML");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Points importants :

Injection de dépendances via Constructor Injection

Par défaut, JavaFX crée une instance du contrôleur en utilisant son constructeur sans argument. Cependant, vous pouvez personnaliser la création du contrôleur en utilisant setControllerFactory pour implémenter l'injection de dépendances :

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class DIApplication extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        // Création des services ou dépendances
        UserService userService = new UserService();
        
        // Configuration du chargeur FXML
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/example/javafxdemo/login-view.fxml"));
        
        // Factory personnalisée pour créer le contrôleur avec ses dépendances
        loader.setControllerFactory(param -> {
            if (param == LoginController.class) {
                return new LoginController(userService);
            }
            try {
                return param.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        
        // Chargement de la vue
        Parent root = loader.load();
        
        // Configuration de la scène
        Scene scene = new Scene(root, 400, 300);
        primaryStage.setTitle("Injection de dépendances");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Ce code va dans src/main/java/com/example/javafxdemo/DIApplication.java

Le contrôleur avec injection par constructeur :

package com.example.javafxdemo;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;

public class LoginController {
    // Services injectés
    private final UserService userService;
    
    // Éléments FXML
    @FXML private TextField usernameField;
    @FXML private PasswordField passwordField;
    @FXML private Label statusLabel;
    
    // Constructeur avec injection de dépendances
    public LoginController(UserService userService) {
        this.userService = userService;
    }
    
    @FXML
    private void initialize() {
        statusLabel.setText("Veuillez vous connecter");
    }
    
    @FXML
    private void handleLogin() {
        String username = usernameField.getText();
        String password = passwordField.getText();
        
        if (userService.authenticate(username, password)) {
            statusLabel.setText("Connexion réussie !");
            statusLabel.setStyle("-fx-text-fill: green;");
        } else {
            statusLabel.setText("Échec de la connexion");
            statusLabel.setStyle("-fx-text-fill: red;");
        }
    }
}

Ce code va dans src/main/java/com/example/javafxdemo/LoginController.java

Un service simple pour l'exemple :

package com.example.javafxdemo;

public class UserService {
    public boolean authenticate(String username, String password) {
        // Logique d'authentification (simplifiée pour l'exemple)
        return "admin".equals(username) && "password123".equals(password);
    }
    
    public void registerUser(String username, String password) {
        // Logique d'enregistrement d'utilisateur
        System.out.println("Utilisateur enregistré : " + username);
    }
}

Ce code va dans src/main/java/com/example/javafxdemo/UserService.java

Exemple complet : Application de salutation

Voici un exemple complet d'une application JavaFX utilisant FXML, avec les fichiers correctement organisés :

Structure du projet

src/
├── main/
│   ├── java/
│   │   ├── module-info.java
│   │   └── com/
│   │       └── example/
│   │           └── javafxdemo/
│   │               ├── MainApplication.java
│   │               └── HelloController.java
│   └── resources/
│       └── com/
│           └── example/
│               └── javafxdemo/
│                   └── hello-view.fxml

module-info.java

module com.example.javafxdemo {
    requires javafx.controls;
    requires javafx.fxml;

    opens com.example.javafxdemo to javafx.fxml;
    exports com.example.javafxdemo;
}

Ce code va dans src/main/java/module-info.java

MainApplication.java

package com.example.javafxdemo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class MainApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(MainApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 400, 300);
        stage.setTitle("FXML Hello World");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

Ce code va dans src/main/java/com/example/javafxdemo/MainApplication.java

HelloController.java

package com.example.javafxdemo;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class HelloController {
    @FXML
    private TextField nameField;

    @FXML
    private Label welcomeText;

    @FXML
    protected void onHelloButtonClick() {
        String name = nameField.getText().trim();
        if (name.isEmpty()) {
            welcomeText.setText("Veuillez entrer un nom !");
        } else {
            welcomeText.setText("Bonjour " + name + " !");
        }
    }
}

Ce code va dans src/main/java/com/example/javafxdemo/HelloController.java

hello-view.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.javafxdemo.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label text="Entrez votre nom :"/>
    <TextField fx:id="nameField"/>
    <Button text="Dire bonjour !" onAction="#onHelloButtonClick"/>
    <Label fx:id="welcomeText"/>
</VBox>

Ce code va dans src/main/resources/com/example/javafxdemo/hello-view.fxml

Bonnes pratiques avec FXML

<!-- Utilisation de fx:include pour la modularité -->
<BorderPane xmlns:fx="http://javafx.com/fxml">
    <top>
        <fx:include source="header.fxml"/>
    </top>
    <center>
        <fx:include source="content.fxml"/>
    </center>
    <bottom>
        <fx:include source="footer.fxml"/>
    </bottom>
</BorderPane>