J'aurais dû écrire cet article beaucoup plus tôt parce que là je me souviens pas de tout.
J'ai "récemment" (lel) passé pas mal de temps à apprendre JavaFX, comme d'hab après tout le monde et quand le projet semble montrer des signes de souffrance et de sérieuse déprime post-hivernale. Genre comme si ça avait été l'hiver pour toujours, puis que l'hiver avait été racheté par Oracle.
Plutôt que de travailler à reécrire le moteur de mon blog qui a plein de problèmes, j'ai écrit un programme pour m'aider à rédiger les articles.
Auparavant j'écrivais mes articles dans Notepad avec des commentaires HTML dedans.
Je peux désormais jouir de cette magnifique interface:
J'ai suivi toutes sortes de tutoriaux et autres vidéos Youtube. Je vais essayer de présenter dans cet article les éléments que j'aurais voulu voir soulignés davantage ou évoqués dans les tutoriaux existants.
Intro à JavaFX
JavaFX sert à faire des interfaces graphiques. C'est tout. Certaines sources vous le présentent comme quelque chose de beaucoup plus compliqué. Certes on peut afficher du contenu multimédia et aligner ses vues de manière plus moderne (par ex. avec des fichiers CSS) que ce les (vieilles) alternatives proposent mais ça reste une librairie qui sert à créer des interfaces graphiques.
Une maladie des applications graphiques en Java c'est qu'elles avaient une apparence qui dépend de l'OS utilisé. Et franchement cette apparence était en général BIEN MOCHE.
Vous avez peut-être déjà utilisé JDownloader et vous êtes demandés pourquoi son interface avait un look [un peu] étrange. C'est un programme Java avec une interface graphique qui n'est pas faite avec JavaFX.
JavaFX met fin à ce problème particulier en proposant par défaut un thème tout gris qui respire la dépression suivant la vente d'une technologie informatique populaire à une multinationale disposant d'argent infini. Blague à part, c'est simple et efficace, les normes d'accessiblités sont bien respectées (en particulier le contrôle qui a le focus est bien évident parce qu'il brille en bleu) et ça a un look assez moderne sans tremper dans le material design, qui est une autre histoire.
Comment moi je faisais
Avant quand j'avais besoin d'une interface graphique, je faisait peter Netbeans et j'utilisais leur éditeur intégré (Matisse je pense qu'il s'appelle) qui est basé sur Swing.
A condition d'un petit peu s'y connaître au niveau des layouts ou de partir dans un plan infernal de positionnement absolu, c'est très simple de construire rapidement quelque chose qui s'ajuste avec les redimentionnements et répond correctement aux ajustements des différentes zones de l'interface, et ce sans devoir créer des infames media-queries et devoir tester sous Internet Explorer 6 en 640x480 pour être sûr qu'il ne manque pas la moitié de l'écran.
A ce propos, pour celui ou celle qui se dit "BWééééé maintenant les interfaces utilisateur ont les fait toutes en web non?" - C'est vrai sauf que quand on sait ce qu'on fait, ça met à peu près 10 fois moins de temps de le faire en "dur" (càd avec Swing, JavaFX, .NET ou autre truc du genre). En plus il ne faut pas gérer les 100000 de versions de navigateurs, les polyfills, les CDN, les versions de serveur ni s'inquiéter d'à quoi ça va ressembler sur un 3310 en WAP. Ce qui contribue largement au fait que le développement soit bien plus rapide.
Pouvoir torcher ses layout visuellement et modifier la hiéarchie des composantes dans la liste à droite, lier les évènements clic etc. Aux composants, changer leur texte en un clic, tout est possible depuis l'éditeur de Netbeans:
De Swing à JavaFX
Pour Swing, la manière habituelle de procéder était la suivante:
- Créer une class qui hérite de JFrame, et qui appelle une méthode "initialize" dans son constructeur ;
- Bourrer toute la création de l'interface dans cette méthode d'initialisation et lier les méthodes d'évènements (qui peuvent être écrites en ligne) ;
- L'éditeur inclus dans Netbeans fait tout ça pour vous
- Instiancier notre classe qui hérite de JFrame quelque part (par ex. dans la méthode Main) et l'afficher.
Exemple de code pour l'affichage de votre jFrame:
public static void main(String[] args) {
JFrame moche = new JFrameMoche();
moche.setVisible(true);
moche.setLocationRelativeTo(null);
}
En bonus, on centre la fenêtre avec setLocationRelativeTo(null).
Avec JavaFX c'est un peu différent:- Il faut absolument sélectionner un type de Projet "JavaFX" ou utiliser Maven et l'archétype JavaFX, ou la compilation ne se passe pas comme il faut (pour Swing un projet Java standard fonctionne) ;
- On utilise une classe qui hérite de javafx.application.Application pour démarrer l'application. La méthode Main est dans cette classe. Je vous conseille de garder le modèle généré par l'IDE ou l'archétype Maven qui surcharge la méthode start pour créer votre première fenêtre ;
A ce moment il y a deux manières de travailler différentes, décrites ci-après.
A la sauvage
Consiste à bourrer tout le code de création de contrôle et créer les listeners etc. dans un genre de méthode d'initialisation (ou bien carrément dans start), un peu comme je faisais avec Swing et l'éditeur de Netbeans sauf qu'il faut tout faire manuellement ici.
Je vois un vague intérêt à cette méthode pour créer rapidement des boites de dialogue ou une interface très très minimaliste. Dans tous les autres cas, j'utiliserais la méthode FXML décrite plus loin.
Les classes qui représentent les composants JavaFX ont des noms très simples, comme Button, MenuItem, TextArea, ... Et sont générallement moins compliqués à composer rapidement que ça ne l'était avec Swing. En plus je suppose que vous travaillez au moins avec Java 8 où il est possible d'utiliser des expressions lambda pour un code encore plus compact.
Exemple:
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;
public class DeleteMe extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button("Say 'Hello World'");
btn.setOnAction((e) -> {
System.out.println("Hellow World!");
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Il s'agit plus ou moins de l'exemple Hello World de la doc officielle sauf que j'ai remplacé la déclaration de l'évènement OnClick du bouton par une expression lambda.
Vu comme ça en partant de rien, on peut se demander ce que c'est que ces histoires de "Scene" et "Stage", je me le suis demandé aussi.
Les Scenes et Stages
En pratique, un Stage est une fenêtre. Si JavaFX était vraiment portable, on pourrait dire que l'écran de l'application d'un smartphone est aussi un Stage.
Un Stage seul ne sert à rien, il faut lui assigner une Scene. La Scène est le conteneur primaire des composants JavaFX. Vous y mettez un "Panel" comme enfant direct puis ajoutez des composants comme enfants de ce panel.
Dans l'exemple plus haut on est partis sur un StackPane qui est un panel sur lequel les composants s'empilent les uns sur les autres. Ouais en fait vous ne l'utiliserez pas souvent le StackPane (-> JAMAIS). Le constructeur de Scene peut directement recevoir un panel auquel vous aurez préalablement ajouté tous vos composants.
Contrairement à Swing où c'eut été étrange de conserver plusieurs conteneurs parents et changer lequel est l'enfant direct d'une JFrame, c'est construit dans JavaFX, que l'on puisse alterner plusieurs scènes dans une même fenêtre. Maintenant je dis pas que vous devez le faire (moi je le ferais pas perso).
En soi, c'est tout à fait possible de créer une classe qui hérite de Stage et procéder exactement comme avec Netbeans et l'éditeur Swing. A part qu'il faudra tout écrire manuellement. Ceci dit, c'est extrêment rare de voir un projet JavaFX codé de cette manière. C'est aussi extrêmement rare de voir un projet JavaFX tout court.
Composer par fichier FXML
Plutôt que d'instancier chaque composant dans le code puis l'ajouter à un parent, qui sera plus tard assigné à une Scene, qui sera plus tard assigné à un Stage, il est possible d'écrire un fichier XML qui définisse chaque composant, charger ce fichier, et l'assigner directement à une Scene.
Ecrire soi-même le fichier FXML n'est pas horriblement compliqué, juste trop compliqué pour mes besoins de codage à l'arrache.
Bonne nouvelle, il existe un éditeur graphique d'interfaces JavaFX auquel est consacré le chapitre suivant.
Si on travaille avec des fichiers FXML pour définir ses scènes, comment on fait pour lier des méthodes à des évènements? Genre comment je sais quand quelqu'un clique mon bouton?
Il s'agit de créer une classe contrôleur et la lier à votre fichier FXML. Des annotations sont mises à votre disposition pour injecter les composants d'interface directement dans votre contrôleur en tant que propriétés ainsi que les méthodes liées aux évènements de votre interface et de ses composants.
Utiliser des fichiers FXML permet dès lors de totalement séparer le code qui génère l'interface du code de contrôleur (ce que l'on plaçait dans notre classe qui hérite de JFrame pour Swing). C'est beaucoup plus facile à maintenir, on peut créer ses vues d'abord puis travailler sur le code, réutiliser des parties de vues facilement, ... (en fait on peut surtout utiliser l'éditeur graphique).
Je conseille de travailler exclusivement avec des fichiers FXML pour définir vos vues dans n'importe quel projet plus ou moins conséquent. De fait tout le reste de cet article parle d'interface JavaFX générée à partir de fichiers FXML.
JavaFX et les environnements de développement
Il n'y a pas à ce jour d'éditeur graphique d'interface JavaFX intégré à un quelconque environnement de développement. Ce que les développeurs de la JVM ont voulu faire, c'est créer eux même un éditeur graphique qui pourrait ensuite être appelé depuis n'importe quel éditeur. Comme ça, pas de jaloux. Le projet s'appelle "Scene Builder".
C'est super Michel, sauf qu'ils ont abandonné le développement de ce truc. Je ne sais pas trop pourquoi. Peut-être que ça va revenir, un jour?
Il est plus ou moins globalement admis que la meilleure version de Scene Builder est maintenue par un tiers à cet endroit.
Ils livrent des versions pour toutes les plateformes, y compris pour Java 9 qui est très récent à dater de la rédaction de cet article.
Scene Builder lui-même est écrit en utilisant JavaFX et les sources sont quelques part sur Github si ça vous intéresse (les sources d'Oracle, pas de Gluon (enfin je pense)).
Comment moi que je commencerais un projet JFX
Il y a plusieurs écoles, la plus "sérieuse" choisirait sans doute Maven. Personnellement j'essaye d'éviter les machins qui récupèrent des paquets avec des dépendances et des dépots tant que je peux. Donc si je peux éviter Maven ou son espèce de successeur dont j'ai oublié le nom, je préfère. J'aime bien télécharger mes librairies et les uploader en gestion de source avec mon projet. Truc de vieux. Ouais je sais pas.
De plus, l'archétype JavaFX de Maven n'a pas l'air super trop maintenu. Corrigez-moi si je me trompe. Mais là je vais utiliser Netbeans.
Je vous conseille de tout de suite installer deux choses:
- Netbeans avec Java 8 ou 9
- Scene Builder (celui de Gluon)
Lancez Netbeans. Créez un nouveau projet, et choisissez JavaFX -> JavaFX FXML Application. Vous pouvez spécifier le nom et package de la classe qui va étendre Application (et donc créer le premier Stage) ainsi que le nom du fichier FXML qui sera auto-généré avec le squelette Hello World similaire à celui présenté dans ma section "A la sauvage" plus haut.
Ensuite j'aime bien réorganiser tous mes packages. Je mets tous les FXML dans un (sous)package "view" ou "views", tous les contrôleurs dans "controller" ou "controllers".
Ceci demande d'adapter le fichier FXML qui a été créé par Netbeans pour modifier le nom complet du contrôleur, qui est renseigné dans l'élément racine du FXML. Par exemple ici il s'agit d'un BorderPane (élément parent que je conseille d'ailleurs en règle générale):
<BorderPane prefHeight="700.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="eu.dkvz.BlogAuthoring.controllers.MainFrameController">
L'attribut à modifier est fx:controller.
Je dessine mon interface
C'est la première étape, on y reviens de temps en temps bien sûr. Il est possible d'ouvrir Scene Builder depuis Netbeans ou Eclipse si c'est configuré de la sorte quelque part. Personnellement j'ouvre simplement Scene Builder seul et commence à travailler.
Les possibilités sont très étendues, surtout que vous pouvez également tout styler avec des CSS, ce qui est d'ailleurs la méthode préférée pour assigner les marges, padding etc. de manière homogène.
Le comportement de l'interface en cas de redimensionnement peut être contrôlé de façon aussi précise qu'il n'y a de consonnes dans redimensionnement. Vous pouvez décider de ce qui grandit, des tailles préférées, minimales ou maximales de chaque composant, de comment certains composants sont autorisés à couper leur texte si l'affichage est trop petit, ...
Ce n'est pas le but de cet article d'expliquer en détails tout ce qui est possible. Je vais plutôt rester fidèle à l'objectif original: cracher une interface graphique à l'arrache, donc on oublie les CSS et tout ce genre de choses.
Voici ce que je vous conseille pour démarrer: ouvrez Scene Builder, puis créez un nouveau projet (qui est en fait un fichier FXML) ou bien ouvrez le fichier FXML créé par le modèle de projet Netbeans et effacez tous les composants dans la liste de gauche pour partir de 0.
Il s'agit d'utiliser le champ de recherche en haut à gauche pour trouver vos composants. Commencez par chercher un Border Pane à glisser-déplacer dans la hiérarchie de composants en bas à gauche.
Ce BorderPane est le parent de tout le reste de votre scène.
Comme pour Swing il est divisé en zones géographiques: Nord, Est, Ouest et Sud. On peut ajouter d'autres types de "Panes" dans ces zones (dont un autre BorderPane si ça vous dit (c'est pas censé vous dire)).
Les "Panes" sont en fait l'équivalent des layouts en Swing.
Si vous sélectionnez le BorderPane dans la hiérarchie, vous avez l'inspecteur à droite qui affiche trois accordéons "Properties", "Layout" et "Code".
Dans un premier temps il est bon de noter que dans Layout, si vous avec une Pref Width ou Pref Height de définis (ils le sont par défaut) ça va définir la taille de départ de la fenêtre dans laquelle vous allez charger cette scène. Utiliser "COMPUTED SIZE" dans ces champs va créer l'équivalent d'utiliser pack() en Swing, c'est à dire que votre interface va être compactée au maximum.
Mettez immédiatement la taille minimum sur autre chose que PREF_SIZE ou des éléments vont commencer à disparaître si un utilisateur réduit la fenêtre en deçà des valeurs de Pref Width et Height.
Pour peupler mon BorderPane j'aime bien faire simple et utiliser au maximum les HBox et VBox qui sont des layouts alignants des composants horizontalement (HBox) ou verticalement (VBOX). C'est tout. Vraiment simple quoi.
Il existe un "Pane" plus flexible et complexe et plutôt populaire appelé MigPane, mais il faut l'installer en supplément. Et on sait combien j'aime pas trop ça.
Dans la zone Sud - AJouter un HBox qui va servir de barre de tâche, avec un label et (par exemple) une progress bar. Sur le HBox c'est une bonne idée de mettre un padding ou du spacing pour espacer les composants.
Par défaut les HBox et VBox ont une taille (PREF_WIDTH et PREF_HEIGHT) qui est définie. Il faut tout replacer sur COMPUTED_* si vous voulez que la zone soit compactée. Ce qui est pratiquement toujours le cas.
Si vous voulez qu'un composant se redimensionne automatiquement (ici horizontalement puisqu'on a une HBox), vous pouvez sélectionner le composant, placer HGrow sur Always dans ses options de Layout et lui mettre un MAX_WIDTH sur MAX_VALUE comme sur le screenshot ce-dessous:
Mettre un composant en HGrow ou VGrow est un peu comme utiliser flex en web. Vous pouvez même utiliser un composant "invisible" (Region) pour obtenir une région transparente qui sépare un côté de l'autre (par ex. un bouton à l'extrême gauche et un bouton à l'extrême droite), qui est également quelque chose que vous pourriez obtenir avec un GridPane et des alignements à droite et à gauche sur deux colonnes de taille max.
C'est un peu l'histoire éternelle avec la création des vues, il y a plusieurs manières d'arriver au même résultat, comme par exemple utiliser un seul MigPane plutôt que tout un tas de HBox et VBox. Personnellement je préfère entasser des composants simples dont je comprends le fonctionnement qu'utiliser un nombre limité de composants complexes sur lesquels je ne suis pas certain d'avoir le même contrôle.
Au final, sans utiliser les zones centrales (gauche, centre et droite), votre listing de composants pour cet exemple ressemblerait à ceci:
Une dernière remarque: j'utilise les icônes de FontAwesome dans mes interfaces.
J'en parle aussi pour présenter quelques preuves que JavaFX est toujours activement utilisé, le projet FontAwesomeFX continue d'être maintenu.
Vous pouvez ajouter la librairie dans votre projet avec Maven ou à l'ancienne en téléchargeant le jar et en le plaçant dans votre Classpath.
Les composants de FontAwesomeFX peuvent être rendus disponibles dans Scene Builder en cliquant sur l'icône d'engrenage qui est à juste à droite de la zone de recherche de composants (volet de gauche donc) et cliquer sur "Jar/FXML Manager". Vous pouvez ensuite ajouter le .jar téléchargé sur leur repo BitBucket.
Pour ajouter une image à un composant, le plus simple est d'utiliser le composant FontAwesomeIconView et lui assigner un nom de glyphe dans sa propriété "Glyph".
J'ajoute les identifiants et définis les méthodes d'évènement
A cette étape-ci c'est une bonne idée d'avoir une classe contrôleur mentionnée dans l'élément racine du FXML.
Il s'agit maintenant de donner des identifiants et des méthodes d'évènement aux composants qui doivent en avoir. Il n'est pas nécessaire de nommer TOUS vos composants, juste le minimum avec lequel vous souhaitez interargir.
Rien de plus simple sinon: sélectionner un composant, et dérouler le volet "Code" dans Scene Builder. Ensuite nommez vos composants et méthodes d'action en utilisant un genre de convention comme par exemple:
Vous pouvez laisser Scene Builder ouvert une fois cette étape terminée. N'oubliez juste pas de sauvegarder les changements dans le FXML.
Je génère mon contrôleur
Dans Netbeans, il suffit de cliquer droit sur un fichier FXML et sélectionner "Make Controller":
La génération fonctionne si la classe existe déjà, et n'écrase pas tous vos changements. La génération va juste ajouter les nouveaux champs et méthodes automatiquement sans altérer le contenu déjà présent.
Attention, si vous effacez un composant, Netbeans va juste retirer les annotations @FXML devant les propriétés/évènements retirés, ils restent présents dans le fichier. Ce qui a du sens parce que si vous faites référence à ces champs dans le contrôleur et que Netbeans les retire purement et simplement ça va faire des trucs soulignés en rouge partout dans votre code.
Enfin, ça c'est si tout se passe bien. Pour une raison obscure l'autogénération ne fonctionne pas dans certains de mes projets, en cela qu'elle génére le contrôleur dans le même package que mes fichiers FXML, même si le chemin du contrôleur le déclare comme étant dans un autre package. Si vous avez ce bug, c'est une bonne idée de placer vos contrôleurs dans le même package que les fichiers FXML.
On n'échappe pas aux BINDINGS
Si vous utilisez un de ces fantastiques (HAHAHAHAHAhdslkjfhdsh) Frameworks Javascript vous êtes sans doute familier à l'idée d'utiliser des bindings. Plutôt que d'aller récupérer la valeur text d'un composant de formulaire, vous la liez directement à une propriété globale. Ou encore, vous avez un champ texte dont le contenu met automatiquement à jour un autre champ, pendant que vous tapez dans ce champ (?????). Heum.... Oui ce genre de choses.
JavaFX introduit le concept de binding au travers de ce qu'il appelle des "propriétés". Ce qui nous ajoute une nouvelle nomenclature et quelques nouveaux objets dans lequel il faudra emballer nos anciens objets. Je dis qu'il faudra parce qu'on échappe pas au binding en JavaFX.
Initiallement je voulais m'en passer pour travailler "à l'ancienne", mais dans plusieurs cas l'utilisation de bindings est inévitable. Alors autant plonger dedans tout nu et tout entier le plus vite possible et essayer d'en tirer de la productivité.
Une des motivations de cet article c'était de parler des bindings comme j'aurais souhaité que les tutoriaux que j'ai consulté en parlent. Parce que là tout le monde vous sort des exemples à la con qui sont loin de démontrer l'utilité de la chose.
En gros, dans l'espace de nom javafx.beans.property vous trouverez toutes sortes de classes du type "IntegerProperty" ou "StringProperty" voire encore "ObjectProperty" qui emballent respectivement leur type de données mentionné et ajoutent:
- La possibilité de lier cette propriété à une autre, dans un sens, dans deux sens, ou avec des opérations appliquées automatiquement ;
- La possibilité d'ajouter des listeners sur cette propriété, par exemple pour appeler une méthode automatiquement quand une modification est détectée.
Vous pouvez bien entendu créer vos propres classes observables, mais ça sort du cadre de cet article et franchement, tout ce dont vous avez besoin existe déjà.
A ce propos, vous aurez sans doute également besoin de l'espace de nom javafx.collections qui implémente les collections classiques de java.util mais que l'on peut bind ou observer.
Nomenclature
Vous constaterez en lisant la documentation des composants JavaFX qu'ils ont un dédoublement de certains éléments sémantiques. Exemple pour TextField:
- getText(), setText() et getTextProperty()
- isDisabled(), setDisabled() et disabledProperty()
- getPromptText(), setPromptText() et promptTextProperty()
- ... Et bien d'autres
Les méthodes *Property() renvoient des objets de type StringProperty, IntegerProperty etc. Sur un ListView par contre, la propriété itemsProperty renvoie une ObservableList, qui est l'équivalent des propriétés pour un élément qui implémente java.util.List quelque part dans sa vie.
Admettons que nous ayons un champ de type String appelé "text" dans notre classe. Ce champ est privé et dispose de deux accesseurs: getText() et setText(). Jusque là c'est du Java normal. Enfin je suppose que vous travaillez comme ça (on parle d'encapsulation dans le métier) parce que si vous avez simplement mis "text" public vous êtes un hérétique (ou un développeur PHP mais c'est similaire).
Le code pourrait ressembler à ceci:
class MaSuperClasse {
private String text;
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
}
Maintenant admettons que l'on souhaite avoir une propriété texte au sens JavaFX dans cet objet, afin de pouvoir la lier à des éléments d'interface JavaFX. Après avoir importé les bons namespaces, on procède de cette manière:
class MaSuperClasse {
private StringProperty text = new SimpleStringProperty();
public final String getText() {
return this.text.get();
}
public final void setText(String text) {
this.text.set(text);
}
public StringProperty textProperty() {
return this.text;
}
}
Nous conservons une compatibilité totale avec la première définition de cette classe (ce qui est permis par l'encapsulation soit dit en passant, au cas où vous êtes vraiment développeur PHP).
Il y a plusieurs choses à noter:
- Le champ text est maintenant de type abstrait StringProperty, que l'on a instancié immédiatement avec son implémentation immédiatement disponible, SimpleStringProperty. En principe ce champ peut être marqué Final parce que vous n'aurez en général aucune raisons de réinstancier cet objet.
- Les getters et setters originaux ont exactement la même signature à part qu'ils sont Final. Pourquoi? Convention et sécurité du comportement si on hérite de cette classe.
- On ajoute un accesseur en plus qui s'appelle <NOM_DE_LA_VARIABLE>Property() et qui retourne simplement la propriété (son type abstrait bien entendu).
On sait faire quoi avec ces propriétés alors?
L'exemple super bateau c'est, si on a un TextField nommé textField1, et un Label nommé label1, je peux écrire ceci dans mon contrôleur (idéalement dans la méthode initialize() du contrôleur pour que le binding soit immédiatement appliqué):
label1.textProperty.bind(textField1.textProperty());
Cette simple ligne de code, une fois exécutée, fait en sorte que le texte du Label se modifie automatiquement quand on écrit dans le TextField. C'est l'exemple qui est sorti dès que ça parle de binding mais personne n'utilise ce truc en pratique.
Un vague cas d'utilisation pourrait être de vouloir que le Label affiche "Bonjour " + ce qui est tapé dans le TextField.
Voici une manière (sale) de le faire (cliquer sur l'image):
Pour chaque exemple, je suis dans le code du contrôleur de mon fichier FXML qui contient juste un label et un TextField.
En texte:
this.label1.textProperty()
.bind(new SimpleStringProperty("Bonjour ")
.concat(textField1.textProperty()));
En fait, il existe un autre espace de nom: javafx.beans.bindings qui contient une classe Bindings qui offre toute une série de fonctions d'aggrégation (je sais pas si ça s'appelle aggrégation ni combien de g il y a dans aggrégation mais ça fait joli Okay?) pour créer des bindings composés complexes.
Vous pouvez vous faire une chaine d'appels de 10000 de bindings si vous voulez.
Pour mon exemple j'aurais pu simplement utiliser Bindings.concat() qui retourne une StringProperty, comme ceci (cliquez pour agrandir):
En texte:
this.label1.textProperty()
.bind(Bindings.concat("Bonjour ")
.concat(textField1.textProperty()));
Ces Bindings comprennent également des "conditions". Par exemple, ce serait pas mal si on pouvait refaire notre exemple mais en n'affichant rien si le TextField, et "Bonjour " + le text s'il n'est pas vide.
Là on part un peu dans le capilotracté et on utilise
Bindings.when(BooleanProperty).then(ObservableValue).otherwise(ObservableValue)
En pratique:
this.label1.textProperty()
.bind(Bindings.when(textField1.textProperty().isNotEmpty())
.then(Bindings.concat("Bonjour ").concat(textField1.textProperty()))
.otherwise(textField1.textProperty())
);
C'est-à-dire que, si la propriété text de mon TextField est non-vide, alors je bind avec la propriété qui est le résultat du concat de "Bonjour " avec mon text, sinon, je bind tout simplement avec la propriété text du TextField (qui est forcément vide à ce moment là d'ailleurs).
A noter que vous n'êtes pas obligés de tout chier en une ligne. Il est possible de déclarer auparavant une partie de ces Bindings, ou l'objet When qui est utilisé, par exemple.
J'espère que vous commencez un petit peu à cerner la puissance (et la bordélisation mono-ligne) des bindings. Ce type de binding étant générallement déclarés dans la méthode initialize() des contrôleurs.
Il est possible de dé-binder un composant en utilisant unbind().
Là je vous ai parlé du mono-directionnel parce que c'est le plus simple à comprendre (ce qui est à gauche de .bind() réagit avec les changements de ce qui est entre les parenthèses de bind()), mais il est possible de faire du binding bi-directionnel.
Je vous conseille de l'éviter si vous pouvez parce que ça complique le déboguage. Un cas d'utilisation du Binding bi-directionnel? Bonne question.
Une idée que j'ai eue consiste à lier un Bean (genre entité de base de données) dans les deux sens pour l'affichage direct de données sans devoir le coder.
Je m'explique: imaginons une classe User avec comme champs (qui doivent être des propriétés, ici des StringProperty pour faire simple) "lastname" et "firstname". Je peux lier de façon bidirectionnelle ces propriétés aux TextField de mon interface qui sont supposés afficher ces données. De cette manière, quand je charge un objet User depuis ma base de données, l'interface est automatiquement mise-à-jour; et si j'édite mon champs de lastname et firstname, mon objet entité est également modifié automatiquement. Il suffit alors d'avoir un bouton un peu plus loin pour persister mon entité.
Cette approche économise quelques lignes de code mais je ne sais pas si je le conseillerais en pratique. A vous de voir à quelle sauce vous voulez vos bindings.
Je présente des cas d'utilisations plus pratiques plus loin dans cet article (sérieusement il se déroule jusqu'où ton article??).
L'approche déclarative
Je déclare générallement tous mes bindings dans la méthode initialize() des contrôleurs, ainsi que dans les méthodes qui créent des tâches threadées ou de nouvelles fenêtres et scènes.
Il est possible de déclarer des bindings dans le fichier FXML, et franchement ça aurait plus de sens. Enfin, j'en sais rien, j'ai pas envie d'avoir le débat pseudo-intellectuel impératif VS déclaratif.
Comme je n'utilise pas les déclarations de bindings dans le fichier FXML, et parce que je pense que vous devriez vous tenir à quasi-100% l'un ou quasi-100% l'autre, je ne vais pas en parler en détail et juste vous vomir l'exemple bateau du TextField qui met à- our un label:
<TextField fx:id="textField"/>
<Label text="${textField.text}"/>
J'imagine qu'il est possible d'écrire ces bindings depuis Scene Builder. Si vous êtes super fan des approches déclaratives bien propres et que vous avez envie de tracer votre propre chemin en espérant qu'il soit moins brun que le mien, je vous conseille d'approfondir le sujet.
Les évènements
Plutôt que d'utiliser un bind() pour mon super exemple de TextField qui met automatiquement à jour le contenu d'un Label j'aurais pu utiliser un Listener.
Les gens qui font du Javascript vont tout de suite comprendre tout ça, voici mon exemple modifié (cliquer pour agrandir):
En texte:
this.textField1.textProperty().addListener((v, p, n) -> {
this.label1.setText(n);
});
J'utilise ici une expression Lambda sans mentionner les types. Les arguments du listener sont les suivants:
- La propriété, sous forme de ObservableValue (interface) - Mais peut en théorie avoir un autre type selon ce qui est à gauche de addListener
- Le type primitif qui correspond à la valeur avant le changement, ici ce sont des String parce que textProperty() et un StringProperty
- Le type primitif qui correspond à la valeur après le changement, ici ce sont des String parce que textProperty() et un StringProperty
Les évènements sont particulièrement importants pour les composants qui doivent détecter une sélection (par exemple ListView). En pratique vous liez l'élément sélectionné de la liste à une propriété, puis vous ajoutez un listener sur cette propriété. Ou bien, vous ajoutez directement un listener sur l'élément sélectionné de la liste.
La plupart du temps, les bindings sont suffisamment puissants pour ne pas avoir à utliser de Listener pour émuler ce que ferait un binding (je me comprends). Evitez-les si possible.
Par ailleurs, vous aurez peut-être remarqué dans Scene Builder dans l'espace "Code" qu'il y a peu d'évènements qui peuvent être déclarés. Pour cause, vous pouvez simplement ajouter des Listener sur les propriétés dont vous souhaitez surveiller les changements vous-même. C'est de cette manière que vous allez pouvoir réagir à un redimensionnement, un changement de focus, ...
Evènements sur les listes
Voilà qui va me permettre d'introduire les listes (fonctionnement identique pour Map etc.) propriét... Propriétéisées (??) pour JavaFX.
Prenons ObservableList comme exemple. Il y a plusieurs moyens d'assigner une valeur à ObservableList, particulièrement vous pouvez utiliser set() et lui filer... Une List. Okay.
L'autre plan consiste à utiliser la classe FXCollections qui a des méthodes statiques qui créent des collections observables.
Voici un exemple avec création de Listener de mutation de la liste:
ObservableList<String> list = FXCollections.observableArrayList();
list.addListener((ListChangeListener.Change<? extends String> c) -> {
while (c.next()) {
c.getAddedSubList().stream().forEach((e) -> {
System.out.println("Ajouté " + e);
});
}
});
Cet exemple me permet aussi de briser tous vos rêves de code élégant à base d'expression lambda parce que dans le cas d'ObservableList le compilateur ne peut pas inférer le type de l'argument du Listener (qui s'appelle "c" dans mon exemple) parce que ObservableList peut créer deux types de Listener différents (celui de changement et un autre d'invalidation qu'on s'en fout complètement).
Par conséquent, il était nécessaire d'ajouter le type complet de la classe Change, qui est encore plus compliqué que prévu puisqu'on a typé notre liste (en tant que String). Beurk.
Pour rendre tout encore plus sale j'ai utilisé stream() de Java 8 pour faire mon foreach alors que j'aurais pu écrire un for (machin : bidule) mais je suis très cruel dans ma pédagogie.
Il est également nécessaire d'utiliser .next() sur l'object Change. Même si la plupart du temps le while ne tourne qu'une seule fois. C'est ainsi les amis.
Si vous explorez l'object Change vous verrez qu'il dispose de booléens qui indiquent le type de mutation que la liste a subi, puis vous pouvez interroger Change pour voir quelles mutations ont été exécutées.
Ces évènements peuvent être utilisés pour des composants qui utilisent des collections observables. Par exemple ListView et sa propriété ItemsProperty().
Les évènements de mutation de collection sont assez complexes en terme de code donc je recommande de les éviter si possible, mais c'est important de savoir qu'ils existent.
On peut passer aux cas d'utilisation vraiment utiles?
Je vous présente quelques cas d'utilisation rarement évoqués dans les tutoriaux alors qu'ils sont extrêmement utiles.
Bindings booléens en général
Imaginons que vous ayez un formulaire avec des champs obligatoires, comme souvent. Vous voulez simplement qu'il ne soit possible de cliquer sur le bouton "Sauvegarder" en bas du formulaire que si le champ "lastname" n'est pas vide.
Facile:
buttonSave.disabledProperty().bind(textFieldLastname.textProperty().isEmpty());
Maintenant vous voulez qu'il soit désactivé si les champs de text lastname ou firstname sont vides:
buttonSave.disabledProperty().bind(
textFieldLastname.textProperty().isEmpty().or(textFieldFirstname.textProperty().isEmpty())
);
J'ai simplement combiné la condition vide des deux champs avec "or()". Les autres fonctions booléennes sont évidemment également disponibles.
J'aurais pu également créer ma propre propriété booléenne qui indique si mes champs obligatoires sont remplis correctement. L'avantage c'est que je peux le réutiliser sur plusieurs boutons / éléments d'interface et que je peux le modifier à un endroit centralisé, et même dynamiquement si je le souhaite (ici c'est un champ du contrôleur lui-même):
private BooleanProperty mandatoryFieldsFilledProperty = new SimpleBooleanProperty(false);
@Override
public void initialize(URL url, ResourceBundle rb) {
this.mandatoryFieldsFilledProperty().bind(this.textFieldLastname.textProperty().isEmpty()
.and(this.textFieldFirstname.textProperty().isEmpty())
.and(...)
// Et davantage
);
// On assigne ensuite cette propriété à tout ce qu'on veut:
this.buttonSave.disabledProperty.bind(this.mandatoryFieldsFilledProperty().not());
this.buttonEdit.disabledProperty.bind(this.mandatoryFieldsFilledProperty().not());
// Et je peux en ajouter d'autres facilement.
}
A noter que j'ai dû ajouter .not() parce que cette fois-ci mon booléan est True si les champs sont remplis correctement (et du coup mes opérateurs logiques sont "and" et pas "or"). Je sais pas pourquoi j'explique tout ça.
Autres exemples de simples bindings booléens? Dans mon éditeur de Blog j'ai un élément de menu qui active ou désactive le "Word Wrap". Je n'ai même pas besoin de créer un évènement pour le clic sur ce menu (qui est un checkbox n'est-ce pas), je peux simplement faire ceci:
this.textAreaArticle.wrapTextProperty().bind(this.checkMenuItemWordWrap.selectedProperty());
Et dès que je change l'état du menu checkbox, ça change automatiquement mon Word Wrap dans le texte. Je peux très facilement appliquer le même bind à tous les composants TextArea qui sont dans mon application si je veux, ils seront tous automatiquement liés à ce checkbox.
Autre cas classique: vous êtes en train de charger des données et vous ne voulez pas que certains composants d'interface soient disponibles pendant que ça charge (je devrais parler de ça en détails dans une section ultérieure sur les stratégies de multi-threading), vous pouvez créer une BooleanProperty globale au contrôleur qui indique quand ça charge. Vous la liez à la visibilité de la progress bar, et la désactivation de certaines zones de saisie et certains boutons, pendant que votre tâche d'arrière plan à sa propriété d'activité liée à la propriété de chargement de contrôleur. Vous pouvez délier et relier ces propriétés selon les tâches en cours.
Si vous êtes un vrai guerrier vous pouvez vous lancer dans l'équivalent des TRAVERSEES DE DOM en Javascript/HTML et parcourir les éléments enfants de votre élément parent (un BorderPane par exemple), vérifier lesquels sont des TextField, et automatiquement ajouter tout un tas de bindings sur ces TextField.
Calculs de taille et tout ça
Je pense qu'il faut le mentionner mais je ne suis pas d'avis qu'il s'agisse d'un cas d'utilisation réellement utile et plutôt quelque chose de rare.
Je parlais plus haut de Bindings qui permet de générer un binding à partir d'opérations logiques, mais aussi mathématiques. Ceci permet de lier une propriété numérique à une autre en y appliquant une opération mathématique. C'est aussi simple que ça.
Ce type de bindings a quelques cas d'utilisation dans le positionnement et dimensionnement. Les valeurs telles que PREF_WIDTH et PREF_HEIGHT dont je parlais dans mon cas d'étude sur Scene Builder existent en tant que propriétés (prefWidthProperty() et prefHeightProperty()), de même la position d'un élément dans un layout au positionnement vaguement absolu (ce que je n'utilise jamais) existe aussi sous forme de propriété.
Exemple tout pourri toujours sur la base de mon TextField et Label de tout à l'heure: je veux que la hauteur de mon TextField soit la moitié de la hauteur de ma fenêtre (représenté par la taille de l'élément parent "mainBox" ici):
textField1.prefHeightProperty().bind(mainBox.heightProperty().divide(2.0));
Les propriétés heightProperty() et widthProperty() donnent la réelle taille actuelle du composant mais sont en lecture seule uniquement. Il va falloir vous habituer à la trinité minHeight, prefHeight et maxHeight (idem pour width) ; Donner une valeur à prefHeight ou prefWidth a en général pour résultat un redimensionnement du composant (bien que cela dépende des conditions de dimensionnement de ses parents etc.).
Le résultat de cet exemple ne sert à rien, mais j'imagine qu'il doit exister des cas d'utilisation des "bindings numériques". Qui peuvent être utilisés dans des conditions également (comme tests booléens) pour déclencher quelque chose si telle grandeur est plus grande que x par exemple.
Pour moi c'est évident mais vous pouvez lier des propriétés d'une fenêtre (un Stage en fait) à une autre, pour par exemple avoir une fenêtre secondaire qui flotte toujours au dessus d'un certain composant.
Par ailleurs, en ajoutant des Listener sur les propriétés de dimensions de positionnement de composants vous pouvez ajouter des comportements. Je pense que c'est la méthode privilégiée pour ajouter une sorte d'évènement "OnResize" sur quelque chose, ajoutez simplement un "change Listener" sur la propriété désirée (widthProperty() par exemple).
Faire un compteur de caractères
C'est nul mais ça peut servir. Puis au cas où vous avez toujours pas compris le principe des bindings ça devrait commencer à rentrer, là.
Toujours avec mon exemple un TextField et un Label:
label1.textProperty().bind(textField1.lengthProperty().asString());
textProperty doit être lié à un StringProperty. Utiliser Integer.toString() ne va pas nous aider puisque ça retourne un String normal.
Fort heureusement la méthode asString() est prévue pour cela.
J'aurais pu également lier le texte du label à textField1.textProperty().length() parce que length() renvoie un IntegerBinding. Le résultat est le même.
Avec deux zones de texte vous pourriez créer un NumberBinding qui crée la somme des deux lengthProperty pour... Avoir la somme des deux tailles. Ouais je radote.
C'est qui qui avait le focus avant?
J'ai deux zones de texte de type TextArea dans mon programme d'édition de Blog, une pour le résumé et une pour l'article lui-même.
Je devais pouvoir ajouter du contenu dans ces zones en cliquant sur des boutons/menus. L'appli doit savoir quelle zone avait le focus avant l'action pour savoir lequel des deux TextArea doit recevoir le contenu.
J'ai ajouté une propriété à mon contrôleur de type TextArea (mais ça aurait pu être un TextInputControl pour être plus générique). Puis j'ai simplement déclaré deux Listeners sur les propriétés focus des deux TextArea:
this.textAreaArticle.focusedProperty().addListener((e, o, n) -> {
this.setLastFocusedTextArea(this.textAreaArticle);
});
this.textAreaArticleSummary.focusedProperty().addListener((e, o, n) -> {
this.setLastFocusedTextArea(this.textAreaArticleSummary);
});
Tâches d'arrière-plan
J'en parle dans "JavaFX avancé" (haha) et dans la section sur le threading.
Si vous lisez ce chapitre vous devriez comprendre à quel point les bindings sont obligatoires. C'est le moyen principal de provoquer des mises-à-jour de votre interface depuis une tâche en arrière-plan (avec les évènements en second).
Je ne m'attarde pas là dessus maintenant puisque c'est détaillé plus tard.
Sélection d'un élément dans une liste
Je pense que j'en ai déjà parlé.
Imaginons que nous avons un ListView dans notre interface avec des éléments affichés, nous voulons savoir quel est l'élément sélectionné ainsi que déclencher le chargement de données quand l'élément sélectionné a changé.
Je pense qu'une bonne pratique serait de commencer par définir une propriété qui va représenter l'object sélectionné, j'aime bien la rendre globale au contrôleur (ici en imaginant que mon ListView contient une liste d'éléments de type MyItemType):
private final ObjectProperty<MyItemType> selectedItem = new SimpleObjectProperty();
Dans la méthode initialize du contrôleur, je lie ensuite cette propriété à l'objet sélectionné de ma liste (qui s'appelle listView1):
this.selectedItem.bind(this.listView1.getSelectionModel().selectedItemProperty());
Je déclare ensuite l'évènement de changement sur ma propriété (toujours dans initialize()):
this.selectedItem.addListener((obsValue, oldVal, newVal) -> {
// Si newVal est null c'est qu'on a désélectionné tout
// Si oldVal est null c'est que rien n'était sélectionné
// précédemment.
// Nous pouvons maintenant charger l'objet newVal
// depuis un DB, par exemple, si oldVal != newVal.
});
Ceci étant la manière standard de travailler avec des composants JavaFX qui gèrent des collections.
J'utilise la version la plus courte d'expression lambda. Ici, obsValue est l'ObservableValue qui correspond à notre ObjectProperty. oldVal et newVal sont de type MyItemType puisque c'est le type associé à notre ObjectProperty et se voient assigner les valeurs avant et après le changement qui a déclenché l'évènement.
JavaFX "avancé"
Vous devriez maintenant avoir une bonne base de projet JavaFX.
Je vais présenter ici quelques trucs utiles de manière totalement non-exhaustive et pas nécessairement "avancée". Ouais.
Les boites de dialogue
L'API des boites de dialogue en JavaFX est très flexible. Vous pouvez en pratique ajouter des composants manuellement à vos boites de dialogue pour leur donner un comportement totalement personalité, tout en conservant une homogénéité des différentes zones de l'interface.
C'est pas du tout ce que je veux faire, moi je veux cracher une boite de dialogue d'info, erreur etc. le plus vite possible. Donc j'ai créé cette horrible classe avec des méthodes statiques (me remerciez pas c'est cado):
public final class UIUtils {
public static void errorAlert(String message, String title) {
Alert alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
alert.setTitle(title);
alert.setHeaderText("");
alert.showAndWait();
}
public static void warningAlert(String message, String title) {
Alert alert = new Alert(Alert.AlertType.WARNING, message, ButtonType.OK);
alert.setTitle(title);
alert.setHeaderText("");
alert.showAndWait();
}
public static void infoAlert(String message, String title) {
Alert alert = new Alert(Alert.AlertType.INFORMATION, message, ButtonType.OK);
alert.setTitle(title);
alert.setHeaderText("");
alert.showAndWait();
}
public static boolean confirmDialog(String message, String title) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText("");
alert.setContentText(message);
Optional result = alert.showAndWait();
return result.get() == ButtonType.OK;
}
public static String inputDialog(String message, String title, String defaultValue) {
TextInputDialog dialog = new TextInputDialog(defaultValue);
dialog.setTitle(title);
dialog.setHeaderText("");
dialog.setContentText(message);
Optional result = dialog.showAndWait();
if (result.isPresent()){
return result.get();
} else {
return null;
}
}
}
"Dis-donc là, t'aurais pu refactorer pas mal de code plutôt que le réécrire!" Ouaip je pense que t'as rien compris à mon style de code, cher lecteur aussi inexistant qu'imaginaire.
Multithreading
Vous êtes tout à fait libres de créer des Runnable, les lancer dans des Thread, ou hériter de Thread, utiliser des collections de threads comme si vous n'utilisiez pas JavaFX.
Cela étant, les classes de base de Java n'ont pas de PROPRIETES. Je pense avoir assez insisté sur l'idée qu'on n'échappe pas aux bindings en JavaFX, ils fournissent dès lors plusieurs classes servant au multithreading, avec des propriétés définies au sens JavaFX.
Stratégie de threading
Il ne s'agit pas vraiment d'une considération propre à JavaFX, mais dès que des interfaces graphiques entrent en jeu c'est une bonne idée d'établir dès le départ une stratégie de threading.
Créez simplement une petite liste avec les opérations qui pourraient rendre l'interface "non-responsive", c'est à dire qu'elle refuse toute intéraction avec l'utilisateur et cesse d'afficher des changements, ce qui provoque à son tour une augmentation de la tension artérielle de l'utilisateur et augmente ses chances de presser Ctrl Alt Del ou de retirer la prise si c'est votre mamy.
En général dès qu'il y a intéraction avec un système de fichier ou récupération d'une ressource distante vous avez quelque chose de potentiellement bloquant. Même si vous n'avez aucune intention d'avoir ces opérations qui tournent en parallèle avec d'autres, il faut tout de même les sortir du thread principal JavaFX (le thread principal de votre application si vous voulez).
Enfin, vous n'êtes pas obligés, je parle juste de bonne pratique.
Listez donc:
- Vos opérations bloquantes et/ou couteuses en ressources et/ou temps
- Si ces opérations doivent empêcher d'autres intéractions
- Si ces opérations peuvent tourner en parallèle avec d'autres
- Auquel cas, est-ce qu'il pourrait y avoir des conflits?
- Est-ce qu'on dispose d'un moyen de mesurer l'avancement de l'opération?
- Si ce n'est pas la cas on peut toujours indiquer l'opération avec un spinner ou un changement de curseur
Là vous vous dites surement, mais avec quoi tu viens VEZDE, t'as bu trop de café ou quoi? Skoi ce bordel?
Je vais essayer de décrire tout ça avec des exemples.
Imaginons que votre application puisse charger un fichier texte à sélectionner depuis le système de fichier. Si vous souhaitez n'afficher qu'un seul fichier texte à la fois, ce serait pas mal:
- De mettre le code de chargement du fichier dans une Task, qui pourra être réutilisée ;
- De surveiller (avec un binding) si cette tâche tourne, auquel cas on empêche de lancer le chargement d'un nouveau fichier par désactivation d'éléments d'interface et vérification dans la Task ou la méthode qui lance la Task ;
- Il est possible d'utiliser un flux pour lire le fichier par blocs et indiquer la progression du chargement en ayant connaissance du nombre de blocs du fichier.
Si on implémente ça comme décidé on a un modèle de threading bien propre pour le chargement de ce fichier.
Plus complexe, on pourrait autoriser des chargements simultanés qui créent des onglets différents, par exemple, comme sur les éditeurs de text multi-onglets.
Dans ce cas plusieurs tâches de chargement peuvent tourner en même temps. Il s'agit juste de complexifier la manière dont on va informer l'utilisateur de la progression des tâches (par exemple avoir une seule ProgressBar mais un compteur de tâches juste à côté).
Attention, quand plusieurs tâches peuvent tourner en même temps et manipulent les mêmes données, cela peut provoquer des problèmes de conflit et d'états à moitié modifié des dites données. Les conflits de multithreading peuvent être extrêmement difficiles à déboguer. Je vous renvoie à la documentation de base de Java pour le mot clé synchronized qui pourrait bien vous être utile.
Enfin, je décris ci-après trois classes qui correspondent directement à certains cas d'utilisation qui vous aurez listés dans votre stratégie de threading. Beuarps.
Bloc de code en arrière plan simple
C'est la manière la plus rapide d'écrire du code qui s'exécute de manière non bloquante par rapport à la méthode d'interface où vous êtes (à utiliser uniquement dans les contrôleurs ou la classe principale qui hérite de Application).
C'est extrêmement simple avec une expression lambda:
// Code bloquant ici.
Platform.runLater(() -> {
// Code non bloquant exécuté en parallèle.
}
// Code bloquant ici.
Si comme moi vous avez parfois 100000 de déclarations de bindings dans votre méthode initialize() d'un contrôleur, vous pouvez parfois décider de générer ces bindings "plus tard" s'ils ne sont pas immédiatement nécessaires pour obtenir une application qui semble répondre plus rapidement, afin d'obtenir des stratégies qui ressemblent à certaines web application qui chargent un squelette visuel de leur app avant d'avoir obtenu les fichiers de style complets.
Les blocs de code runLater() sont également la solution à presque tous les problèmes de mise-à-jour de l'interface depuis un autre Thread (chose qui n'est pas recommandée par ailleurs).
Si vous avez une Task qui met directement à jour un Label (on parle de Task juste après) dans le corps de sa méthode call, il faut entourer cette mise-à-jour d'un Platform->runLater(() -> {}); ou ça ne va pas fonctionner (à priori sans erreurs mais l'interface ne sera pas rafraichie).
Ne perdez pas non plus de vue que si votre Task ou Service ou autre semble ne pas fonctionner c'est peut-être qu'elle génère une erreur. Si vous n'avez pas défini d'évènement en cas d'échec de la tâche rien ne se passera.
Parfois (oui parfois) juste mettre à jour une propriété interne à une Task ou un Service requiert d'être entouré d'un runLater(), et parfois pas. Cherchez pas, essayez juste de vous souvenir de cette technique en cas de problème dans votre app.
La classe Task
La classe Task implémente Runnable et ajoute des propriétés à binder (ou pas) selon ses goûts. Il s'agit en fait d'une classe abstraite, il est donc impossible d'instancier Task.
C'est très courant de voir des Task définies "inline". Ce qui a un certain sens puisque la syntaxe de déclaration est rapide et une "Task" n'a de sens que dans un contrôleur JavaFX (ou éventuellement la classe qui hérite de Application et qui a la méthode Main de votre projet).
Définir la classe en ligne dans un contrôleur lui donne aussi automatiquement accès au contexte du contrôleur (et donc vos composants d'interface), mais vous n'êtes pas censés y accéder. Vous risquez de déclencher une erreur (ou plutôt un genre de warning) indiquant que vous avez essayé d'accéder à une propriété en dehors du "FX thread".
Si vous avez besoin d'accéder à des propriétés depuis l'intérieur vous serez obligés de définir une classe à part entière qui hérite de Task et lui ajouter de nouveaux bindings.
En réalité ça n'est pas tout à fait vrai, il est possible d'utiliser Platform->runLater() comme workaround. Je vous offre ce qu'en dit la doc Java **BRUIT DE PROUT**:
Generally, Tasks should not interact directly with the UI. Doing so creates a tight coupling between a specific Task implementation and a specific part of your UI. However, when you do want to create such a coupling, you must ensure that you use Platform.runLater so that any modifications of the scene graph occur on the FX Application Thread.
Gaffe aux tight couplings les gars.
C'est aussi une bonne idée de définir votre propre classe qui hérite de Task dans un fichier .java à part si la tâche est complexe et doit être réutilisable par nature.
Voici un exemple de tâche basique qui remplit un ListView
Task<ObservableList<String>> task = new Task<ObservableList<String>>() {
@Override
protected ObservableList<String> call() throws Exception {
ObservableList<String> list = FXCollections.observableArrayList();
for (int i = 0; i < 10; i++) {
list.add("Element " + Integer.toString(i));
Thread.sleep(1000);
}
return list;
}
};
this.listView1.itemsProperty().bind(task.valueProperty());
Thread t = new Thread(task);
t.start();
Remarquez que la Task reçoit un type. Il s'agit en fait du type de retour de la tâche. Attends, c'est quoi cette histoire de type de retour? C'est normalement ce que génère votre tâche. Si votre tâche ne génère rien du tout vous pouvez simplement renvoyer null (et donc typer comme Object) ou un booléen. La solution officielle étant de typer avec Void:
Task<Void>task = new Task<>() { };
Task s'applique particulièrement bien à remplir des composants tels que ListView ou TableView en renvoyant leur modèle.
On déclare alors la méthode call() qui balance toutes les exceptions (important) avec le bon type de retour. La manière de vérifier si tout s'est passé sans exceptions consiste à utiliser un Listener sur un évènement particulier de Task. Ceci veut malheureusement dire que Task est difficile à deboguer sans enregistrer cet évènement puisque vous ne verrez pas si votre code génère une exception.
Dans cet exemple je crée une liste et j'ajoute 10 éléments en attendant 1 seconde entre chaque ajout.
Notez bien que le binding fait avec itemsProperty() du ListView ne va pas permettre de montrer les changements au fur et à mesure, la "valeur" de la Task n'est assignée qu'au moment où call() atteint un return.
Pour renvoyer au fur et à mesure ou renvoyer des résultats intermédiaires, souvenez-vous que modifier directement un élément d'interface externe depuis la Task est une mauvaise idée à cause des histoires de tight coupling (laule le mec qui sait pas de quoi il parle). L'idéal serait dès lors de créer votre propre classe qui hérite de Task dans un fichier à part et ajouter une propriété en plus que vous mettez à jour pendant le déroulement de votre tâche et pas juste à la fin.
Si nous revenons à l'essentiel, il y a plusieurs choses que vous pouvez faire dans votre Task:
- Vérifier que isCancelled() n'est pas true. C'est normalement comme ça qu'on demande l'arrêt propre d'une tâche en dehors de celle-ci. Si vous n'implémentez pas type de test dans vos boucles de tâches, faire un cancel() de la tâche n'aura pas d'effet.
- Mettre à jour "message" via la méthode updateMessage. La propriété messageProperty() peut être liée à un Label de status quelque part.
- Mettre à jour la progression via la méthode updateProgress. Vous pouvez également lier la propriété de progrès à une ProgressBar (c'est le but en fait).
- updateProgress(current, max) prend deux arguments (des double) la valeur actuelle et la value max (le 100% quoi).
Pour gérer l'exécution de ltâche en dehors vous avez bien entendu des propriétés pour connaître si elle est terminée, en cours, en echec etc.
Il y a également les évènements:
task.setOnSucceeded(e -> {
// Code en cas de succès
});
task.setOnFailed(e -> {
// Code en cas d'échec (peut être fixé manuellement)
// Ou d'exception
});
En cas d'erreur il y a moyen de récupérer l'Exception (getException()), il y a même une propriété pour ça.
La classe Service
La classe Service est un générateur de Task, qui enrobe de base les méthodes nécessaires à créer un Thread et exécuter la tâche (on peut utiliser monSuperService.start()).
La classe expose d'autres méthodes de contrôle du thread comme restart() (qui en gros utilise setCancel(true) sur la Task interne, en recrée une et la démarre) ou encore reset() qui avorte et recrée.
Il s'agit en quelque sorte d'un contexte d'exécution pour Task avec des fonctionnalités simples qui sont déjà écrites pour vous et s'assure qu'une seule tâche s'effectue à la fois puisque appeler start() sur un service dont la Task est déjà en cours d'exécution n'a aucun effet.
Créer un service consiste à déclarer une classe qui hérite de Service, puis surcharger la méthode createTask() dans laquelle on peut tout à fait créer la Task de manière anonyme, comme c'est montré dans la documentation officielle:
@Override
protected Task<String> createTask() {
return new Task<String>() {
@Override
protected String call() throws Exception {
return "Salut";
}
}
};
Task et Service implémentent tous les deux l'interface Worker, qui est en fait l'endroit où sont définis toutes les méthodes et propriétés sur l'avancement, si la tâche est annulée, etc. qui étaient présentés plus haut dans la section sur Task, ils sont également utilisables sur Service. Cela inclus bien entendu les évènements avec les méthodes à appeler en cas de succès ou échec de la Task dont le Service est responsable.
Quand utiliser Service? Si vous voulez une présentation de code plus propre qu'utiliser toute une bande de définitions de Task anonymes dans votre contrôleur et qu'en plus ça a du sens de pouvoir démarrer, arrêter ou redémarrer une opération en arrière-plan mais qui ne doit jamais s'exécuter plusieurs fois en parallèle (un seul Thread à la fois). Reportez-vous à votre "Stratégie de threading" (voir plus haut) pour savoir si un Service est utile pour vous.
Attention Service doit être typé, comme ceci par exemple:
class MonService extends Service<String> {
// La Task devra être typée de la même manière.
}
La classe ScheduledService
La classe ScheduledService est une extension de la classe Service pour répondre au cas d'utilisation classique d'une opération d'arrière plan qui se répète en permanence.
En Java c'est quelque chose qui peut être réalisé avec un while(true) ou un while(!isCancel()) dans la méthode exécutée par le Thread, accompagné générallement d'un genre de pause quelque part (par ex. Thread.sleep()) parce que sinon votre process va utiliser un max de CPU (ce qui est parfois désiré, mais en général pas trop).
JavaFX propose une classe toute faite pour ça sans avoir à utiliser de boucle et de temps d'attente etc.
Quand on démarre un ScheduledService (avec start() comme pour Service), il crée une Task par le biais de la méthode à surcharger createTask, comme un service quoi. Sauf que quand la tâche est terminée il en démarre une autre et continue comme ça à l'infini à moins d'arrêter le service.
Prenons comme cas d'étude ce Service totalement inutile:
public class TestService extends ScheduledService<String> {
private IntegerProperty count = new SimpleIntegerProperty(0);
@Override
protected Task<String> createTask() {
return new Task<String>() {
@Override
protected String call() throws Exception {
count.set(count.get() + 1);
return Integer.toString(count.get());
}
};
}
public int getCount() {
return count.get();
}
public void setCount(int count) {
this.count.set(count);
}
public IntegerProperty countProperty() {
return this.count;
}
}
J'ai typé mon ScheduledService comme String, je lui ai ajouté une propriété pour le fun (qui aurait pu être un int normal). La tâche sous-jacente incrémente cette propriété puis retourne la valeur.
Pour mettre à jour notre interface par rapport à ce que fait le service, toujours avec mon Label et mon Textfield, j'ai écrit ceci dans la méthode initialize() de mon contrôleur:
TestService test = new TestService();
test.setPeriod(Duration.seconds(2.0));
test.start();
this.textField1.textProperty().bind(test.countProperty().asString());
this.label1.textProperty().bind(test.lastValueProperty());
Remarquez le setPeriod() qui prend une valeur spéciale qui peut être récupérée depuis les méthodes et propriétés statiques de la classe Duration. Il s'agit de l'attente entre les différentes créations de tâches.
Il est possible de mettre un délai avant le démarrage de la tâche avec setDelay().
Pour une raison obscure, utiliser un binding sur valueProperty() du ScheduledService n'a aucun effet. Ce que vous voulez bind c'est lastValueProperty() comme dans l'exemple.
Pour le fun j'ai aussi bind mon IntegerProperty interne, les deux textes (Label et TextField) sont mis-à-jour en même temps avec la même valeur.
Doc officielle
Pour des infos un peu différentes mais similaires sur le sujet du threading en JavaFX, je vous invite à consulter la doc officielle qui présente plusieurs exemples.
Aussi intéressant, la page Javadoc de la class Task présente beaucoup d'exemples dans son intro.
Communiquer des valeurs d'un contrôleur à un autre
Le plus simple en JavaFX c'est d'associer un contrôleur différent à chaque vue (entendez fichier FXML), qui injecte les composants d'interface qu'il requiert, ainsi que leurs méthodes d'évènements.
Créer une nouvelle fenêtre ou changer de Scene dans une fenêtre existante va instancier ce contrôleur. Mais à priori nous n'avons aucun moyen de communiquer directement des valeurs à ce contrôleur et donc à la nouvelle fenêtre (ou Scene).
La solution consiste à récupérer l'intance du contrôleur une fois qu'il a été initialisé.
Pour ce faire, il s'agit juste de conserver une référence à l'instance FXMLLoader qui est utilisée:
FXMLLoader loader = new FXMLLoader(getClass()getResource("/package/MaSuperVue.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
// Dans cet exemple on créer une nouvelle fenêtre:
Stage stage = new Stage();
MaSuperVueController controller = loader.getController();
controller.SetSuperPropriete("une valeur");
stage.show();
Ici j'ai récupéré directement le contrôleur correspondant au fichier FXML avec son type final (MaSuperVueController en l'occurence) et j'utilise une de ses méthodes pour lui assigner une valeur à une propriété interne.
C'est à ce moment-ci qu'il convient de placer ses bindings avec l'autre contrôleur. Vous pouvez ainsi coupler des éléments qui ne sont pas sur la même fenêtre / Scene et dès lors ne font pas partie du même contrôleur.
Une autre manière de travailler consisterait à conserver des références à des éléments à partager entre différents contrôleurs dans des propriétés statiques. Par exemple dans un Singleton ou dans la classe qui hérite de Application et qui démarre l'application JavaFX.
J'utilise personnellement très souvent un Singleton comme "contexte" dans lequel je sauve la configuration de mon application par exemple. Attention ce type de pratique rend votre code de contrôleur encore moins réutilisable. Mais franchement le code contrôleur est selon moi de base non-réutilisable.
Note sur le support de JavaFX
Bien qu'il existe quelques projets actifs sur javaFX (dont des composants Material Design par exemple) le projet est dans un genre de limbo.
JavaFX n'est pas encore supporté nativement par des JVM comme OpenJDK qui est utilisé par défaut sur la plupart des environnements Linux. Il y a un paquet "OpenJFX" mais arriver à le faire fonctionner correctement avec vos applications n'est pas forcément évident.
Le mieux reste encore d'installer la JVM officielle Oracle qui elle supporte JavaFX nativement.
Tout ça pour dire que si votre projet doit avoir un max de compatibilité, JavaFX n'est peut-être pas un bon choix.
C'est tout?
Je comptais évoquer d'autres "astuces" mais cet article est trop long, comme mon chat. Je l'ai déjà faite celle là je pense.















Commentaires
Il faut JavaScript activé pour écrire des commentaires ici
#1
#2