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).
- Séparation des préoccupations : le design (FXML) est séparé de la logique (Java), ce qui facilite la maintenance.
- Lisibilité : la structure hiérarchique de l'interface est plus claire en XML qu'en Java procédural.
- Compatibilité avec les outils visuels : FXML est compatible avec Scene Builder, un outil graphique de conception d'interfaces.
- Modification sans recompilation : les fichiers FXML peuvent être modifiés sans recompiler le code Java.
- Collaboration facilitée : les designers peuvent travailler sur l'interface pendant que les développeurs travaillent sur la logique.
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:
fx:id
- Identifiant pour l'injection dans le contrôleurfx:controller
- Classe contrôleur associéefx:value
- Valeur à définir pour une propriétéfx:constant
- Référence à une constante
<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 :
getResource()
charge le fichier FXML depuis le classpathloader.load()
analyse le fichier FXML et crée la hiérarchie des nœudsloader.getController()
récupère l'instance du contrôleur associé
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
- Nommage cohérent : utilisez des conventions de nommage cohérentes pour les fichiers FXML et leurs contrôleurs associés.
- Injection sélective : n'annotez avec
@FXML
que les éléments dont vous avez besoin dans le contrôleur. - Organisation MVC : suivez le modèle MVC (Modèle-Vue-Contrôleur) en séparant clairement la logique métier, l'interface utilisateur et le code de contrôle.
- Modularité : divisez les interfaces complexes en plusieurs fichiers FXML réutilisables.
- Gestionnaires d'événements spécifiques : créez des méthodes de gestion d'événements distinctes plutôt qu'une méthode générique pour tous les événements.
- Utilisation de fx:include : pour inclure d'autres fichiers FXML dans votre interface.
<!-- 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>