Accueil / Articles PiApplications. / La plate-forme Java / Java FX

Emploi d'un formulaire FXML comme composant.

Si vous avez déjà programmé une application JavaFX en vous appuyant sur des formulaires créés avec des outils comme Scene Builder, vous avez sans doute constaté que le volume de code augmente rapidement avec le nombre de contrôles sur le formulaire notamment si vous utilisez des conteneurs d etype "accordéon" ou "onglet".

De plus, il arrive assez souvent qu'un groupe de contrôles et sa logique associée doivent être utilisés plusieurs fois au sein de fenêtres différentes.

Dans ces deux cas, il est commode de décomposer le problème sous forme de composants réutilisables. Ici, un "composant" est une "vue" associée à son "contrôleur" (modèle MVC). En fait, JavaFX permet ceci assez facilement.

Nous allons nous appuyer sur un exemple. Notre application affiche une fenêtre dont le fond est constitué d'un conteneur de type SpliPane. Chaque panneau du SplitPane comporte un composant identique. Ce composant est constitué d'un bouton qui un fois cliqué indique si on se trouve sur la panneau gauche ou sur la panneau droit. Nous développons cet exemple de manière assez générique pour en faciliter la transposition à d'autres cas.

Voici le code FXML de notre formulaire base de la fenêtre principale :

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

<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.AnchorPane?>


<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" 
            xmlns:fx="http://javafx.com/fxml/1"
            xmlns="http://javafx.com/javafx/10.0.1" fx:controller="proto0026.FXMLMainWindowController">
   <children>
      <SplitPane dividerPositions="0.4765886287625418" layoutX="232.0" layoutY="93.0" 
                    prefHeight="160.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" 
                    AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <items>
          <AnchorPane fx:id="_apnLeft" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0" />
          <AnchorPane fx:id="_apnRight" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0" />
        </items>
      </SplitPane>
   </children>
</AnchorPane>

Nous vous recommandons de toujours nommer les conteneurs qui accueilleront vos composants (ici _apnLeft et _apnRight).

A ce stade, la classe contrôleur est de la forme :

package proto0026;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

/**
 * Classe contrôleur FXML.
 *
 * @author Jean-Marie Piatte 2018.
 */
public class FXMLMainWindowController implements Initializable
{
  private Stage _stg;
  
  /**
   * Panneau gauche du SplitPane.
   */
  @FXML
  private AnchorPane _apnLeft;
  
  /**
   * Panneau droit du SplitPane.
   */
  @FXML
  private AnchorPane _apnRight;
  
  /**
   * Initialisation du contrôleur.
   * Nous avons volontairement fait afficher le titre de la fenêtre ici pour montrer comment
   * transmettre des paramètres au contrôleur depuis l'appelant.
   * @param stg Fond de la fenêtre principale.
   * @param sSide Côté du SplitPane.
   */
  public void initController(Stage stg, String sSide)
  {
    _stg = stg;
    _stg.setTitle("Prototype 0026");
  }
  
  /**
   * Initialisation de la classe contrôleur.
   * @param url URL de l'application.
   * @param rbn Ressources de l'application (inutilisé ici).
   */
  @Override
  public void initialize(URL url, ResourceBundle rbn)
  {
    // TODO
  }  
}

Notre classe principale est encore plus simple :

package proto0026;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;

/**
 * Composant réutilisable avec JavaFX.
 * @author Jean-Marie Piatte.
 */
public class Proto0026 extends Application
{
  @Override
  public void start(Stage primaryStage) throws IOException
  {
    try
    {
      Parent prn = FXMLLoader.load(getClass().getResource("FXMLMainWindow.fxml"));
      Scene scn = new Scene(prn);
      Stage stg = new Stage();
      stg.setScene(scn);
      stg.show();
    }
    catch(Throwable thw)
    {
      Alert alr = new Alert(Alert.AlertType.ERROR, thw.getLocalizedMessage(), ButtonType.OK);
      alr.showAndWait();
    }
  }

  /**
   * Permet le lancement en mode standalone.
   * @param args Ligne de commande (inutilisée ici).
   */
  public static void main(String[] args)
  {
    launch(args);
  }
}

Bref, rien que du très classique jusqu'à présent. Intéressons nous à notre composant construit sur le formulaire FXMLComponent. Voici le code FXML de la définition de son interface :

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>


<AnchorPane id="AnchorPane" prefHeight="246.0" prefWidth="374.0" 
            xmlns:fx="http://javafx.com/fxml/1" 
            xmlns="http://javafx.com/javafx/10.0.1" fx:controller="proto0026.FXMLComponentController">
   <children>
      <Button alignment="CENTER" layoutX="150.0" layoutY="123.0" mnemonicParsing="false" onAction="#handleClick" text="Cliquez !" 
              AnchorPane.bottomAnchor="100.0" AnchorPane.leftAnchor="100.0" 
              AnchorPane.rightAnchor="100.0" AnchorPane.topAnchor="100.0" />
   </children>
</AnchorPane>

Le code de sa classe est également des plus simple :

package proto0026;

import javafx.event.ActionEvent;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;

/**
 * Classe contrôleur FXML.
 *
 * @author Jean-Marie Piatte 2018.
 */
public class FXMLComponentController implements Initializable
{
  /**
   * Message à afficher lors du clic.
   */
  private String _sMessage;
  
  /**
   * Contrôle de notre composant.
   */
  @FXML
  private Button _btnOk;
  
  /**
   * Gestionnaire invoqué lors du clic sur le bouton.
   * @param aev Paramètre associé à l'évènement.
   */
  @FXML
  private void handleClick(ActionEvent aev)
  {
    String s;
    if (_sMessage == null || _sMessage.isEmpty())
      s = "Contôleur non initialisé !!";
    else
      s = _sMessage;
    Alert alr = new Alert(Alert.AlertType.INFORMATION, _sMessage, ButtonType.OK);
    alr.showAndWait();
  }
  
  /**
   * Transmission du "modèle" (au sens MVC).
   * @param sMessage Message à afficher.
   */
  public void initController(String sMessage)
  {
    _sMessage = sMessage;
  }
  
  /**
   * Initialisation de la classe contrôleur.
   * @param url URL de l'application.
   * @param rbn Ressources de l'application (inutilisé ici).
   */
  @Override
  public void initialize(URL url, ResourceBundle rbn)
  {
    // TODO
  }  
}

Le point-clef est ici la méthode initController qui permet de transmettre le "modèle" du composant depuis le code qui l'invoque. Il ne reste plus qu'à adapter le code de notre formulaire "principal" pour qu'il utilise ce composant avec des données dédiées. Il suffit pour cela de compléter la méthode initialize du formulaire "principal" :

@Override
  public void initialize(URL url, ResourceBundle rbn)
  {
    try
    {
      // Composant gauche
      FXMLLoader ldrRight = new FXMLLoader(getClass().getResource("FXMLComponent.fxml"), rbn);
      AnchorPane apnLeft = ldrRight.load();
      FXMLComponentController ccnLeft = (FXMLComponentController)ldrRight.getController();
      ccnLeft.initController("Bouton gauche !");
      // Composant droit
      FXMLLoader ldrLeft = new FXMLLoader(getClass().getResource("FXMLComponent.fxml"), rbn);
      AnchorPane apnRight = ldrLeft.load();
      FXMLComponentController ccnRight = (FXMLComponentController)ldrLeft.getController();
      ccnRight.initController("Bouton droit !");
      // Mise en place du composant "gauche" sur la panneau gauche
      _apnLeft.getChildren().add(apnLeft);
      AnchorPane.setTopAnchor(apnLeft, 0.0);
      AnchorPane.setBottomAnchor(apnLeft, 0.0);
      AnchorPane.setLeftAnchor(apnLeft, 0.0);
      AnchorPane.setRightAnchor(apnLeft, 0.0);
      // Mise en place du composant "droit" sur la panneau droitr
      _apnRight.getChildren().add(apnRight);
      AnchorPane.setTopAnchor(apnRight, 0.0);
      AnchorPane.setBottomAnchor(apnRight, 0.0);
      AnchorPane.setLeftAnchor(apnRight, 0.0);
      AnchorPane.setRightAnchor(apnRight, 0.0);
    }
    catch (IOException ioe)
    {
      Alert alr = new Alert(Alert.AlertType.ERROR, ioe.getLocalizedMessage(), ButtonType.OK);
      alr.setResizable(true);
      alr.setWidth(400);
      alr.setHeight(300);
      alr.showAndWait();
    }
  }

La réutilisation d'une "vue" et de sa logique (son "contrôleur") permet de construire des applications complexes aisée à mettre au point. L'emploi d'un outil de développement comme NetBeans (Apache) couplé à un outil graphique comme Scene Builder (Gluon) rend ces réalisations rapides. Notez qu'à l'heure actuelle ces deux outils sont gratuits.

Ce prototype conforme à un projet NetBeans 9.0 (apache) sous JDK 10 peut être librement téléchargé ici. La totalité des fichiers est libre de droits. Leur utilisation est de la seule responsabilité de la personne (morale ou physique) qui les exploite. Elle n'engage en aucun cas la responsabilité son auteur.
Attention : à partir du JDK 11, deux points importants doivent être pris en compte.

  1. Les librairies JavaFX ne font plus parties du JDK et doivent don être associées manuellement au projet ;
  2. Les moutures du JDK d'Oracle et de l'OpenJDK sont susceptibles de diverger avec un impact fort sur la structuration du JDK choisi. Si vous disposez détenir une version librement distribuable, vous devez impérativement faire le choix de l'OpenJDk (voir nouvelle licence Oracle qui renie les annonces faites après le rachat de Sun Microsystem et de sa technologie Java).

(c) PiApplications 2015