Tutoriel sur une introduction au langage FXML

Comment définir son UI hors de son code source en JavaFX.

Fabrice Bouyé

Cet article a pour but de vous initier au FXML, le nouveau langage basé sur XML utilisé par JavaFX pour permettre de découpler le design des interfaces graphiques du code qui les manipule et les contrôle.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez

- Titre : Tutoriel sur une introduction au langage FXML

- Auteur : Fabrice Bouyé

- Parution : 11 mai 2015

- Mise à jour : 5 juin 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Fabrice Bouyé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les Bases

- II-A. Entête

- II-B. Imports

- II-C. Racine

- II-D. Charger le FXML

- III. Définir une arborescence

- III-A. Étendre la structure

- III-B. Appeler des propriétés

- III-C. Appeler des propriétés statiques

- III-D. Utiliser des classes ne provenant pas de l'API

- III-E. Charger des images

- III-F. Utiliser du texte internationalisé

- III-G. Nommer des objets

- III-H. Inclure du FXML dans du FXML

- III-I. Définir du code dans le FXML

- IV. Le contrôleur

- IV-A. Spécifier un contrôleur dans le FXML

- IV-B. Récupérer une référence sur le contrôleur

- IV-C. Accéder au contenu du FXML

- IV-D. La méthode initialize()

- IV-E. Accéder au sous-contrôleur

- IV-F. Appeler des méthodes du contrôleur depuis le FXML

- V. SceneBuilder

- V-A. Lancement

- V-B. Présentation rapide

- V-C. Aperçu de l'internationalisation

- V-D. Aperçu du style

- V-E. CSS analyzer

- V-F. Contrôles customisés

- V-F-1. Dans SceneBuilder 1.1

- V-F-2. Dans SceneBuilder 2.0

- VI. Code final

- VI-A. Main.java

- VI-B. string.properties

- VI-C. test10.fxml

- VI-D. TestController.java

- VI-E. proxy.fxml

- VI-F. ProxyController.java

- VI-G. ProxyType.java

- VI-H. CustomProxySelector.java

- VII. Conclusion

- VIII. Remerciements

- IX. Liens

I - Introduction

Découpler la définition des interfaces utilisateur du code n'a rien d'un concept nouveau : inclure des pages et des pages entières de code dans un projet pour définir ne serait-ce qu'un simple formulaire avec un bouton de validation correctement positionné est probablement une étape par laquelle nous sommes tous passés… sauf peut-être les habitués de Visual Studio puisque Microsoft prit ce virage très tôt dans la conception de ses outils de développement. Ce n'est pas pour rien que les langages de définition d'interface graphique par balisage (User Interface Markup Language) fleurissent sur la toile.

Côté Java, les choses ont longtemps été à la traine : même si NetBeans et Eclipse disposent désormais d'éditeurs graphiques performants pour Swing et leurs propres application frameworks, l'accouchement a été plutôt difficile et il s'agit dans tous les cas d'une surcouche rajoutée par-dessus l'API standard. En essayant d'intégrer une prise en charge directe par la plateforme, Oracle a tout d'abord effectué un premier essai avec FXD dans JavaFX 1.x. Le FXD permettait de décrire des UI dans un langage de script simple qui était un sous-ensemble du langage JavaFX Script utilisé dans l'API de l'époque. Le problème évident était qu'il s'agissait d'un nouveau langage à part entière, la plateforme est restée peu populaire.

Avec le retour vers Java dans JavaFX 2.x, et l'ouverture des runtimes à tous les langages tournant sur la JVM, Oracle a, au contraire, décidé de s'inspirer de ce qui se faisait déjà sur le marché : le MXML largement utilisé par Flex et Flash d'Adobe ou encore le XAML de Microsoft destiné à Silverlight et WPF. Voici donc venir le FXML, un nouveau dérivé du langage de balisage XML, directement pris en charge par les runtimes JavaFX et accompagné de nouveaux outils.

II - Les Bases

Un fichier au format FXML est donc un document écrit en XML contenant le descriptif d'une arborescence graphique dans l'API SceneGraph. Un FXML tout simple tel que créé par NetBeans ou SceneBuilder ressemblera au code suivant :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.scene.*?>

6. <?import javafx.scene.control.*?>

7. <?import javafx.scene.layout.*?>

9. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

10. </AnchorPane>

L'arborescence SceneGraph ne contient rien de plus qu'un simple layout de type AnchorPane et dont les dimensions ont été mises à 600 x 400.

II-A - Entête

Comme tout fichier XML, l'entête d'un fichier FXML doit être :

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

Évidemment, le fichier doit être au format texte et encodé à la valeur appropriée.

II-B - Imports

À la suite de l'entête, on trouvera une série d'imports de packages similaires à ceux qu'on peut trouver dans le code d'une classe Java :

1. <?import java.lang.*?>

2. <?import java.util.*?>

3. <?import javafx.scene.*?>

4. <?import javafx.scene.control.*?>

5. <?import javafx.scene.layout.*?>

Il est possible d'importer n'importe quel package Java qui se trouve sur le CLASSPATH à l'exécution, y compris les vôtres ou ceux de bibliothèques externes.

Ces déclarations sont optionnelles : comme dans du code Java, il est en effet possible d'utiliser les noms longs des classes (c'est-à-dire l'arborescence des packages suivie du nom de la classe) en lieu et place du nom court (uniquement le nom de la classe).

II-C - Racine

Chaque fichier FXML dispose d'une balise racine qui peut être n'importe quel nœud graphique de l'API SceneGraph ou même des nœuds customisés ou provenant de bibliothèques externes. Comme le FXML est avant tout destiné à décrire des bouts d'interface graphique, il vaut cependant mieux que la balise racine soit un nœud du SceneGraph, cependant, en théorie, vous pouvez aussi charger des classes non graphiques.

La racine est la seule balise dans laquelle on peut et on doit déclarer l'attribut xmlns:fx.

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. </AnchorPane>

La balise porte le nom de la classe à utiliser tel qu'écrit dans l'API et avec la même casse. En général, dans la plupart des cas, mieux vaut se contenter d'utiliser un AnchorPane comme balise racine du FXML, cela vous permet de positionner vos sous-nœuds comme bon vous semble.

Outre la déclaration de son id, la définition de xmlns:fx et la description des propriétés du nœud (ici prefWidth et prefHeight), si le FXML dispose également d'un contrôleur, la balise racine peut contenir également une définition de l'attribut fx:controller qui définit le nom long (package + nom de classe) de la classe du contrôleur. Nous y reviendrons plus en détail dans un chapitre ultérieur.

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml" fx:controller="test.TestController">

2. </AnchorPane>

II-D - Charger le FXML

Pour charger le fichier FXML ainsi créé, il suffit de résoudre l'URL du fichier via le mécanisme du chargement de ressources habituel en Java (voir ClassLoader) et de créer une nouvelle instance de javafx.fxml.FXMLLoader sur laquelle on va appeler la méthode load(). Cette méthode retournera une instance correspondant au nœud racine décrit dans notre fichier. Si l'URL n'est pas correcte, la méthode load() lèvera une IllegalStateException.

1. public final class Main extends Application {

3. @Override

4. public void start(final Stage primaryStage) {

5. try {

6. // Localisation du fichier FXML.

7. final URL url = getClass().getResource("test.fxml");

8. // Création du loader.

9. final FXMLLoader fxmlLoader = new FXMLLoader(url);

10. // Chargement du FXML.

11. final AnchorPane root = (AnchorPane) fxmlLoader.load();

12. // Création de la scène.

13. final Scene scene = new Scene(root, 300, 250);

14. primaryStage.setScene(scene);

15. } catch (IOException ex) {

16. System.err.println("Erreur au chargement: " + ex);

17. }

18. primaryStage.setTitle("Test FXML");

19. primaryStage.show();

20. }

22. public static void main(String[] args) {

23. launch(args);

24. }

25. }

Ce qui au final donne l'affichage suivant qui n'est pas très excitant en soi, mais c'est normal, notre fichier FXML ne contient rien en dehors du nœud racine :

Figure 1 - Notre premier FXML.

Comme je vous le disais tantôt, on peut, en théorie, charger une racine qui n'est pas un nœud graphique d'où le fait que la méthode load() retourne une instance de Object plutôt que de Node.

III - Définir une arborescence

Nous allons maintenant nous attacher à remplir notre fichier FXML de manière à afficher un peu plus de contenu.

III-A - Étendre la structure

Les sous-balises des nœuds qui doivent être inclus dans votre UI se déclarent de la même manière que la balise racine : en utilisant le nom de la classe en respectant la casse. Leur agencement dépend cependant des propriétés propres à chacune des classes parentes utilisées. Par exemple, la plupart des layouts ont une propriété children qui est de type ObservableList<Node>. Rajouter des sous-nœuds dans notre racine revient donc à écrire :

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. <children>

3. <!--Lister les sous-noeuds ici. -->

4. </children>

5. </AnchorPane>

Par exemple :

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. <children>

3. <Button layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="Button" />

4. <Button layoutX="14.0" layoutY="46.0" mnemonicParsing="false" text="Button" />

5. <Button layoutX="14.0" layoutY="77.0" mnemonicParsing="false" text="Button" />

6. </children>

7. </AnchorPane>

La plupart des nœuds graphiques parents, utilisent également l'annotation @DefaultProperty sur leur propriété qui permet de spécifier leur contenu. Cette annotation permet d'éviter de spécifier un niveau de balise dans l'écriture du FXML. Ainsi, étant donné que children est la propriété par défaut de la classe AnchorPane, il est tout à fait possible d'écrire le contenu en omettant les balises <children> sans pour autant que cela ne cause d'erreur au chargement du fichier :

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. <Button layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="Button" />

3. <Button layoutX="14.0" layoutY="46.0" mnemonicParsing="false" text="Button" />

4. <Button layoutX="14.0" layoutY="77.0" mnemonicParsing="false" text="Button" />

5. </AnchorPane>

À défaut d'être joli, notre affichage devient tout de suite bien plus intéressant :

Figure 2 - Quelques boutons.

III-B - Appeler des propriétés

On peut remarquer dans cet exemple qu'on utilise des attributs dans les balises pour faire des nouveaux appels à des propriétés ; cette fois-ci, des propriétés qui font partie de la classe Button : layoutX, layoutY, mnemonicParsing et text. Ces attributs sont nommés exactement comme les propriétés qu'ils ciblent en respectant la casse :

1. <Button layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="Button" />

On peut également utiliser des balises au lieu d'attributs pour définir les propriétés, mais évidemment le code XML en devient plus verbeux et moins facile à appréhender :

1. <Button>

2. <layoutX>

3. <Double fx:value="14.0"/>

4. </layoutX>

5. <layoutY>

6. <Double fx:value="14.0"/>

7. </layoutY>

8. <mnemonicParsing>

9. <Boolean fx:value="false"/>

10. </mnemonicParsing>

11. <text>

12. <String fx:value="Button"/>

13. </text>

14. </Button>

III-C - Appeler des propriétés statiques

Nous allons maintenant placer nos boutons dans un layout permettant de faciliter leur placement à l'écran :

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. <children>

3. <VBox prefHeight="200.0" prefWidth="100.0" spacing="6.0" AnchorPane.bottomAnchor="6.0" AnchorPane.leftAnchor="6.0" AnchorPane.rightAnchor="6.0" AnchorPane.topAnchor="6.0">

4. <children>

5. <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Button" VBox.vgrow="ALWAYS" />

6. <Button mnemonicParsing="false" text="Button" />

7. <Button mnemonicParsing="false" text="Button" />

8. </children>

9. </VBox>

10. </children>

11. </AnchorPane>

Le layout VBox est une boite verticale qui place tous ses éléments en colonne. Ses quatre côtés sont ancrés sur les bordures de l'AnchorPane qui sert de racine au document. Désormais, le premier bouton voit sa hauteur et sa largeur grandir ou diminuer pour qu'il remplisse tout l'espace occupé par la boite verticale.

Figure 3 - Application des propriétés statiques sur le premier bouton.

Nous pouvons voir que la balise VBox ainsi que la première balise Button contiennent des attributs qui ne sont pas des accès aux propriétés de ces deux classes :

1. AnchorPane.bottomAnchor="6.0" AnchorPane.leftAnchor="6.0" AnchorPane.rightAnchor="6.0" AnchorPane.topAnchor="6.0"

2. [...]

3. VBox.vgrow="ALWAYS"

Grosso modo, on peut considérer que lorsque le FXML sera chargé en mémoire, l'API effectuera les appels aux méthodes statiques suivantes :

1. AnchorPane.setTopAnchor(vbox, 6.0);

2. AnchorPane.setLeftAnchor(vbox, 6.0);

3. AnchorPane.setRightAnchor(vbox, 6.0);

4. AnchorPane.setBottomAnchor(vbox, 6.0);

5. [...]

6. VBox.setVgrow(button, Priority.ALWAYS);

Ces éléments sont appelés des propriétés « statiques » ou propriétés « attachées » ; elles ne proviennent pas de l'objet lui-même, mais de son conteneur parent et n'ont vraiment de sens que par rapport au contexte de leur utilisation. Par exemple, définir AnchorPane.bottomAnchor="6.0" dans le code de la balise Button, n'aurait eu aucun effet au final.

De plus, on peut voir que j'ai, cette fois-ci, utilisé une valeur tirée d'une enum pour la valeur de l'attribut VBox.vgrow dans la balise Button. Cela vous montre que le chargement du FXML peut résoudre des types de valeurs un peu plus complexes que de simples nombres, booléens ou chaine de caractères. Si une valeur non définie dans l'enum Priority avait été utilisée à la place, le chargement du FXML aurait levé une exception de type InvocationTargetException.

III-D - Utiliser des classes ne provenant pas de l'API

Dans du FXML, vous pouvez sans problème utiliser des classes qui ne sont pas parties de l'API. Certaines limitations s'appliquent lors de l'utilisation de nœuds autres que ceux de l'API :

- il faut qu'il y ait eu un import du package contenant la classe au début du FXML (ou alors, utiliser le nom long de la classe) ;

- la classe doit avoir un constructeur par défaut (sans argument) accessible (public) ;

- et bien sûr à l'exécution, le bytecode de cette classe doit être accessible sur le CLASSPATH.

Ces restrictions s'appliquent tant pour la déclaration en balise racine que pour la déclaration en sous-balise. Si la classe n'est pas résolue au chargement du FXML, une exception de type javafx.fxml.LoadException sera levée.

Si les propriétés de vos classes sont correctement décrites, vous pouvez également les utiliser depuis le FXML en déclarant des attributs correspondants dans les balises.

Il est aussi possible d'utiliser des classes qui ne sont pas du tout des nœuds graphiques, ce qui peut être utile quand on doit remplir le contenu d'une ListView ou d'une ComboBox avec des valeurs par défaut. Prenons, par exemple, la classe suivante qui définit une voiture avec une propriété brand qui contient la marque de la voiture :

1. package test;

3. import javafx.beans.property.SimpleStringProperty;

4. import javafx.beans.property.StringProperty;

6. public class Car {

7. // Définition de la propriété brand.

8. private final StringProperty brand = new SimpleStringProperty(this, "brand", null);

10. // Getter.

11. public final String getBrand() {

12. return brand.get();

13. }

15. // Setter.

16. public final void setBrand(final String value) {

17. brand.set(value);

18. }

20. // Accès à la propriété.

21. public final StringProperty brandProperty() {

22. return brand;

23. }

24. }

Il est tout à fait possible d'initialiser des instances de la classe Car dans un fichier FXML pour remplir une ComboBox :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.collections.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.layout.*?>

9. <?import test.*?>

11. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

12. <children>

13. <ComboBox layoutX="14.0" layoutY="14.0">

14. <items>

15. <FXCollections fx:factory="observableArrayList">

16. <Car brand="Peugeot"/>

17. <Car brand="Renault"/>

18. <Car brand="Citroën"/>

19. </FXCollections>

20. </items>

21. </ComboBox>

22. </children>

23. </AnchorPane>

Ce qui donne le résultat suivant :

Figure 4 - Le contenu de la boite déroulante « telle quelle ».

Bon d'accord, on ne voit que des chaines de texte dans le style test.Car@6bab735a. C'est tout à fait normal puisque nous n'avons pas mis de cellule appropriée sur la ComboBox pour afficher des instances de Car. Mais si vous rajoutez la méthode suivante dans la classe Car, alors vous verrez que les objets listés contiennent bien les valeurs que nous leur avons données :

1. @Override

2. public String toString() {

3. return getBrand();

4. }

C'est tout de même mieux comme cela, non ?

Figure 5 - Un affichage un peu plus correct.

Nous pouvons remarquer plusieurs choses :

- nous avons importé deux nouveaux packages qui ne sont pas liés au SceneGraph : le package javafx.collections et le package test ;

- nous avons effectivement chargé trois instances de la class test.Car et nous avons initialisé directement le contenu de la propriété brand depuis le FXML !

- Afin de remplir la propriété items de la classe ComboBox, nous avons dû créer une nouvelle instance de ObservableList.

Certaines propriétés comme children sur les layouts permettent de faire des déclarations implicites sans devoir manuellement créer une nouvelle instance de la classe ObservableList. Apparemment, ce n'est pas encore le cas pour la propriété items de la classe ComboBox et donc j'ai dû manuellement faire une telle déclaration. Si un jour, une telle déclaration implicite est acceptée par le loader FXML, il suffira alors de faire :

1. <ComboBox layoutX="14.0" layoutY="14.0">

2. <items>

3. <Car brand="Peugeot"/>

4. <Car brand="Renault"/>

5. <Car brand="Citroën"/>

6. </items>

7. </ComboBox>

Il ne faut pas hésiter à poster des Request for Enhancement sur le Java Bug Database d'Oracle et le Java Bug System de l'OpenJDK pour obtenir de telles améliorations.

III-E - Charger des images

Pour charger et afficher une image, on a besoin de deux choses, tout comme dans le SceneGraph :

- une instance de la classe Image qui contient notre bitmap chargée en mémoire ;

- et une instance du nœud graphique ImageView pour l'afficher.

Nous nous retrouvons donc avec le FXML suivant qui modifie la propriété graphic du bouton et qui cache son texte par la même occasion :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.scene.*?>

6. <?import javafx.scene.control.*?>

7. <?import javafx.scene.image.*?>

8. <?import javafx.scene.layout.*?>

10. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

11. <children>

12. <Button contentDisplay="GRAPHIC_ONLY" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="Button">

13. <graphic>

14. <ImageView id="logo" pickOnBounds="true">

15. <image>

16. <Image url="@logo.png" preserveRatio="true" smooth="true" />

17. </image>

18. </ImageView>

19. </graphic>

20. </Button>

21. </children>

22. </AnchorPane>

Nous voyons que nous avons dû inclure le package supplémentaire javafx.scene.image de manière à pouvoir utiliser ces classes. Au fait, j'ai à nouveau utilisé une valeur d'une enum, ici javafx.scene.control.ContentDisplay, pour l'attribut contentDisplay de la balise Button. Lorsque vous écrivez vos FXML manuellement, pensez à vous reporter à la javadoc des classes pour savoir quelles sont les bonnes valeurs à utiliser pour les propriétés des objets et les attributs des balises.

Ceci nous donne le résultat suivant :

Figure 6 - L'image sur le bouton provient du site de Développez !

Le chemin de l'image est donc défini par le caractère @ sur l'attribut url de la balise Image. Ici l'image se trouvait dans le même package que mon FXML, je n'ai donc pas eu besoin de spécifier un chemin particulier. Si elle avait été placée ailleurs, j'aurais pu tout aussi bien définir un chemin relatif par rapport au package courant ou absolu sur le CLASSPATH ou encore donner une URL web  ou l'URI d'un fichier local pour une image externe :

1. <Image url="@http://www.developpez.com/template/images/logo.png" preserveRatio="true" smooth="true" />

III-F - Utiliser du texte internationalisé

Avoir du texte écrit en dur dans le code ou dans le fichier de définition de l'UI ce n'est pas trop mon truc. J'ai l'habitude de créer des applications multilingues et donc il est tout à fait naturel pour moi de mettre mes textes dans des fichiers de ressources externes. Ça tombe bien, le FXML supporte les ressources internationalisées.

Ainsi je peux déclarer le code suivant dans lequel j'ai rajouté une infobulle sur le bouton :

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

2. <children>

3. <Button contentDisplay="GRAPHIC_ONLY" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="Button">

4. <graphic>

5. <ImageView id="logo" pickOnBounds="true">

6. <image>

7. <Image url="@logo.png" preserveRatio="true" smooth="true" />

8. </image>

9. </ImageView>

10. </graphic>

11. <tooltip>

12. <Tooltip id="tooltip" text="%visit.developpez.web"/>

13. </tooltip>

14. </Button>

15. </children>

16. </AnchorPane>

Ici c'est le caractère % qui permet de spécifier qu'on a affaire à une chaine dont la valeur est externalisée.

Si vous cherchez à charger directement le FXML avec le code Java que je vous ai donné précédemment, malheureusement une exception de type javafx.fxml.LoadException avec le message « No resources specified » sera levée lors de l'appel à la méthode load(). En effet, désormais notre FXMLLoader s'attend à recevoir un ResourceBundle en paramètre qui lui permettra de récupérer la traduction appropriée pour la clé visit.developpez.web. Le ResourceBundle est le mécanisme classique qui permet l'internationalisation (I18N) et la localisation (L10N) dans l'API standard Java.

Rajoutons donc dans le package un fichier texte nommé strings.properties qui contient la ligne suivante :

1. visit.developpez.web=Visitez le site web de développez !

Et modifions le code qui charge le FXML :

1. // Localisation du fichier FXML.

2. URL url = getClass().getResource("test5.fxml");

3. // Chargement du bundle:

4. ResourceBundle bundle = ResourceBundle.getBundle("test/strings");

5. // Creation du loader.

6. FXMLLoader fxmlLoader = new FXMLLoader(url, bundle);

7. // Chargement du FXML.

8. AnchorPane root = (AnchorPane) fxmlLoader.load();

Ce qui nous donne, quand on passe le curseur de la souris au-dessus du bouton :

Figure 7 - Le texte de l'infobulle provient d'un fichier externe.

III-G - Nommer des objets

Pour le moment, nous nous sommes contentés d'intégrer directement des objets dans le fichier FXML. Cependant, étant donné que nous allons être amenés à les manipuler, il va nous falloir trouver un moyen de référencer ces objets. Et puis nous allons vouloir créer une interface un peu plus complexe par la même occasion. Insérons un nouveau FXML dans notre projet, et nommons-le proxy.fxml. Il contient le code suivant :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.scene.*?>

6. <?import javafx.scene.control.*?>

7. <?import javafx.scene.layout.*?>

9. <AnchorPane id="AnchorPane" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml">

10. <children>

11. <GridPane style="-fx-hgap: 6; -fx-vgap:6;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

12. <children>

13. <RadioButton fx:id="noProxyRadio" mnemonicParsing="false" selected="true" text="%web.no.proxy" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="0">

14. <toggleGroup>

15. <ToggleGroup fx:id="optionToggleGroup" />

16. </toggleGroup>

17. </RadioButton>

18. <RadioButton fx:id="systemProxyRadio" mnemonicParsing="false" text="%web.system.proxy" toggleGroup="$optionToggleGroup" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />

19. <RadioButton fx:id="manualProxyRadio" mnemonicParsing="false" text="%web.manual.proxy" toggleGroup="$optionToggleGroup" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="2" />

20. <Label text="%web.proxy.host" GridPane.columnIndex="0" GridPane.rowIndex="3" />

21. <TextField fx:id="hostField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.rowIndex="3" />

22. <Label text="%web.proxy.port" GridPane.columnIndex="2" GridPane.rowIndex="3" />

23. <TextField fx:id="portField" prefWidth="200.0" GridPane.columnIndex="3" GridPane.rowIndex="3" />

24. <CheckBox fx:id="authenticationCheck" mnemonicParsing="false" text="%web.use.authentication" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="4" />

25. <Label text="%web.authenticate.username" GridPane.columnIndex="0" GridPane.rowIndex="5" />

26. <TextField fx:id="userField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="5" />

27. <Label text="%web.authenticate.password" GridPane.columnIndex="0" GridPane.rowIndex="6" />

28. <PasswordField fx:id="passwordField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="6" />

29. </children>

30. <columnConstraints>

31. <ColumnConstraints hgrow="SOMETIMES" minWidth="50.0" prefWidth="-1.0" />

32. <ColumnConstraints hgrow="SOMETIMES" minWidth="100.0" prefWidth="100.0" />

33. <ColumnConstraints hgrow="SOMETIMES" minWidth="-1.0" prefWidth="-1.0" />

34. <ColumnConstraints hgrow="SOMETIMES" minWidth="50.0" prefWidth="50.0" />

35. </columnConstraints>

36. <rowConstraints>

37. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

38. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

39. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

40. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

41. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

42. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

43. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="SOMETIMES" />

44. </rowConstraints>

45. </GridPane>

46. </children>

47. </AnchorPane>

Nous avons inclus un GridPane dans notre AnchorPane racine. Ce layout permet de disposer des nœuds suivant une grille de manière assez similaire à une table HTML et vous serez donc probablement amenés à l'utiliser souvent si vous devez créer des formulaires.

Au passage, nous pouvons voir qu'une nouveauté est apparue dans ce nouveau FXML : à plusieurs endroits, nous avons utilisé un nouvel attribut qui sert à définir une identité, fx:id. Cette identité doit être unique pour chaque objet dans un même fichier FXML. D'ailleurs NetBeans soulignera en rouge les duplicata s'il en trouve. Nous avons défini des identités sur la plupart des champs et des contrôles qui récupéreront des valeurs saisies par l'utilisateur. De plus, dans le tout premier RadioButton, nous avons défini un objet de type ToggleGroup et nous lui avons donné une identité avec l'attribut fx:id.

1. <toggleGroup>

2. <ToggleGroup fx:id="optionToggleGroup" />

3. </toggleGroup>

Comme en Swing avec un ButtonGroup, ici en JavaFX, un ToggleGroup permet de regrouper des cases à cocher ou des boutons radio et de s'assurer qu'un seul est sélectionné à la fois. Les autres instances de RadioButton utilisent le même groupe tout en s'y référant grâce au caractère $ et à son identité.

1. <RadioButton [...] toggleGroup="$optionToggleGroup" [...]

Note : aucune exception ne sera levée si vous vous référez à une identité qui n'existe pas.

Maintenant, rajoutons les chaines de texte suivantes dans notre fichier de ressources, strings.properties :

1. web.no.proxy=Pas de proxy

2. web.system.proxy=Utiliser le proxy système

3. web.manual.proxy=Proxy manuel

4. web.proxy.host=Hôte

5. web.proxy.port=Port

6. web.use.authentication=Utiliser l'authentification ?

7. web.authenticate.username=Utilisateur

8. web.authenticate.password=Mot de passe

Si on affiche notre FXML, cela donnera un contenu similaire à :

Figure 8 - Un panneau de configuration du proxy.

III-H - Inclure du FXML dans du FXML

Il est assez courant de découper une UI en sous-parties ; cela permet de rendre le code qui gère chaque partie, plus lisible et facile à maintenir, tout en offrant la possibilité de réutiliser certaines parties à plusieurs endroits comme dans des assistants, des palettes ou des boites de dialogue. Il est tout à fait possible d'intégrer notre FXML dans un autre FXML :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.geometry.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.image.*?>

9. <?import javafx.scene.layout.*?>

10. <?import javafx.scene.web.*?>

12. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

13. <children>

14. <BorderPane prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="6.0" AnchorPane.leftAnchor="6.0" AnchorPane.rightAnchor="6.0" AnchorPane.topAnchor="6.0">

15. <bottom>

16. <fx:include source="proxy.fxml" />

17. </bottom>

18. <center>

19. <WebView prefHeight="200.0" prefWidth="200.0">

20. <BorderPane.margin>

21. <Insets bottom="6.0" top="6.0" />

22. </BorderPane.margin>

23. </WebView>

24. </center>

25. <top>

26. <Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" text="Button">

27. <graphic>

28. <ImageView id="logo" pickOnBounds="true">

29. <image>

30. <Image url="@logo.png" preserveRatio="true" smooth="true" />

31. </image>

32. </ImageView>

33. </graphic>

34. <tooltip>

35. <Tooltip id="tooltip" text="%visit.developpez.web" />

36. </tooltip>

37. </Button>

38. </top>

39. </BorderPane>

40. </children>

41. </AnchorPane>

Nous avons inclus un BorderPane dans notre AnchorPane racine. Les utilisateurs de Swing reconnaîtront dans ce contrôle, un layout qui dispose son contenu de manière similaire au BorderLayout. Notre bouton se trouve dans la partie top, tandis que nous incluons notre FXML proxy.fxml dans la partie bottom ; j'ai également inclus un contrôle WebView dans la partie center. WebView est un composant capable de rendre des pages HTML 5 via WebKit, c'est un équivalent plus moderne du JEditorPane de Swing.

1. <fx:include source="proxy.fxml" />

La balise fx:include permet d'inclure un fichier FXML dans un autre fichier FXML. Son attribut source permet de définir la localisation du fichier à inclure. Cette valeur peut être un chemin relatif ou absolu par rapport au package courant. Comme n'importe quelle autre entité, il est possible de lui donner une identité avec l'attribut fx:id ce qui se révélera utile par la suite.

L'affichage donnera quelque chose comme ça ; j'en ai profité également pour agrandir un peu la taille de la scène dans le code de mon programme :

Figure 9 - Comment réagir à un clic sur le bouton ?

III-I - Définir du code dans le FXML

Pour le moment, notre interface n'est pas très vivante ; on peut certes cliquer sur quelques boutons et remplir quelques champs texte, mais il ne se passe pas grand-chose… Via le XML nous pouvons également spécifier des callbacks permettant d'appeler des fonctions lorsqu'on interagit avec certains contrôles. Après tout, les callbacks sont des propriétés comme des autres. Vous pouvez même définir le corps de ces méthodes directement dans le fichier FXML.

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.geometry.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.image.*?>

9. <?import javafx.scene.layout.*?>

10. <?import javafx.scene.web.*?>

12. <?language javascript?>

14. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml">

15. <fx:script>

16. function goToDeveloppez() {

17. java.lang.System.out.println("En avant vers le site de développez !");

18. browser.getEngine().load("http://www.developpez.com/")

19. }

20. </fx:script>

21. <children>

22. <BorderPane prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="6.0" AnchorPane.leftAnchor="6.0" AnchorPane.rightAnchor="6.0" AnchorPane.topAnchor="6.0">

23. <bottom>

24. <fx:include fx:id="proxyConfiguration" source="proxy.fxml" />

25. </bottom>

26. <center>

27. <WebView fx:id="browser" prefHeight="200.0" prefWidth="200.0">

28. <BorderPane.margin>

29. <Insets bottom="6.0" top="6.0" />

30. </BorderPane.margin>

31. </WebView>

32. </center>

33. <top>

34. <Button fx:id="goToWebButton" contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="goToDeveloppez(event);" text="Button">

35. <graphic>

36. <ImageView id="logo" pickOnBounds="true">

37. <image>

38. <Image url="@logo.png" preserveRatio="true" smooth="true" />

39. </image>

40. </ImageView>

41. </graphic>

42. <tooltip>

43. <Tooltip id="tooltip" text="%visit.developpez.web" />

44. </tooltip>

45. </Button>

46. </top>

47. </BorderPane>

48. </children>

49. </AnchorPane>

En théorie, vous pouvez utiliser n'importe quel langage de script qui fonctionne sur la JVM. En pratique, seul l'interpréteur JavaScript (Rhino dans Java 6-7, Nashorn dans Java 8) est présent par défaut, il vous faudra packager tout autre interpréteur avec votre application. Au final, votre degré de réussite dépendra du degré d'implémentation de la JSR223 par votre langage de script ainsi que de sa maturité.

Parmi les changements apportés à ce fichier FXML, le premier est l'ajout de la directive suivante au début du fichier :

1. <?language javascript?>

Cette directive indique que vos scripts seront exécutés par l'interpréteur JavaScript de la JVM. Si vous utilisez un autre langage de script, vous devrez spécifier ici quel est l'interpréteur à utiliser.

Vous trouverez ensuite, dans la balise racine, un bout de code défini entre des balises fx:script qui contient la définition de la fonction goToDeveloppez().

1. <fx:script>

2. function goToDeveloppez() {

3. java.lang.System.out.println("En avant vers le site de développez !");

4. browser.getEngine().load("http://www.developpez.com/")

5. }

6. </fx:script>

C'est bien du JavaScript, qui appelle l'API Java : la première ligne de la fonction fait une impression sur la console, tandis que la seconde charge la page de garde du site de Développez dans la WebView. Notez au passage que j'ai donné une identité à ma WebView avec l'attribut fx:id et que j'utilise cette identité comme nom de variable dans ma fonction JavaScript.

J'ai, de plus, ajouté l'attribut onAction dans mon bouton en lui passant le nom de la fonction à appeler :

1. <Button [...] onAction="goToDeveloppez(event);" [...]

Quelques tests simples montrent qu'on aurait tout aussi bien pu écrire le nom de la fonction en omettant le paramètre et le ; final. Les parenthèses sont quant à elles obligatoires :

1. <Button [...] onAction="goToDeveloppez()" [...]

Lorsque je clique sur le bouton, désormais, la console va afficher :

1. En avant vers le site de développez !

Et la page de garde du site de Développez s'affiche dans la partie centrale de l'UI. Enfin, si vous n'avez pas besoin de spécifier un proxy sur votre système, bien sûr ! Vous n'alliez pas imaginer que ce contrôle permettant de spécifier un proxy était juste là pour faire joli, tout de même ?

Figure 10 - Chargement de la page de garde du site de Développez.

Sinon, pour en revenir à notre fonction JavaScript, vous pouvez normalement utiliser l'intégralité de l'API Java et manipuler tous les composants nommés de votre FXML. Évidemment cela reste du JavaScript interprété au vol au moment de l'exécution, donc il peut être un peu compliqué de trouver les erreurs de syntaxe ou de gérer les exceptions qui seraient levées par le code.

IV - Le contrôleur

Même en utilisant du code scripté directement dans le FXML, notre UI est encore bien trop statique, l'ensemble manque sérieusement de flexibilité. Si on s'en tient au modèle MVC (modèle-vue-contrôleur), on aura quand même envie de découpler la vue du contrôleur en externalisant le code qui gère les différents contrôles du formulaire.

IV-A - Spécifier un contrôleur dans le FXML

Le format FXML vous permet de spécifier une classe contrôleur via l'attribut fx:controller du nœud racine. Cet attribut doit contenir le nom long (package + nom de classe) de la classe qui va servir de contrôleur à notre FXML.

Évidemment cette classe doit être accessible sur le CLASSPATH au moment de l'exécution, faute de quoi une exception de type javafx.fxml.LoadException, contenant le message « java.lang.ClassNotFoundException: <classe du contrôleur> », sera levée. Si NetBeans ne trouve pas la classe en question, sa déclaration sera soulignée de rouge.

1. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml" fx:controller="test.TestController">

Tout ce que vous avez besoin de faire désormais, c'est de rajouter une nouvelle classe dans votre projet :

1. package test;

3. public class TestController {

5. }

Bravo, vous avez désormais un contrôleur pour votre FXML ! Même s'il ne fait pas grand-chose pour le moment. Vous devrez cependant vous assurer que la classe est bien publique et que son constructeur par défaut (constructeur sans argument) est bien accessible sous peine de lever des exceptions au chargement du FXML.

IV-B - Récupérer une référence sur le contrôleur

Vous pouvez récupérer une référence sur le contrôleur de votre FXML une fois que ce dernier a été chargé. Pour cela, il suffit d'appeler la méthode getController() du FXMLLoader et de caster le résultat si besoin. Une nouvelle instance du contrôleur est créée à chaque fois que la méthode load() du FXMLLoader est appelée. Chaque contrôleur est propre à une seule structure chargée en mémoire.

1. // Chargement du FXML.

2. AnchorPane root = (AnchorPane) fxmlLoader.load();

3. // Accès au contrôleur.

4. TestController controller = (TestController) fxmlLoader.getController();

Une fois une référence sur le contrôleur récupérée, vous pouvez appeler n'importe quelle méthode ou propriété que vous avez définie dessus comme n'importe quel autre objet. Depuis le contrôleur, vous pouvez modifier le contenu de l'arbre graphique du nœud comme bon vous semble : installer/désinstaller des écouteurs, faire du binding sur les propriétés des nœuds, injecter de nouveaux nœuds dans l'arborescence, cacher ou retirer des nœuds existants. Cela ne modifiera pas le contenu du fichier FXML : une fois qu'il a été chargé en mémoire, l'arborescence graphique est totalement découplée du fichier source.

IV-C - Accéder au contenu du FXML

Bien sûr, tout cela serait plus intéressant si on pouvait interagir avec le contenu décrit dans le FXML. Il vous est possible de récupérer des références sur tous les objets qui ont reçu des identités dans le FXML via l'annotation @FXML :

1. package test;

3. import javafx.fxml.FXML;

4. import javafx.scene.control.Button;

5. import javafx.scene.layout.AnchorPane;

6. import javafx.scene.web.WebView;

8. public class TestController {

10. @FXML

11. private Button goToWebButton;

12. @FXML

13. private WebView browser;

14. @FXML

15. private AnchorPane proxyConfiguration;

16. }

Si vous ajoutez un constructeur dans votre classe, vous verrez que ses membres sont initialement à une valeur null. Les valeurs en provenance du FXML sont injectées durant l'initialisation de la classe après l'appel au constructeur. Si vous essayez de référencer des identités qui ne sont pas dans votre FXML, leur valeur restera null après le chargement. Si vous spécifiez la mauvaise classe pour un membre, une ClassCastException sera levée au chargement.

Pour les classes supportant les Generics, vous pouvez spécifier le type exact même si ce n'était pas possible de le faire dans le FXML. Par exemple, souvenez-vous de notre ComboBox qui contenait des instances de la class Car ; donnons-lui d'abord une identité :

1. <ComboBox fx:id="carCombo" layoutX="14.0" layoutY="14.0">

2. <items>

3. <FXCollections fx:factory="observableArrayList">

4. <Car brand="Peugeot"/>

5. <Car brand="Renault"/>

6. <Car brand="Citroën"/>

7. </FXCollections>

8. </items>

9. </ComboBox>

Il est tout à fait possible d'écrire dans le contrôleur :

1. @FXML

2. private ComboBox<Car> carCombo;

Une autre conclusion s'impose : si vous avez deux FXML à la mise en page totalement différente, mais contenant les mêmes identités pour les mêmes types d'objets, il vous est tout à fait possible de conserver la même classe contrôleur pour chacun des deux FXML. En quelque sorte, vous pouvez désormais avoir des contrôles utilisant la même classe contrôleur, mais avec des skins différents.

IV-D - La méthode initialize()

Si vous faites que votre contrôleur hérite de l'interface javafx.fxml.Initializable, il est désormais possible d'étendre la méthode initialize() dans le corps de votre classe. Cette méthode prend en paramètre une URL qui est l'emplacement du fichier FXML source qui a été chargé, de même que le ResourceBundle qui est utilisé pour récupérer les textes internationalisés. La méthode initialize() est appelée après que le contenu du FXML a été injecté dans le contrôleur ; donc, sauf erreur de votre part, toutes les références vers vos nœuds et tous les contrôles identifiés auront été correctement initialisées.

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.fxml.FXML;

6. import javafx.fxml.Initializable;

7. import javafx.scene.control.Button;

8. import javafx.scene.layout.AnchorPane;

9. import javafx.scene.web.WebView;

11. public class TestController implements Initializable {

13. @FXML

14. private Button goToWebButton;

15. @FXML

16. private WebView browser;

17. @FXML

18. private AnchorPane proxyConfiguration;

20. @Override

21. public void initialize(URL url, ResourceBundle rb) {

22. // Tapez votre code ici.

23. }

24. }

Vous pouvez utiliser cette méthode pour vérifier que vos références ont toutes été correctement chargées (par exemple, via des tests assert), mettre des valeurs par défaut dans vos contrôles, faire du binding ou encore installer les écouteurs de base avant que le contrôle ne soit attaché au reste de votre UI et ne puisse être manipulé par la classe appelante. Conserver une référence sur le ResourceBundle permet également de continuer à utiliser des ressources internationalisées lorsqu'on change le texte dans l'interface ultérieurement.

IV-E - Accéder au sous-contrôleur

Chouette, nous pouvons définir des contrôleurs pour chacun de nos FXML ! Cependant ce n'est pas très pratique dans le cas de FXML inclus dans d'autres FXML puisque nous ne contrôlons pas le chargement et donc que nous ne pouvons pas récupérer de références sur le sous-contrôleur.

Reprenons notre exemple d'inclusion de FXML dans un autre FXML. Tout ce que nous avons à faire c'est de modifier la description de la balise racine pour pointer sur une classe contrôleur. C'est exactement ce que nous avons fait à la section précédente :

1. <AnchorPane id="AnchorPane" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml" fx:controller="test.ProxyController">

Et inclure la classe test.ProxyController dans notre projet. Encore une fois, pareil que précédemment. Je vais juste rajouter un peu de code dans la méthode initialize() de manière à ce que notre UI commence un peu à se comporter normalement : faire en sorte que les contrôles et champs soient désactivés quand on n'est pas censé pouvoir les utiliser.

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.fxml.FXML;

6. import javafx.fxml.Initializable;

7. import javafx.scene.control.CheckBox;

8. import javafx.scene.control.PasswordField;

9. import javafx.scene.control.RadioButton;

10. import javafx.scene.control.TextField;

12. public class ProxyController implements Initializable {

14. @FXML

15. private RadioButton noProxyRadio;

16. @FXML

17. private RadioButton systemProxyRadio;

18. @FXML

19. private RadioButton manualProxyRadio;

20. @FXML

21. private TextField hostField;

22. @FXML

23. private TextField portField;

24. @FXML

25. private CheckBox authenticationCheck;

26. @FXML

27. private TextField userField;

28. @FXML

29. private PasswordField passwordField;

31. @Override

32. public void initialize(URL url, ResourceBundle rb) {

33. hostField.editableProperty().bind(manualProxyRadio.selectedProperty());

34. portField.editableProperty().bind(manualProxyRadio.selectedProperty());

35. authenticationCheck.disableProperty().bind(manualProxyRadio.selectedProperty().not());

36. userField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

37. passwordField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

38. }

39. }

Souvenez-vous, dans notre FXML principal, nous avions assigné l'identité proxyConfiguration au FXML secondaire proxy.fxml que nous avons inclus :

1. <fx:include fx:id="proxyConfiguration" source="proxy.fxml" />

En fait, nous avons déjà implicitement accès au contrôleur de proxy.fxml depuis le contrôleur de notre FXML principal. Pour rendre cet accès explicite, il suffit pour cela de déclarer :

1. @FXML

2. private AnchorPane proxyConfiguration;

3. @FXML

4. private ProxyController proxyConfigurationController;

La convention est simple :

@FXML

private <type du contrôleur du FXML> <identité du FXML>Controller;

Et c'est tout ! Nous pouvons désormais accéder au sous-contrôleur et manipuler ses méthodes et propriétés exactement comme avec n'importe quel autre objet Java.

IV-F - Appeler des méthodes du contrôleur depuis le FXML

Reprenons le code de notre bouton, précédemment nous avions ajouté un callback qui appelait une fonction écrite en JavaScript dans le corps même du FXML. Nous allons supprimer notre code JavaScript et modifier ce callback pour appeler une méthode qui se trouve maintenant dans le contrôleur :

1. <Button fx:id="goToWebButton" contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#goToDeveloppez" text="Button">

2. <graphic>

3. <ImageView id="logo" pickOnBounds="true">

4. <image>

5. <Image url="@logo.png" preserveRatio="true" smooth="true" />

6. </image>

7. </ImageView>

8. </graphic>

9. <tooltip>

10. <Tooltip id="tooltip" text="%visit.developpez.web" />

11. </tooltip>

12. </Button>

Ici, le caractère # permet de spécifier le nom d'une fonction du contrôleur qui sera associée au callback onAction de notre bouton. À l'exécution, lorsqu'on clique sur le bouton, une fonction portant ce nom sera recherchée et, si elle est découverte, elle sera appelée. Pour le moment cette fonction n'existe pas encore, le nom de la fonction sera même souligné en rouge dans NetBeans puisqu'il n'arrive pas à la trouver.

Nous devons rajouter le code suivant dans notre contrôleur principal :

1. @FXML

2. private void goToDeveloppez(ActionEvent event) {

3. System.out.println("Méthode du contrôleur");

4. browser.getEngine().load("http://www.developpez.com/");

5. }

Désormais c'est cette méthode qui sera appelée lorsque l'utilisateur clique sur le bouton. Si la fonction n'est pas définie dans le contrôleur lors du chargement du FXML, une exception de type LoadException incluant le message « Controller method "goToDeveloppez" not found » sera levée.

V - SceneBuilder

Comme vous devez vous en douter, je n'ai, en fait, pratiquement pas tapé une ligne de code FXML dans la plupart des exemples précédents que je vous ai donnés. Tout d'abord, le support du FXML s'améliore dans NetBeans à chaque nouvelle version, ce qui fait que NetBeans 7.3 et les versions ultérieures supportent par exemple la complétion automatique des valeurs et est capable de suggérer les noms des attributs/propriétés à utiliser dans la majorité des cas. J'imagine qu'Eclipse et IntelliJ IDEA ne doivent pas être en reste non plus.

De plus, Oracle a développé en interne et a rendu public gratuitement un outil de conception graphique nommé SceneBuilder. Cet outil permet une édition visuelle du contenu du FXML, pour tout ce qui est des classes graphiques (les classes non graphiques demandant toujours une édition manuelle). La version 1.0 a été rendue disponible pour Windows et MacOS en septembre 2012, suivie de la version 1.1 pour Windows, MacOS et Linux en septembre 2013. La 2.0, a quant à elle, été publiée en mai 2014 et qui offre un support des contrôles de JavaFX 8 (disponible dans le JDK8).

Cette dernière version est passée en open source dans le cadre de l'OpenJFX et son code source est disponible sur le Mercurial du projet (sur le chemin rt/apps/scenebuilder). Le projet continue d'être maintenu pour ajouter des fonctionnalités suivant les évolutions de JavaFX ou pour corriger des bogues. Il est possible de signaler des bogues sur le Java Bug Database d'Oracle et le Java Bug System de l'OpenJDK.

À partir du JDK 8_40, Oracle a cessé de distribuer des binaires précompilés de SceneBuilder. Vous pouvez donc :

- soit télécharger les sources de l'OpenJFX et tenter de recompiler l'application ;

- soit récupérer un fichier JAR ou des installeurs natifs contenant une version précompilée du logiciel, par exemple sur le site de la compagnie Gluon (dans la section Products → Downloads - leur version de SceneBuilder reste un outil gratuit). La version de SceneBuilder disponible chez Gluon, nommée SceneBuilder 8.0.0, a été mise à jour pour supporter les nouveautés du JDK 8_40.

Les anciennes versions de SceneBuilder sont toujours disponibles chez Oracle, dans la page des archives accessibles depuis la page produit du logiciel.

Résumons :

- SceneBuilder 1.0 ou 1.1 : publié par Oracle et destiné à JavaFX 2.x ;

- SceneBuilder 2.0 : publié par Oracle et destiné à JavaFX 2.2 ou JavaFX 8.0 ;

- SceneBuilder 8.0.0 : publié par Gluon et destiné à JavaFX 8.0_40.

V-A - Lancement

S'il est correctement installé, SceneBuilder sera automatiquement lancé par NetBeans lorsque vous double-cliquez sur un fichier FXML dans l'arborescence du projet. Il est possible de configurer le chemin d'accès vers le logiciel, en allant, dans NetBeans dans Tools → Options puis en choisissant la catégorie Java et l'onglet JavaFX.

Note : actuellement cela ne fonctionne pas avec la version de SceneBuilder fournie par Gluon. La compagnie a confirmé que ceci est dû à un bogue de NetBeans. Il faudra patienter le temps que l'équipe de développement de l'IDE corrige ce souci pour avoir de nouveau accès à l'intégration de SceneBuilder dans NetBeans.

Astuce : si SceneBuilder est activé, pour éditer un FXML directement dans l'éditeur de code de NetBeans plutôt que dans SceneBuilder, il faut cliquer avec le bouton de droite sur le FXML et choisir Edit au lieu de Open.

V-B - Présentation rapide

Par défaut SceneBuilder 1.0 ou 1.1 s'ouvre sur une page simple contenant un simple AnchorPane en nœud racine. En fait, il s'agit exactement du même contenu que le tout premier FXML que je vous ai montré au début de cet article.

Figure 11 - SceneBuilder 1.0 ou 1.1 au démarrage.

SceneBuilder 2.0 (ou ultérieur) s'ouvre, quant à lui, sur un document vierge et vous laisse le loisir de placer vous-même l'élément racine de votre FXML. N'importe quel conteneur peut faire l'affaire, mais je vous conseille en général d'utiliser un AnchorPane qui suffit largement pour la plupart des usages.

Figure 12 - SceneBuilder 2.0 au démarrage.

Le fonctionnement est similaire à ce que vous pouvez trouver dans la plupart des éditeurs d'interfaces graphiques.

- L'espace de travail au centre permet de sélectionner des nœuds et de les déplacer ou de les redimensionner à la souris.

- La partie Library en haut à gauche contient la liste des nœuds graphiques les plus courants dans l'API. Vous pouvez faire du drag'n drop de nœud depuis la Library vers la Hierarchy ou vers l'espace de travail.

- La partie Hierarchy en bas à gauche vous montre l'arborescence graphique qui correspond au contenu de votre FXML.

- La partie Inspector sur la droite vous montre les différentes propriétés et options que vous pouvez configurer sur le nœud actuellement sélectionné (par défaut la racine du document). Les propriétés sont réparties en trois grandes catégories :

- les Properties, qui sont les propriétés générales du nœud ;

- le Layout, des propriétés liées au positionnement dans la scène et le contrôle parent ;

- le Code permet de donner le nom des méthodes utilisées par les callbacks. Dans SceneBuilder 1.x, il est aussi possible d'y spécifier le nom long la classe du contrôleur (uniquement sur le nœud racine).

- La partie Controller (SceneBuilder 2.0 ou ultérieur) tout en bas à gauche sous la partie Hierarchy. Permet de spécifier le nom long la classe du contrôleur (uniquement sur le nœud racine).

V-C - Aperçu de l'internationalisation

Lorsque vous chargez une UI contenant du texte internationalisé dans SceneBuilder, par défaut, il ne vous affichera que les clés de traduction.

Figure 13 - SceneBuilder 1.1.

Vous pouvez avoir un aperçu des valeurs traduites dans votre interface en allant dans le menu Preview → Internationalisation → Set Resources… et en choisissant le fichier de propriétés contenant les traductions de votre UI.

Dans SceneBuilder 1.0 ou 1.1, cela aura pour effet d'insérer une ligne supplémentaire dans votre XML de la forme :

1. <?scenebuilder-preview-i18n-resource strings.properties?>

Cette directive contient le chemin relatif vers le fichier properties utilisé pour l'internationalisation. Elle n'a pas d'usage en dehors de SceneBuilder 1.0 et 1.1.

SceneBuilder 2.0 utilise un mécanisme différent pour stocker ce genre de valeurs (dans ses propriétés utilisateur) et ne rajoute aucune ligne dans le fichier FXML.

Dans tous les cas, le programmeur devra quand même manuellement attacher un ResourceBundle à son FXMLLoader dans son code, comme montré précédemment, car cette fonctionnalité ne sert qu'à avoir un aperçu de la traduction dans SceneBuilder.

Figure 14 - Aperçu de l'internationalisation.

V-D - Aperçu du style

Les contrôles et nœuds de JavaFX supportent les Cascading Syle Sheets (CSS). Lorsque vous chargez une UI dans SceneBuilder, elle s'affichera avec le style par défaut et le style qui est décrit de manière inline dans le FXML (dans l'attribut style de chaque balise décrivant un nœud). Si vous avez une feuille de style redéfinissant les styles par défaut des nœuds ou définissant vos propres styles (voir attribut styleClass des nœuds), SceneBuilder peut l'utiliser.

Pour cela, allez dans le menu Preview → Scene Style Sheets → Add a Style Sheet… et choisissez un ou plusieurs fichiers CSS.

Dans SceneBuilder 1.0 ou 1.1, cela aura pour effet d'insérer une ou plusieurs lignes supplémentaires dans votre XML de la forme :

1. <?scenebuilder-stylesheet test.css?>

Cette directive contient le chemin relatif vers le fichier css utilisé pour le style. Elle n'a pas d'usage en dehors de SceneBuilder 1.0 et 1.1 et les feuilles de style définies ne seront pas appliquées au chargement du fichier.

Ici aussi, SceneBuilder 2.0 stocke ses valeurs différemment (via ses préférences utilisateur) sans rajouter de ligne dans le fichier FXML.

Comme pour l'internationalisation, il s'agit uniquement d'un aperçu pour l'édition dans SceneBuilder. Une fois chargé dans son application, le fichier FXML sera soumis aux feuilles de style attachées à la scène ou sur ses contrôles parents.

Il est cependant possible de spécifier des feuilles de style indépendamment pour chaque nœud en utilisant le champ Stylesheets de la partie Inspector ce qui aura pour effet d'insérer un attribut styleSheet dans le nœud XML concerné dans le fichier FXML. Ces feuilles de style là font partie intégrante du FXML et seront bien appliquées au chargement du fichier.

V-E - CSS analyzer

Il peut-être parfois assez prise de tête de comprendre quel est le style qui est en train de s'appliquer à tel ou tel attribut ou sous-partie d'un nœud. À partir de SceneBuilder 1.1, nous avons accès à un nouvel outil connu sous le nom de CSS Analyzer. Il est accessible via le menu View → Show CSS Analyzer.

Figure 15 - CSS Analyzer sur le GridPane sélectionné.

Un nouveau panneau s'affichera en bas de l'UI qui permet d'explorer l'origine de la valeur d'un attribut graphique avec, par ordre de priorité : Defaults < Inspector < Stylesheets < Inline Styles.

- Defaults - il s'agit des valeurs provenant du style par défaut (Caspian dans JavaFX 2.x, Modena dans JavaFX 8.x).

- Inspector - les valeurs settées via la partie Inspector de SceneBuilder qui se traduisent généralement par des attributs de la balise du nœud dans le FXML.

- StyleSheets - les valeurs définies dans la ou les feuilles de styles utilisées en aperçu ainsi que celles placées sur chacun des nœuds. L'ordre des styles dépend de leur ordre d'écriture dans les fichiers et de l'ordre d'ajout des feuilles de style. Ils sont également impactés par les styles définis dans le champ styleClass de la partie Inspector.

- Inline Styles - les styles définis dans le champ style de la partie Inspector, qui sont sauvegardés dans l'attribut styleClass de la balise du nœud dans le FXML.

V-F - Contrôles customisés

Dans sa version 1.1, SceneBuilder ne supporte pas très bien l'édition de FXML contenant des contrôles et classes customisées ou provenant de bibliothèques externes. Cela fonctionne de manière un peu plus transparente dans la version 2.0, sans toutefois être parfait.

Lorsqu'une classe customisée est utilisée dans le FXML, s'il s'agit de nœuds graphiques, vous devriez pouvoir avoir accès à leurs propriétés du moins pour celles utilisant des types Java classiques ; mais dans le meilleur des cas, le plus souvent, si votre rendu est dépendant de valeurs qui ne sont settées que lors de l'exécution, l'affichage de votre nœud se limitera à un rectangle gris. Dans le pire des cas, le FXML ne se chargera pas.

V-F-1 - Dans SceneBuilder 1.1

Si vous utilisez des classes non supportées dans la version 1.1, SceneBuilder fera apparaitre au démarrage une boite de dialogue vous demandant de rajouter les versions compilées de ces classes sur son CLASSPATH. Voici par exemple ce que j'obtiens quand j'essaie d'ouvrir le FXML dans lequel je manipulais des instances de la classe test.Car avec SceneBuilder 1.1 :

Figure 16 - Ce qui arrive dans SceneBuilder 1.1 avec des contrôles customisés.

Vous devrez donc rajouter manuellement le chemin vers un endroit où des JAR ou des fichiers .class sont disponibles (ici le répertoire racine de compilation du projet NetBeans dans lequel se trouvent le package test et ses classes compilées) :

Figure 17 - Édition du chemin vers les binaires.

Et ensuite, cliquer sur le bouton Apply de manière à voir apparaitre « Unknown types resolved » :

Figure 18 - Résolution du problème.

Vous pouvez enfin cliquer sur le bouton Close.

V-F-2 - Dans SceneBuilder 2.0

Le support des contrôles customisés est un peu plus fonctionnel dans SceneBuilder 2.0. à l'ouverture du fichier FXML, le contrôle sera marqué comme étant inconnu et n'aura pas de taille.

Figure 19 - Un contrôle inconnu dans SceneBuilder 2.0.

Il est possible de résoudre ce problème en allant dans le menu de configuration de l'onglet Library et de choisir la commande Import JAR/FXML File… Cela aura pour effet d'afficher une boite de dialogue qui permettra de sélectionner le fichier JAR contenant le contrôle à importer.

Figure 20 - importer un fichier JAR.

Le logiciel explorera alors le contenu du fichier sélectionné pour afficher les nœuds graphiques qu'il contient de manière à ce que vous puissiez les utiliser.

Figure 21 - Sélection des contrôles.

Lorsque votre sélection est terminée, cliquez sur Import Component et votre contrôle apparaitra alors dans une nouvelle catégorie Custom dans l'onglet Library.

Figure 22 - Après l'importation.

Quelques-unes de ses propriétés sont visibles dans l'onglet Inspector ; elles restent cependant accessibles uniquement en lecture seule. Certaines propriétés restent invisibles cependant sans qu'on en sache la raison réelle.

Pour supprimer un contrôle customisé de SceneBuilder, il faut naviguer jusqu'au répertoire dans lequel le logiciel a recopié le fichier JAR contenant le contrôle et supprimer ce fichier. Cela peut être fait en allant dans le menu de configuration de l'onglet Library et en choisissant la commande nommée Reveal in Explorer (sous Windows, l'intitulé de la commande variera en fonction de votre système d'exploitation).

Figure 23 - Ouverture du répertoire de stockage de SceneBuilder.

Cela ouvrira votre explorateur de fichiers dans le répertoire utilisé par le logiciel pour stocker ses fichiers utilisateur. Fermez alors SceneBuilder et, dans votre navigateur de fichiers, déplacez-vous dans le sous-répertoire Library, vous devriez alors être en mesure de supprimer le fichier JAR incriminé avant de relancer SceneBuilder.

VI - Code final

Il est temps de voir la version finale du programme. J'ai rajouté deux classes pour prendre en charge les redéfinitions du proxy et un peu de code pour ajouter du binding et quelques écouteurs dans la méthode initialize() du contrôleur secondaire. De plus, cette dernière classe expose désormais quelques propriétés et dispose d'une méthode qui modifie le proxy de la JVM.

VI-A - Main.java

Programme principal :

1. package test;

3. import java.io.IOException;

4. import java.net.URL;

5. import java.util.ResourceBundle;

6. import javafx.application.Application;

7. import javafx.fxml.FXMLLoader;

8. import javafx.scene.Scene;

9. import javafx.scene.layout.AnchorPane;

10. import javafx.stage.Stage;

12. public class Main extends Application {

14. @Override

15. public void start(Stage primaryStage) {

16. try {

17. // Localisation du fichier FXML.

18. URL url = getClass().getResource("test10.fxml");

19. // Chargement du bundle:

20. ResourceBundle bundle = ResourceBundle.getBundle("test/strings");

21. // Création du loader.

22. FXMLLoader fxmlLoader = new FXMLLoader(url, bundle);

23. // Chargement du FXML.

24. AnchorPane root = (AnchorPane) fxmlLoader.load();

25. // Accès au contrôleur.

26. TestController controller = (TestController) fxmlLoader.getController();

27. // Création de la scène.

28. Scene scene = new Scene(root, 600, 600);

29. primaryStage.setScene(scene);

30. } catch (IOException ex) {

31. System.err.println("Erreur au chargement: " + ex);

32. }

33. primaryStage.setTitle("Test FXML");

34. primaryStage.show();

35. }

37. public static void main(String[] args) {

38. launch(args);

39. }

40. }

VI-B - string.properties

Fichier de ressources, contient les traductions des textes de l'UI :

1. visit.developpez.web=Visitez le site web de développez !

2. web.no.proxy=Pas de proxy

3. web.system.proxy=Utiliser le proxy système

4. web.manual.proxy=Proxy manuel

5. web.proxy.host=Hôte

6. web.proxy.port=Port

7. web.use.authentication=Utiliser l'authentification ?

8. web.authenticate.username=Utilisateur

9. web.authenticate.password=Mot de passe

VI-C - test10.fxml

FXML principal :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.geometry.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.image.*?>

9. <?import javafx.scene.layout.*?>

10. <?import javafx.scene.web.*?>

11. <?scenebuilder-preview-i18n-resource strings.properties?>

13. <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml" fx:controller="test.TestController">

14. <children>

15. <BorderPane prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

16. <bottom>

17. <fx:include fx:id="proxyConfiguration" source="proxy.fxml" />

18. </bottom>

19. <center>

20. <WebView fx:id="browser" prefHeight="200.0" prefWidth="200.0">

21. <BorderPane.margin>

22. <Insets bottom="6.0" top="6.0" />

23. </BorderPane.margin>

24. </WebView>

25. </center>

26. <top>

27. <Button fx:id="goToWebButton" contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#goToDeveloppez" text="Button">

28. <graphic>

29. <ImageView id="logo" pickOnBounds="true">

30. <image>

31. <Image url="@logo.png" preserveRatio="true" smooth="true" />

32. </image>

33. </ImageView>

34. </graphic>

35. <tooltip>

36. <Tooltip id="tooltip" text="%visit.developpez.web" />

37. </tooltip>

38. </Button>

39. </top>

40. </BorderPane>

41. </children>

42. </AnchorPane>

VI-D - TestController.java

Contrôleur principal, demande la reconfiguration du proxy avant d'accéder au site web :

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.event.ActionEvent;

6. import javafx.fxml.FXML;

7. import javafx.fxml.Initializable;

8. import javafx.scene.control.Button;

9. import javafx.scene.layout.AnchorPane;

10. import javafx.scene.web.WebView;

12. public class TestController implements Initializable {

14. @FXML

15. private Button goToWebButton;

16. @FXML

17. private WebView browser;

18. @FXML

19. private AnchorPane proxyConfiguration;

20. @FXML

21. private ProxyController proxyConfigurationController;

23. @Override

24. public void initialize(URL url, ResourceBundle rb) {

25. }

27. @FXML

28. private void goToDeveloppez(ActionEvent event) {

29. proxyConfigurationController.setupProxy();

30. browser.getEngine().load("http://www.developpez.com/");

31. }

32. }

VI-E - proxy.fxml

FXML secondaire :

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

3. <?import java.lang.*?>

4. <?import java.util.*?>

5. <?import javafx.scene.*?>

6. <?import javafx.scene.control.*?>

7. <?import javafx.scene.layout.*?>

8. <?scenebuilder-stylesheet test.css?>

10. <AnchorPane id="AnchorPane" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml" fx:controller="test.ProxyController">

11. <children>

12. <GridPane style="-fx-hgap: 6; -fx-vgap:6;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

13. <children>

14. <RadioButton fx:id="noProxyRadio" mnemonicParsing="false" selected="true" text="%web.no.proxy" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="0">

15. <toggleGroup>

16. <ToggleGroup fx:id="optionToggleGroup" />

17. </toggleGroup>

18. </RadioButton>

19. <RadioButton fx:id="systemProxyRadio" mnemonicParsing="false" text="%web.system.proxy" toggleGroup="$optionToggleGroup" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />

20. <RadioButton fx:id="manualProxyRadio" mnemonicParsing="false" text="%web.manual.proxy" toggleGroup="$optionToggleGroup" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="2" />

21. <Label text="%web.proxy.host" GridPane.columnIndex="0" GridPane.rowIndex="3" />

22. <TextField fx:id="hostField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.rowIndex="3" />

23. <Label text="%web.proxy.port" GridPane.columnIndex="2" GridPane.rowIndex="3" />

24. <TextField fx:id="portField" prefWidth="200.0" GridPane.columnIndex="3" GridPane.rowIndex="3" />

25. <CheckBox fx:id="authenticationCheck" mnemonicParsing="false" text="%web.use.authentication" GridPane.columnIndex="0" GridPane.columnSpan="2147483647" GridPane.rowIndex="4" />

26. <Label text="%web.authenticate.username" GridPane.columnIndex="0" GridPane.rowIndex="5" />

27. <TextField fx:id="userField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="5" />

28. <Label text="%web.authenticate.password" GridPane.columnIndex="0" GridPane.rowIndex="6" />

29. <PasswordField fx:id="passwordField" prefWidth="200.0" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="6" />

30. </children>

31. <columnConstraints>

32. <ColumnConstraints hgrow="NEVER" minWidth="50.0" prefWidth="-1.0" />

33. <ColumnConstraints hgrow="ALWAYS" minWidth="100.0" prefWidth="100.0" />

34. <ColumnConstraints hgrow="NEVER" minWidth="-1.0" prefWidth="-1.0" />

35. <ColumnConstraints hgrow="NEVER" minWidth="50.0" prefWidth="50.0" />

36. </columnConstraints>

37. <rowConstraints>

38. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

39. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

40. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

41. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

42. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

43. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

44. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

45. </rowConstraints>

46. </GridPane>

47. </children>

48. </AnchorPane>

VI-F - ProxyController.java

Contrôleur secondaire, se charge également de réinitialiser le proxy. Certaines valeurs sont accessibles publiquement par des propriétés en lecture seule ce qui permet de les observer pour les stocker dans des préférences pour plus tard. Le mot de passe de connexion reste interne à la classe.

JDK7

1. package test;

3. import java.net.Authenticator;

4. import java.net.PasswordAuthentication;

5. import java.net.ProxySelector;

6. import java.net.URL;

7. import java.util.ResourceBundle;

8. import javafx.beans.property.ReadOnlyBooleanProperty;

9. import javafx.beans.property.ReadOnlyBooleanWrapper;

10. import javafx.beans.property.ReadOnlyIntegerProperty;

11. import javafx.beans.property.ReadOnlyIntegerWrapper;

12. import javafx.beans.property.ReadOnlyObjectProperty;

13. import javafx.beans.property.ReadOnlyObjectWrapper;

14. import javafx.beans.property.ReadOnlyStringProperty;

15. import javafx.beans.property.ReadOnlyStringWrapper;

16. import javafx.beans.value.ChangeListener;

17. import javafx.beans.value.ObservableValue;

18. import javafx.fxml.FXML;

19. import javafx.fxml.Initializable;

20. import javafx.scene.control.CheckBox;

21. import javafx.scene.control.PasswordField;

22. import javafx.scene.control.RadioButton;

23. import javafx.scene.control.TextField;

24. import javafx.scene.control.Toggle;

25. import javafx.scene.control.ToggleGroup;

26. import static test.ProxyType.MANUAL;

27. import static test.ProxyType.NONE;

28. import static test.ProxyType.SYSTEM;

30. public final class ProxyController implements Initializable {

32. @FXML

33. private ToggleGroup optionToggleGroup;

34. @FXML

35. private RadioButton noProxyRadio;

36. @FXML

37. private RadioButton systemProxyRadio;

38. @FXML

39. private RadioButton manualProxyRadio;

40. @FXML

41. private TextField hostField;

42. @FXML

43. private TextField portField;

44. @FXML

45. private CheckBox authenticationCheck;

46. @FXML

47. private TextField userField;

48. @FXML

49. private PasswordField passwordField;

51. @Override

52. public void initialize(URL url, ResourceBundle rb) {

53. optionToggleGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {

54. @Override

55. public void changed(ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) {

56. if (newValue == noProxyRadio) {

57. proxyType.set(ProxyType.NONE);

58. } else if (newValue == systemProxyRadio) {

59. proxyType.set(ProxyType.SYSTEM);

60. } else if (newValue == manualProxyRadio) {

61. proxyType.set(ProxyType.MANUAL);

62. }

63. }

64. });

65. //

66. hostField.editableProperty().bind(manualProxyRadio.selectedProperty());

67. proxyHost.bind(hostField.textProperty());

68. //

69. portField.editableProperty().bind(manualProxyRadio.selectedProperty());

70. portField.textProperty().addListener(new ChangeListener<String>() {

71. @Override

72. public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {

73. try {

74. int port = Integer.parseInt(newValue);

75. port = Math.max(1, port);

76. port = Math.min(65535, port);

77. proxyPort.set(port);

78. } catch (Exception e) {

79. e.printStackTrace();

80. }

81. }

82. });

83. //

84. authenticationCheck.disableProperty().bind(manualProxyRadio.selectedProperty().not());

85. useAuthentication.bind(authenticationCheck.selectedProperty());

86. //

87. userField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

88. proxyUser.bind(userField.textProperty());

89. //

90. passwordField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

91. }

92. // Propriété proxy type.

93. private final ReadOnlyObjectWrapper<ProxyType> proxyType = new ReadOnlyObjectWrapper<>(this, "proxyType", ProxyType.NONE);

95. public ProxyType getProxyType() {

96. return proxyType.get();

97. }

99. public ReadOnlyObjectProperty<ProxyType> proxyTypeProperty() {

100. return proxyType.getReadOnlyProperty();

101. }

102. // Propriété proxy host.

103. private final ReadOnlyStringWrapper proxyHost = new ReadOnlyStringWrapper(this, "proxyHost", null);

105. public String getProxyHost() {

106. return proxyHost.get();

107. }

109. public ReadOnlyStringProperty proxyHostProperty() {

110. return proxyHost.getReadOnlyProperty();

111. }

112. // Propriété proxy port.

113. private final ReadOnlyIntegerWrapper proxyPort = new ReadOnlyIntegerWrapper(this, "proxyPort", 0);

115. public int getProxyPort() {

116. return proxyPort.get();

117. }

119. public ReadOnlyIntegerProperty proxyPortProperty() {

120. return proxyPort.getReadOnlyProperty();

121. }

122. // Propriété use authentication.

123. private final ReadOnlyBooleanWrapper useAuthentication = new ReadOnlyBooleanWrapper(this, "userAuthentication", false);

125. public boolean isUseAuthentication() {

126. return useAuthentication.get();

127. }

129. public ReadOnlyBooleanProperty UseAuthenticationProperty() {

130. return useAuthentication.getReadOnlyProperty();

131. }

132. // Propriété proxy user.

133. private final ReadOnlyStringWrapper proxyUser = new ReadOnlyStringWrapper(this, "proxyUser", null);

135. public String getUserHost() {

136. return proxyUser.get();

137. }

139. public ReadOnlyStringProperty proxyUserProperty() {

140. return proxyUser.getReadOnlyProperty();

141. }

142. //

143. private final ProxySelector defaultProxySelector = ProxySelector.getDefault();

145. public void setupProxy() {

146. ProxyType proxyType = getProxyType();

147. boolean useSystemProxies = false;

148. ProxySelector proxySelector = defaultProxySelector;

149. boolean useAuthentication = false;

150. switch (proxyType) {

151. case NONE:

152. break;

153. case SYSTEM:

154. useSystemProxies = true;

155. break;

156. case MANUAL:

157. useAuthentication = isUseAuthentication();

158. String proxyHost = getProxyHost();

159. int proxyPort = getProxyPort();

160. proxySelector = new CustomProxySelector(defaultProxySelector, proxyHost, proxyPort);

161. break;

162. }

163. System.setProperty("java.net.useSystemProxies", String.valueOf(useSystemProxies)); // NOI18N.

164. ProxySelector.setDefault(proxySelector);

165. // Pas 100 % sûr pour l'authentification.

166. Authenticator proxyAuthenticator = null;

167. if (useAuthentication) {

168. final String user = proxyUser.get();

169. final String password = passwordField.getText();

170. proxyAuthenticator = new Authenticator() {

171. @Override

172. public PasswordAuthentication getPasswordAuthentication() {

173. return new PasswordAuthentication(user, password.toCharArray());

174. }

175. };

176. }

177. Authenticator.setDefault(proxyAuthenticator);

178. //

179. printSystemProxyInfo();

180. }

182. private void printSystemProxyInfo() {

183. System.out.printf("java.net.useSystemProxies\t%s", System.getProperty("java.net.useSystemProxies")).println();

184. System.out.printf("http.proxyHost\t%s", System.getProperty("http.proxyHost")).println();

185. System.out.printf("http.proxyPort\t%s", System.getProperty("http.proxyPort")).println();

186. }

187. }

JDK8

1. package test;

3. import java.net.Authenticator;

4. import java.net.PasswordAuthentication;

5. import java.net.ProxySelector;

6. import java.net.URL;

7. import java.util.ResourceBundle;

8. import javafx.beans.property.ReadOnlyBooleanProperty;

9. import javafx.beans.property.ReadOnlyBooleanWrapper;

10. import javafx.beans.property.ReadOnlyIntegerProperty;

11. import javafx.beans.property.ReadOnlyIntegerWrapper;

12. import javafx.beans.property.ReadOnlyObjectProperty;

13. import javafx.beans.property.ReadOnlyObjectWrapper;

14. import javafx.beans.property.ReadOnlyStringProperty;

15. import javafx.beans.property.ReadOnlyStringWrapper;

16. import javafx.beans.value.ObservableValue;

17. import javafx.fxml.FXML;

18. import javafx.fxml.Initializable;

19. import javafx.scene.control.CheckBox;

20. import javafx.scene.control.PasswordField;

21. import javafx.scene.control.RadioButton;

22. import javafx.scene.control.TextField;

23. import javafx.scene.control.Toggle;

24. import javafx.scene.control.ToggleGroup;

25. import static test.ProxyType.MANUAL;

26. import static test.ProxyType.NONE;

27. import static test.ProxyType.SYSTEM;

29. public final class ProxyController implements Initializable {

31. @FXML

32. private ToggleGroup optionToggleGroup;

33. @FXML

34. private RadioButton noProxyRadio;

35. @FXML

36. private RadioButton systemProxyRadio;

37. @FXML

38. private RadioButton manualProxyRadio;

39. @FXML

40. private TextField hostField;

41. @FXML

42. private TextField portField;

43. @FXML

44. private CheckBox authenticationCheck;

45. @FXML

46. private TextField userField;

47. @FXML

48. private PasswordField passwordField;

50. @Override

51. public void initialize(URL url, ResourceBundle rb) {

52. optionToggleGroup.selectedToggleProperty().addListener((ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) -> {

53. if (newValue == noProxyRadio) {

54. proxyType.set(ProxyType.NONE);

55. } else if (newValue == systemProxyRadio) {

56. proxyType.set(ProxyType.SYSTEM);

57. } else if (newValue == manualProxyRadio) {

58. proxyType.set(ProxyType.MANUAL);

59. }

60. });

61. //

62. hostField.editableProperty().bind(manualProxyRadio.selectedProperty());

63. proxyHost.bind(hostField.textProperty());

64. //

65. portField.editableProperty().bind(manualProxyRadio.selectedProperty());

66. portField.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {

67. try {

68. int port = Integer.parseInt(newValue);

69. port = Math.max(1, port);

70. port = Math.min(65535, port);

71. proxyPort.set(port);

72. } catch (Exception e) {

73. e.printStackTrace();

74. }

75. });

76. //

77. authenticationCheck.disableProperty().bind(manualProxyRadio.selectedProperty().not());

78. useAuthentication.bind(authenticationCheck.selectedProperty());

79. //

80. userField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

81. proxyUser.bind(userField.textProperty());

82. //

83. passwordField.editableProperty().bind(manualProxyRadio.selectedProperty().and(authenticationCheck.selectedProperty()));

84. }

85. // Propriété proxy type.

86. private final ReadOnlyObjectWrapper<ProxyType> proxyType = new ReadOnlyObjectWrapper<>(this, "proxyType", ProxyType.NONE);

88. public ProxyType getProxyType() {

89. return proxyType.get();

90. }

92. public ReadOnlyObjectProperty<ProxyType> proxyTypeProperty() {

93. return proxyType.getReadOnlyProperty();

94. }

95. // Propriété proxy host.

96. private final ReadOnlyStringWrapper proxyHost = new ReadOnlyStringWrapper(this, "proxyHost", null);

98. public String getProxyHost() {

99. return proxyHost.get();

100. }

102. public ReadOnlyStringProperty proxyHostProperty() {

103. return proxyHost.getReadOnlyProperty();

104. }

105. // Propriété proxy port.

106. private final ReadOnlyIntegerWrapper proxyPort = new ReadOnlyIntegerWrapper(this, "proxyPort", 0);

108. public int getProxyPort() {

109. return proxyPort.get();

110. }

112. public ReadOnlyIntegerProperty proxyPortProperty() {

113. return proxyPort.getReadOnlyProperty();

114. }

115. // Propriété use authentication.

116. private final ReadOnlyBooleanWrapper useAuthentication = new ReadOnlyBooleanWrapper(this, "userAuthentication", false);

118. public boolean isUseAuthentication() {

119. return useAuthentication.get();

120. }

122. public ReadOnlyBooleanProperty UseAuthenticationProperty() {

123. return useAuthentication.getReadOnlyProperty();

124. }

125. // Propriété proxy user.

126. private final ReadOnlyStringWrapper proxyUser = new ReadOnlyStringWrapper(this, "proxyUser", null);

128. public String getUserHost() {

129. return proxyUser.get();

130. }

132. public ReadOnlyStringProperty proxyUserProperty() {

133. return proxyUser.getReadOnlyProperty();

134. }

135. //

136. private final ProxySelector defaultProxySelector = ProxySelector.getDefault();

138. public void setupProxy() {

139. ProxyType proxyType = getProxyType();

140. boolean useSystemProxies = false;

141. ProxySelector proxySelector = defaultProxySelector;

142. boolean useAuthentication = false;

143. switch (proxyType) {

144. case NONE:

145. break;

146. case SYSTEM:

147. useSystemProxies = true;

148. break;

149. case MANUAL:

150. useAuthentication = isUseAuthentication();

151. String proxyHost = getProxyHost();

152. int proxyPort = getProxyPort();

153. proxySelector = new CustomProxySelector(defaultProxySelector, proxyHost, proxyPort);

154. break;

155. }

156. System.setProperty("java.net.useSystemProxies", String.valueOf(useSystemProxies)); // NOI18N.

157. ProxySelector.setDefault(proxySelector);

158. // Pas 100 % sûr pour l'authentification.

159. Authenticator proxyAuthenticator = null;

160. if (useAuthentication) {

161. final String user = proxyUser.get();

162. final String password = passwordField.getText();

163. proxyAuthenticator = new Authenticator() {

164. @Override

165. public PasswordAuthentication getPasswordAuthentication() {

166. return new PasswordAuthentication(user, password.toCharArray());

167. }

168. };

169. }

170. Authenticator.setDefault(proxyAuthenticator);

171. //

172. printSystemProxyInfo();

173. }

175. private void printSystemProxyInfo() {

176. System.out.printf("java.net.useSystemProxies\t%s", System.getProperty("java.net.useSystemProxies")).println();

177. System.out.printf("http.proxyHost\t%s", System.getProperty("http.proxyHost")).println();

178. System.out.printf("http.proxyPort\t%s", System.getProperty("http.proxyPort")).println();

179. }

180. }

VI-G - ProxyType.java

Classe annexe, permet de définir les différents types de configuration du proxy :

1. package test;

3. public enum ProxyType {

4. NONE, SYSTEM, MANUAL;

5. }

VI-H - CustomProxySelector.java

Classe annexe, servira de sélectionneur de proxy dans le cas où l'utilisateur décide d'utiliser un proxy manuel :

1. package test;

3. import java.io.IOException;

4. import java.net.InetSocketAddress;

5. import java.net.Proxy;

6. import java.net.ProxySelector;

7. import java.net.SocketAddress;

8. import java.net.URI;

9. import java.util.ArrayList;

10. import java.util.List;

12. final class CustomProxySelector extends ProxySelector {

14. private ProxySelector defaultProxySelector = null;

15. private String hostname;

16. private int port;

18. public CustomProxySelector(ProxySelector defaultProxySelector, String hostname, int port) {

19. this.defaultProxySelector = defaultProxySelector;

20. this.hostname = hostname;

21. this.port = port;

22. }

24. @Override

25. public List<Proxy> select(URI uri) {

26. System.out.printf("CustomProxySelector::select(%s)", uri).println();

27. if (uri == null) {

28. throw new IllegalArgumentException("URI can't be null.");

29. }

30. String protocol = uri.getScheme();

31. ArrayList<Proxy> result = new ArrayList<>();

32. if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) {

33. // Populate the ArrayList with proxies

34. Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(hostname, port));

35. result.add(proxy);

36. }

37. if (defaultProxySelector != null) {

38. result.addAll(defaultProxySelector.select(uri));

39. } else {

40. result.add(Proxy.NO_PROXY);

41. }

42. return result;

43. }

45. @Override

46. public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {

47. if (uri == null || sa == null || ioe == null) {

48. throw new IllegalArgumentException("Arguments can't be null.");

49. }

50. if (defaultProxySelector != null) {

51. defaultProxySelector.connectFailed(uri, sa, ioe);

52. }

53. }

54. }

VII - Conclusion

Voilà, vous avez désormais réalisé une première UI JavaFX à base de FXML ; vous savez comment modifier les propriétés des nœuds, inclure un FXML dans un autre FXML ou encore comment écrire un contrôleur pour réagir aux actions de l'utilisateur. Il reste encore d'autres fonctionnalités intéressantes dans FXML comme le fait de pouvoir injecter le nœud racine du document ou le contrôleur dans le chargeur et je vous invite à consulter la FAQ JavaFX de Développez pour en savoir plus.

VIII - Remerciements

Je tiens à remercier toute l'équipe du forum Développez ainsi que Mickael Baron et Logan Mauzaize pour leurs suggestions et leur relecture du présent article. Je tiens également à remercier Claude Leloup pour ses corrections orthographiques.

IX - Liens

- Introduction to FXML

- FXML tutorial

- Scene Builder is now open source!