Cours IHM-1 JavaFX
Transcription
Cours IHM-1 JavaFX
Java GUI – Bref historique [2] § Une refonte importante du toolkit a pris en compte les critiques formulées et a conduit à une nouvelle mouture : JavaFX 2 Cours IHM-1 JavaFX § Caractéristiques principales : • Abandon du langage de script JavaFX Script • Choix de deux modes : interfaces basées sur du code Java (API) et/ou sur un langage descriptif utilisant une syntaxe XML : FXML 1 - Introduction Concepts de base • Création d'un outil interactif Scene Builder pour créer graphiquement des interfaces et générer automatiquement du code FXML • Utilisation possible de feuilles de styles CSS pour adapter la présentation sans toucher au code (créer des thèmes, des skins, etc.) • Application du modèle de conception (design pattern) Builder avec un chaînage de méthodes (Fluent API) Jacques BAPST [email protected] IHM-1 – FX01 – Jacques BAPST Java GUI – Bref historique [1] Java GUI – Bref historique [3] § A l'origine du langage Java, les interfaces graphiques étaient créées en utilisant la librairie AWT (java.awt) § Avec la sortie de Java 8, une nouvelle version baptisée JavaFX 8 a été développée : • Composants "lourds" (heavyweight) basés sur ceux de la machine cible • Difficulté de créer des applications multiplateformes (write once, run anywhere), lourdeur § Rapidement, la librairie Swing (javax.swing) est venu compléter (et partiellement remplacer) la librairie AWT • Composant "légers" (lightweight) dessinés par la librairie • Pluggable Look&Feel ð ð § JavaFX 1 a tenté – sans grand succès – de remplacer Swing – Jacques BAPST ð Plus de librairie externe à télécharger et à référencer • Scene Builder 2 : nouvelle version de l'outil d'édition graphique de GUI (FXML) • Cohabitation améliorée avec les composants Swing • Prise en compte des nouveaux concepts introduits en Java 8 et notamment les expressions lambda et les streams • • • • Abandon des Builders introduits dans la version 2 (deprecated) Ajout d'une nouvelle API pour gérer l'impression Ajout de nouveaux composants riches (DatePicker, TreeTableView, …) Gestion des écrans tactiles (TouchEvent, GestureEvent, …) Amélioration des librairies graphiques 2D et 3D • Ajout d'un outil de packaging pour simplifier le déploiement des applications • JavaFX devient le standard officiel pour le développement des interfaces des applications Java • Essentiellement basé sur un (nouveau) langage de script (JavaFX Script) • Vaine tentative pour concurrencer Flex (basé sur Flash et MXML) IHM-1 – FX01 • Intégration dans la distribution de la plateforme standard Java (JDK, JRE) ð L&F multiplateforme ("Metal") L&F imitant (plus ou moins bien) ceux des OS spécifiques (Windows, OS X, …) 3 2 IHM-1 – FX01 – Jacques BAPST 4 Java GUI – Bref historique [4] Potentiel de JavaFX [2] § Chronologie des principales étapes : § La possibilité de découpler le design graphique (grâce à l'outil Scene Builder et à FXML) permet de déléguer la conception graphique de l'interface à un spécialiste (UI designer) qui n'a pas l'obligation de connaître et maîtriser le langage Java et ses librairies (API). § L'application possible de feuilles de style CSS renforce encore cette séparation entre le design graphique et les traitements qui seront effectués à l'aide de code Java. § Différents composants complexes sont disponibles et permettent, avec un minimum d'effort, de créer des applications riches : • • • • IHM-1 – FX01 – Jacques BAPST 5 Effets visuels (ombrages, transitions, animations, …) Graphiques 2D (charts) Navigateur web (WebKit) Images, audio, vidéo (media player) IHM-1 – FX01 – Jacques BAPST 7 Potentiel de JavaFX [1] Potentiel de JavaFX [3] § JavaFX étant le résultat de développements récents, il bénéficie de concepts modernes qui en font un framework intéressant pour la réalisation d'applications dans des domaines très divers. § Actuellement, de nombreuses applications sont des applications web, basées sur les technologies HTML5 + CSS + JavaScript (avec un grand nombre de frameworks disponibles) ou sur Flash/Flex ou Silverlight (les deux derniers cités étant clairement en perte de vitesse). § JavaFX est très bien doté pour développer des interfaces riches en relation avec des données stockées dans des bases de données ou accessibles au travers de serveurs d'informations. § Une comparaison détaillée entre ces technologies web et JavaFX sort du cadre de ce cours mais plusieurs billets de blog et documents décrivent les caractéristiques ainsi que les avantages et inconvénients de ces différentes approches technologiques, par exemple : § Sa riche librairie graphique 2D et 3D lui donne également un intéressant potentiel dans des domaines variés : • • • • • • • Introduction du livre Mastering JavaFX 8 Controls, Hendrik Ebbers, Oracle Press, 2014 • Blog Code Makery : code.makery.ch/blog/javafx-vs-html5 Représentations graphiques Animations graphiques Modélisation (CAD, …) Applications multimédia Réalité virtuelle et augmentée Jeux IHM-1 – FX01 – Jacques BAPST 6 IHM-1 – FX01 – Jacques BAPST 8 I/F déclaratives vs procédurales [1] Déploiement [1] § La plateforme JavaFX offre deux techniques complémentaires pour créer les interfaces (I/F) graphiques des applications : § Une application JavaFX peut être déployée (mise à disposition des utilisateurs) de différentes manières : • Manière déclarative • Installée localement comme une application autonome (standalone/desktop application) En décrivant l'interface dans un fichier FXML (syntaxe XML) ð L'utilitaire graphique Scene Builder facilite la création et la gestion des fichiers FXML ð L'interface peut être créée par un designer (sans connaissance Java, ou presque...) ð Séparation entre présentation et logique de l'application ( MVC ) ð ð ð • Installée sur un serveur et intégrée dans une page web • Manière procédurale Lancée depuis un navigateur, en cliquant sur un lien ou sur un autre élément actif de la page ð Lancée automatiquement dès qu'une page est chargée ð Utilise la technique Java Web Start (fichier JNLP + descripteur de déploiement XML) ð Une fois téléchargée, l'application peut également être lancée horsconnexion (mise en cache local) ð Utilisation d'API pour construire l'interface avec du code Java ð Création et manipulation dynamique des interfaces ð Création d'extensions et variantes (par héritage) ð Homogénéité des sources de l'application ð § Il est possible de mélanger les deux techniques au sein d'une même application (l'API javafx.fxml permet de faire le lien entre les deux). IHM-1 – FX01 Semblable à une application native La machine virtuelle Java peut être intégrée ou non dans l'exécutable (.exe ou .jar) – Jacques BAPST 9 IHM-1 – FX01 – Jacques BAPST 11 I/F déclaratives vs procédurales [2] Déploiement [2] § Technique déclarative. § Différents efforts de développement sont en cours pour permettre de déployer des applications JavaFX sur des terminaux mobiles (smartphones et tablettes Android, iOS, …) ainsi que sur des systèmes embarqués (Raspberry Pi, …). <?xml version="1.0" encoding="UTF-8"?> <?import <?import <?import <?import <?import Résultat javafx.geometry.*?> javafx.scene.text.*?> javafx.scene.control.*?> java.lang.*?> javafx.scene.layout.*?> <BorderPane maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="100.0" prefWidth="400.0" …> <top> <Label id="title" fx:id="title" text="Titre" textFill="#0e17c2" …> <font> Fichier <Font name="SansSerif Bold" size="20.0" /> FXML </font> </Label> </top> <bottom> <Button fx:id="btnHello" mnemonicParsing="false" onAction="#handleButtonAction"…/> </bottom> <padding> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> </padding> </BorderPane> § La communauté javafxports.org (gluonhq.com) est notamment active dans ce domaine assez prometteur qui permettrait, à partir d'un code unique, de générer de réelles applications multiplateformes (standalone et mobiles) comportant des interfaces riches. • Affaire à suivre… Scene Builder IHM-1 – FX01 – Jacques BAPST 10 IHM-1 – FX01 – Jacques BAPST 12 Projets connexes Documentation [2] § JavaFX devrait, à terme, être totalement publié en open-source (ce n'est que partiellement le cas) dans le cadre du projet OpenJFX. § Quelques livres : § De nombreux projets contribuent à enrichir l'écosystème JavaFX. § Parmi les principaux (et les plus dynamiques) on peut mentionner : • ControlsFX : Projet open-source destiné à offrir des composants supplémentaires (controls) de qualité fxexperience.com/controlsfx • JFXtras • DataFX • TestFX : Projet open-source destiné à fournir aux développeurs des éléments utiles dans leur vie de tous les jours et qui manquent dans la version de base de JavaFX jfxtras.org : Projet open-source destiné à faciliter la collaboration entre une application JavaFX et un système de gestion des données (DB, …) javafxdata.org : Librairie pour automatiser le test des applications JavaFX github.com/TestFX IHM-1 – FX01 13 – Jacques BAPST • Learn JavaFX 8 - Building User Experience and Interfaces with Java 8 Kishori Sharan Apress, 2015 ISBN: 978-1484211434 • Pro JavaFX 8 - A Definitive Guide to Building Desktop, Mobile, and Embedded Java Clients James Weaver, Weiqi Gao, Stephen Chin, Dean Iverson, Johan Vos, Adrian Chin Apress, 2014 ISBN: 978-1430265740 • JavaFX 8 - Introduction by Example Carl Dea et Mark Heckler Apress, 2014 ISBN: 978-1430264606 • Mastering JavaFX 8 Controls Hendrik Ebbers McGraw-Hill Professional - Oracle Press, 2014 ISBN: 978-0071833776 IHM-1 – FX01 – Jacques BAPST 15 Documentation [1] Programmation Java § Quelques références web utiles (à mettre dans vos bookmarks) : § Avant d'aborder les concepts principaux de JavaFX il est nécessaire de présenter quelques éléments de programmation Java qui sont utiles voire nécessaires pour développer des applications avec des interfaces graphiques. • Tutoriel officiel Oracle docs.oracle.com/javase/8/javase-clienttechnologies.htm • FX-Experience : Blog géré par des experts du domaine (news, demos, …) • Autre blog dédié à différentes thématiques JavaFX fxexperience.com guigarage.com • Communauté des développeurs du projet open-source OpenFJX (qui est un sous-projet de OpenJDK) javafxcommunity.com • API JavaFX (Javadoc) docs.oracle.com/javase/8/javafx/api Attention : On trouve, sur le web, encore passablement de documentation et de code JavaFX en version 1.x. Des changements majeurs sont intervenus dans les versions 2.x et 8.x. Tout ce qui date d'avant 2012 doit être considéré avec beaucoup de prudence et de circonspection. IHM-1 – FX01 – Jacques BAPST § Il s'agit notamment des notions suivantes : • • • • • • • Types énumérés (enum) Généricité (classes, interfaces et méthodes génériques) Structures de données prédéfinies (Collections) Annotations (@...) Expressions lambda Références de méthodes Streams § Ces notions seront sommairement présentées au chapitre suivant qui constitue une petite parenthèse technique de programmation Java, indispensable pour aborder la suite. 14 IHM-1 – FX01 – Jacques BAPST 16 Cours IHM-1 JavaFX Énumérations 2 - Compléments de programmation Jacques BAPST [email protected] IHM-1 – FX01 – Jacques BAPST Préambule Énumérations [1] § Ce chapitre introduit de manière sommaire quelques éléments de programmation qui, faute de temps, n'ont pas été vus durant le cours de programmation du premier semestre. § En Java, le mot-clé enum permet de définir un type énuméré. § Ces éléments sont cependant utiles, voire nécessaires dans le cadre des programmes Java / JavaFX comportant des interfaces graphiques (GUI). § La présentation de ces concepts se limite aux éléments essentiels qui sont nécessaires dans le cadre du cours IHM-1. § On ne peut donc pas prétendre maîtriser ces concepts après cette brève introduction. Elle devrait cependant suffire pour en comprendre les mécanismes principaux . – Jacques BAPST § L'utilisation de enum définit une classe spéciale (appelée enum type) qui hérite implicitement de java.lang.Enum et qui permet de représenter un ensemble fixe de constantes (des champs qui sont implicitement static et final) que l'on écrit donc en majuscules. § Une variable de type énuméré ne peut prendre qu'une des valeurs constantes définies pour le type considéré (ou null ). § Exemple de définition du type énuméré EColor : public enum EColor {RED, BLUE, GREEN, YELLOW} § EColor correspond au type énuméré. Les variables de ce type ne peuvent prendre que les valeurs des constantes RED, BLUE, GREEN, YELLOW ou la référence null. § Pour plus de détail, il faut consulter l'abondante documentation disponible (livres, web) sur ces sujets. IHM-1 – FX02 3 2 IHM-1 – FX02 – Jacques BAPST 4 Énumérations [2] Énumérations [4] § Exemple d'utilisation du type EColor : § Pour tous les types énumérés, une méthode statique values() est implicitement déclarée. public class TestEColor { § Elle retourne un tableau des constantes d'énumération dans leur ordre de déclaration. public static void main(String[] args) { EColor color = EColor.BLUE; if (args.length > 0) color = EColor.RED; System.out.println(color); } § Utile, par exemple, pour parcourir tous les éléments d'un type énuméré : public class TestEColor { } public static void main(String[] args) { for (EColor color : EColor.values()) { System.out.print(color + " "); } } § Le programme affichera BLUE (si aucun paramètre n'est passé au lancement) ou RED (si au moins un paramètre est transmis au lancement). } § Le programme affichera : RED BLUE GREEN YELLOW IHM-1 – FX02 – Jacques BAPST 5 IHM-1 – FX02 7 – Jacques BAPST Énumérations [3] Énumérations [5] § On peut s'épargner la notation préfixée (type.constante) en ajoutant une importation statique des éléments du type énuméré. § Pour tous les types énumérés, une méthode statique valueOf() est implicitement déclarée. § Exemple d'utilisation avec importation statique des constantes : § Cette méthode convertit une chaîne de caractères (passée en paramètre) en un élément du type énuméré ou lève l'exception IllegalArgumentException si la conversion n'est pas possible (attention, la méthode est sensible à la casse [minuscules/majuscules]). import static ex.chap02.EColor.*; public class TestEColor { public static void main(String[] args) { EColor color = BLUE; if (args.length > 0) color = RED; System.out.println(color); } § Exemple de conversion d'une chaîne de caractères en un élément de type énuméré : public class TestEColor { public static void main(String[] args) { EColor yColor = EColor.valueOf("YELLOW"); System.out.println(yColor); } EColor pColor = EColor.valueOf(args[0]); } } IHM-1 – FX02 – Jacques BAPST 6 IHM-1 – FX02 Quels risques ? – Jacques BAPST 8 Énumérations [6] Types génériques [1] § La méthode d'instance name() retourne un String correspondant à la représentation textuelle d'une variable de type énuméré (elle se comporte de la même manière que la méthode toString()). § Un type générique est une classe ou une interface qui prend en paramètre un type. • Exemple : § Prenons l'exemple d'une classe (non-générique) qui permet de stocker et restituer un objet de type Integer : String colName = yColor.name(); § La méthode ordinal() retourne un int correspondant à la position de l'élément dans la déclaration d'une variable de type énuméré (la numérotation commence à zéro). • Exemple : int pos = yColor.ordinal(); public class IntegerBox { private Integer anInt; public Integer get() { return anInt; } // 3 for YELLOW § Une énumération peut être définie à l'intérieur d'une classe ou dans une unité de compilation séparée (comme toute classe Java). § Une énumération ne peut pas être définie dans une méthode. } § Il est possible d'ajouter des champs, des constructeurs et des méthodes dans une classe de type enum. Consulter la § Que faudrait-il faire pour gérer de la même manière d'autres types d'éléments, par exemple Float, String, Point, Rectangle, ... ? documentation pour plus de détails. IHM-1 – FX02 – Jacques BAPST public void set(Integer value) { anInt = value; } 9 IHM-1 – FX02 11 – Jacques BAPST Types génériques [2] § Créer les variantes FloatBox, StringBox, ... ? § Créer une classe Box avec un champ de type Object ? § Une solution : créer une classe générique : Généricité Types génériques public class Box<T> { private T obj; public T get() { return obj; } Attention : le principe d'encapsulation n'est respecté que si T est immuable. public void set(T obj) { this.obj = obj; } } § La syntaxe className<type> permet de passer un type en paramètre à la classe. Dans l'exemple précédent T peut être considéré comme un paramètre formel qui représente un type (non-primitif) quelconque. IHM-1 – FX01 – Jacques BAPST 10 IHM-1 – FX02 – Jacques BAPST 12 Types génériques [3] Types génériques [5] § Lorsqu'on utilise la classe (comme type ou pour l'instancier), on passe, entre < et >, le type effectif souhaité : § On peut aussi utiliser un type générique comme paramètre effectif d'un type paramétré. • Par exemple pour créer une liste de "paires" : public static void main(String[] args) { Box<Integer> intBox = new Box<>(); // ou new Box<Integer>(); Box<String> strBox = new Box<>(); // ou new Box<String>(); Box<Point2D> p2dBox = new Box<>(); // ou new Box<Point2D>(); List< Pair<String, Integer> > pList = new ArrayList<>(); pList.add(new Pair<>("Small", 3)); intBox.set(5); strBox.set("Hello"); p2dBox.set(new Point2D(1.2, 3.4)); System.out.println(p2dBox.get()); § Pour assurer la compatibilité du code écrit avant le JDK 5.0, il est possible d'omettre le paramètre effectif et de déclarer ce que l'on nomme un raw type. } § Dans ce cas, le type du paramètre est implicitement Object, certaines limitations s'appliquent et des mises en garde peuvent apparaître à la compilation (unchecked conversions). § On peut passer plus d'un type générique à une classe ou à une interface en les séparant par une virgule : public class PairBox<T, U> { … } IHM-1 – FX02 – Jacques BAPST 13 Box b = new Box(); // Raw type b.set(8); b.set("Hello"); // Autoboxing int => Integer => Object // String => Object IHM-1 – FX02 15 – Jacques BAPST Types génériques [4] Types génériques [6] § Par convention, on utilise une seule lettre majuscule pour nommer les paramètres formels de type. § Pour définir un paramètre générique qui inclut une classe donnée ainsi que toutes ses sous-classes, on utilise la syntaxe : < T extends type > § La syntaxe <> (nommée diamond) peut être utilisée lorsque le compilateur est capable de déterminer (d'inférer) le type en fonction du contexte. § Les interfaces génériques se basent exactement sur les mêmes conventions que celles des classes génériques. § De nombreuses interfaces et classes de la plateforme Java (notamment les Collections) utilisent la généricité. List<Point2D> polygon = new ArrayList<>(); – Jacques BAPST public class ShapeBox<T extends Polygon> { private T shape; // Polygon or sub-class of Polygon . . . } § La formulation "extends type" doit être prise ici dans un sens général (extends or implements). Elle doit être interprétée comme : • La classe type ainsi que toutes ses sous-classes • Toute classe qui implémente l'interface type Pair<String, Integer> p1 = new Pair<>("Even", 8); Pair<String, String> p2 = new Pair<>("Hi", "World"); IHM-1 – FX02 § Par exemple : ou § Avec cette syntaxe, on indique ainsi la "borne supérieure" du type accepté comme paramètre (upper bound). 14 IHM-1 – FX02 – Jacques BAPST 16 Types génériques [9] § Le symbole '?' peut être utilisé comme caractère joker (wildcard) dans différentes situations en lien avec les classes et interfaces génériques : type d'un paramètre, d'un champ, d'une variable locale, etc. § Lors de l'introduction de la généricité dans le langage, la machine virtuelle n'a pas été modifiée. Une classe paramétrée est compilée en une classe non paramétrée. public static int countNonNull(List<?> list) { int count = 0; for (Object e : list) { if (e != null) count++; } return count; } < < < § Par exemple : Compléme nt § Il représente un type (inconnu) quelconque. > > > Types génériques [7] § Dans cet exemple, seules les méthodes de l'interface List ou de Object (sur les éléments de la liste) peuvent être utilisées. IHM-1 – FX02 public class Box<T> { . . . T temp = new T(); . . . // Erreur à la compilation } 17 – Jacques BAPST § Les variables de types sont donc perdues (oubliées) après la compilation (type erasure). Certaines opérations qui utilisent les types génériques à l'exécution (par exemple certains transtypages, création d'objets, utilisation de instanceof, …) ne sont donc pas autorisées et provoquent donc des erreurs (qui peuvent paraître un peu surprenantes) lors de la compilation. IHM-1 – FX02 – Jacques BAPST Types génériques [8] Types génériques [10] § Le caractère joker ? n'est pas équivalent à Object. § Le caractère joker '?' (unbounded wildcard) peut également être combiné avec les mots-clés extends ou super pour définir des bornes supérieures (upper bound) respectivement des bornes inférieures (lower bound) pour les types acceptés. : une liste d'éléments de n'importe quel type • List<Object> : une liste d'éléments de type Object > > > • List<?> List<Integer> n'est pas une sous-classe de List<Object> Object Integer IHM-1 – FX02 List<Object> X List<Integer> Object List<Object> – Jacques BAPST List<Integer> 18 < < < Compléme nt § Attention : Même si (par ex.) Integer est une sous-classe de Object, 19 • <? extends type> acceptera la classe type ainsi que toutes ses sous- classes. Cette notation est également valable pour toutes les classes qui implémentent l'interface type. [upper bounded] • <? super type> acceptera la classe type ainsi que toutes les classes parentes de type. [lower bounded] § Par exemple, pour ajouter des nombres à une liste d'Integer, de Number ou d'Object : public static void addOneToTen(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } IHM-1 – FX02 – Jacques BAPST 20 Types génériques [11] < < < Compléme nt > > > Quelques règles et recommandations § L'utilisation du caractère joker <? …> dépendra du rôle que joue le paramètre générique dans la méthode/fonction : • Paramètre 'in' (valeur utilisée en entrée de la méthode) [Typiquement la méthode consomme les éléments d'une collection] Annotations • Paramètre 'out' (valeur utilisée en sortie, fournie par la méthode) [Typiquement la méthode produit les éléments d'une collection] • Paramètre 'in/out' (valeur utilisée en entrée et modifié par la méthode) § Il est recommandé d'appliquer les règles suivantes : • Tous les paramètres 'in' seront définis avec extends (upper-bound) • Tous les paramètres 'out' seront définis avec super (lower-bound) • Pour les paramètres 'in' accédés exclusivement avec des méthodes de Object, on utilisera un joker non borné (unbounded-wildcard <?>) • Pour tous les paramètres 'in/out' on n'utilisera pas de joker • On n'utilisera pas de joker comme type de retour IHM-1 – FX02 – Jacques BAPST 21 IHM-1 – FX01 – Jacques BAPST 23 Types génériques [12] Annotations [1] § Dans une classe (générique ou non) il est possible d'écrire une méthode générique (statique ou non-statique). § En Java, une annotation constitue une métadonnée que l'on peut appliquer à différents éléments du code source. § Syntaxiquement, le ou les types génériques <…> se placent, dans l'en-tête, après les modificateurs et avant le type de retour. § Les annotations n'ont pas d'effet direct sur le fonctionnement des instructions annotées. § Exemple : § Le caractère '@' indique au compilateur que l'identificateur qui suit est une annotation. public static <T> boolean contains(Box<T> box, T value) { return box.get().equals(value); } § Un exemple simple : @Override public String toString() { //--- A String representation of current object return "Circle: Radius=" + radius; } § En général, le paramètre générique n'est pas nécessaire lors de l'invocation de la méthode, il est inféré par le contexte. Box.contains(fBox, 1.2f); Box.<Float>contains(fBox, 1.2f); IHM-1 – FX02 § L'annotation @Override indique au compilateur que la méthode redéfinit une méthode héritée. Il pourra ainsi générer une erreur si l'en-tête n'est pas correcte // Syntaxe rarement nécessaire – Jacques BAPST 22 IHM-1 – FX02 – Jacques BAPST 24 Annotations [2] Annotations [4] § Sans l'annotation @Override le code fonctionne parfaitement (et de manière identique) mais le programmeur ne serait pas averti qu'il crée une nouvelle méthode d'instance (faute de frappe par ex.) alors qu'il voulait redéfinir une méthode d'une des classes parentes. § Il existe un certain nombre d'annotations prédéfinies, traitées par le compilateur ou par certains outils standard de la plateforme Java. § Une annotation peut inclure un ou plusieurs paramètres : § Il est également possible de définir ses propres annotations en écrivant une interface particulière (@interface …) @Author (name = "Tryphon Tournesol", date = "06.06.2016" ) public class MyClass { . . . } // Metadata about the class import java.lang.annotation.Documented; @Documented // Will appear in Javadoc-generated documentation public @interface ClassPreamble { String author(); // Parameter String date(); int currentRevision() default 1; // Param. with default value String lastModifiedBy() default "N/A"; String[] reviewers(); // Array parameter } § Si l'annotation ne comporte qu'un seul paramètre dont le nom est value, le nom peut être omis : @SuppressWarnings("unchecked") public class MyClass { . . . // Warnings about unchecked generic operations are suppressed } IHM-1 – FX02 – Jacques BAPST • Exemples : @Deprecated, @Override, @SuppressWarnings, @SafeVarargs, @FunctionalInterface, … 25 IHM-1 – FX02 – Jacques BAPST 27 Annotations [3] § Des annotations multiples peuvent être attribuées à une même déclaration (une même annotation peut également être répétée). @Author (name = "Tryphon Tournesol") @Author (name = "Archibald Haddock") @EBook public class MyClass { . . . } // Repeating annotations // Repeating annotations Expressions Lambda Références de méthodes § Des annotations peuvent être appliquées à des déclarations (classes, champs, méthodes, etc.) mais également lors de l'utilisation des types (ces Type Annotations requièrent l'installation d'un Checker Framework, voir par exemple types.cs.washington.edu/checker-framework). public void check(String name) { @NonNull String loginName = name; . . . } IHM-1 – FX02 // Exception thrown if null – Jacques BAPST 26 IHM-1 – FX01 – Jacques BAPST 28 Expressions lambda [1] Expressions lambda [3] § Une expression lambda peut être assimilée à une fonction anonyme qui a potentiellement accès au contexte du code englobant. § Pour illustrer l'utilisation des expressions lambda, nous allons donner un premier exemple qui fait appel à l'interface fonctionnelle Runnable (prédéfinie dans le package java.lang) : § Il s'agit essentiellement d'un bloc de code avec des paramètres et qui est destiné à être exécuté ultérieurement (une ou plusieurs fois). § Une expression lambda peut être utilisée pour implémenter une interface fonctionnelle (interface ne comportant qu'une seule méthode d'instance abstraite). § La syntaxe de base est : ou (parameters) -> expression (parameters) -> { statements; } – Jacques BAPST § Les objets de type Runnable sont notamment utilisés pour exécuter du code en parallèle avec l'activité principale de l'application (par exemple imprimer en tâche de fond pendant qu'on continue à travailler sur l'application principale). Ces "processus légers" ou "fils d'exécution" (pseudo-)parallèles sont nommé "threads". § Remarque : Les parenthèses autour des paramètres sont obligatoires s'il y a zéro (burger arrow : () -> ) ou plusieurs paramètres, mais optionnelles s'il n'y en a qu'un seul et que le type peut être inféré (voir page suivante). IHM-1 – FX02 @FunctionalInterface public interface Runnable { public void run(); } § Par défaut le code Java s'exécute dans un thread appelé main-thread (créé automatiquement). Mais il est possible de créer d'autres threads qui permettent une exécution parallèle (ou pseudo-parallèle) du code. 29 IHM-1 – FX02 – Jacques BAPST 31 Expressions lambda [2] Expressions lambda [4] § S'il y a plusieurs paramètres, il seront séparés par des virgules. § Pour créer un thread il faut créer une instance de la classe Thread qui se chargera d'exécuter le code transmis sans interrompre l'exécution des instructions du thread en cours (exécution en parallèle avec elles). § Le type des paramètres peut être indiqué explicitement ou être automatiquement déterminé par le compilateur (inféré) en fonction du contexte. § Le type de retour est toujours inféré du contexte. § Concrètement, pour lancer du code dans un thread séparé, il faut § Exemples d'expressions lambda : (int x, int y) -> { return x + y; } // Types explicites, valeur de retour (x, y) -> x + y // Types inférés, valeur de retour x -> x * x // Type inféré, valeur de retour () -> 123 // Pas de paramètre, valeur de retour s -> System.out.println(s) // Type inféré, pas de valeur de retour () -> { for (int i=0; i<10; i++) doIt(); } // Pas de valeur de retour IHM-1 – FX02 § Le constructeur de la classe Thread peut recevoir en paramètre un objet de type Runnable dont la méthode run() déterminera les instructions à exécuter dans le thread parallèle. – Jacques BAPST • Créer une instance de la classe Thread en passant l'objet Runnable à son constructeur : Thread t = new Thread(myRunnable); • Appeler la méthode start() de l'instance de Thread : t.start(); qui lancera les instructions de la méthode run(). La méthode start() retourne immédiatement (elle n'attend pas la fin d'exécution de run()). Pour information, il y a une autre manière de faire : • Créer une sous-classe de Thread et redéfinir la méthode run(). Créer une instance et invoquer start(). 30 IHM-1 – FX02 – Jacques BAPST 32 Expressions lambda [5] Interfaces fonctionnelles [2] Quelques interfaces fonctionnelles prédéfinies § Exemple : création de deux threads à l'aide d'expressions lambda. public static void main(String[] args) { Runnable rA = () -> { for (int i=0; i<100; i++) System.out.println("A"+i); }; Runnable rB = () -> { for (int i=0; i<100; i++) System.out.println("B"+i); }; Thread tA = new Thread(rA); Thread tB = new Thread(rB); tA.start(); tB.start(); // Création du thread // Lancement du thread : méthode run() } Un des résultats possibles à l'exécution IHM-1 – FX02 – Jacques BAPST A0 B0 B1 B2 A1 A2 B3 B4 B5 A3 B6 B7 B8 A4 B9 A5 A6 A7 A8 A9 A10 A11 B10 ... 33 Interfaces fonctionnelles [1] Interface fonctionnelle Type Param Type Retour Méthode Description abstraite Runnable - void run() Exécute une action Supplier<T> - T get() Fournit une valeur de type T Consumer<T> T void accept() Consomme une valeur de type T chain void accept() Consomme des valeurs de type T et U chain R apply() Fonction avec un seul paramètre compose, andThen, identity BiFunction T, U <T, U, R> R apply() Fonction avec deux paramètres andThen UnaryOperator T <T> T apply() Opérateur unaire sur le type T compose, andThen, identity BinaryOperator T, T <T> T apply() Opérateur binaire sur le type T andThen BiConsumer T, U <T, U> Function <T, R> T IHM-1 – FX02 Interfaces fonctionnelles [3] Quelques interfaces fonctionnelles prédéfinies Interface fonctionnelle Type Param Type Retour § Deux exemples représentatifs (utilisés par la suite) : Predicate<T> T boolean test() Fonction booléenne (prédicat) boolean test() Fonction booléenne avec 2 paramètres // Supplier : Represents a supplier of results //--------------------------------------------------------------- BiPredicate T, U <T, U> @FunctionalInterface public interface Supplier<T> { T get(); // Gets a T type result } Méthode abstraite Description Autres Méthodes and, or, negate, isEqual and, or, negate § Il existe également un certain nombre d'interfaces fonctionnelles prédéfinies pour les types primitifs int, long et double. Exemples : // Function : Represents a function that accepts one argument // of type T and produces a result of type R //--------------------------------------------------------------- @FunctionalInterface public interface Function<T, R> { R apply(T t); // Applies this function to the given argument } – Jacques BAPST 35 – Jacques BAPST § Les interfaces fonctionnelles jouent un rôle important en lien avec les expressions lambda. Plusieurs de ces interfaces sont prédéfinies (notamment dans le package java.util.function). IHM-1 – FX02 Autres Méthodes 34 Interface fonctionnelle Type Param Type Retour Méthode abstraite Description IntFunction<T> int T apply() Fonction avec paramètre int LongPredicate long boolean test() Fonction booléenne avec paramètre long and, or, negate ... ... ... . . . et beaucoup d'autres . . . ... IHM-1 – FX02 ... – Jacques BAPST Autres Méthodes 36 Références de méthodes [1] Références de méthodes [3] § Une méthode existante peut être utilisée en tant qu'implémentation de la méthode abstraite d'une interface fonctionnelle. § Dans la troisième variante (Class::instanceMethodName), même si on mentionne le nom de la classe en préfixe, c'est bien une méthode d'instance qui sera invoquée. Le premier paramètre de l'expression lambda représente l'objet sur lequel la méthode sera invoquée. § On définit pour cela une référence de méthode avec l'opérateur :: § Trois variantes existent : § Exemple variante 3 : Class :: staticMethodName instance :: instanceMethodName Class :: instanceMethodName Function<String, Integer> fLen = String::length; String s1 = "Hello"; String s2 = "World !"; § Exemples avec équivalences en expressions lambda : int s1Len = fLen.apply(s1); int s2Len = fLen.apply(s2); // s1.length() // s2.length() Variante Exemple Lambda équivalent Class::staticMethodName String::valueOf (s) -> String.valueOf(s) instance::instanceMethodName obj::toString () -> obj.toString() Class::instanceMethodName String::toString (s) -> s.toString() § Les références de méthodes représentent donc une alternative aux expressions lambda dans le cas où il n'y a qu'une seule et unique méthode à exécuter. Class::instanceMethodName String::charAt (s, i) -> s.charAt(i) § Cela améliore généralement la lisibilité et la clarté du code. IHM-1 – FX02 37 – Jacques BAPST IHM-1 – FX02 – Jacques BAPST 39 Références de méthodes [2] Références de constructeurs [1] § Dans les deux premières variantes de syntaxe (Class::staticMethodName et instance::instanceMethodName), la référence de méthode est équivalente à une expression lambda qui fournit les paramètres de la méthode. § Les références de constructeurs sont semblables aux références de méthodes à la différence que le nom de la "méthode" est new. § Exemple variante 1 : Supplier<Double> random = Math::random; double result = random.get(); // Math.random(); § Exemple variante 2 : Random r = new Random(); Supplier<Double> random = r::nextDouble; double result2 = random.get(); // r.nextDouble(); § Syntaxe : Invocation sur l'instance r – Jacques BAPST Class :: new § Exemples avec équivalences en expressions lambda : Ces deux exemples utilisent l'interface fonctionnelle prédéfinie Supplier<T> décrite dans les pages précédentes. IHM-1 – FX02 § Les références de constructeurs servent à instancier une interface fonctionnelle avec un constructeur compatible avec le contexte d'utilisation. 38 Syntaxe Exemple Lambda équivalent Class::new String::new () -> new String() Class::new PointHolder::new (p) -> new PointHolder(p) IHM-1 – FX02 – Jacques BAPST 40 Références de constructeurs [2] Accès aux variables locales [2] § Exemple d'utilisation de références de constructeurs pour créer des objets de type Button (un des composants classiques de JavaFX) . § Dans l'exemple qui suit, les variables text et count ne sont pas définies dans l'expression lambda. Ce sont des paramètres de la méthode englobante capturés par l'expression lambda (variables locales effectivement finales). • Un constructeur de la classe Button permet de passer le libellé du bouton en paramètre. public static void repeatMessage(String text, int count) { Runnable r = () -> { for (int i=0; i<count; i++) { System.out.println(text); Thread.yield(); // Gives a chance other threads to run } }; new Thread(r).start(); } Function<String, Button> btFactory = Button::new; Button bOk = btFactory.apply("Ok"); Button bQuit = btFactory.apply("Quit"); // new Button("Ok"); // new Button("Quit"); § Ces variables restent accessibles malgré l'exécution différée du code. public static void main(String[] args) { repeatMessage("Hello", 500); repeatMessage("Jane", 1000); } IHM-1 – FX02 – Jacques BAPST 41 IHM-1 – FX02 – Jacques BAPST 43 Accès aux variables locales [1] § En plus des éventuels paramètres définis, des variables déclarées dans son corps et des champs statiques, une expression lambda à accès à toutes les variables locales qui sont effectivement finales. § Une variable effectivement finale est une variable locale : • déclarée avec le modificateur final ou • non modifiée après sa première affectation Stream § Une expression lambda qui utilise (capture) des variables locales externes est parfois désignée par le terme de capturing lambda. § On utilise également le terme closure pour désigner le bloc de code de l'expression lambda accompagné de l'ensemble des variables locales capturées. IHM-1 – FX02 – Jacques BAPST 42 IHM-1 – FX01 – Jacques BAPST 44 Stream API [1] Stream API [3] § Un stream représente une séquence d'éléments. Cette structure supporte différents types d'opérations sur ces éléments. § Les opérations appliquées à un stream peuvent être : § Les streams constituent un concept important en lien avec les expressions lambda qui sont souvent utilisées pour les manipuler. § L'essentiel des librairies (API) liées aux streams se trouve dans le package java.util.stream avec notamment l'interface Stream<T> § Il est possible de créer un stream à partir d'un tableau, d'une collection, d'un générateur (Stream.generate()), d'un itérateur (Stream.iterate()), d'un canal d'entrée/sortie, etc. • D'un stream IHM-1 – FX02 : Arrays.stream(array) § Après avoir exécuté une opération terminale, un stream est fermé et ne peut plus être réutilisé (si on veut le faire, il faut en créer un nouveau). collection.stream() Stream.of(v1, v2, …) stream.filter(predicate) 45 – Jacques BAPST Stream API [2] § Un exemple d'utilisation à partir d'une liste de String sur laquelle on applique une série d'opérations : public static void main(String[] args) { List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "d1", "c1", "e2"); myList.stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println); Operation pipeline } § A l'exécution, ce programme affichera : C1 C2 § Une expression lambda et deux références de méthodes sont utilisées dans cet exemple pour définir certaines des opérations effectuées sur le flux. IHM-1 – FX02 – Jacques BAPST § Les opérations intermédiaires ne sont exécutées que s'il y a une opération terminale (laziness). § L'ordre dans lequel les opérations sont enchaînées peut jouer un rôle important sur le résultat et sur le nombre d'opérations à effectuer par le système (typiquement, un filtrage est à effectuer le plus tôt possible dans la chaîne des opérations). § Quelques manières classiques de créer un stream à partir : • D'un tableau : • D'une collection : • D'une séquence de valeurs : • Intermédiaires : elles retournent un stream ce qui permet un enchaînement d'opérations (pipeline, fluent API) • Terminales : elles ne retournent rien (effectuent une opération) ou retournent autre chose qu'un stream 46 IHM-1 – FX02 – Jacques BAPST 47 Première application [1] § Comment se présente une application JavaFX ? Cours IHM-1 JavaFX (petite immersion avant de décrire plus en détail les concepts de base). § L'application est codée en créant une sous-classe de Application. § La fenêtre principale d'une application est représentée par un objet de type Stage qui est fourni par le système au lancement de l'application. 3 - Architecture Concepts techniques § L'interface est représentée par un objet de type Scene qu'il faut créer et associer à la fenêtre (Stage). § La scène est composée des différents éléments de l'interface graphique (composants de l'interface graphique) qui sont des objets de type Node. Jacques BAPST [email protected] § La méthode start() construit le tout. IHM-1 – FX03 – Jacques BAPST 3 Métaphore de la salle de spectacle § Les éléments structurels principaux d'une application JavaFX se basent sur la métaphore de la salle de spectacle (theater). Remarque : En français, on utilise le terme 'scène' pour parler de l'endroit où se passe le spectacle (l'estrade, les planches) mais également pour parler de ce qui s'y déroule (jouer ou tourner une scène) ce qui peut conduire à un peu de confusion avec cette métaphore. Une première immersion ( Hello World ! ) § Stage : L'endroit où a lieu l'action, où se déroule la scène § Scene : Tableau ou séquence faisant intervenir les acteurs § Controls : Acteurs, figurants, éléments du Components décor, … (éléments actifs/passifs) Nodes qui font partie de la scène en Widgets train d'être jouée. IHM-1 – FX03 – Jacques BAPST 2 IHM-1 – FX03 – Jacques BAPST 4 Hello World [1] Cycle de vie d'une application [1] § Une première application, le traditionnel Hello World ! § Le point d'entrée d'une application JavaFX est constitué d'une instance de la classe Application (généralement une sous-classe). public class HelloWorld extends Application { //--------------------------------------------@Override public void start(Stage primaryStage) { primaryStage.setTitle("My First JavaFX App"); BorderPane root = new BorderPane(); Button btnHello = new Button("Hello World"); root.setCenter(btnHello); Scene scene = new Scene(root, 250, 100); primaryStage.setScene(scene); primaryStage.show(); } § Lors du lancement d'une application par la méthode statique Application.launch() le runtime JavaFX effectue les opérations suivantes : 1. Crée une instance de la classe qui hérite de Application 2. Appelle la méthode init() 3. Appelle la méthode start() et lui passe en paramètre une instance de Stage (qui représente la fenêtre principale [primary stage]) 4. Attend ensuite que l'application se termine; cela se produit lorsque : ü //--------------------------------------------public static void main(String[] args) { launch(args); } ü La dernière fenêtre de l'application a été fermée (et Platform.isImplicitExit() retourne true) L'application appelle Platform.exit() (ne pas utiliser System.Exit()) 5. Appelle la méthode stop() } launch() IHM-1 – FX03 5 – Jacques BAPST IHM-1 – FX03 init() start() stop() – Jacques BAPST Hello World [2] Cycle de vie d'une application [2] § JavaFX étant intégré à la plateforme de base Java, aucune librairie externe n'est nécessaire au fonctionnement de l'exemple précédent. § La méthode launch() est généralement lancée depuis la méthode main(). Elle est implicitement lancée s'il n'y a pas de méthode main() (ce qui est toléré depuis Java 8). § Un certain nombre d'importations doivent cependant être effectuées (on y retrouve les classes principales Application, Stage, § D'éventuels paramètres de lancement peuvent être récupérés en invoquant la méthode getParameters() dans la méthode init() ou ultérieurement (par ex dans la méthode start()). Scene ainsi que les composants BorderPane et Button) : • • • • • import import import import import javafx.application.Application; javafx.scene.Scene; javafx.scene.control.Button; javafx.scene.layout.BorderPane; javafx.stage.Stage; En principe, Eclipse s'en charge automatiquement § Au lancement d'une application JavaFX, il est possible de passer des paramètres nommés (syntaxe --name=value) ou anonymes. • java -jar Hello.jar --steps=50 --mode=Debug 300 FR § Dans cet exemple, la méthode main() lance uniquement la méthode statique launch() (qui est héritée de Application). • Dans une application un peu plus complexe, elle pourrait effectuer d'autres opérations d'initialisation avant l'invoquer launch(). IHM-1 – FX03 – Jacques BAPST 7 § getParameters() retourne un objet de type Parameters dont on peut extraire les valeurs des paramètres à l'aide des méthodes : • getRaw() List<String> Liste brute de tous les paramètres • getNamed() Map<String, String> Liste des paramètres nommés • getUnnamed() List<String> Liste des paramètres anonymes 6 IHM-1 – FX03 – Jacques BAPST 8 Cycle de vie d'une application [3] § La méthode start() est abstraite et doit être redéfinie. § Les méthodes init() et stop() ne doivent pas obligatoirement être redéfinies (par défaut elle ne font rien). § La méthode start() s'exécute dans le JavaFX Application Thread. C'est dans ce thread que doit être construite l'interface (notamment la création de l'objet Scene) et que doivent être exécutées toutes les opérations qui agissent sur des composants attachés à une scène placée dans une fenêtre (live components). Propriétés § La méthode stop() s'exécute aussi dans le JavaFX Application Thread. § La méthode init() ainsi que le constructeur de la classe qui étend Application s'exécutent par contre dans le thread JavaFX Launcher. • Dans ce code, il est possible de créer des composants et des conteneurs mais on ne peut pas les placer dans une scène active (live scene). IHM-1 – FX03 – Jacques BAPST 9 IHM-1 – FX03 – Jacques BAPST Traiter une action de l'utilisateur Notion de propriété [1] § Dans l'exemple Hello World, pour que le clic sur le bouton déclenche une action, il faut traiter l'événement associé. § La notion de "propriété" (property) est très présente dans JavaFX. § La méthode setOnAction() du bouton permet d'enregistrer un Event Handler (c'est une interface fonctionnelle possédant la méthode handle(event) qui définit l'action à effectuer). btnHello.setOnAction( event -> System.out.println("Hello World !")); § En plus des méthodes get…() et set…(), les propriétés JavaFX possèdent une troisième méthode …Property() qui retourne un objet qui implémente l'interface Property [ … : nom de la propriété ]. § Intérêt des propriétés : • Elles peuvent être liées entre-elles (Binding), c-à-d que le changement d'une propriété entraîne automatiquement la mise à jour d'une autre. • Elles peuvent déclencher un événement lorsque leur valeur change et un gestionnaire d'événement (Listener) peut réagir en conséquence. root.setCenter(btnHello); Scene scene = new Scene(root, 250, 100); primaryStage.setScene(scene); primaryStage.show(); } – Jacques BAPST § Une propriété est un élément d'une classe que l'on peut manipuler à l'aide de getters (lecture) et de setters (écriture). § Les propriétés sont généralement représentées par des attributs de la classe mais elles pourraient aussi être stockées dans une base de données ou autre système d'information. public void start(Stage primaryStage) { primaryStage.setTitle("My First JavaFX App"); BorderPane root = new BorderPane(); Button btnHello = new Button("Say Hello"); IHM-1 – FX03 11 10 IHM-1 – FX03 – Jacques BAPST 12 Notion de propriété [2] Notion de propriété [4] § Exemple d'une classe définissant la propriété balance (solde) pour un compte bancaire. public class BankAccount { private DoubleProperty balance = new SimpleDoubleProperty(); public final double getBalance() { return balance.get(); } public final void setBalance(double amount) { balance.set(amount); } public final DoubleProperty balanceProperty() { return balance; } Par convention, les getters et les setters sont déclarés final. Interfaces principales associées aux propriétés. . . . } IHM-1 – FX03 – Jacques BAPST 13 IHM-1 – FX03 15 – Jacques BAPST Notion de propriété [3] Propriétés read-only [1] § La classe abstraite DoubleProperty permet d'emballer une valeur de type double et d'offrir des méthodes pour consulter et modifier la valeur mais également pour "observer" et "lier" les changements. § Il existe des propriétés qui sont en lecture seule (read-only property). § SimpleDoubleProperty est une classe concrète prédéfinie. § La plateforme Java offre des classes similaires pour la plupart des types primitifs, les chaînes de caractères, certaines collections ainsi que le type Object qui peut couvrir tous les autres types. / / • ListProperty<E> / • ObjectProperty<T> / • IntegerProperty SimpleIntegerProperty • StringProperty SimpleStringProperty SimpleListProperty<E> § La création de telles propriétés nécessite l'utilisation de classes wrappers qui servent à isoler la partie de la propriété en lecture seule (visible de l'extérieur) de la partie qui permet de modifier la valeur de la propriété, ce qui doit être possible uniquement dans le composant (bean) qui définit cette propriété. § A l'interieur du wrapper, la propriété en lecture seule est en réalité liée (bound) à une autre propriété interne qui est, elle, en lecture et écriture. Visible à l'extérieur SimpleObjectProperty<T> • ... § Des classes existent également pour définir des propriétés read-only Intérieur du composant (bean) • ReadOnlyIntegerWrapper, ReadOnlyIntegerProperty • ... IHM-1 – FX03 – Jacques BAPST 14 IHM-1 – FX03 – Jacques BAPST 16 Propriétés read-only [2] Observation des propriétés [1] § Exemple d'une classe définissant la propriété balance (solde) pour un compte bancaire en lecture seule (read-only). § Toutes les classes de propriétés implémentent l'interface Observable et offrent de ce fait, la possibilité d'enregistrer des observateurs (Listener) qui seront avertis lorsque la valeur de la propriété change. public class BankAccount { private ReadOnlyDoubleWrapper balance = new ReadOnlyDoubleWrapper(); § Une instance de l'interface fonctionnelle ChangeListener<T> pourra ainsi être créée pour réagir à un tel changement. La méthode changed() sera alors invoquée et recevra en paramètre la valeur observée ainsi que l'ancienne et la nouvelle valeur de la propriété. public final double getBalance() { return balance.get(); } public final void addInterests(double interestsRate) { double newBalance = balance.get() * (1+interestsRate); balance.set(newBalance); Mise à jour de la propriété } seulement possible en interne. DoubleProperty sum = account.balanceProperty(); sum.addListener( (ObservableValue<? extends Number> obsVal, Number oldVal, Number newVal ) -> { //--- ChangeListener code System.out.println(oldVal+" becomes "+ newVal); . . . } ); public final ReadOnlyDoubleProperty balanceProperty() { return balance.getReadOnlyProperty(); } Et surtout pas : . . . return balance; } – Jacques BAPST 17 IHM-1 – FX03 – Jacques BAPST 19 Observation des propriétés [2] § Exemple de diagramme de classes (partiel) pour les propriétés Integer. Remarque: Dans l'exemple précédent, une expression lambda est utilisée pour implémenter la méthode changed(). On pourrait également utiliser une classe anonyme ou créer l'instance d'une classe "ordinaire" qui implémente l'interface ChangeListener<T>. > > > API des propriétés JavaFX Compléme nt § Des classes/interfaces équivalentes existent pour les autres types (Boolean, Double, Float, Long, String, List, Map, Set, Object) • Cela donne une idée de la complexité des API des propriétés JavaFX. • On utilise généralement seulement quelquesunes de ces classes. IHM-1 – FX03 < < < < < < Compléme nt > > > IHM-1 – FX03 – Jacques BAPST 18 § L'interface fonctionnelle InvalidationListener<T> permet également de réagir aux changements des valeurs de propriétés dans les situations où les propriétés sont calculées à partir d'autres et que l'on veut éviter d'effectuer les calculs à chaque changement. § Avec cette interface, c'est la méthode invalidated(Observable o) qui est invoquée lorsqu'un changement potentiel de la valeur de la propriété est intervenu. § Cette méthode peut cependant être invoquée alors que le résultat observé n'a finalement pas changé (cela dépend des opérations effectuées). IHM-1 – FX03 – Jacques BAPST 20 Propriétés des composants Lier des propriétés [2] § Les composants utilisés dans les interfaces graphiques (boutons, champs texte, cases à cocher, sliders, etc.) possèdent tous de nombreuses propriétés. § Illustration des liens possibles entre deux propriétés A et B : A § Pour chacun des composants, la documentation (Javadoc) décrit dans une des rubriques (Property Summary) la liste des propriétés de la classe concernée ainsi que celles qui sont héritées. IHM-1 – FX03 – Jacques BAPST A B.bindBidirectional(A) B.bind(A) 21 ou A.bindBidirectional(B) B B Liaison unidirectionnelle Liaison bidirectionnelle (la valeur de la propriété B dépend de la valeur de la propriété A) (la valeur de la propriété B dépend de la valeur de la propriété A et inversement) IHM-1 – FX03 – Jacques BAPST Lier des propriétés [1] Lier des propriétés [3] § Un des avantages des propriétés JavaFX est la possibilité de pouvoir les lier entre-elles. Ce mécanisme, appelé binding, permet de mettre à jour automatiquement une propriété en fonction d'une autre. § La méthode bind() permet de créer un lien unidirectionnel. § Dans les interfaces utilisateurs, on a fréquemment ce type de liens. • Par exemple, lorsqu'on déplace le curseur d'un slider, la valeur d'un champ texte changera (ou la luminosité d'une image, la taille d'un graphique, le niveau sonore, etc.). BankAccount account = new BankAccount(); slider = new Slider(); slider.valueProperty().bind(account.balanceProperty()); • Unidirectionnelle : un changement de A entraînera un changement de B mais pas l'inverse (B non modifiable autrement) • Bidirectionnelle : un changement de A entraînera un changement de B et réciproquement (les deux sont modifiables) – Jacques BAPST § La méthode doit être appelée sur la propriété qui sera "soumise" à l'autre (celle qui est passée en paramètre). Slider § Il est possible de lier deux propriétés A et B de manière IHM-1 – FX03 23 22 § Dans cet exemple la position du curseur du slider est liée (asservie) à la valeur du solde du compte bancaire. Si l'on tente de modifier la valeur de la propriété associée à la position du slider (qui deviendra non-éditable) d'une autre manière, une exception sera générée. § Une liaison bidirectionnelle s'effectue de manière similaire, mais en utilisant la méthode bindBidirectional(). § Pour supprimer le lien : unbind() et unbindBidirectional() IHM-1 – FX03 – Jacques BAPST 24 Lier des propriétés [4] Lier des propriétés [6] § Une propriété ne peut être liée (asservie) qu'à une seule autre si le lien est unidirectionnel (bind()). Par contre, les liens bidirectionnels (bindBidirectional()) peuvent être multiples. § Même exemple, en utilisant des méthodes chaînées (Fluent API) : Slider Slider Slider § Parfois, une propriété dépend d'une autre mais avec une relation plus complexe. Il est ainsi possible de créer des propriétés calculées. areaPty.bind(widthPty.multiply(heightPty)); § Un jeu d'opérations est disponible aussi bien avec la classe Bindings qu'avec les méthodes chaînables. § Des opérations de conversions sont parfois nécessaires si le type des propriétés à lier n'est pas le même. Par exemple pour lier un champ texte (StringProperty) à un slider dont la valeur est numérique (DoubleProperty). IHM-1 – FX03 25 – Jacques BAPST // Input // Input // Ouput DoubleProperty widthPty = widthSlider.valueProperty(); DoubleProperty heightPty = heightSlider.valueProperty(); DoubleProperty areaPty = areaSlider.valueProperty(); § Deux techniques sont à disposition (elles peuvent être combinées) : • Utiliser la classe utilitaire Bindings qui possède de nombreuses méthodes statiques permettant d'effectuer des opérations. • Utiliser les méthodes disponibles dans les classes qui représentent les propriétés; ces méthodes peuvent être chaînées (Fluent API). widthSlider = new Slider(); heightSlider = new Slider(); areaSlider = new Slider(); • • • • • min(), max() equal(), notEqual(), lessThan(), lessThanOrEqual(), … isNull(), isNotNull(), isEmpty(), isNotEmpty() convert(), concat(), format(), … et beaucoup d'autres . . . IHM-1 – FX03 27 – Jacques BAPST Lier des propriétés [5] Lier des propriétés [7] § Exemple de binding permettant de coupler la valeur d'un slider (Area) aux valeurs d'entrée de deux autres sliders (Width et Height) § Si les opérations disponibles dans les API, dites de haut-niveau, ne permettent pas d'exprimer la relation entre les propriétés, il est possible de définir une liaison de plus bas niveau (low-level binding) en redéfinissant la méthode abstraite computeValue() d'une des classes de binding (DoubleBinding, BooleanBinding, StringBinding, …). § En utilisant les méthodes statiques de la classe Bindings : Slider Slider Slider widthSlider = new Slider(); heightSlider = new Slider(); areaSlider = new Slider(); // Input // Input // Ouput DoubleBinding complexBinding = new DoubleBinding() { { //--- Set listeners to 'in'-properties super.bind(aSlider.valueProperty(), bSlider.valueProperty()); } DoubleProperty widthPty = widthSlider.valueProperty(); DoubleProperty heightPty = heightSlider.valueProperty(); DoubleProperty areaPty = areaSlider.valueProperty(); @Override //--- Compute 'out' value protected double computeValue() { double w = aSlider.valueProperty().get(); double h = bSlider.valueProperty().get(); return Math.sqrt(h) * Math.pow(w, 3); } }; areaPty.bind(Bindings.multiply(widthPty, heightPty)); Width Slider Height Slider Devient non éditable car lié aux deux autres IHM-1 – FX03 Area Slider (produit des 2 valeurs) – Jacques BAPST outSlider.valueProperty().bind(complexBinding); 26 IHM-1 – FX03 – Jacques BAPST // Do the binding 28 Éléments d'une application Component Control § Structure générale d'une application JavaFX. Architecture JavaFX Concepts techniques Stage Container Layout Component Control Scene Component Control Container Layout ... Component Control Container Layout Component Control ... Component Control ... ... ... Graphe de scène (défini en Java ou avec FXML) IHM-1 – FX03 29 – Jacques BAPST IHM-1 – FX03 – Jacques BAPST Architecture technique [1] Graphe de scène [1] § L'architecture technique de la plateforme JavaFX est composée de plusieurs couches (library stack) qui reposent sur la machine virtuelle Java (JVM). § Le graphe de scène (scene graph) est une notion importante qui représente la structure hiérarchique de l'interface graphique. Applications JavaFX Public Layer Private Layer Native Layer 31 § Techniquement, c'est un graphe acyclique orienté (arbre orienté) avec : • une racine (root) • des nœuds (nodes) • des arcs qui représentent les relations parent-enfant § Les nœuds (nodes) peuvent être de trois types : § Les développeurs ne devrait utiliser que la couche (API) publique car les couches inférieures sont susceptibles de subir des changements importants au fil des versions (sans aucune garantie de compatibilité). • Racine • Nœud intermédiaire • Feuille (leaf) § Les "briques" intermédiaires ne seront donc pas décrites dans ce cours. IHM-1 – FX03 – Jacques BAPST 30 IHM-1 – FX03 – Jacques BAPST 32 Graphe de scène [2] Graphe de scène [4] § Les feuilles de l'arbre sont généralement constitués de composants visibles (boutons, champs texte, …) et les nœuds intermédiaires (y compris la racine) sont généralement des éléments de structuration (souvent invisibles), typiquement des conteneurs ou panneaux de différents types (HBox, VBox, BorderPane, …). § Parmi les sous-classes de Node on distingue différentes familles : • Les formes primitives (Shape) 2D et 3D ð Line, Circle, Rectangle, Box, Cylinder, … • Les conteneurs (Layout-Pane) qui se chargent de la disposition (layout) des composants enfants et qui ont comme classe parente Pane. ð AnchorPane, BorderPane, GridPane, HBox, VBox, … • Les composants standard (Controls) qui étendent la classe Control. ð Label, Button, TextField, ComboBox, … • Les composants spécialisés qui sont dédiés à un domaine particulier (par exemple : lecteur multimédia, navigateur web, etc.). ð IHM-1 – FX03 33 – Jacques BAPST MediaView, WebView, ImageView, Canvas, Chart, … IHM-1 – FX03 35 – Jacques BAPST Graphe de scène [3] Graphe de scène [5] § Tous les éléments contenus dans un graphe de scène sont des objets qui ont pour classe parente la classe Node. § Application JavaFX et son graphe de scène. § La classe Node comporte de nombreuses sous-classes : Stage HBox Conteneur Button Composant Scene BorderPane Composants (Controls) Conteneurs (Layout-Panes) [simplifié] [simplifié] Label Simplifié ! (il y a des classes intermédiaires) IHM-1 – FX03 – Jacques BAPST 34 VBox ChoiceBox TextField IHM-1 – FX03 HBox Button Button VBox ChoiceBox TextField – Jacques BAPST HBox Button Button 36 Conteneurs Region - Structure visuelle [2] § Les conteneurs (Layout-Pane) représentent une famille importante parmi les sous-classes de Node. Ils ont pour classe parente Pane et Region qui possèdent de nombreuses propriétés et méthodes héritées par tous les conteneurs. § Comme la classe Region est la classe parente de tous les conteneurs et de tous les composants, ses propriétés sont héritées et peuvent s'appliquer à une grande variété d'éléments qui sont utilisés pour créer les interfaces. § Parmi ces propriétés, on peut mentionner : • border ð Bordure autour de la région • background ð Couleur ou image d'arrière-plan de la région • padding ð Marge (espace) autour du contenu de la région § L'utilisation et le codage des propriétés border et background sont expliqués plus en détail dans le chapitre suivant consacré aux conteneurs et layout-panes (à la section Disposition des composants Généralités). ... IHM-1 – FX03 37 – Jacques BAPST IHM-1 – FX03 – Jacques BAPST Region - Structure visuelle [1] Style – Look and Feel [1] § La classe Region est la classe parente des composants (Controls) et des conteneurs (Layout-Panes). § La notion de style, skin, thème ou look and feel (L&F) caractérise l'ensemble des aspects visuels de l'interface graphique et de ses composants (forme, couleur, texture, ombre, police de caractères, …). § Elle définit des propriétés qui affectent la représentation visuelle. § En JavaFX, le style des composants est défini par des feuilles de style de type CSS. Il est ainsi possible de changer globalement l'aspect de l'interface sans avoir à modifier le code de l'application. § Les différentes zones d'une région sont basées sur la spécification du Box-Model CSS 3 (selon normalisation du W3C). • Elles définissent les notions Margin, Border, Padding, Insets, Content § La méthode setUserAgentStylesheet() de la classe Application permet d'indiquer l'URL de la feuille de style qui est à appliquer globalement. Les zones Border et Padding définissent les Insets (encarts) de la région § Deux styles, nommés Modena et Caspian, sont prédéfinis; ils sont associés aux constantes : • STYLESHEET_MODENA Les composants enfants sont ajoutés dans la zone Content IHM-1 – FX03 – Jacques BAPST 39 • STYLESHEET_CASPIAN 38 IHM-1 – FX03 : Utilisé par défaut depuis JavaFX 8 : A été défini pour JavaFX 2 – Jacques BAPST 40 Style – Look and Feel [2] § Style Modena sur différents OS. IHM-1 – FX03 41 – Jacques BAPST Style – Look and Feel [3] § Le style utilisé par défaut peut évoluer au fil des versions de JavaFX. § Si l'on veut fixer ou changer le look and feel des interfaces, on peut le faire au démarrage de l'application : @Override public void start(Stage primaryStage) { . . . setUserAgentStylesheet(STYLESHEET_CASPIAN); . . . § Des styles externes (third-party) peuvent également être importés et appliqués. On trouve différentes réalisations, par exemple : • • • • Apple Aqua Microsoft Modern UI Twitter Bootstrap Flatter (Embedded UI), IHM-1 – FX03 (AquaFX) (JMetro) (Fextile) ... Windows-7 Aero – Jacques BAPST (AeroFX) 42 Disposition des composants [1] § La qualité d'une interface graphique repose sur de nombreux éléments mais la disposition des composants dans la fenêtre figure certainement parmi les plus importants. Cours IHM-1 JavaFX § Quand on parle de la disposition (layout) d'une interface, on s'intéresse plus particulièrement : • A la taille des composants • A la position des composants 4 - Conteneurs Layout-Panes ð ð Position dans la fenêtre Position relative des éléments les uns par rapport aux autres • Aux alignements et espacements qui favorisent la structuration visuelle • Aux bordures et aux marges (notamment autour des conteneurs) • Au comportement dynamique de l'interface lorsqu'on redimensionne la fenêtre, lorsqu'on déplace une barre de division (splitpane), etc. Jacques BAPST [email protected] IHM-1 – FX04 – Jacques BAPST 3 Disposition des composants [2] § Avec JavaFX, il est possible de dimensionner et de positionner les composants de manière absolue (en pixels, mm, etc.). § Une disposition avec des valeurs absolues peut être utile et même nécessaire dans certaines situations particulières. Cependant, dans la plupart des cas, elle présente de nombreux inconvénients, car : Disposition des composants Généralités Notions de base • La taille naturelle des composants peut varier, en fonction De la langue choisie (libellés, boutons, menus, …) De la configuration de la machine cible (paramètres de l' OS) ð Du look & feel, thème, skin, style (CSS) choisi par l'utilisateur ð ð • La taille de la fenêtre peut également varier ð ð IHM-1 – FX04 – Jacques BAPST 2 Par le souhait de l'utilisateur Par obligation, pour s'adapter à la résolution de l'écran de la machine cible (pour afficher l'intégralité de l'interface) IHM-1 – FX04 – Jacques BAPST 4 Disposition des composants [3] Système de coordonnées [1] § Pour éviter ces inconvénients on préfère déléguer la disposition des composants à des gestionnaires de disposition (layout managers) qui sont associés à des conteneurs. § Le système de coordonnées utilisé par JavaFX place l'origine dans le coin supérieur gauche du conteneur ou du composant considéré. § L'idée principale est de définir des règles de disposition (des contraintes) que le gestionnaire se chargera de faire respecter en fonction du contexte spécifique de la machine cible. § Les valeurs de l'axe y croissent vers le bas (traditionnel en infographie). § La taille des composants est définie par leur largeur (width) et par leur hauteur (height). § C'est donc par le choix du layout-pane et en fonction des contraintes données que sont déterminées les règles de disposition. § Plusieurs layout-panes sont prédéfinis dans JavaFX. Il est également possible de définir ses propres conteneurs avec des règles de disposition spécifiques, mais c'est rarement nécessaire. – Jacques BAPST 5 x xc [0, 0] yc Component Height § Avec JavaFX, les layout managers sont intégrés aux conteneurs (layout-panes) et ne sont pas manipulés directement par les programmeurs (seulement au travers des propriétés des conteneurs). IHM-1 – FX04 § Les valeurs de l'axe x croissent vers la droite. Width y IHM-1 – FX04 – Jacques BAPST Taille des composants Système de coordonnées [2] § Les composants (nodes) que l'on peut placer dans des conteneurs possèdent des propriétés qui peuvent être prises en compte lors du calcul de leur disposition. § Coordonnées d'origine de différents éléments d'une application JavaFX. • minWidth • prefWidth • maxWidth • minHeight • prefHeight • maxHeight : : : : : : Largeur minimale souhaitée pour le composant Largeur préférée (idéale) du composant Largeur maximale souhaitée pour le composant Hauteur minimale souhaitée pour le composant Hauteur préférée (idéale) du composant Hauteur maximale souhaitée pour le composant 7 Screen Window / Stage Scene / Root Node / Component § L'effet de ces propriétés dépend naturellement du type de conteneur (layout-pane) utilisé et de ses règles spécifiques de positionnement et de dimensionnement. IHM-1 – FX04 – Jacques BAPST 6 IHM-1 – FX04 – Jacques BAPST 8 Système de coordonnées [3] Fenêtre principale [2] § En réalité, toute la librairie graphique est basée sur un système de coordonnées en trois dimensions (3D). § Autres méthodes de l'objet Stage : § La troisième dimension est représentée par l'axe z dont les valeurs augmentent lorsqu'on s'éloigne de l'observateur. § Pour la gestion des interfaces "classiques" (de type WIMP), on ne s'occupe généralement pas de l'axe z et on travaille uniquement dans le plan x/y. : Place la fenêtre en mode plein-écran ou en mode standard (si paramètre false) (selon OS) • getIcons().add() : Définit l'icône dans la barre de titre • setAlwaysOnTop() : Place la fenêtre toujours au dessus des autres (généralement à éviter) : Définit la scène (sa racine) qui est associée à la fenêtre • setScene() • showAndWait() • Le système n'est donc pas nécessairement lié aux pixels de l'écran • Des transformations sont ainsi facilement possibles (translation, homothétie, rotation, …) en utilisant l'algèbre linéaire • Lors du rendu un alignement sur les pixels peut être automatiquement effectué (voir méthode snapToPixel()) – Jacques BAPST : Définit le titre de la fenêtre (affiché selon OS) • setFullScreen() • show() § Dans les API, pratiquement toutes les coordonnées sont données avec des nombres à virgule (généralement des double). IHM-1 – FX04 • setTitle() 9 • . . . IHM-1 – FX04 : Affiche la fenêtre à l'écran (et la scène qu'elle contient) : Affiche la fenêtre à l'écran et attend que la fenêtre soit fermée pour retourner (méthode bloquante). Cette méthode n'est pas applicable à la fenêtre principale (primary stage). Et beaucoup d'autres (consulter la documentation) – Jacques BAPST 11 Fenêtre principale [1] Fenêtre principale [3] § Par défaut, au lancement d'une application, la fenêtre principale (primary stage) est centrée sur l'écran. § Pour déterminer la taille de l'écran, on peut utiliser la classe Screen et rechercher le rectangle qui englobe la zone utilisable de l'écran (ou l'intégralité de la surface de l'écran). § Différentes méthodes peuvent être invoquées sur l'objet Stage pour influencer la position et la taille de cette fenêtre : • setX() : Position en x du coin supérieur gauche • setY() : Position en y du coin supérieur gauche • centerOnScreen() : : : : : : • setMinWidth() • setMinHeight() • setMaxWidth() • setMaxHeight() • setResizable() • sizeToScene() IHM-1 – FX04 § La méthode getBounds() retourne par contre un rectangle qui représente la surface totale de l'écran. Centrage sur l'écran (par défaut) Fixe la largeur minimale de la fenêtre Fixe la hauteur minimale de la fenêtre Fixe la largeur maximale de la fenêtre Fixe la hauteur maximale de la fenêtre Détermine si la fenêtre est redimensionnable Screen screen = Screen.getPrimary(); Rectangle2D bounds = screen.getVisualBounds(); : Adapte la taille de la fenêtre à la taille de la scène liée à cette fenêtre (utile si l'on change dynamiquement le contenu du graphe de scène) – Jacques BAPST § La méthode getVisualBounds() prend en compte l'espace occupé par certains éléments du système d'exploitation (barre de tâche, menu, etc.). 10 double screenWidth = bounds.getWidth(); double screenHeight = bounds.getHeight(); double screenDpi IHM-1 – FX04 = screen.getDpi(); // Dot per inch – Jacques BAPST 12 Fenêtre principale [4] Bordures [2] § La fenêtre principale, appelée primary stage est celle qui est passée en paramètre (par le système) à la méthode start(). § Exemple 1 : bordure rectangulaire § Dans une application JavaFX, il est possible de créer d'autres fenêtres indépendantes (d'autres objets Stage) et de les gérer. Button btnA = new Button("A Border Test"); HBox root = new HBox(); root.getChildren().add(btnA); private void createStage() { Stage secondStage = new Stage(); secondStage.setTitle("Second Stage"); root2 = . . . primaryStage.setTitle("Test Border 1"); root.setPadding(new Insets(30, 50, 30, 50)); root.setAlignment(Pos.CENTER); // Création de la scène de la deuxième fenêtre Border border1 = new Border( new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(6), new Insets(0) )); root.setBorder(border1); secondStage.setScene(new Scene(root2)); secondStage.show(); } @Override public void start(Stage primaryStage) { . . . primaryStage.setScene(new Scene(root)); primaryStage.show(); createStage(); . . . } IHM-1 – FX04 – Jacques BAPST 13 IHM-1 – FX04 – Jacques BAPST Bordures [1] Bordures [3] § Des bordures peuvent être appliquées à toutes les sous-classes de Region, notamment autour des conteneurs (layout-panes) et autour des composants (moins fréquent). § Exemple 2 : bordure pointillée, aux coins arrondis primaryStage.setTitle("Test Border 2"); Button btnA = new Button("A Border Test"); HBox root = new HBox(); root.getChildren().add(btnA); § C'est la propriété border (héritée de Region) qui est utilisée pour définir les caractéristiques de la bordure. § Les classes Border et BorderStroke permettent de créer des bordures et de les assigner à la propriété border. Border est immuable et permet ainsi de créer des objets réutilisables (on peut appliquer une même bordure à plusieurs conteneurs ou composants). root.setPadding(new Insets(30)); root.setAlignment(Pos.CENTER); § Pour les bordures, JavaFX suit le modèle de conception proposé pour les feuilles de style CSS 3. Cette spécification est relativement complexe et permet la gestion de bordures assez sophistiquées. Border border2 = new Border( new BorderStroke(Color.ORANGE, BorderStrokeStyle.DOTTED, new CornerRadii(30), new BorderWidths(10), new Insets(20) )); root.setBorder(border2); § Les exemples ci-après illustrent une utilisation simple des bordures pour entourer un conteneur (de type HBox). primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX04 – Jacques BAPST 15 14 IHM-1 – FX04 – Jacques BAPST 16 Arrière-plans [1] Arrière-plans [3] § Des couleurs et/ou des images d'arrière-plan peuvent être appliquées à toutes les sous-classes de Region, et donc aussi bien aux conteneurs (layout-panes) qu'aux composants (controls). § Exemple avec la répétition d'une image d'arrière-plan : Button btnA = new Button("A Background Test"); Image img = new Image("/resources/warn_1.png"); HBox root = new HBox(); root.getChildren().add(btnA); § C'est la propriété background (héritée de Region) qui est utilisée pour définir les caractéristiques de l'arrière-plan. § Les classes Background, BackgroundFill et BackgroundImage permettent de créer des arrière-plans qui peuvent être composés d'une superposition de remplissage (fill) et/ou d'images. Ces classes sont immuables et permettent ainsi de créer des objets réutilisables (on peut appliquer un même arrière-plan à plusieurs conteneurs ou composants). § Les exemples qui suivent illustrent l'application d'arrière-plans à des conteneurs (de type HBox). root.setPadding(new Insets(30, 50, 30, 50)); root.setAlignment(Pos.CENTER); Background bgImg = new Background( new BackgroundImage( img, BackgroundRepeat.ROUND, BackgroundRepeat.ROUND, BackgroundPosition.CENTER, BackgroundSize.DEFAULT )); root.setBackground(bgImg); IHM-1 – FX04 – Jacques BAPST 17 IHM-1 – FX04 – Jacques BAPST 19 Arrière-plans [2] § Exemple avec deux remplissages (fill) d'arrière-plan superposés : Button btnA = new Button("A Background Test"); HBox root = new HBox(); root.getChildren().add(btnA); Conteneurs Layout-Panes de base root.setPadding(new Insets(30, 50, 30, 50)); root.setAlignment(Pos.CENTER); (disponibles par défaut) Background bgGreen = new Background( new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, null ), new BackgroundFill(Color.GREEN, new CornerRadii(20), new Insets(15) )); root.setBackground(bgGreen); IHM-1 – FX04 – Jacques BAPST 18 IHM-1 – FX04 – Jacques BAPST 20 Layout-Panes HBox [2] § Dans la création des graphes de scène, les conteneurs (appelés layout-panes ou parfois simplement layouts ou panes) jouent donc un rôle important dans la structuration et la disposition des composants qui seront placés dans les interfaces. § L'ajout des composants enfants dans le conteneur s'effectue en invoquant d'abord la méthode générale getChildren() qui retourne la liste des enfants du conteneur et en y ajoutant ensuite le composant considéré (méthodes add() ou addAll()) : § En fonction du design adopté (phase de conception de l'interface), il est important de réfléchir au choix des conteneurs qui permettront au mieux de réaliser la mise en page souhaitée. § Les pages qui suivent décrivent sommairement les layouts-panes qui sont prédéfinis dans JavaFX. § Seules les caractéristiques principales sont mentionnées dans ce support de cours. Ce n'est en aucun cas un manuel de référence. § Il faut donc impérativement consulter la documentation disponible (Javadoc, tutorials, …) pour avoir une description détaillée de l'API (constantes, propriétés, constructeurs, méthodes, …). IHM-1 – FX04 – Jacques BAPST 21 • HBox root = new HBox(); • root.getChildren().add(btnA); § Des méthodes statiques de HBox peuvent être invoquées pour appliquer des contraintes de positionnement : • hgrow() : permet d'agrandir le composant passé en paramètre jusqu'à sa taille maximale selon la priorité (Priority) donnée • margin() : fixe une marge (objet de type Insets) autour du composant passé en paramètre (zéro par défaut Insets.EMPTY) IHM-1 – FX04 – Jacques BAPST HBox [1] HBox [3] § Le layout HBox place les composants sur une ligne horizontale. Les composants sont ajoutés à la suite les uns des autres (de gauche à droite). § Exemple (déclaration des composants et code de la méthode start()) private private private private HBox Button Label ComboBox<String> 23 root; btnA = new Button("Alpha"); lblB = new Label("Bravo"); cbbC = new ComboBox<>(); primaryStage.setTitle("Test HBox"); § L'alignement des composants enfants est déterminé par la propriété alignment, par défaut TOP_LEFT (type énuméré Pos). § L'espacement horizontal entre les composants est défini par la propriété spacing (0 par défaut). La valeur de cette propriété peut être passée en paramètre au constructeur (new HBox(8)). § Si possible, le conteneur respecte la taille préférée des composants. Si le conteneur est trop petit pour afficher tous les composants à leur taille préférée, il les réduit jusqu'à minWidth. IHM-1 – FX04 – Jacques BAPST 22 root = new HBox(10); root.setAlignment(Pos.CENTER); root.getChildren().add(btnA); root.getChildren().add(lblB); // Horizontal Spacing : 10 cbbC.getItems().addAll("Charlie", "Delta"); cbbC.getSelectionModel().select(0); root.getChildren().add(cbbC); primaryStage.setScene(new Scene(root, 300, 100)); primaryStage.show(); IHM-1 – FX04 – Jacques BAPST 24 HBox [4] VBox [1] § La propriété padding permet de définir l'espace (marge) entre le bord du conteneur et les composants enfants. § Le layout VBox place les composants verticalement, sur une colonne. Les composants sont ainsi ajoutés à la suite les uns des autres (de haut en bas). • Un paramètre de type Insets est passé en paramètre, il définit les espacements dans l'ordre suivant : Top, Right, Bottom, Left ou une valeur identique pour les quatre côtés si l'on passe un seul paramètre root.setPadding(new Insets(10, 5, 10, 5)); root.setPadding(new Insets(10)); § Une couleur de fond peut être appliquée au conteneur en définissant la propriété style qui permet de passer en chaîne de caractères, un style CSS. § Toutes les propriétés et méthodes décrites pour le conteneur HBox s'appliquent également au conteneur VBox avec seulement quelques adaptations assez évidentes, par exemple la propriété hgrow devient vgrow. La différence essentielle est donc simplement que le sens de l'empilement des composants enfants est vertical et non horizontal. • On définit la propriété CSS -fx-background-color. root.setStyle("-fx-background-color: #FFFD33"); § Ce conteneur ne sera donc pas décrit plus en détail ici. On peut aussi utiliser setBorder() discuté au début du chapitre IHM-1 – FX04 – Jacques BAPST 25 IHM-1 – FX04 – Jacques BAPST HBox [5] VBox [2] § Exemple (conteneur avec padding et couleur de fond) § Exemple (avec agrandissement vertical d'un composant) private private private private primaryStage.setTitle("Test Padding+Background"); root = new HBox(10); //--- Top, Right, Bottom, Left Spacing root.setPadding(new Insets(20, 10, 20, 10)); root.setStyle("-fx-background-color: #FFFE99"); primaryStage.setTitle("Test VBox"); root = new VBox(10); // Vertical Spacing : 10 root.setAlignment(Pos.TOP_CENTER); root.getChildren().add(btnA); root.getChildren().add(lblB); cbbC.getItems().addAll("Charlie", "Delta"); cbbC.getSelectionModel().select(0); root.getChildren().add(cbbC); cbbC.getItems().addAll("Charlie", "Delta"); cbbC.getSelectionModel().select(0); root.getChildren().add(cbbC); primaryStage.setScene(new Scene(root)); primaryStage.show(); – Jacques BAPST root; btnA = new Button("Alpha"); lblB = new Label("Bravo"); cbbC = new ComboBox<>(); VBox.setVgrow(btnA, Priority.ALWAYS); btnA.setMaxHeight(Double.MAX_VALUE); root.getChildren().add(btnA); root.getChildren().add(lblB); IHM-1 – FX04 VBox Button Label ComboBox<String> 27 primaryStage.setScene(new Scene(root, 180, 150)); primaryStage.show(); 26 IHM-1 – FX04 – Jacques BAPST 28 FlowPane [1] FlowPane [3] § Le layout FlowPane place les composants sur une ligne horizontale ou verticale et passe à la ligne ou à la colonne suivante (wrapping) lorsqu'il n'y a plus assez de place disponible. § Exemple (déclaration du conteneur et des composants) § Un des paramètres du constructeur (de type Orientation) détermine s'il s'agit d'un FlowPane horizontal (par défaut) ou vertical. private FlowPane root; private private private private private private private btnA lblB btnC lblD btnE lblF btnG Button Label Button Label Button Label Button = = = = = = = new new new new new new new Button("__Alpha__"); Label("__Bravo__"); Button("__Charlie__"); Label("__Delta__"); Button("__Echo__"); Label("__Foxtrot__"); Button("__Golf__"); . . . FlowPane horizontal IHM-1 – FX04 FlowPane vertical – Jacques BAPST 29 IHM-1 – FX04 – Jacques BAPST FlowPane [2] FlowPane [4] § L'ajout des composants enfants dans un conteneur FlowPane s'effectue en invoquant getChildren().add(node) ou addAll(n, …) § Exemple (code de la méthode start()) § Quelques propriétés importantes du conteneur FlowPane : : Espacement horizontal entre les composants ou colonnes (peut aussi être passé au constructeur) • vgap : Espacement vertical entre les composants ou lignes (peut aussi être passé au constructeur) • padding : Espacement autour du conteneur (marge) • alignment : Alignement global des composants dans le conteneur • rowValignment : Alignement vertical dans les lignes (si horizontal-pane) • columnHalignment : Alignement horizontal dans les colonnes (si vertical-pane) • prefWrapLength : Détermine la largeur préférée (si horizontal-pane) ou la hauteur préférée (si vertical-pane) • orientation : Orientation du FlowPane (peut aussi être passé au constructeur) • hgap IHM-1 – FX04 – Jacques BAPST 30 31 primaryStage.setTitle("FlowPane (horizontal)"); root = new FlowPane(); root.getChildren().add(btnA); root.getChildren().add(lblB); root.getChildren().add(btnC); root.getChildren().add(lblD); root.getChildren().add(btnE); root.getChildren().add(lblF); root.getChildren().add(btnG); // Ajout // des // composants root.setPadding(new Insets(5)); root.setHgap(10); root.setVgap(15); // Marge extérieure // Espacement horiz. entre composants // Espacement vertical entre lignes root.setPrefWrapLength(250); // Largeur préférée du conteneur root.setRowValignment(VPos.BOTTOM); // Alignement vertical dans lignes primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX04 – Jacques BAPST 32 TilePane [1] TilePane [3] § Le layout TilePane place les composants dans une grille alimentée soit horizontalement (par ligne, de gauche à droite) soit verticalement (par colonne, de haut en bas). § Suite des propriétés : § Un des paramètres du constructeur (de type Orientation) détermine s'il s'agit d'un TilePane horizontal (par défaut) ou vertical. § On définit pour la grille un certain nombre de colonnes (propriété prefColumns) si l'orientation est horizontale ou un certain nombre de lignes (propriété prefRows) si l'orientation est verticale. • prefTileWidth : Détermine la largeur préférée des tuiles (cellules). Change la valeur calculée par défaut (USE_COMPUTED_SIZE). • prefTileHeight : Détermine la hauteur préférée des tuiles (cellules). Change la valeur calculée par défaut (USE_COMPUTED_SIZE). Remarque : § Toutes les cellules de cette grille (les tuiles) ont la même taille qui correspond à la plus grande largeur préférée et à la plus grande hauteur préférée parmi les composants placés dans ce conteneur. Les propriétés prefColumns et prefRows ne reflètent pas nécessairement le nombre de colonnes ou le nombre de lignes actuels de la grille. Ces propriétés servent uniquement à calculer la taille préférée du conteneur. Si la taille du conteneur change, le nombre de lignes et de colonnes sera adapté à l'espace à disposition (wrapping automatique des tuiles). § Le conteneur TilePane est très proche du conteneur FlowPane. La différence principale réside dans le fait que toutes les cellules ont obligatoirement la même taille (ce qui n'est pas le cas pour FlowPane). IHM-1 – FX04 – Jacques BAPST 33 IHM-1 – FX04 – Jacques BAPST TilePane [2] TilePane [4] § Quelques propriétés importantes du conteneur TilePane : § L'ajout des composants enfants dans un conteneur TilePane s'effectue en invoquant l'une des deux méthodes : : Espacement horizontal entre les composants ou colonnes (peut aussi être passé au constructeur). • vgap : Espacement vertical entre les composants ou lignes (peut aussi être passé au constructeur). • padding : Espacement autour du conteneur (marge). • alignment : Alignement global de la grille dans le conteneur (par défaut TOP_LEFT). • tileAlignment : Alignement des composants à l'intérieur de chaque tuile (par défaut CENTER). Peut être redéfini individuellement (par la propriété alignment de chaque composant). • prefColumns : Nombre préféré de colonnes (par défaut 5) si horizontal. Détermine la largeur préférée du conteneur. • prefRows : Nombre préféré de lignes (par défaut 5) si vertical. Détermine la hauteur préférée du conteneur. • hgap IHM-1 – FX04 – Jacques BAPST 35 • getChildren().add(node) • getChildren().addAll(node1, node2, node3, …) § Exemple avec des libellés (Label) et des boutons (Button) • La police de caractères du bouton "__Echo__" a été agrandie et détermine la taille de toutes les tuiles (cellules) 34 IHM-1 – FX04 – Jacques BAPST 36 BorderPane [1] BorderPane [3] § Le conteneur BorderPane permet de placer les composants enfants dans cinq zones : Top, Bottom, Left, Right et Center. § Certaines zones peuvent être laissées libres (sans composant). Elles n'occupent alors aucune place. § Un seul objet Node (composant, conteneur, …) peut être placé dans chacun de ces emplacements. § Si les composants n'occupent pas tout l'espace disponible dans leur zone, leur point d'ancrage par défaut (alignement) respectera la disposition suivante : IHM-1 – FX04 37 – Jacques BAPST BorderPane [2] IHM-1 – FX04 – Jacques BAPST 39 BorderPane [4] § Les composants placés dans les zones Top et Bottom : setTop() setBottom() • Gardent leur hauteur préférée • Sont éventuellement agrandis horizontalement jusqu'à leur largeur maximale ou réduit à leur taille minimale en fonction de la largeur du conteneur. § Les composants placés dans les zones Left et Right : setLeft() setRight() • Gardent leur largeur préférée • Sont éventuellement agrandis verticalement jusqu'à leur hauteur maximale ou réduit à leur taille minimale en fonction de la hauteur restante entre les (éventuelles) zones Top et Bottom du conteneur. § Le composant placé dans la zone Center : § Des méthodes statiques de BorderPane peuvent être invoquées pour appliquer des contraintes de positionnement : • alignment() : permet de modifier l'alignement par défaut du composant passé en paramètre (point d'ancrage) • margin() : fixe une marge (objet de type Insets) autour du composant passé en paramètre (par défaut Insets.EMPTY) BorderPane.setAlignment(btnTop, Pos.CENTER); BorderPane.setAlignment(btnRight, Pos.BOTTOM_CENTER); BorderPane.setMargin(btnBottom, new Insets(10)); setCenter() • Est éventuellement agrandi (jusqu'à sa taille maximale) ou réduit (à sa taille minimale) dans les deux directions (largeur et hauteur) pour occuper l'espace qui reste au centre du conteneur. Remarque : Par défaut, la taille maximale des boutons (Button) est égale à leur taille préférée. Pour qu'ils s'agrandissent, il faut modifier la propriété maxWidth : btnT.setMaxWidth(Double.MAX_VALUE); IHM-1 – FX04 – Jacques BAPST 38 IHM-1 – FX04 – Jacques BAPST 40 BorderPane [5] AnchorPane [1] § Le conteneur BorderPane est fréquemment utilisé comme conteneur racine du graphe de scène car il correspond à une division assez classique de la fenêtre principale d'une application (barre de titre, barre d'état, zone d'options, zone principale, etc.). § Le conteneur AnchorPane permet de positionner (ancrer) les composants enfants à une certaine distance des côtés du conteneur (Top, Bottom, Left et Right). § Pour placer plusieurs composants dans les zones du BorderPane, il faut y ajouter des nœuds de type conteneur et ajouter ensuite les composants dans ces conteneurs imbriqués. § Plusieurs composants peuvent être ancrés à un même côté. § Il est donc très fréquent d'imbriquer plusieurs conteneurs pour obtenir la disposition désirée des composants de l'interface. – Jacques BAPST 41 BorderPane et HBox private private private private private private BorderPane HBox Label Button Button Button root btnPanel lblTitle btnSave btnQuit btnCancel = = = = = = new new new new new new § Ce conteneur ressemble par certains aspects à BorderPane mais il y a quelques différences essentielles : • Le conteneur AnchorPane n'est pas divisé en zones, les composants sont simplement liés (ancrés) par rapport aux bords du conteneur. • Un composant peut être ancré à plusieurs bords et même à des bords opposés (left et right par exemple). Dans ce cas, le composant pourra être étiré ou comprimé pour respecter les contraintes d'ancrage. • Plusieurs composants peuvent être ancrés à un même bord. Il pourront dans ce cas, être partiellement ou totalement superposés. § Le graphe de scène représente donc un arbre d'imbrication dont la hauteur (nombre de niveaux) dépend du nombre de composants et de la complexité de la structure de l'interface graphique. IHM-1 – FX04 § Un composant peut être ancré, simultanément, à plusieurs côtés. IHM-1 – FX04 – Jacques BAPST 43 AnchorPane [2] § Par défaut (sans contrainte), les composants sont ancrés en haut et à gauche (Top - Left). BorderPane(); HBox(10); Label("App Title"); Button("Save"); Button("Quit"); Button("Cancel"); § Exemple de positionnement avec les contraintes représentées par des flèches : lblTitle.setFont(Font.font("SansSerif", FontWeight.BOLD, 24)); lblTitle.setTextFill(Color.BLUE); root.setTop(lblTitle); //--- Build Buttons Panel btnPanel.getChildren().add(btnSave); btnPanel.getChildren().add(btnQuit); btnPanel.getChildren().add(btnCancel); btnPanel.setAlignment(Pos.CENTER); root.setBottom(btnPanel); BorderPane.setMargin(lblTitle, new Insets(10, 0, 10, 0)); BorderPane.setMargin(btnPanel, new Insets(10, 0, 10, 0)); BorderPane.setAlignment(lblTitle, Pos.CENTER); primaryStage.setScene(new Scene(root)); IHM-1 – FX04 – Jacques BAPST 42 IHM-1 – FX04 – Jacques BAPST 44 AnchorPane [3] AnchorPane [5] § L'ajout des composants enfants dans un conteneur AnchorPane s'effectue en invoquant l'une des deux méthodes : • getChildren().add(node) • getChildren().addAll(node1, node2, node3, …) § Pour définir les contraintes d'ancrage, il faut invoquer des méthodes statiques de AnchorPane : • topAnchor() : définit la distance (double) par rapport au haut • bottomAnchor() : définit la distance (double) par rapport au bas : définit la distance (double) par rapport au côté gauche § Exemple de contraintes d'ancrage : AnchorPane.setTopAnchor (btnOk, 10.0); AnchorPane.setRightAnchor (btnOk, 0.0); AnchorPane.setBottomAnchor(btnQuit, 15.0); IHM-1 – FX04 – Jacques BAPST = new AnchorPane(); private private private private = = = = Button Button Button Button btnA btnB btnC btnD new new new new Button("Alpha"); Button("Bravo"); Button("Charlie"); Button("Delta"); primaryStage.setTitle("Test AnchorPane"); root.getChildren().addAll(btnA, btnB, btnC, btnD); //--- Contraintes AnchorPane.setRightAnchor (btnB, AnchorPane.setTopAnchor (btnC, AnchorPane.setLeftAnchor (btnC, AnchorPane.setBottomAnchor(btnD, AnchorPane.setLeftAnchor (btnD, AnchorPane.setRightAnchor (btnD, • rightAnchor() : définit la distance (double) par rapport au côté droit • leftAnchor() private AnchorPane root 20.0); 80.0); 60.0); 20.0); 40.0); 40.0); primaryStage.setScene(new Scene(root, 250, 250)); primaryStage.show(); 45 IHM-1 – FX04 – Jacques BAPST 47 AnchorPane [4] StackPane [1] § Les distances définies dans les contraintes d'ancrage sont données par rapport aux côtés du conteneur. Si le conteneur possède une bordure (setBorder()) et/ou un padding (setPadding()), les dimensions de ces éléments seront pris en compte et s'ajouteront à la distance donnée en contrainte. § Le conteneur StackPane empile les composants enfants les uns au dessus des autres dans l'ordre d'insertion : les premiers "au fond", les derniers "au-dessus" (back-to-front). § Si nécessaire, les composants enfants sont redimensionnés pour s'adapter à la taille du conteneur (la valeur de la propriété maxSize est respectée, mais pas celle de minSize !). § Le layout AnchorPane respecte, si possible, la taille préférée des composants enfants. § Cependant, selon les contraintes données, et si les composants enfants sont redimensionnables (isResizable()), leur taille devra être adaptée pour respecter les contraintes données. Dans ce cas, le layout AnchorPane ne tiendra pas compte des tailles minimales et maximales des composants lors de leur redimensionnement. § Si des composants se superposent, c'est l'ordre des ajouts dans le conteneur qui déterminera leur visibilité (le dernier ajouté sera audessus). IHM-1 – FX04 – Jacques BAPST 46 § Si les composants enfants n'occupent pas toute la place disponible, ils seront alignés selon la valeur de la propriété alignment du conteneur (CENTER par défaut). • Cet alignement par défaut peut être modifié par les contraintes d'alignement spécifiques des composants enfants (voir page suivante) § L'ajout des composants enfants dans un conteneur de type StackPane s'effectue en invoquant l'une des deux méthodes : • getChildren().add(node) • getChildren().addAll(node1, node2, node3, …) IHM-1 – FX04 – Jacques BAPST 48 StackPane [2] GridPane [1] § Des méthodes statiques de StackPane peuvent être invoquées pour appliquer des contraintes de positionnement aux composants : § Le conteneur GridPane permet de disposer les composants enfants dans une grille flexible (arrangement en lignes et en colonnes), un peu à la manière d'une table HTML. • alignment() : permet de modifier l'alignement par défaut du composant passé en paramètre • margin() : fixe une marge (objet de type Insets) autour du composant passé en paramètre (par défaut Insets.EMPTY) StackPane.setAlignment(icon, Pos.CENTER_LEFT); StackPane.setAlignment(label, Pos.CENTER_RIGHT); § Le conteneur StackPane peut être utile pour créer un composant complexe à partir d'éléments existants en les superposant (placer du texte au dessus d'une forme ou d'une image, combiner des éléments graphiques, etc.). – Jacques BAPST § La zone occupée par un composant peut s'étendre (span) sur plusieurs lignes et/ou sur plusieurs colonnes. § Le nombre de lignes et de colonnes de la grille est déterminé automatiquement par les endroits où sont placés les composants. StackPane.setMargin(label, new Insets(5)); IHM-1 – FX04 § La grille peut être irrégulière, la hauteur des lignes et la largeur des colonnes de la grille ne sont pas nécessairement uniformes. 49 § Par défaut, la hauteur de chaque ligne est déterminée par la hauteur préférée du composant le plus haut qui s'y trouve. § Par défaut, la largeur de chaque colonne est déterminée par la largeur préférée du composant le plus large qui s'y trouve. IHM-1 – FX04 StackPane [3] GridPane [2] § Exemple (état initial et après redimensionnement de la fenêtre) : § Exemple simple avec illustration du découpage (structure de la grille). private StackPane root = new StackPane(); private Label private Button private Rectangle lblA btnS rect = new Label("This is a label"); = new Button("This is a button"); = new Rectangle(80, 80, Color.YELLOW); Column 0 primaryStage.setTitle("Test StackPane"); Padding 51 – Jacques BAPST Column 1 HGap StackPane.setAlignment(lblA, Pos.CENTER_LEFT); StackPane.setAlignment(btnS, Pos.CENTER_RIGHT); StackPane.setMargin(btnS, new Insets(5)); Spanning (2 columns) Alignment (center) root.getChildren().addAll(rect, lblA, btnS); Margin (bottom) primaryStage.setScene(new Scene(root, 200, 100)); primaryStage.show(); Row 0 Row 1 VGap Row 2 Padding Alignment (right) IHM-1 – FX04 – Jacques BAPST 50 IHM-1 – FX04 – Jacques BAPST 52 GridPane [3] GridPane [5] § Pour placer les composants dans un conteneur GridPane , on utilise la méthode add() qui permet de passer en paramètre le composant ainsi que les contraintes principales de placement : § Il est possible de placer plusieurs composants dans une même cellule de la grille. • Indice de la colonne et de la ligne (numérotation commence à zéro) • Nombre de colonnes et de lignes de spanning (par défaut : 1) • Par exemple, pour l'interface de la page précédente : root.add(lblTitle, 0, 0, 2, 1); root.add(lblUsername, 0, 1); ð • Position : Modifier les propriétés columnIndex et rowIndex • Spanning : Modifier les propriétés columnSpan et rowSpan // Title (2 cols spanning) root.add(tfdUsername, 1, 1); // Username text-field // Password label // Password text-field § La taille des lignes et des colonnes de la grille peut être gérée • Par des contraintes de lignes et de colonnes qui s'appliquent pour tous les composants placés dans la ligne ou la colonne concernée • Par des contraintes individuelles, appliquées aux composants placés dans la grille • Les contraintes individuelles sont prioritaires sur celles appliquées globalement aux lignes et aux colonnes. § Les composants peuvent être ajoutés dans n'importe quel ordre. § On peut ajouter dans un GridPane n'importe quel objet de type Node, donc également des conteneurs (ð imbrication). IHM-1 – FX04 – Jacques BAPST Les composants seront empilés, le dernier ajouté sera "au-dessus" § Les contraintes de placement des composants peuvent être modifiées dynamiquement (durant le cours du programme) : // Username label root.add(lblPassword, 0, 2); root.add(pwfPassowrd, 1, 2); • Ils s'afficheront comme dans un conteneur StackPane 53 IHM-1 – FX04 – Jacques BAPST GridPane [4] GridPane [6] § Quelques propriétés importantes du conteneur GridPane : § Les contraintes globales de lignes/colonnes sont définies dans des objets de type : • hgap : Espacement horizontal entre les colonnes • vgap : Espacement vertical entre les lignes • alignment : Alignement de la grille dans le conteneur (si elle n'occupe pas tout l'espace) • padding : Espacement autour de la grille (marge) 55 : Pour les lignes • ColumnConstraints : Pour les colonnes • RowConstraints § Les contraintes globales sont ensuite associées aux lignes/colonnes du GridPane en les ajoutant dans une liste, avec les méthodes : • gridLinesVisible : Affichage des lignes de construction de la grille. • getRowConstraints.add(row_constraint) • getColumnConstraints.add(column_constraint) Très utile pour la mise au point (debugging) de l'interface. • L'ordre des ajouts correspond à l'ordre des lignes/colonnes § Exemple : ColumnConstraints ctCol0 = new ColumnConstraints(50, 100, 200, Priority.ALWAYS, HPos.CENTER, true ); root.getColumnConstraints().add(ctCol0); IHM-1 – FX04 – Jacques BAPST 54 IHM-1 – FX04 – Jacques BAPST 56 GridPane [7] GridPane [9] § Les contraintes de lignes RowConstraints possèdent les propriétés suivantes : § On peut également appliquer des contraintes individuelles aux composants placés dans un GridPane. Ces contraintes sont prioritaires sur les contraintes de lignes et colonnes. : • prefHeight : • maxHeight : • percentHeight : Hauteur minimale souhaitée pour la ligne Hauteur préférée (idéale) pour la ligne Hauteur maximale souhaitée pour la ligne Hauteur de la ligne en pourcent de la hauteur de la grille (est prioritaire sur min-, pref- et maxHeight) : Alignement par défaut des composants dans la ligne (de type énuméré VPos : TOP, CENTER, BOTTOM, BASELINE) : Priorité d'agrandissement vertical (de type énuméré Priority : ALWAYS, SOMETIMES, NEVER) • minHeight • valignment • vgrow : Booléen indiquant si le composant doit s'agrandir (true) jusqu'à sa hauteur maximale ou alors garder sa hauteur préférée (false). Par défaut : true. • fillHeight IHM-1 – FX04 – Jacques BAPST 57 GridPane [8] : • prefWidth : • maxWidth : • percentWidth : • halignment • hgrow • fillWidth IHM-1 – FX04 : Alignement horizontal du composant passé en paramètre (HPos : LEFT, CENTER, RIGHT) • valignement : Alignement vertical du composant passé en paramètre (VPos : TOP, CENTER, BOTTOM, BASELINE) • hgrow : Priorité d'agrandissement horizontal (Priority : ALWAYS, SOMETIMES, NEVER) • vgrow : Priorité d'agrandissement vertical (Priority : ALWAYS, SOMETIMES, NEVER) • margin : Marge autour du composant (de type Insets) • halignment IHM-1 – FX04 – Jacques BAPST 59 GridPane et HBox [1] § Les contraintes de colonnes ColumnConstraints possèdent les propriétés suivantes : • minWidth § Les contraintes sont appliquées en invoquant des méthodes statiques de GridPane qui permettent de gérer les propriétés suivantes : Largeur minimale souhaitée de la colonne Largeur préférée (idéale) de la colonne Largeur maximale souhaitée de la colonne Largeur de la colonne en pourcent de la largeur de la grille (est prioritaire sur min-, pref- et maxWidth) : Alignement par défaut des composants dans la colonne (de type énuméré HPos : LEFT, CENTER, RIGHT) : Priorité d'agrandissement horizontal (de type énuméré Priority : ALWAYS, SOMETIMES, NEVER) : Booléen indiquant si le composant doit s'agrandir (true) jusqu'à sa largeur maximale ou alors garder sa largeur préférée (false). Par défaut : true. – Jacques BAPST 58 private GridPane private HBox root btnPanel = new GridPane(); = new HBox(12); private private private private private private private lblTitle lblUsername tfdUsername lblPassword btnLogin btnCancel pwfPassword = = = = = = = Label Label TextField Label Button Button PasswordField new new new new new new new Label("JafaFX Course Login"); Label("Username or email:"); TextField(); Label("Password:"); Button("Login"); Button("Cancel"); PasswordField(); primaryStage.setTitle("Login Panel"); //--- Title lblTitle.setFont(Font.font("System", FontWeight.BOLD, 20)); lblTitle.setTextFill(Color.rgb(80, 80, 180)); root.add(lblTitle, 0, 0, 2, 1); GridPane.setHalignment(lblTitle, HPos.CENTER); GridPane.setMargin(lblTitle, new Insets(0, 0, 10,0)); IHM-1 – FX04 – Jacques BAPST 60 GridPane et HBox [2] Résumé des conteneurs [1] //--- Username (label and text-field) tfdUsername.setPrefColumnCount(20); root.add(lblUsername, 0, 1); root.add(tfdUsername, 1, 1); GridPane.setHalignment(lblUsername, HPos.RIGHT); //--- Password (label and text-field) pwfPassword.setPrefColumnCount(12); root.add(lblPassword, 0, 2); root.add(pwfPassword, 1, 2); GridPane.setHalignment(lblPassword, HPos.RIGHT); GridPane.setFillWidth(pwfPassword, false); //--- Button panel btnPanel.getChildren().add(btnLogin); btnPanel.getChildren().add(btnCancel); btnPanel.setAlignment(Pos.CENTER_RIGHT); root.add(btnPanel, 1, 3); GridPane.setMargin(btnPanel, new Insets(10, 0, 0,0)); Classe Description HBox, VBox Place les composants horizontalement (sur une ligne) ou verticalement (dans une colonne). FlowPane (horizontal) Place les composants horizontalement sur une ligne et passe à la ligne suivante s'il n'y a plus assez de place dans le conteneur (linewrapping). FlowPane (vertical) Place les composants verticalement (de haut en bas), en colonne et passe à la colonne suivante s'il n'y a plus assez de place dans le conteneur (column-wrapping). TilePane (horizontal) Place les composants dans une grille dont les cellules (les tuiles) ont toutes la même taille. Les composants sont ajoutés horizontalement, ligne par ligne. TilePane (horizontal) Place les composants dans une grille dont les cellules (les tuiles) ont toutes la même taille. Les composants sont ajoutés verticalement, colonne par colonne. setGridLinesVisible(true) IHM-1 – FX04 – Jacques BAPST 61 GridPane et HBox [3] IHM-1 – FX04 //--- GridPane properties root.setAlignment(Pos.CENTER); root.setPadding(new Insets(20)); root.setHgap(10); root.setVgap(15); Classe Description BorderPane Dispose de cinq emplacements pour placer les composants : Top, Bottom, Left, Right, Center. AnchorPane Place les composants en respectant une contrainte de distance par rapport à un ou plusieurs bords du conteneur. StackPane Place les composants les uns au dessus des autres (empilement). Le dernier ajouté est placé au-dessus des autres. GridPane Place les composants dans une grille potentiellement irrégulière (par défaut, la taille des lignes et des colonnes est déterminée par le plus grand composant qui y est placé. Les composants sont ajoutés en donnant l'indice de la colonne et de la ligne (la numérotation commence à zéro). La zone d'un composant n'est pas limitée à une seule cellule, elle peut s'étendre sur plusieurs colonnes et plusieurs lignes (spanning). Un composant peut s'agrandir pour occuper toute sa zone. primaryStage.setMinWidth(300); primaryStage.setMinHeight(200); root.setGridLinesVisible(true); // Uncomment to display grid lines primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX04 – Jacques BAPST 63 Résumé des conteneurs [2] //--- Column global constraints ColumnConstraints ctCol0 = new ColumnConstraints(); // No constraint ColumnConstraints ctCol1 = new ColumnConstraints(50, 200, 400, Priority.ALWAYS, HPos.LEFT, true); root.getColumnConstraints().add(ctCol0); root.getColumnConstraints().add(ctCol1); // – Jacques BAPST 62 IHM-1 – FX04 – Jacques BAPST 64 Suppression de composants § Pour supprimer un ou plusieurs composants d'un conteneur, il faut invoquer la méthode remove(node) ou removeAll(node1, node2, …) sur la liste des composants du conteneur (retournée par la méthode getChildren()). § Si l'on souhaite remplacer un composant par un autre, il faut supprimer l'ancien composant et ajouter ensuite le nouveau. § Après une modification dynamique du graphe de scène (ajout ou suppression de composants), il est souvent utile d'invoquer la méthode sizeToScene() pour forcer le redimensionnement de la fenêtre principale (stage) en prenant en compte les changements effectués. . . . root.getChildren().remove(btnSave); root.getChildren().add(cbxMail); // Replace button 'Save' // by checkbox 'Mail' primaryStage.sizeToScene(); // Resize window (pack) . . . IHM-1 – FX04 – Jacques BAPST 65 Composants – Controls [2] § Les composants ont tous pour classe parente Control qui est une sous-classe de Node. Cours IHM-1 JavaFX § Une version simplifiée des dépendances entre les différentes classes est illustrée par le diagramme suivant : 5 - Composants de base Création et utilisation Jacques BAPST [email protected] ... IHM-1 – FX05 – Jacques BAPST 3 Composants – Controls [1] Composants – Controls [3] § La librairie JavaFX offre un ensemble de composants (kit de développement) pour créer les interfaces utilisateurs graphiques. § Certains composants comme ScrollPane ou SplitPane jouent, en partie, un rôle de conteneur mais, formellement, ils ne font pas partie de cette famille (ils héritent de Control et non de Pane). § Ces composants d'interface sont fréquemment nommés controls dans la documentation en anglais (parfois widgets). § Dans ce cours, nous utiliserons généralement le terme composant pour parler des éléments qui servent à afficher des informations ou permettre à l'utilisateur d'interagir avec l'application. • Libellés, icônes, boutons, champs-texte, menus, cases à cocher, etc. – Jacques BAPST § Dans ce chapitre, nous présenterons quelques composant simples et décrirons la manière de les créer et de les utiliser. § Une fois que l'on a compris le principe de fonctionnement, il est plus facile de consulter la documentation officielle et de découvrir les propriétés et les comportements des composants offerts par la librairie JavaFX (les mêmes principes de base sont appliqués partout). § Bien qu'ils constituent les deux des nœuds (node) du graphe de scène, les composants sont à distinguer des conteneurs (layoutpanes) qui servent à disposer les composants et qui ne sont pas directement visibles dans l'interface (les bordures et les couleurs d'arrière-plan permettent cependant de révéler leur présence). IHM-1 – FX05 § On fait parfois la distinction entre composants simples (labels, champs texte, boutons, …) et composants complexes (tables, arbres, media-player, navigateur, …). § Le cours ne décrira donc pas en détail l'ensemble des composants. Le support de cours ne constitue pas un manuel de référence et il faut, en complément, consulter la documentation disponible. 2 IHM-1 – FX05 – Jacques BAPST 4 Composants avec libellés [1] Composants avec libellés [3] § De nombreux composants affichent et gèrent des textes (libellés, boutons, cases à cocher, etc.). graphic § Les comportements communs de ces composants sont gérés par la classe parente Labeled. contentDisplay graphicTextGap Certains composants qui héritent aussi de Labeled ne sont pas mentionnés dans ce diagramme. Par exemple Cell ou TitledPane. mnemonicParsing textOverrun labelPadding * ellipsisString * IHM-1 – FX05 – Jacques BAPST 5 Autre composant (type Node) qui accompagne le texte. Généralement un graphique, une image ou une icône. Position du composant additionnel (graphic) par rapport au texte. Type énuméré ContentDisplay (LEFT, RIGHT, TOP, BOTTOM, TEXT_ONLY, GRAPHIC_ONLY). Espacement entre le texte et le composant additionnel (graphic). Type Double. Active le parsing des mnémoniques dans le texte (le caractère qui suit le caractère '_'). Type Boolean. Comportement si le texte est trop long pour être affiché. Type énuméré OverrunStyle (ELLISPSIS, CLIP, …). Définit l'espace autour du texte (et du graphique éventuel). Type Insets. Chaîne de caractères utilisée lorsque le texte est tronqué (ellipsis). Par défaut : "…" property : Cette couleur est utilisée pour les propriétés en lecture seule (read-only) IHM-1 – FX05 – Jacques BAPST 7 Composants avec libellés [2] Label [1] § Les textes de ces composants peuvent être accompagnés d'un autre composant, généralement un graphique, une image ou une icône. § Le composant Label représente un libellé (= un texte non éditable). § Quelques propriétés communes aux composants Labeled : text font textFill underline alignment wrapText textAlignment lineSpacing IHM-1 – FX05 Texte affiché (String). Police de caractères (famille, style, taille, …), type Font. Couleur du texte, uniforme ou avec gradient (type Paint). Indique si le texte doit être souligné (type Boolean). Alignement général du texte (et du graphique éventuel) dans la zone (type Pos). Valable seulement si texte sur une seule ligne. Booléen qui définit si le texte passe à la ligne suivante lorsqu'il atteint la limite de la zone. Le caractère ' \n' peut également être inséré pour forcer un retour à la ligne (inconditionnel). Alignement des lignes si le texte est multiligne. Type énuméré TextAlignment (LEFT, RIGHT, CENTER, JUSTIFY). Espacement des lignes pour les textes multilignes. Type Double. – Jacques BAPST 6 § Les constructeurs permettent de définir le contenu du texte et de l'éventuel composant additionnel (graphic). • new Label("Hello"); • new Label("Warning", warningIcon); § L'essentiel des fonctionnalités sont héritées de Labeled. Une seule propriété additionnelle se trouve dans Label : • setLabelFor : Permet de définir un (autre) composant (Node) auquel le libellé est associé (utile pour définir un mnémonique). Remarque : Les objets de type Text possèdent certaines similitudes avec les composants de type Label mais Text n'est pas une sous-classe de Control. Cette classe fait partie de la famille des graphiques (sous-classe de Shape) et possède donc des propriétés et des fonctionnalités un peu différentes. IHM-1 – FX05 – Jacques BAPST 8 Label [2] Button [2] § Quelques propriétés du composant Button : private HBox root = new HBox(20); private Label private Label private Label lblA = new Label("Hello"); lblB = new Label("Big and colored"); lblC = new Label("A Multiline\nText\nLabel"); armed primaryStage.setTitle("Label Test"); onAction root.setAlignment(Pos.CENTER); root.setPadding(new Insets(10)); root.getChildren().add(lblA); lblB.setFont(Font.font("SansSerif", FontWeight.BOLD, 20)); lblB.setTextFill(Color.rgb(180, 50, 50)); lblB.setGraphic(new Rectangle(50, 5, Color.ORANGE)); lblB.setContentDisplay(ContentDisplay.BOTTOM); root.getChildren().add(lblB); root.getChildren().add(lblC); cancelButton defaultButton primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 – Jacques BAPST 9 Booléen qui indique si le bouton est "armé" et prêt à déclencher une action (par exemple souris placée sur le bouton et touche gauche pressée). Détermine l'événement à générer lorsque l'action du bouton est déclenchée (par ex. lorsque la touche de la souris a été relâchée). Type EventHandler<ActionEvent>. Booléen qui indique si le bouton est un bouton Cancel c'est-àdire que l'action du bouton doit être déclenchée lorsque l'utilisateur presse sur la touche Escape (VK_ESC). Cette propriété ne doit être appliquée qu'à un seul bouton de l'interface. Booléen qui indique si le bouton est un bouton par défaut c'està-dire que l'action du bouton doit être déclenchée lorsque l'utilisateur presse sur la touche Enter (VK_ENTER). Cette propriété ne doit être appliquée qu'à un seul bouton de l'interface. IHM-1 – FX05 – Jacques BAPST Button [1] Button [3] § Le composant Button représente un bouton permettant à l'utilisateur de déclencher une action. § La manière de traiter l'événement généré par le bouton sera expliquée dans un prochain chapitre consacré à la gestion des événements et à l'écriture de contrôleurs qui se chargeront d'exécuter du code associé aux différents éléments actifs de l'interface. § La classe parente ButtonBase rassemble les propriétés communes à différents composants qui se comportent comme des boutons : Button, CheckBox, Hyperlink, MenuButton, ToggleButton. § Les constructeurs permettent de définir le contenu du texte et de l'éventuel composant additionnel (graphic). • new Button("Ok"); • new Button("Save", saveIcon); § Par héritage, toutes les propriétés qui ont été mentionnées pour les composants avec libellés (sous-classes de Labeled) sont naturellement applicables pour le composant Button. 11 § Pour ajouter une image à un bouton (où à n'importe quel autre composant de type Labeled), on peut utiliser la classe ImageView (év. associée à la classe Image) qui permet de représenter une image stockée dans une ressource locale (fichier de type gif, jpeg, png, …) ou en donnant l'URL d'une image sur un serveur web (par exemple : http://www.myblog.ch/images/logo.png). § La classe ImageView permet également de redimensionner les images et d'en afficher qu'une partie (viewport). § Dans l'exemple qui suit, un des boutons (btnLogin) est affiché en associant une image au texte du libellé. IHM-1 – FX05 – Jacques BAPST 10 IHM-1 – FX05 – Jacques BAPST 12 Button [4] Saisie de textes [2] § Quelques propriétés de la classe TextInputControl : private static final String FLOGO = "/resources/EIA_FR.jpg"; private private private private private VBox Button Button Button Button root btnOk btnLogin btnSave btnMulti = = = = = new new new new new VBox(10); Button("OK"); Button("Login"); Button("Save"); Button("A Multiline\nRight-Justified\nText"); root.setAlignment(Pos.CENTER); root.setPadding(new Insets(20)); text editable font length caretPosition root.getChildren().add(btnOk); btnLogin.setTextFill(Color.BLUE); btnLogin.setFont(Font.font(null, FontWeight.BOLD, 14)); btnLogin.setGraphic(new ImageView(FLOGO)); btnLogin.setContentDisplay(ContentDisplay.TOP); root.getChildren().add(btnLogin); promptText btnSave.setDefaultButton(true); root.getChildren().add(btnSave); selectedText btnMulti.setTextAlignment(TextAlignment.RIGHT); root.getChildren().add(btnMulti); anchor IHM-1 – FX05 textFormatter selection – Jacques BAPST 13 Le texte contenu dans le composant (String). Le texte peut être édité par l'utilisateur (Boolean). Police de caractères du texte (Font). Longueur du texte (Integer). Position courante du curseur / point d'insertion (caret). Texte affiché si aucun texte n'a été défini ou saisi par l'utilisateur (String). Ce texte n'est pas affiché lorsque le composant possède le focus (avec le curseur qui clignote dans le champ). Ce texte peut éventuellement remplacer un libellé ou une bulle d'aide pour le champ texte. Formateur de texte associé au composant (TextFormatter<?>). Texte sélectionné (String). Indices (de...à) de la zone sélectionnée (IndexRange). Point d'ancrage (début) de la sélection (Integer). IHM-1 – FX05 – Jacques BAPST Saisie de textes [1] Saisie de textes [3] § La classe abstraite TextInputControl est la classe parente de différents composants qui permettent à l'utilisateur de saisir des textes. Il s'agit notamment des composants d'interface TextField, PasswordField et TextArea § Quelques méthodes de la classe TextInputControl : § La classe TextInputControl définit les propriétés de base et les fonctionnalités communes aux composants qui offrent une saisie de texte et notamment : • • • • – Jacques BAPST Efface le texte (vide le champ). copy/cut/paste() Transfert du texte dans ou depuis le clipboard. positionCaret() Positionne le curseur à une position donnée. forward() backward() nextWord() Déplace d'un caractère le curseur (caret). Déplace le curseur (caret) au début du prochain mot. insertText() Insère une chaîne de caractères dans le texte. appendText() Ajoute une chaîne de caractères à la fin du texte. deleteText() Supprime une partie du texte (de...à). deleteNextChar() Efface le prochain caractère. replaceText() Remplace une partie du texte par un autre. selectAll() Sélectionne l'ensemble du texte. deselect() Annule la sélection courante du texte. La sélection de texte L'édition de texte La gestion du curseur à l'intérieur du texte (caret) Le formatage du texte IHM-1 – FX05 clear() 15 14 IHM-1 – FX05 – Jacques BAPST 16 TextField [1] TextFormatter [1] § Le composant TextField représente un champ texte d'une seule ligne qui est éditable par défaut mais qui peut également être utilisé pour afficher du texte. § On peut associer un formateur de texte à tous les composants qui héritent de TextInputControl (propriété TextFormatter). § Le composant n'intègre pas de libellé. Il faut utiliser un composant de type Label si l'on veut lui associer un libellé. § En plus des propriétés héritées (notamment de TextInputControl), le composant TextField possède les propriétés suivantes : Alignement du texte dans le champ (Pos). Nombre de colonnes du champ texte; permet de déterminer la prefColumnCount largeur préférée du composant. La valeur par défaut est définie par la constante DEFAULT_PREF_COLUMN_COUNT (12). Détermine l'événement à générer lorsque l'action du champ onAction texte est déclenchée, en général lorsque l'utilisateur presse sur la touche Enter (EventHandler<ActionEvent>). alignment § Ce formateur est un composant de type TextFormatter<V> qui permet de définir : • Un convertisseur permettant de convertir le texte du composant en une valeur d'un autre type (par exemple un type numérique, int, double, …). • Un filtre permettant d'intercepter et de modifier les caractères saisis par l'utilisateur pendant l'édition du texte (n'accepter que les chiffres par ex.). § Le formateur peut définir un filtre ou un convertisseur ou les deux. § Le filtre et le convertisseur sont transmis dans le constructeur du formateur qui possède les surcharges suivantes : TextFormatter(StringConverter<V> valueConverter) TextFormatter(StringConverter<V> valueConverter, V defaultValue) TextFormatter(UnaryOperator<TextFormatter.Change> filter) TextFormatter(StringConverter<V> valueConverter, V defaultValue, UnaryOperator<TextFormatter.Change> filter) IHM-1 – FX05 – Jacques BAPST 17 TextField [2] private private private private private HBox Label Label TextField TextField root lblName lblMobile tfdName tfdMobile – Jacques BAPST 19 TextFormatter [2] = = = = = new new new new new § Le paramètre générique V du formateur (TextFormatter<V>) définit le type de la propriété value. On ne peut utiliser cette propriété que si l'on a défini un convertisseur dans le formateur. HBox(5); Label("Name"); Label("Mobile"); TextField(); TextField(); § Le convertisseur est un objet de type StringConverter<V> qui doit implémenter les méthodes de conversions entre les propriétés text (String) et value (V) : primaryStage.setTitle("TextField Test"); root.setAlignment(Pos.CENTER); root.getChildren().add(lblName); tfdName.setPrefColumnCount(12); tfdName.setPromptText("First and Last-Name"); root.getChildren().add(tfdName); • fromString() : text ® value • toString() : value ® text § Il existe des implémentations prédéfinies de convertisseurs pour certains types courants : root.getChildren().add(lblMobile); HBox.setMargin(lblMobile, new Insets(0,0,0,10)); tfdMobile.setPrefColumnCount(8); tfdMobile.setPromptText("Mobile Tel Nr"); root.getChildren().add(tfdMobile); • BooleanStringConverter, DoubleStringConverter, IntegerStringConverter, NumberStringConverter, DateTimeStringConverter, . . . § Si l'on souhaite gérer le format d'affichage et/ou traiter certaines erreurs, il est préférable de redéfinir les méthodes de conversion. root.setPadding(new Insets(10)); primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 IHM-1 – FX05 – Jacques BAPST 18 IHM-1 – FX05 – Jacques BAPST 20 TextFormatter [3] TextFormatter [5] § Exemple de convertisseur associé à un champ texte. § Le filtre que l'on peut greffer à un formateur est un objet de type UnaryOperator<TextFormatter.Change> : //------------------------ TextField Formatter --------------------------------TextFormatter<Double> tFmt = new TextFormatter<Double>( new StringConverter<Double>() { @Override //--- Convert text (String) to Double (NaN if not possible) public Double fromString(String text) { try { return Double.parseDouble(text); } catch (NumberFormatException e) { return Double.NaN; } } • Change • L'ajout de texte (isAdded()) • Le remplacement de texte (isReplaced()) • La suppression de texte (isDeleted()) § Les opérations disponibles sont des opérations de bas niveau qui permettent d'intervenir lors de la frappe des caractères dans le champ mais qui nécessitent plus de travail pour créer des filtres plus complexes (adresse e-mail ou numéro de téléphone valide, etc.) . } ); tfdPrice.setTextFormatter(tFmt); 21 – Jacques BAPST : classe interne de TextFormatter représentant l'état des changements effectués § La classe contient de nombreuses méthodes permettant de réagir aux changements effectués dans le texte lors de @Override //--- Convert Double to String and format (2 decimals) public String toString(Double value) { return String.format("%.2f", value); } IHM-1 – FX05 • UnaryOperator<T> : interface fonctionnelle avec la méthode abstraite Change apply(Change c) IHM-1 – FX05 – Jacques BAPST TextFormatter [4] TextFormatter [6] § Résumé du fonctionnement du convertisseur. § Exemple de filtre qui n'accepte que des chiffres ou '.' ou '-'. • Exemple avec un champ texte (TextField) prévu pour gérer une valeur numérique (double). TextFormatter<Double> "56" Propriété text • Expression régulière (regex) utilisée pour rejeter les autres caractères //---------- TextField formatter accepting [0..9] or dot or dash --------------TextFormatter<String> tFmtNb = new TextFormatter<String>(change -> { change.setText(change.getText().replaceAll("[^0-9.-]", "")); return change; }); tfdTemperature.setTextFormatter(tFmtNb); Ÿ Converter (String « Double) Ÿ Filter 56.0 Propriété value String Double TextField tfd = new … Double v = fmt.getValue(); String txt = tfd.getText(); fmt.setvalue(newVal); TextFormatter<Double> fmt = new … tfd.setTextFormatter(fmt); IHM-1 – FX05 23 – Jacques BAPST § Filtre qui n'accepte que 4 caractères (sans espaces) TextFormatter<String> tFmt4 = new TextFormatter<String>(change -> { String content = change.getControlNewText(); // New text if changes accepted if (content.length()>4 || change.getText().equals(' ')) { change.setText(""); // Rejected --> no change } return change; }); tfdCode.setTextFormatter(tFmt4); 22 IHM-1 – FX05 – Jacques BAPST 24 Label - TextField - Button [1] Label - TextField - Button [3] § L'exemple qui suit illustre une utilisation des composants Label, TextField et Button qui sont assemblés pour créer un panneau de login simple. . . . //--- Print username/password on button click btnLogin.setOnAction(event -> System.out.println("Login: " + tfdUser.getText() + " / " + tfdPass.getText())); § La propriété disable (héritée de Node) permet d'activer ou de désactiver un composant de l'interface. Un composant désactivé reste visible mais n'est plus actif (il est généralement 'grisé'). § Une liaison (binding) est effectuée entre la propriété text (contenu du champ texte) et la propriété disable en utilisant des opérations intermédiaires (isEmpty() et or()). Voir code surligné. primaryStage.setScene(new Scene(root)); primaryStage.setTitle("Login Panel"); primaryStage.show(); } § De cette manière, le champ Password est désactivé tant que le champ Username est vide. D'autre part, le bouton n'est activé que si les deux champs texte sont remplis. § Une manière simple et élégante de gérer l'interaction et d'éviter des traitements d'erreurs. IHM-1 – FX05 – Jacques BAPST 25 Label - TextField - Button [2] private private private private private private GridPane Label Label TextField PasswordField Button root lblUser lblPass tfdUser tfdPass btnLogin = = = = = = new new new new new new Disabled IHM-1 – FX05 – Jacques BAPST 27 PasswordField § Le composant PasswordField est une sous-classe de TextField et permet de saisir un texte sans que celui-ci soit affiché dans le champ texte (il est remplacé par des caractères de substitution). GridPane(); Label("Username:"); Label("Password:"); TextField(); PasswordField(); Button("Login"); § C'est un composant qui sert principalement à saisir des mots-depasse ou d'autres informations à caractère confidentiel. public void start(Stage primaryStage) { tfdPass.disableProperty().bind(tfdUser.textProperty().isEmpty()); btnLogin.disableProperty().bind(tfdUser.textProperty().isEmpty() .or(tfdPass.textProperty().isEmpty())); root.setHgap(6); root.setVgap(12); root.setPadding(new Insets(15)); root.add(lblUser, 0, 0); root.add(tfdUser, 1, 0); root.add(lblPass, 0, 1); root.add(tfdPass, 1, 1); root.add(btnLogin, 0, 2, 2, 1); GridPane.setHalignment(btnLogin, HPos.CENTER); GridPane.setMargin(btnLogin, new Insets(10, 0, 0, 0)); Disabled § Le composant PasswordField ne comporte ni propriété, ni méthode supplémentaires par rapport au composant TextField. Il s'utilise donc strictement de la même manière. § Les méthodes cut() et copy() sont sans effet pour ce composant (ainsi, l'utilisateur ne pourra pas récupérer le mot de passe par copier / coller). . . . IHM-1 – FX05 – Jacques BAPST 26 IHM-1 – FX05 – Jacques BAPST 28 TextArea [1] TextArea [3] § Le composant TextArea permet d'afficher et de saisir du texte dans un champ multiligne (une zone de texte). § Le texte peut être renvoyé à la ligne automatiquement (wrapping) et des barres de défilement (scrollbar) horizontales et/ou verticales sont ajoutées automatiquement si la taille du composant ne permet pas d'afficher l'entier du texte (lignes ou colonnes tronquées). § Tous les caractères du texte possèdent les mêmes attributs (police, style, taille, couleur, …). § Si le texte doit pouvoir être formaté de manière plus riche, il faut utiliser le composant HTMLEditor qui permet d'éditer le texte en appliquant certains formatages sur des parties sélectives du texte. private VBox private TextArea private Button root = new VBox(10); txaA = new TextArea(); btnB = new Button("Print"); primaryStage.setTitle("TextArea Test"); root.setAlignment(Pos.CENTER); root.setPadding(new Insets(10)); txaA.setWrapText(true); txaA.setPrefColumnCount(14); txaA.setPrefRowCount(8); root.getChildren().add(txaA); root.getChildren().add(btnB); btnB.setOnAction(event -> { System.out.println(txaA.getText()); }); primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 – Jacques BAPST 29 IHM-1 – FX05 – Jacques BAPST TextArea [2] ToggleButton [1] § Quelques propriétés de la classe TextArea : § Le composant ToggleButton représente un bouton bistable. Il comporte donc deux états : un clic le met à l'état sélectionné (on), un nouveau clic le remet à l'état désélectionné (off). Nombre de colonnes de la zone de texte; permet de déterminer prefColumnCount la largeur préférée du composant. La valeur par défaut est définie par la constante DEFAULT_PREF_COLUMN_COUNT (40). prefRowCount wrapText scrollLeft scrollTop IHM-1 – FX05 Nombre de lignes de la zone de texte; permet de déterminer la hauteur préférée du composant. La valeur par défaut est définie par la constante DEFAULT_PREF_ROW_COUNT (10). Booléen qui définit si le texte passe à la ligne suivante lorsqu'il atteint la limite de la zone. Le caractère '\n' peut également être inséré pour forcer un retour à la ligne (inconditionnel). Par défaut : false Détermine de combien de pixels le texte est scrollé horizontalement (Double). Détermine de combien de pixels le texte est scrollé verticalement (Double). – Jacques BAPST 30 31 § Visuellement, il se présente généralement de la même manière qu'un composant Button et, comme lui, hérite de ButtonBase. § On peut former des groupes de ToggleButton en associant les boutons concernés à un ToggleGroup. • ToggleGroup groupA = new ToggleGroup(); • tButton.setToggleGroup(groupA); § Dans un tel groupe, un seul bouton peut être sélectionné. Si on en sélectionne un, celui qui était préalablement sélectionné sera automatiquement désélectionné. § Dans un groupe, il est possible qu'aucun bouton ne soit sélectionné (contrairement au composant RadioButton). IHM-1 – FX05 – Jacques BAPST 32 ToggleButton [2] RadioButton [1] § Par défaut, les boutons ToggleButton ne sont pas enregistrés dans un groupe (plusieurs boutons peuvent donc être simultanément à l'état sélectionné). § Le composant RadioButton est une sous-classe de ToggleButton et représente une option que l'utilisateur peut sélectionner (généralement parmi un groupe d'options). § Les constructeurs de la classe permettent de définir le contenu du texte et d'un éventuel composant additionnel (graphic), comme pour le bouton ordinaire. § L'utilisation du composant RadioButton doit être réservée à la situation où l'utilisateur doit choisir une seule option parmi plusieurs (même si le composant n'impose pas cette sémantique). • new ToggleButton("Sound"); • new ToggleButton("Sound", speakerImage); § On placera donc systématiquement tous les RadioButton dans des groupes de type ToggleGroup afin d'obtenir ce comportement (sélection mutuellement exclusive). § Quelques propriétés de la classe ToggleButton : selected toggleGroup Indique si le bouton est sélectionné ou non (Boolean). Enregistre le groupe auquel appartient le bouton (ToggleGroup). IHM-1 – FX05 – Jacques BAPST 33 ToggleButton [3] private private private private HBox ToggleButton ToggleButton ToggleButton root tbnGif tbnJpg tbnPng = = = = new new new new § Par défaut aucun bouton radio n'est sélectionné au départ mais dès que l'un d'eux est sélectionné on ne peut plus revenir dans cet état initial "sans sélection" (contrairement au composant ToggleButton). IHM-1 – FX05 – Jacques BAPST 35 RadioButton [2] § Les constructeurs permettent de définir le contenu du texte et d'un éventuel composant additionnel (graphic), comme pour le bouton ordinaire. HBox(10); ToggleButton("GIF"); ToggleButton("JPEG"); ToggleButton("PNG"); • new RadioButton("Down"); • new RadioButton("Up", upArrowImage); primaryStage.setTitle("ToggleButton Test"); root.setAlignment(Pos.CENTER); root.setPadding(new Insets(20)); § Le composant RadioButton ne possède pas de propriétés spécifiques (elles sont héritées de ToggleButton). ToggleGroup tg = new ToggleGroup(); tbnGif.setToggleGroup(tg); tbnJpg.setToggleGroup(tg); tbnPng.setToggleGroup(tg); § Pour rappel, deux propriétés importantes : selected root.getChildren().add(tbnGif); root.getChildren().add(tbnJpg); root.getChildren().add(tbnPng); primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 • ToggleGroup grpLanguage = new ToggleGroup(); • rbnEnlish.setToggleGroup(grpLanguage); Avec le style Modena, la différence visuelle entre un bouton sélectionné et un non-sélectionné est faible. – Jacques BAPST 34 toggleGroup IHM-1 – FX05 Indique si le bouton radio est sélectionné ou non (Boolean). Enregistre le groupe auquel appartient le bouton radio (ToggleGroup). – Jacques BAPST 36 RadioButton [3] private private private private VBox RadioButton RadioButton RadioButton root rbnGif rbnJpg rbnPng = = = = new new new new CheckBox [2] § La classe dispose des constructeurs suivants : VBox(10); RadioButton("Red"); RadioButton("Green"); RadioButton("Blue"); • new CheckBox(); • new CheckBox("Mute"); primaryStage.setTitle("RadioButton Test"); § Si l'on veut ajouter un graphique, une icône ou autre, il faut utiliser la méthode setGraphic() (héritée de Labeled). root.setPadding(new Insets(10, 75, 10, 75)); § Quelques propriétés de la classe CheckBox : ToggleGroup tg = new ToggleGroup(); rbnGif.setToggleGroup(tg); rbnJpg.setToggleGroup(tg); rbnPng.setToggleGroup(tg); Indique si la case à cocher est sélectionnée (pour autant que getIndeterminate==false) (Boolean). Indique si l'état de la case à cocher est indéterminé indeterminate (Boolean). Autorise l'état indéterminé (Boolean). Si true : Trois états possibles allowIndeterminate (désélectionné, sélectionné et indéterminé). Si false : Seulement deux états possibles (désélectionné et sélectionné). Valeur par défaut. selected root.getChildren().add(rbnGif); root.getChildren().add(rbnJpg); root.getChildren().add(rbnPng); primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 – Jacques BAPST 37 CheckBox [1] IHM-1 – FX05 – Jacques BAPST 39 CheckBox [3] § Le composant CheckBox représente une case à cocher que l'utilisateur peut sélectionner ou désélectionner (en cliquant dessus par exemple). § Ce composant est typiquement utilisé lorsque l'utilisateur peut choisir parmi plusieurs options qui peuvent être simultanément activées. private private private private private private VBox TitledPane CheckBox CheckBox CheckBox CheckBox vbx root chbHci chbAlgo chbMl chbCp = = = = = = new new new new new new VBox(10); TitledPane("Elective Courses", vbx); CheckBox("Human-Computer Interaction"); CheckBox("Algorithms and Data-Structure"); CheckBox("Machine Learning"); CheckBox("Concurrent Programming"); primaryStage.setTitle("CheckBox Test"); § La case à cocher peut potentiellement se trouver dans trois états différents. Dans ce cas, chaque clic fera passer le composant à l'état suivant : • Désélectionné • Sélectionné • Indéterminé / non-défini root.setCollapsible(false); vbx.setPadding(new Insets(15)); TitledPane : un panneau avec un titre chbHci.setAllowIndeterminate(true); chbAlgo.setAllowIndeterminate(true); chbMl.setAllowIndeterminate(true); chbCp.setAllowIndeterminate(true); vbx.getChildren().add(chbHci); vbx.getChildren().add(chbAlgo); vbx.getChildren().add(chbMl); vbx.getChildren().add(chbCp); primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX05 – Jacques BAPST 38 IHM-1 – FX05 – Jacques BAPST 40 Hyperlink [1] Hyperlink [3] § Le composant Hyperlink se présente comme un lien hypertexte HTML dans une page web. Il agit comme un bouton lorsqu'on clique sur le texte associé et peut déclencher n'importe quelle action (comme un bouton). § Dans l'exemple ci-après, les textes des liens (captions), les URL ainsi que les composants Hyperlink sont enregistrés dans des tableaux parallèles (liés par l'indice). § Ce composant est une sous-classe de ButtonBase et de Labeled. Il peut donc contenir un autre composant (graphic), typiquement une image ou un graphique. § Les constructeurs permettent de définir le contenu du texte et d'un éventuel composant additionnel (graphic). • new Hyperlink("www.myblog.ch"); • new Hyperlink("Print", printerIcon); private String[] tCaption = {"FX-Experience", "JFxtras", "GUI Garage"}; private String[] tUrl = {"http://fxexperience.com", "http://jfxtras.org", "http://www.guigarage.com"}; private VBox root = new VBox(); private HBox lnkPanel = new HBox(20); private Hyperlink[] tLinks = new Hyperlink[tUrl.length]; § Le composant Hyperlink ne comporte qu'une propriété spécifique : visited Indique si le lien a déjà été visité, c'est-à-dire cliqué (Boolean). IHM-1 – FX05 – Jacques BAPST 41 Hyperlink [2] IHM-1 – FX05 Hyperlink [4] § Les trois états "non-visité", "durant le clic" et "visité" sont généralement représentés de manière distincte (couleur, souligné, …) en fonction du style de l'interface. § Si l'on souhaite que le lien hypertexte affiche le contenu d'une page web, on peut utiliser le composant WebView qui contient un moteur de rendu de pages web de type WebEngine (basé sur le projet opensource WebKit). primaryStage.setTitle("Hyperlink Test"); lnkPanel.setAlignment(Pos.CENTER); lnkPanel.setPadding(new Insets(10)); WebView browser = new WebView(); WebEngine wEngine = browser.getEngine(); lnkPanel.getChildren().add(tLinks[i]); String url = tUrl[i]; tLinks[i].setOnAction(event -> { wEngine.load(url); }); de paramétrages possibles. Leur fonctionnement n'est pas décrit en détail ici. § Si l'on se contente d'une utilisation basique, le code à produire n'est pas spécialement complexe. § L'exemple qui suit illustre l'utilisation de liens hypertextes pour afficher le contenu de pages web. – Jacques BAPST Création du navigateur for (int i=0; i<tUrl.length; i++) { tLinks[i] = new Hyperlink(tCaption[i]); § WebView et WebEngine sont des composants riches avec beaucoup IHM-1 – FX05 43 – Jacques BAPST Association de l'URL des pages } root.getChildren().add(lnkPanel); root.getChildren().add(browser); primaryStage.setScene(new Scene(root)); primaryStage.show(); 42 IHM-1 – FX05 – Jacques BAPST 44 Hyperlink [5] Tooltip [1] § Résultat à l'exécution de l'application : § Le composant Tooltip permet d'afficher une bulle d'aide ou bulle d'information lorsque l'utilisateur s'arrête sur un des éléments de l'interface avec le curseur de la souris (ou autre dispositif de pointage). • Écran initial (liens hypertextes au haut de la fenêtre) • Résultat après avoir cliqué sur le lien FX-Experience § Ce composant est très courant et permet souvent de faciliter la compréhension de l'interface et le guidage de l'utilisateur. § Le constructeur permet de définir le contenu du texte (qui peut aussi changer dynamiquement avec setText()). • new Tooltip("Enter your password"); § Le texte du tooltip peut être affiché sur plusieurs lignes (avec retour à la ligne imposés ou wrapping automatique). § La propriété graphic permet d'ajouter au texte un composant additionnel (icône, graphique, …) dont le positionnement peut être précisé. IHM-1 – FX05 – Jacques BAPST 45 IHM-1 – FX05 – Jacques BAPST 47 Hyperlink [6] Tooltip [2] § Et si l'on clique sur chacun des deux autres liens, on atteint les pages associées. § La fenêtre de l'application est redimensionnable et on peut naviguer dans les pages web (on dispose d'un véritable navigateur, mais sans certaines commodités : bouton back, barre d'adresse, onglets, etc.). § Un tooltip peut être ajouté à n'importe quel composant UI standard (sous-classe de Control) avec la méthode : • setToolTip(tooltip); § Pour les autres éléments d'un graphe de scène (conteneur, graphique, chart, …), un tooltip peut également être ajouté en utilisant la méthode statique : • Tooltip.install(node, tooltip); § Un même composant Tooltip peut être associé à plusieurs composants différents (c'est un composant partageable). § Si plusieurs composants se superposent dans l'interface (partiellement ou totalement), l'activation des bulles d'aide n'est pas clairement définie. IHM-1 – FX05 – Jacques BAPST 46 IHM-1 – FX05 – Jacques BAPST 48 Tooltip [3] § Quelques propriétés de la classe Tooltip : text textAlignment font wrapText graphic graphicTextGap contentDisplay textOverrun activated Texte à afficher dans la bulle (String). Des retours à la ligne ('\n') peuvent être insérés. Alignement du texte (si multiligne) (TextAlignment). Police de caractères du texte (Font). Booléen qui indique si des retours à la ligne automatiques doivent intervenir lorsque la place manque. Autre composant (type Node) qui accompagne le texte. Généralement un graphique, une image ou une icône. Espace entre le graphique et le texte (Double). Positionnement du graphique relativement au texte (ContentDisplay). Spécifie le comportement si le texte dépasse la place allouée (OverrunStyle). Booléen qui passe à true dès que le curseur s'arrête sur le composant associé (tooltip pas nécessairement déjà affiché). IHM-1 – FX05 – Jacques BAPST 49 Tooltip [4] private private private private private private Rectangle Button Button Tooltip Tooltip Tooltip rect btnLogin btnSave ttpRect ttpLogin ttpSave = = = = = = new new new new new new Rectangle(180, 15, Color.ORANGE); Button("Login"); Button("Save"); Tooltip("Click to change the color"); Tooltip("Open a new session"); Tooltip("Save state on cloud"); . . . ttpRect.setGraphic(new ImageView(ICON)); Tooltip.install(rect, ttpRect); root.getChildren().add(rect); btnLogin.setTooltip(ttpLogin); root.getChildren().add(btnLogin); btnSave.setTooltip(ttpSave); root.getChildren().add(btnSave); . . . IHM-1 – FX05 – Jacques BAPST 50 Architecture MVC [1] § Il existe différentes manières de structurer le code des applications interactives (celles qui comportent une interface utilisateur). Cours IHM-1 JavaFX § Une des architectures, communément utilisée, et qui comporte de nombreuses variantes, est connue sous l'acronyme MVC qui signifie Model - View - Controller. 6 - Architecture MVC Gestion des événements § Dans cette architecture on divise le code des applications en entités distinctes (modèles, vues et contrôleurs) qui communiquent entre elles au moyen de divers mécanismes (invocation de méthodes, génération et réception d'événements, etc.). § Cette architecture (ou modèle de conception, design pattern) a été introduite avec le langage Smalltalk-80 dans le but de simplifier le développement ainsi que la maintenance des applications, en répartissant et en découplant les activités dans différents soussystèmes (plus ou moins) indépendants. Jacques BAPST [email protected] IHM-1 – FX06 – Jacques BAPST 3 Architecture MVC [2] § Le principe de base de l'architecture MVC est relativement simple, on divise le code du système interactif en trois parties distinctes : • Le ou les modèles (Models) qui se chargent de la gestion des données (accès, transformations, calculs, etc.). Le modèle enregistre (directement ou indirectement) l'état du système et le tient à jour. • Les vues (Views) qui comprennent tout ce qui touche à l'interface utilisateur (composants, fenêtres, boîtes de dialogue) et qui a pour tâche de présenter les informations (visualisation). Les vues participent aussi à la détection de certaines actions de l'utilisateur (clic sur un bouton, déplacement d'un curseur, geste swipe, saisie d'un texte, …). • Les contrôleurs (Controllers) qui sont chargés de réagir aux actions de l'utilisateur (clavier, souris, gestes) et à d'autres événements internes (activités en tâches de fond, timer) et externes (réseau, serveur). Architecture MVC Structure d'une application § Une application peut également comporter du code qui n'est pas directement affecté à l'une de ces trois parties (librairies générales, classes utilitaires, etc.). IHM-1 – FX06 – Jacques BAPST 2 IHM-1 – FX06 – Jacques BAPST 4 Interactions MVC [1] MVC – Modèle § Un exemple de communication entre les éléments (MVC synchrone). § Le modèle (Model) est responsable de la gestion des données qui caractérisent l'état du système et son évolution. § Dans certaines situations (simples) le modèle peut contenir luimême les données mais, la plupart du temps, il agit comme un intermédiaire (proxy) et gère l'accès aux données qui sont stockées dans une base de données, un serveur d'informations, le cloud, … La vue peut consulter les données du modèle. Model View La vue détermine quels événements sont passés au contrôleur. Le contrôleur peut consulter et mettre à jour le modèle en fonction des événements. Controller Le contrôleur peut demander la mise à jour des éléments affichés. § Le modèle est souvent défini par une ou plusieurs interfaces Java qui permettent de s'abstraire de la façon dont les données (les objets métier) sont réellement stockées (notion de DAO Data Access Object). § Il offre également les méthodes et fonctions permettant de gérer, transformer et manipuler ces données. § Les informations gérées par le modèle doivent être indépendantes de la manière dont elles seront affichées. Le modèle doit pouvoir exister indépendamment de la représentation visuelle des données. Flux de données Événements IHM-1 – FX06 5 – Jacques BAPST IHM-1 – FX06 – Jacques BAPST Interactions MVC [1] MVC – Vue § Lorsqu'un utilisateur interagit avec une interface, les différents éléments de l'architecture MVC interviennent pour interpréter et traiter l'événement. § La vue (View) est chargée de la représentation visuelle des informations en faisant appel à des écrans, des fenêtres, des composants, des conteneurs (layout), des boîtes de dialogue, etc. Interface utilisateur Vue § Plusieurs vues différentes peuvent être basées sur le même modèle (plusieurs représentations possibles d'un même jeu de données). Résultats Stockage Traitements Décisions Action interprétée Utilisateur IHM-1 – FX06 Mise à jour de l'interface – Jacques BAPST § La vue intercepte certaines actions de l'utilisateur et les transmet au contrôleur pour qu'il les traite (souris, clavier, gestes, …). § La mise à jour de la vue peut être déclenchée par un contrôleur ou par un événement signalant un changement intervenu dans les données du modèle par exemple (mode asynchrone). Opérations (fonctions) Contrôleur 7 Modèle 6 § La représentation visuelle des informations affichées peut dépendre du Look-and-Feel adopté (ou imposé) et peut varier d'une plateforme à l'autre. L'utilisateur peut parfois modifier lui même le thème de présentation des informations. IHM-1 – FX06 – Jacques BAPST 8 Variantes de l'architecture MVC [1] § Le contrôleur (Controller) est chargé de réagir aux différentes actions de l'utilisateur ou à d'autres événements qui peuvent survenir. § Il existe de nombreuses déclinaisons de l'architecture MVC ainsi que des variantes dont les plus connues sont : § Dans les applications simples, le contrôleur gère la synchronisation entre la vue et le modèle (rôle de chef d'orchestre). § Le contrôleur est informé des événements qui doivent être traités et sait d'où ils proviennent. < < < § La plupart des actions étant interceptées (ou en lien) avec la vue, il existe un couplage assez fort entre la vue et le contrôleur. § Le contrôleur communique généralement avec le modèle et avec la vue. C'est le sens des transferts et le mode de communication qui caractérisent différentes variantes de l'architecture MVC. IHM-1 – FX06 – Jacques BAPST Compléme nt § Le contrôleur définit le comportement de l'application et sa logique (comment elle réagit aux sollicitations, business logic). > > > MVC – Contrôleur 9 • MVP : Model - View - Presenter • MVVM : Model - View - View-Model § Dans ces variantes le modèle et la vue sont définis de manière quasi identique. C'est le rôle du contrôleur et sa manière de communiquer avec les autres parties qui distinguent ces variantes de l'architecture MVC classique. Ces différentes variantes d'architecture ne pourront pas être explorées plus en détail dans le cadre de ce cours. Source: tomyrhymond.wordpress.com/2011/09/16/mvc-mvp-and-mvvm IHM-1 – FX06 – Jacques BAPST 11 Structure d'une application [1] § Une application JavaFX qui respecte l'architecture MVC comprendra généralement différentes classes et ressources : • Le modèle sera fréquemment représenté par une ou plusieurs classes qui implémentent généralement une interface permettant de s'abstraire des techniques de stockage des données. • Les vues seront soit codées en Java ou déclarées en FXML. Des feuilles de styles CSS pourront également être définies pour décrire le rendu. • Les contrôleurs pourront prendre différentes formes : Gestion des événements Ils peuvent être représentés par des classes qui traitent chacune un événement particulier ou qui traitent plusieurs événements en relation (menu ou groupe de boutons par exemple) ð Si le code est très court, ils peuvent parfois être inclus dans les vues, sous forme de classes locales anonymes ou d'expressions lambda. • La classe principale (celle qui comprend la méthode main()) peut faire ð l'objet d'une classe séparée ou être intégrée à la classe de la fenêtre principale (vue principale). • D'autres classes utilitaires peuvent venir compléter l'application. IHM-1 – FX06 – Jacques BAPST 10 IHM-1 – FX06 – Jacques BAPST 12 Programmation événementielle [1] Programmation événementielle [3] § La programmation des applications avec interfaces graphiques est généralement basée sur un paradigme nommé programmation événementielle (Event Programming). § En programmation événementielle, on prépare les actions (code) à exécuter en les associant aux événements que l'on souhaite traiter (enregistrement des callback) et on attend que le processus de surveillance nous avertisse en exécutant le code prévu. § Avec la programmation événementielle, ce sont les événements (généralement déclenchés par l'utilisateur, mais aussi par le système) qui pilotent l'application. Ce mode non directif convient bien à la gestion des interfaces graphiques où l'utilisateur à une grande liberté d'action (l'interface est au service de l'utilisateur et non l'inverse). § La programmation événementielle nécessite qu'un processus (en tâche de fond) surveille constamment les actions de l'utilisateur susceptibles de déclencher des événements qui pourront être ensuite traités (ou non) par l'application (contrôleurs). IHM-1 – FX06 – Jacques BAPST EventProg { Initialize(); CreateGUI(); RegisterCallback(); StartEventLoop(); Après initialisation, on crée l'interface graphique puis on associe du code à chaque événement que l'on souhaite traiter (RegisterCallback). // Thread ________________________________________________________________________________ Event Loop Thread § Dans la programmation impérative (séquence d'instructions), c'est le programme qui dirige les opérations (par exemple, il demande à l'utilisateur d'entrer des valeurs, calcule et affiche un résultat, etc.). Callback1() code1; // Button clicked Callback2() code2; // Key pressed Callback3() code3 // Window resized . . . . . . // Pinch gesture Lorsque les événements enregistrés se produisent, le code associé est automatiquement exécuté par le processus de surveillance qui tourne en tâche de fond Event Thread. } 13 IHM-1 – FX06 – Jacques BAPST Programmation événementielle [2] Événement [1] § En programmation séquentielle, une interface utilisateur (en lignes de commandes) pourrait être codée selon le pseudo-code suivant qui illustre le principe. § Un événement (event) constitue une notification qui signale que quelque chose s'est passé (un fait, un acte digne d'intérêt). SequentialProg { Initialize(); Loop { cmd = readCommand(); Switch (cmd) { Case: command1 process_cmd1(); Case: command2 process_cmd2(); Case: command3 process_cmd3(); Case: . . . . . . } } } IHM-1 – FX06 15 § Un événement peut être provoqué par : • Une action de l'utilisateur Un clic avec la souris La pression sur une touche du clavier ð Le déplacement d'une fenêtre ð Un geste sur un écran tactile ð ... ð Une boucle sans fin répète le cycle suivant : - Demande à l'utilisateur de saisir une commande (prompt) - Attend que la prochaine commande soit entrée au clavier (+décodage) - Selon la commande entrée, du code spécifique est exécuté. ð • Un changement détecté par le système Une valeur a changé (propriété) Un timer est arrivé à échéance ð Un processus a terminé un calcul ð Une information est arrivée par le réseau ð ... ð ð Lorsque le traitement est terminé, le cycle recommence (on attend la prochaine commande). – Jacques BAPST 14 IHM-1 – FX06 – Jacques BAPST 16 Événement [2] Gestion des événement [1] § En JavaFX les événements sont représentés par des objets de la classe Event ou, plus généralement, d'une de ses sous-classes. § Le traitement des événements implique les étapes suivantes : • La sélection de la cible (Target) de l'événement Événement clavier ® le composant qui possède le focus Événement souris ® le composant sur lequel se trouve le curseur ® le composant au centre de la position initiale ð Gestes continus ð § De nombreux événements sont prédéfinis (MouseEvent, KeyEvent, DragEvent, ScrollEvent, …) mais il est également possible de créer ses propres événements en créant des sous-classes de Event. ð Si plusieurs composant se trouvent à un emplacement donné c'est celui qui est "au-dessus" qui est considéré comme la cible. § Chaque objet de type "événement" comprend (au moins) les informations suivantes : • La détermination de la chaîne de traitement des événements • Le type de l'événement (EventType consultable avec getEventType()) Le type permet de classifier les événements à l'intérieur d'une même classe (par exemple, la classe KeyEvent englobe KEY_PRESSED, KEY_RELEASED, KEY_TYPED) • La source de l'événement (Object consultable avec getSource()) ð Objet qui est à l'origine de l'événement selon la position dans la chaîne de traitement des événements (event dispatch chain). • La cible de l'événement (EventTarget consultable avec getTarget()) ð Composant cible de l'événement (indépendamment de la position dans la chaîne de traitement des événements (event dispatch chain) ð IHM-1 – FX06 – Jacques BAPST 17 (Event Dispatch Chain : chemin des événements dans le graphe de scène) ð Le chemin part de la racine (Stage) et va jusqu'au composant cible en parcourant tous les nœuds intermédiaires • Le traitement des filtres d'événement (Event Filter) ð Exécute le code des filtres en suivant le chemin descendant, de la racine (Stage) jusqu'au composant cible • Le traitement des gestionnaires d'événement (Event Handler) ð Exécute le code des gestionnaires d'événement en suivant le chemin montant, du composant cible à la racine (Stage) IHM-1 – FX06 – Jacques BAPST Types d'événements Gestion des événement [2] § Chaque événement est d'un certain type (objet de type EventType). § Un exemple d'application avec son graphe de scène. § Chaque type d'événement possède un nom (getName()) et un type parent (getSuperType()). § Si l'utilisateur clique sur le bouton Insert, un événement de type Action va être déclenché et va se propager le long du chemin correspondant à la chaîne de traitement (Event Dispatch Chain). § Les types d'événement forment donc une hiérarchie. • Par exemple si on presse une touche le nom de l'événement est KEY_PRESSED et le type parent est KeyEvent.ANY. Stage § A la racine, on a Scene Event.ANY (= EventType.ROOT) BorderPane Label La figure représente une partie seulement de la hiérarchie des types d'événements. IHM-1 – FX06 19 – Jacques BAPST 18 IHM-1 – FX06 TextArea HBox Button Button Button Insert Delete Quit – Jacques BAPST 20 Gestion des événement [3] Gestion des événements [5] § L'événement se propage d'abord vers le bas, depuis le nœud racine (Stage) jusqu'à la cible (Target) - c'est-à-dire le bouton cliqué - et les filtres (Event Filter) éventuellement enregistrés sont exécutés (dans l'ordre de passage). § Pour gérer un événement (exécuter des instructions), il faut créer un récepteur d'événement (Event Listener), appelé aussi écouteur d'événement, et l'enregistrer sur les nœuds du graphe de scène où l'on souhaite intercepter l'événement et effectuer un traitement. Stage § Un récepteur d'événement peut être enregistré comme filtre ou comme gestionnaire d'événement. La différence principale entre les deux réside dans le moment où le code est exécuté : Event Capturing Phase Scene • Les filtres (filters) sont exécutés dans la phase descendante de la chaîne de traitement des événements (avant les gestionnaires) • Les gestionnaires (handlers) sont exécutés dans la phase montante de la chaîne de traitement des événements (après les filtres) BorderPane Label TextArea HBox Button Button Button Insert Delete Quit IHM-1 – FX06 – Jacques BAPST § Les filtres, comme les gestionnaires d'événements, sont des objets qui doivent implémenter l'interface fonctionnelle (générique) EventHandler<T extends Event> qui impose l'unique méthode handle(T event) qui se charge de traiter l'événement. 21 IHM-1 – FX06 – Jacques BAPST 23 Gestion des événement [4] Gestion des événements [6] § L'événement remonte ensuite depuis la cible jusqu'à la racine et les gestionnaires d'événements (Event Listener) éventuellement enregistrés sont exécutés (dans l'ordre de passage). § Pour enregistrer un récepteur d'événement sur un nœud du graphe de scène, on peut : Stage Event Bubbling Phase • Utiliser la méthode addEventHandler() que possèdent tous les nœuds (les sous-classes de Node) et qui permet d'enregistrer un gestionnaire d'événement Scene • Utiliser une des méthodes utilitaires (convenience methods) dont disposent certains composants et qui permettent d'enregistrer un gestionnaire d'événement en tant que propriété du composant. La plupart des composants disposent de méthodes nommées selon le schéma setOnEventType(EventHandler), par exemple : BorderPane Label IHM-1 – FX06 TextArea • Utiliser la méthode addEventFilter() que possèdent tous les nœuds (les sous-classes de Node) et qui permet d'enregistrer un filtre HBox Button Button Button Insert Delete Quit – Jacques BAPST 22 IHM-1 – FX06 ð setOnAction(Handler) ð setOnKeyTyped(Handler) – Jacques BAPST 24 Gestion des événements [7] Event Handling [1] § Par défaut, les événements se propagent donc le long de la chaîne de traitement (Event Dispatch Chain) en traversant le graphe de scène de la racine jusqu'au composant cible et retour. private private private private private private private § Sur chaque nœud du graphe de scène peuvent être enregistrés • un ou plusieurs filtres • un ou plusieurs gestionnaires d'événements qui se chargeront de traiter différents types d'événements avant de les propager au nœud suivant en parcourant la chaîne de traitement. § Cependant, chaque récepteur d'événement (filtre ou gestionnaire) peut interrompre la chaîne de traitement en consommant l'événement, c’est-à-dire en invoquant la méthode consume(). § Si un récepteur d'événement appelle la méthode consume(), la propagation de l'événement s'interrompt et les autres récepteurs (qui suivent dans la chaîne de traitement) ne seront plus activés. IHM-1 – FX06 – Jacques BAPST 25 = = = = = = = new new new new new new new BorderPane(); HBox(10); Label("Event Handling"); TextArea(); Button("Insert"); Button("Delete"); Button("Quit"); primaryStage.setTitle("Event Handling"); root.setPadding(new Insets(10)); //--- Title lblTitle.setFont(Font.font("System", FontWeight.BOLD, 20)); lblTitle.setTextFill(Color.DARKGREEN); BorderPane.setAlignment(lblTitle, Pos.CENTER); BorderPane.setMargin(lblTitle, new Insets(0, 0, 10, 0)); root.setTop(lblTitle); IHM-1 – FX06 27 – Jacques BAPST Event Handling [2] § Si un nœud du graphe de scène possède plusieurs récepteurs d'événements enregistrés, l'ordre d'activation de ces récepteurs sera basé sur la hiérarchie des types d'événement : //--- Button Panel btnPanel.getChildren().add(btnInsert); btnPanel.getChildren().add(btnDelete); btnPanel.getChildren().add(btnQuit); btnPanel.setAlignment(Pos.CENTER_RIGHT); btnPanel.setPadding(new Insets(10, 0, 0, 0)); root.setBottom(btnPanel); • Un récepteur pour un type spécifique sera toujours exécuté avant un récepteur pour un type plus générique • Par exemple un filtre enregistré pour MouseEvent.MOUSE_PRESSED sera exécuté avant un filtre pour MouseEvent.ANY qui sera exécuté avant un filtre pour InputEvent.ANY • L'ordre d'exécution des récepteurs pour des types de même niveau n'est pas défini • La consommation d'un événement n'interrompt pas le traitement des autres récepteurs enregistrés sur le même nœud – Jacques BAPST root btnPanel lblTitle txaMsg btnInsert btnDelete btnQuit //--- Text-Area txaMsg.setWrapText(true); txaMsg.setPrefColumnCount(15); txaMsg.setPrefRowCount(10); root.setCenter(txaMsg); Gestion des événements [8] IHM-1 – FX06 BorderPane HBox Label TextArea Button Button Button Code de l'interface sans la gestion des événements. Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.show(); 26 IHM-1 – FX06 – Jacques BAPST 28 Event Handling [3] Event Handling [5] § Pour traiter les événements des boutons, on peut créer une classe contrôleur qui implémente EventHandler et effectue les opérations souhaitées dans la méthode handle(). § Une autre manière de faire consiste à créer le contrôleur sous la forme d'une classe locale anonyme. Par exemple, pour le bouton Delete : . . . public class InsertButtonController implements EventHandler<ActionEvent> { private TextArea tArea; //--- Constructeur --------------------------------public InsertButtonController(TextArea tArea) { this.tArea = tArea; } //--- Code exécuté lorsque l'événement survient ---@Override public void handle(ActionEvent event) { tArea.appendText("A"); } Si on veut agir sur des composants de la vue, il faut transmettre les références nécessaires. Si le code est plus complexe, on invoquera de préférence une méthode de la vue. } IHM-1 – FX06 – Jacques BAPST 29 Event Handling [4] //--- Button Events Handling btnDelete.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { txaMsg.deletePreviousChar(); } }); . . . § A chaque clic sur le bouton Delete, le gestionnaire d'événement sera exécuté et un caractère sera supprimé dans le composant TextArea. IHM-1 – FX06 – Jacques BAPST 31 Event Handling [5] § Dans la vue, il faut ensuite créer une instance de ce contrôleur et l'enregistrer comme gestionnaire d'événement (type ACTION) sur le bouton concerné en invoquant la méthode addEventHandler(). § Une troisième possibilité pour traiter les événements des boutons, est d'utiliser la méthode setOnAction() et passer en paramètre une expression lambda implémentant la méthode handle() de l'interface EventHandler. . . . //--- Button Events Handling InsertButtonController insertCtrl = new InsertButtonController(txaMsg); § Par exemple pour traiter les trois boutons de l'interface : //--- Button Events Handling btnInsert.setOnAction(event -> { btnInsert.addEventHandler(ActionEvent.ACTION, insertCtrl); . . . txaMsg.appendText("A"); }); § A chaque clic sur le bouton Insert, le gestionnaire d'événement sera exécuté et un caractère 'A' sera ajouté dans le composant TextArea. btnDelete.setOnAction(event -> { txaMsg.deletePreviousChar(); }); btnQuit.setOnAction(event -> { Platform.exit(); }); IHM-1 – FX06 – Jacques BAPST 30 IHM-1 – FX06 – Jacques BAPST 32 Classe 'contrôleur' Méthodes setOn…() [2] § Dans la variante MVC synchrone, il est fréquent que la classe du contrôleur reçoive dans son constructeur les références du modèle et de la vue. public class ButtonController implements EventHandler<ActionEvent> { private AppModel model; private MainView view; Références du modèle et de la vue. Action de l'utilisateur Événement Texte modifié (durant la saisie) InputMethodEvent Bouton cliqué ComboBox ouverte ou fermée Une des options d'un menu contextuel activée ActionEvent Option de menu activée Pression sur Enter dans un champ texte //--- Constructeur ------------------------------------------public ButtonController(AppModel model, MainView view) { this.model = model; this.view = view; } Élément (Item) d'une liste, ListView. … d'une table ou TableColumn. //--- Code exécuté lorsque l'événement survient -------------@Override public void handle(ActionEvent event) { Le contrôleur accède int newVal = model.getInfo(); aux données du modèle view.updateInfo(newVal); et met à jour la vue. } Erreur survenue dans le media-player Menu est affiché (déroulé) ou masqué (enroulé) Fenêtre popup masquée Onglet sélectionné ou fermé Fenêtre affichée, fermée, masquée – Jacques BAPST 33 Méthodes setOn…() [1] IHM-1 – FX06 Événement IHM-1 – FX06 EditEvent MediaErrorEvent Event Event Event WindowEvent ButtonBase ComboBoxBase ContextMenu MenuItem TextField ListView TableColumn TreeView MediaView Menu PopupWindow Tab Window – Jacques BAPST 35 Fantas'TIP [1] § Liste des principales actions associées à des méthodes utilitaires qui permettent d'enregistrer des gestionnaires d'événements (il faut rechercher les méthodes setOnEventType() dans la classe). Action de l'utilisateur Pression sur une touche du clavier Déplacement de la souris ou pression sur une de ses touches Glisser-déposer avec la souris (Drag-and-Drop) Glisser-déposer propre à la plateforme (geste par exemple) Composant "scrollé" Geste de rotation Geste de balayage/défilement (swipe) Un composant est touché Geste de zoom Activation du menu contextuel CellEditEvent TreeView. … d'un arbre a été édité } IHM-1 – FX06 EditEvent Dans classe Node, Scene KeyEvent Dans classe Node, Scene MouseEvent Node, Scene MouseDragEvent Node, Scene DragEvent Node, Scene ScrollEvent Node, Scene RotateEvent Node, Scene § Exemple d'application pour illustrer différentes manières de réaliser le découpage du code en exploitant plusieurs des techniques qui sont à disposition (classes EventHandler, expressions lambda, binding de propriétés, …), tout en respectant les principes de l'architecture MVC. § L'application Fantas'TIP est un utilitaire qui calcule le pourboire à prévoir et le montant par personne, en fonction du montant de la note, du pourcentage octroyé et du nombre de convives. Node, Scene TouchEvent Node, Scene ZoomEvent Node, Scene ContextMenuEvent Node, Scene SwipeEvent – Jacques BAPST 34 IHM-1 – FX06 – Jacques BAPST 36 Fantas'TIP [2] Fantas'TIP [4] § Quatre variantes de cette application ont été créées : //---------------------------------------------------------------------------@Override public void setTipPercent(int percent) { if (percent < 0) throw new IllegalArgumentException("Percent < 0"); this.tipPercent = percent; • Variante 1 : Avec un contrôleur réalisé sous forme de classe 'ordinaire' • Variante 2 : Avec un contrôleur réalisé sous forme d'expression lambda • Variante 3 : Avec un contrôleur réalisé sous forme de liaisons de hautniveau (high-level binding) entre les données d'entrée (saisies par l'utilisateur) et les données de sortie (calculées) • Variante 4 : Avec un contrôleur réalisé sous forme de liaisons de basniveau (low-level binding) entre les données d'entrée (saisies par l'utilisateur) et les données de sortie (calculées) } //---------------------------------------------------------------------------@Override public void setNbPeople(int nbPeople) { if (nbPeople <= 0) throw new IllegalArgumentException("Nb people <= 0"); this.nbPeople = nbPeople; } //---------------------------------------------------------------------------@Override public double getTipPerPerson() { return bill * tipPercent / 100.0 / nbPeople; } § Quelques extraits de code (les éléments importants) figurent dans les pages qui suivent. //---------------------------------------------------------------------------@Override public double getTotalPerPerson() { return bill/nbPeople + getTipPerPerson(); } • L'intégralité du code source des applications est disponible sur la page : http://jacques.bapst.home.hefr.ch/ihm1/src/chap06_FantasTIP } • Les applications (exécutables) sont disponibles sur la page : http://jacques.bapst.home.hefr.ch/ihm1/applic/fantastip IHM-1 – FX06 – Jacques BAPST 37 IHM-1 – FX06 Fantas'TIP [3] § Variante 1 : classe contrôleur (du bouton 'Calculate') • Interface du modèle • Classe implémentant cette interface public class FantasTipController implements EventHandler<ActionEvent> { private IFantasTipModel model; private FantasTipView view; //---------------------------------------------------------------------------// Constructor receives model and view references //---------------------------------------------------------------------------public FantasTipController(IFantasTipModel model, FantasTipView view) { this.model = model; this.view = view; } public interface IFantasTipModel { void setBill(double amount); void setTipPercent(int percent); void setNbPeople(int nbPeople); double getTipPerPerson(); double getTotalPerPerson(); //---------------------------------------------------------------------------// Method executed when 'Calculate' button is pressed //---------------------------------------------------------------------------@Override public void handle(ActionEvent event) { //--- Get values from view (check for errors) and update model data try { double bill = view.getBillValue(); int tipPercent = view.getTipPercentValue(); int nbPeople = view.getNbPeopleValue(); } public class FantasTipModel implements IFantasTipModel { bill tipPercent nbPeople 39 Fantas'TIP [5] § Variante 1 : modèle de l'application private double private int private int – Jacques BAPST = 0; = 0; = 1; //---------------------------------------------------------------------------public FantasTipModel() { } model.setBill(bill); model.setTipPercent(tipPercent); model.setNbPeople(nbPeople); } catch (IllegalStateException e) { return; } //---------------------------------------------------------------------------@Override public void setBill(double amount) { if (amount < 0) throw new IllegalArgumentException("Amount < 0"); this.bill = amount; } // Errors in some input values //--- Update view with values from model output data view.updateTipPerPerson(model.getTipPerPerson()); view.updateTotalPerPerson(model.getTotalPerPerson()); } } IHM-1 – FX06 – Jacques BAPST 38 IHM-1 – FX06 – Jacques BAPST 40 Fantas'TIP [6] Fantas'TIP [8] § Variante 1 : création d'une instance du contrôleur et association au bouton Calculate. • Ces activités sont effectuées dans le code de la vue (la méthode privée createController() est appelée dans la méthode start()). . . . //---------------------------------------------------------------------------// Create button controller instance and associate it to button //---------------------------------------------------------------------------private void createController() { controller = new FantasTipController(model, this); btnCalc.setOnAction(controller); } . . . IHM-1 – FX06 41 – Jacques BAPST Fantas'TIP [7] • L'expression lambda est écrite dans le code de la vue (la méthode createController() est appelée dans la méthode start()). . . . //---------------------------------------------------------------------------// Create button controller with lambda expression //---------------------------------------------------------------------------private void createController() { btnCalc.setOnAction(event -> { try { double bill = getBillValue(); int tipPercent = getTipPercentValue(); int nbPeople = getNbPeopleValue(); model.setBill(bill); model.setTipPercent(tipPercent); model.setNbPeople(nbPeople); // Errors in some input values Même si le code du contrôleur est écrit dans le fichier source de la vue, l'expression lambda constitue bien un contrôleur qui sera représenté par une instance d'une classe anonyme qui implémente l'interface EventHandler. //--- Update view with values from model output data updateTipPerPerson(model.getTipPerPerson()); updateTotalPerPerson(model.getTotalPerPerson()); }); IHM-1 – FX06 – Jacques BAPST 43 – Jacques BAPST § Variante 3 : le contrôleur est constitué par les liaisons (bindings) créées entre les propriétés des composants d'entrée et ceux de sortie. § Dans cette variante, on crée des liaisons dites de haut-niveau (highlevel bindings) car les opérations sont effectuées par des invocations de méthodes en cascade. Cet enchaînement d'appels est rendu possible par l'utilisation d'un modèle de conception appelé fluent interface pattern ou fluent API. • Les opérations disponibles sont limitées mais suffisantes pour les calculs nécessaires dans l'application proposée. • Ex : result = a.multiply(b).add(c.multiply(d)); § Des méthodes statiques de la classe Bindings peuvent également être utilisées pour effectuer des opérations de haut-niveau entre des propriétés. • Ex : } IHM-1 – FX06 • Les données de sortie sont automatiquement mises à jour lorsqu'on change les données d'entrée (durant la saisie des champs texte). Fantas'TIP [9] § Variante 2 : le contrôleur est créé sous forme d'expression lambda. } catch (IllegalStateException e) { return; } § Dans les variantes 3 et 4, les vues possèdent des liaisons (par binding) entre les données d'entrées et les données de sortie. L'interface n'a donc plus besoin de bouton pour déclencher le calcul. 42 IHM-1 – FX06 result = Bindings.add(Bindings.multiply(a, b), Bindings.multiply(c, d)); – Jacques BAPST 44 Fantas'TIP [10] Fantas'TIP [12] § Variante 3 : méthode createViewModelBindings() appelée dans la méthode start() pour créer les liaisons entre les propriétés. . . . private void createViewModelBindings() { //--- Bind view text data with model number data (converter needed) tfdBill.textProperty().bindBidirectional(model.getBillPty(), dsConverter()); tfdTipPct.textProperty().bindBidirectional(model.getTipPercentPty(), isConverter()); tfdNbPple.textProperty().bindBidirectional(model.getNbPeoplePty(), isConverter()); § Variante 4 : le contrôleur est constitué par les liaisons (bindings) de bas-niveau (low-level bindings) entre les propriétés des composants d'entrée et ceux de sortie. § Pour créer des liaisons de bas-niveau, on redéfinit les méthodes computeValue() de liaisons existantes (on crée des sous-classes de IntegerBinding, DoubleBinding, StringBinding, etc.). • On dispose dans ce cas de tout le potentiel des instructions et des librairies à disposition pour effectuer les calculs qui lient les propriétés (aussi complexes soient-ils). • Ne pas oublier de définir toutes les propriétés dont dépend la liaison en invoquant la méthode parente super.bind(p1, p2, …) //--- Bind 'output' model properties to calculated properties model.getTipPerPersonPty().bind(model.getBillPty() .multiply(model.getTipPercentPty()) .divide(100) .divide(model.getNbPeoplePty())); model.getTotalPerPersonPty().bind((model.getBillPty() .divide(model.getNbPeoplePty()) .add(model.getTipPerPersonPty()))); //--- Bind TextField properties to model properties converted to String tfdRTip.textProperty().bind(model.getTipPerPersonPty().asString()); tfdRTotal.textProperty().bind((model.getTotalPerPersonPty().asString())); } IHM-1 – FX06 – Jacques BAPST 45 IHM-1 – FX06 Fantas'TIP [11] – Jacques BAPST 47 Fantas'TIP [13] § Variante 3 : méthode dsConverter() utilisée pour convertir une propriété de type double en String et inversement. § Variante 4 : méthode dblTipPerPersonBinding() qui retourne une spécialisation de DoubleBinding (qui calcule le pourboire par convive). . . . . . . //---------------------------------------------------------------------------// Number(Double) <--> String Converter //---------------------------------------------------------------------------private NumberStringConverter dsConverter() { return new NumberStringConverter() { @Override public Number fromString(String value) { try { return Double.parseDouble(value); } catch (NumberFormatException e) { return Double.NaN; } } //---------------------------------------------------------------------------// Low-level binding (calculate TipPerPerson from 'input' properties) //---------------------------------------------------------------------------private DoubleBinding dblTipPerPersonBinding() { DoubleBinding dblBinding = new DoubleBinding() { { super.bind(model.getBillPty(), model.getTipPercentPty(), model.getNbPeoplePty()); } @Override protected double computeValue() { double tipPP = model.getBillPty().get() * model.getTipPercentPty().get()/100.0 / model.getNbPeoplePty().get(); return tipPP; } @Override public String toString() { return super.toString(); } }; }; return dblBinding; } } IHM-1 – FX06 – Jacques BAPST 46 IHM-1 – FX06 – Jacques BAPST 48 Fantas'TIP [14] § Variante 4 : méthode strTipPerPersonBinding() qui retourne une spécialisation de StringBinding (qui convertit et formate la valeur). . . . //---------------------------------------------------------------------------// Low-level binding (convert TipPerPerson to formatted string) //---------------------------------------------------------------------------private StringBinding strTipPerPersonBinding() { StringBinding strBinding = new StringBinding() { { super.bind(model.getTipPerPersonPty()); } @Override protected String computeValue() { double tipPP = model.getTipPerPersonPty().get(); if (tipPP < 0) return "n/a"; String fmtRes = String.format("%.2f", tipPP); return fmtRes; } }; return strBinding; } IHM-1 – FX06 – Jacques BAPST 49 Fantas'TIP [15] § Variante 4 : méthode createViewModelBindings() qui effectue l'ensemble des liaisons entre les différentes propriétés. . . . //---------------------------------------------------------------------------// Create all bindings (view-model-view) //---------------------------------------------------------------------------private void createViewModelBindings() { //--- Bind view text data with model number data (converter needed) tfdBill.textProperty().bindBidirectional(model.getBillPty(), dsConverter()); tfdTipPct.textProperty().bindBidirectional(model.getTipPercentPty(), isConverter()); tfdNbPple.textProperty().bindBidirectional(model.getNbPeoplePty(), isConverter()); //--- Bind 'output' model properties to low-level calculated properties model.getTipPerPersonPty().bind(dblTipPerPersonBinding()); model.getTotalPerPersonPty().bind(dblTotalPerPersonBinding()); //--- Bind TextField properties to model properties converted to String tfdRTip.textProperty().bind(strTipPerPersonBinding()); tfdRTotal.textProperty().bind(strTotalPerPersonBinding()); } IHM-1 – FX06 – Jacques BAPST 50 Fichiers FXML [1] § Au centre de l'approche déclarative, se trouvent les fichiers FXML. Cours IHM-1 JavaFX § Un fichier FXML est un fichier au format XML dont la syntaxe est conçue pour décrire l'interface (la vue) avec ses composants, ses conteneurs, sa disposition, … • Le fichier FXML décrit le "quoi" mais pas le "comment" 7 - FXML SceneBuilder § A l'exécution, le fichier FXML sera chargé par l'application (classe FXMLLoader) et un objet Java sera créé (généralement la racine est un conteneur) avec les éléments que le fichier décrit (les composants, conteneurs, graphiques, …). Jacques BAPST § Il est possible de créer les fichiers FXML avec un éditeur de texte mais, plus généralement, on utilise un outil graphique (SceneBuilder) qui permet de concevoir l'interface de manière conviviale et de générer automatiquement le fichier FXML correspondant. • Un fichier FXML constitue une forme particulière de sérialisation d'objets, utilisée spécifiquement pour décrire les interfaces [email protected] IHM-1 – FX07 – Jacques BAPST I/F procédurales « déclaratives Fichiers FXML [2] § Avec JavaFX, les interfaces peuvent être créées de deux manières : § Les objets créés par le chargement de fichiers FXML peuvent être assignés à la racine d'un graphe de scène ou représenter un des nœuds dans un graphe de scène créé de manière procédurale. • Procédurale : en écrivant du code Java qui fait appel aux API de la plateforme et qui utilise les composants/conteneurs à disposition (classes et interfaces) • Déclarative : en décrivant l'interface dans un fichier au format FXML qui sera ensuite chargé dynamiquement dans l'application § Une fois chargés, les nœuds issus de fichiers FXML sont totalement équivalents à ceux créés de manière procédurale. Les mêmes opérations et manipulations peuvent leur être appliquées. § Les premiers chapitres du cours ont décrit les bases de la technique procédurale (programmatique) permettant de créer des interfaces. § Le présent chapitre abordera la deuxième possibilité de créer les interfaces (les vues) en utilisant notamment l'outil SceneBuilder qui permet, de manière interactive, de créer les fichiers FXML. § SceneBuilder est une application qui doit être installée et, pour une utilisation avec Eclipse, le plugin e(fx)clipe est recommandé. IHM-1 – FX07 – Jacques BAPST 3 2 § Le langage FXML n'est pas associé à un schéma XML mais la structure de sa syntaxe correspond à celle des API JavaFX : • Les classes JavaFX (conteneurs, composants) peuvent être utilisées comme éléments dans la syntaxe XML • Les propriétés des composants correspondent à leurs attributs § Même si certaines possibilités existent (en lien notamment avec du code JavaScript) on conseille généralement d'utiliser les fichiers FXML exclusivement pour décrire les interfaces, et d'effectuer tous les traitements (activité des contrôleurs) dans le code Java. IHM-1 – FX07 – Jacques BAPST 4 SceneBuilder [1] Say Hello [1] § L'outil graphique SceneBuilder permet de concevoir l'interface de manière interactive (WYSIWYG) en assemblant les conteneurs et les composants et en définissant leurs propriétés. § Un exemple d'application très simple : § Le mode de fonctionnement de cet utilitaire est assez classique avec une zone d'édition centrale, entourée d'un certain nombre d'outils : palettes de conteneurs, de composants, de menus, de graphiques, vue de la structure hiérarchique de l'interface, inspecteurs de propriétés, de layout, etc. • Un conteneur BorderPane • Deux composants : Label et Button • Quelques adaptations de propriétés (taille, couleur, marge, …) § Graphe de scène et apparence finale de l'application Stage § L'emploi de cet outil n'est pas décrit en détail dans ce support de cours, il faut se référer à la documentation disponible. Son utilisation est cependant assez intuitive, pour autant que les éléments affichés soient connus (notamment les caractéristiques et propriétés principales des conteneurs et des composants). Scene BorderPane Malgré l'outil graphique, on n'échappe donc pas à une compréhension minimale des API (composants, conteneurs, propriétés, …). IHM-1 – FX07 – Jacques BAPST 5 SceneBuilder [2] TOP BOTTOM Label Button IHM-1 – FX07 – Jacques BAPST 7 Say Hello [2] § Aperçu de l'écran principal : § Variante procédurale avec les API JavaFX : BorderPane root = new BorderPane(); root.setPrefWidth(250); root.setPrefHeight(80); root.setStyle("-fx-background-color: #FFFCAA"); root.setPadding(new Insets(10, 5, 10, 5)); Label lblTitle = new Label(); lblTitle.setText("Titre"); lblTitle.setTextFill(Color.web("#0022CC")); lblTitle.setFont(Font.font("SansSerif", FontWeight.BOLD, 20)); BorderPane.setAlignment(lblTitle, Pos.CENTER); root.setTop(lblTitle); Button btnSayHello = new Button(); btnSayHello.setText("Say Hello"); BorderPane.setAlignment(btnSayHello, Pos.CENTER); root.setBottom(btnSayHello); btnSayHello.setOnAction(event -> { lblTitle.setText("H e l l o !"); lblTitle.setTextFill(Color.FUCHSIA); }); IHM-1 – FX07 – Jacques BAPST 6 IHM-1 – FX07 – Jacques BAPST 8 Say Hello [3] Say Hello [5] § La même application a été créée de manière déclarative et la vue est donc décrite dans un fichier FXML qui a été créé avec SceneBuilder. § Le fichier XML comporte tout d'abord une partie déclarative (en-tête et importations nécessaires à la création dynamique de l'objet Java). § Ensuite on trouve la description de la structure de l'interface (graphe de scène avec conteneurs, composants et propriétés). § La méthode start() de la classe principale peut charger le fichier FXML en invoquant la méthode statique FXMLLoader.load(url) qui prend en paramètre l'URL de la ressource à charger. § La méthode getResource(name) de la classe Class permet de trouver (par le classloader) l'URL d'une ressource à partir de son nom. • Référence relative au package courant par défaut ("views/Login.fxml") • Référence absolue si '/' initial dans le nom ("/resources/log/Error.fxml") (attention: le caractère '/' est aussi utilisé même s'il s'agit de packages et sous-packages) <?xml version="1.0" encoding="UTF-8"?> <?import <?import <?import <?import <?import javafx.geometry.*?> javafx.scene.text.*?> javafx.scene.control.*?> java.lang.*?> javafx.scene.layout.*?> <BorderPane . . . . . . public void start(Stage primaryStage) throws Exception { //--- Chargement du fichier FXML BorderPane root = FXMLLoader.load(getClass().getResource("SayHello.fxml")); Racine du graphe de scène. Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.setTitle("SayHello FXML"); primaryStage.show(); > Conteneurs et composants du graphe de scène. } </BorderPane> IHM-1 – FX07 – Jacques BAPST 9 IHM-1 – FX07 Say Hello [4] 11 Say Hello [6] § Partie principale du fichier FXML créé avec SceneBuilder <BorderPane prefHeight="80.0" prefWidth="250.0" style="-fx-background-color: #FFFCAA;" xmlns=http://javafx.com/javafx/8 xmlns:fx=http://javafx.com/fxml/1 fx:controller="supp_cours.chap07.SayHelloController"> <top> <Label id="title" fx:id="title" text="Titre" textFill="#0022cc" BorderPane.alignment="CENTER"> <font> <Font name="SansSerif Bold" size="20.0" /> </font> </Label> </top> <bottom> <Button fx:id="btnHello" onAction="#handleButtonAction" text="Say Hello" BorderPane.alignment="CENTER" /> </bottom> <padding> <Insets bottom="10.0" left="5.0" right="5.0" top="10.0" /> </padding> </BorderPane> IHM-1 – FX07 – Jacques BAPST – Jacques BAPST 10 § Dans la variante déclarative, une classe séparée joue le rôle de contrôleur pour traiter l'action du clic sur le bouton. § Le principe de fonctionnement et le rôle des annotations seront expliqués dans les pages qui suivent. public class SayHelloController { @FXML private Button btnHello; // Object injected by FXMLLoader (fx:id="btnHello") @FXML private Label // Object injected by FXMLLoader (fx:id="title") title; @FXML private void handleButtonAction(ActionEvent event) { title.setText("H e l l o !"); title.setTextFill(Color.FUCHSIA); } } IHM-1 – FX07 – Jacques BAPST 12 Say Hello [7] Interprétation des fichiers FXML [2] § Les deux variantes de l'application (avec interface créée de manière procédurale et déclarative) fonctionnent de manière strictement identique. • Un clic sur le bouton modifie le texte et la couleur du composant Label. Variante procédurale § Pour les propriétés qui ne peuvent pas facilement être représentées par une chaîne de caractères, un élément est imbriqué (plutôt que de déclarer des attributs). Par exemple, si l'on considère l'élément <Label id="title" fx:id="title" text="Titre" textFill="#0022cc" BorderPane.alignment="CENTER"> <font> <Font name="SansSerif Bold" size="20.0" /> </font> </Label> Variante déclarative on constate que la propriété Font est codée comme un élément imbriqué dans l'élément Label. § Pour les propriétés de type liste (par exemple children), les éléments de la liste sont simplement imbriqués et répétés dans l'élément représentant la liste (par exemple, les composants enfants seront listés entre les balises <children> et </children>). IHM-1 – FX07 – Jacques BAPST 13 IHM-1 – FX07 – Jacques BAPST Interprétation des fichiers FXML [1] Liens FXML « programme [1] § Lors du chargement du fichier FXML, son contenu est interprété et des objets Java correspondants sont créés. § Le lien entre les composants décrits dans le fichier FXML et le programme est établi par les attributs fx:id : § Par exemple, l'élément 15 <Label id="title" fx:id="title" text="Titre" textFill="#0022cc" ... <BorderPane prefHeight="80.0" prefWidth="250.0" . . . § L'attribut fx:id fonctionne en lien avec l'annotation @FXML que l'on peut utiliser dans les contrôleurs, et qui va indiquer au système que le composant avec le nom fx:id pourra être injecté dans l'objet correspondant de la classe contrôleur. sera interprété comme BorderPane rootPane = new BorderPane(); rootPane.setPrefHeight(80.0); rootPane.setPrefWidth(250.0); . . . public class SayHelloController { § Quand un attribut commence par le nom d'une classe suivi d'un point et d'un identificateur, par exemple @FXML private Button btnHello; <TextField GridPane.columnIndex="3" . . . l'attribut sera interprété comme une invocation de méthode statique @FXML private Label title; // fx:id="title" // Object injected by FXMLLoader TextField tfd = new TextField(); GridPane.setColumnIndex(tfd, 3); . . . IHM-1 – FX07 – Jacques BAPST 14 IHM-1 – FX07 – Jacques BAPST 16 Liens FXML « programme [2] Liens FXML « programme [4] § La classe qui joue le rôle de contrôleur pour une interface déclarée en FXML doit être annoncée dans l'élément racine, en utilisant l'attribut fx:controller : § Dans les classes qui agissent comme "contrôleurs", on peut définir une méthode initialize() (qui doit être annotée avec @FXML) pour effectuer certaines initialisations. <BorderPane prefHeight="80.0" prefWidth="250.0" style="-fx-background-color: #FFFCAA;" xmlns=http://javafx.com/javafx/8 xmlns:fx=http://javafx.com/fxml/1 fx:controller="supp_cours.chap07.SayHelloController"> . . . _________________________________________________________________________________________________________ § Cette méthode est automatiquement invoquée après le chargement du fichier FXML. § Elle peut être utile pour initialiser certains composants, en faisant par exemple appel au modèle. . . . @FXML private void initialize() { cbbCountry.getItems().addAll("Allemagne", "Angleterre", "Belgique", "Espagne", "France", "Italie", "Pays-Bas", "Portugal", "Suisse"); Attention : Les attributs qui définissent les namespaces xmlns=… ainsi que xmlns:fx=… sont utilisés par l'environnement JavaFX (FXMLLoader, SceneBuilder, etc.). Ils ne doivent donc pas être modifiés ! lstProducts.getItems().addAll(model.getProducts()); . . . } IHM-1 – FX07 – Jacques BAPST 17 IHM-1 – FX07 – Jacques BAPST 19 Liens FXML « programme [3] Liens FXML « programme [5] § Pour les composants actifs déclarés dans une interface en FXML, on peut indiquer la méthode du contrôleur qui doit être invoquée en utilisant l'attribut fx:onEvent="#methodName" : § Dans l'éditeur SceneBuilder, ces différents attributs de liaison avec le programme doivent être indiqués dans les champs prévus. <Button fx:id="btnHello" onAction="#handleButtonAction" text="Say Hello" BorderPane.alignment="CENTER" /> • Attribut fx:id et attribut fx:controller . . . § Dans la classe contrôleur, ces méthodes devront (comme les composants associés) être annotées avec @FXML. . . . @FXML private void handleButtonAction(ActionEvent event) { title.setText("H e l l o !"); title.setTextFill(Color.FUCHSIA); } • Attribut fx:on… IHM-1 – FX07 – Jacques BAPST 18 IHM-1 – FX07 Le bloc Controller est situé en bas, à gauche de l'écran principal. – Jacques BAPST 20 Liens FXML « programme [6] Liens FXML « programme [8] § L'attribut id ne doit pas être confondu avec l'attribut fx:id. § Si le contrôleur d'une interface déclarée avec FXML ne possède pas de constructeur par défaut, il faut créer le contrôleur et l'associer dans le code du programme, avant le chargement du fichier FXML. <Label id="title" fx:id="title" text="Titre" textFill="#0022cc" ... § L'attribut id définit un sélecteur CSS de type Id qui permet d'associer des règles de style aux composants portant cet Id. • Exemple dans un fichier CSS : . . . <BorderPane stylesheets="@SayHello.css" . . . > § Dans ce cas, il n'est pas nécessaire de déclarer le contrôleur dans le fichier FXML (fx:controller="..."). § L'éditeur SceneBuilder permet (dans l'inspecteur de propriétés) de créer l'attribut id et de définir le fichier CSS associé (Stylesheets). – Jacques BAPST 21 Liens FXML « programme [7] § Il est possible d'accéder au contrôleur associé au fichier FXML en créant un chargeur (loader) pour ce fichier (plutôt que d'utiliser la méthode statique FXMLLoader.load()) . § Cela peut être utile pour avoir accès au contrôleur, par exemple pour lui communiquer la référence du modèle de l'application : public void start(Stage primaryStage) throws Exception { //--- Chargement du fichier FXML et recherche du contrôleur associé FXMLLoader loader = new FXMLLoader(getClass().getResource("SayHello.fxml")); BorderPane root = loader.load(); SayHelloController ctrl = loader.getController(); ctrl.setModel(model); Scene scene = new Scene(root); . . . § Dans ce cas, on utilisera la méthode d'instance load() pour charger le fichier FXML et obtenir la référence de la racine du graphe de scène. IHM-1 – FX07 – Jacques BAPST public void start(Stage primaryStage) throws Exception { //--- Chargement du fichier FXML et association du contrôleur FXMLLoader loader = new FXMLLoader(getClass().getResource("SayHello.fxml")); loader.setController(new SayHelloController("a param")); BorderPane root = loader.load(); #title { -fx-font-size: 24pt; } § Dans le fichier FXML, une feuille de style (fichier CSS) peut être associé à un composant avec l'attribut stylesheets="@CSS_File" IHM-1 – FX07 § Le code suivant illustre la manière de le faire : 22 IHM-1 – FX07 – Jacques BAPST 23 Menu [1] § Les menus sont des éléments de l'interface permettant à l'utilisateur de choisir des options qui pourront déclencher des actions et/ou changer l'état de certaines propriétés de l'application. Cours IHM-1 JavaFX § Le principe du menu est (comme au restaurant) que l'utilisateur puisse voir et parcourir la liste des options avant de se décider. 8 - Menus Choix / Sélection § Dans les applications, les menus peuvent prendre différentes formes. Parmi les plus classiques, on trouve : • Les menu déroulants (drop-down menu) ð ð Des en-têtes de menus sont placés dans un conteneur sous forme de barre Un clic sur ces en-têtes "déroule" le menu et fait apparaître, dans une fenêtre popup, les options de ce menu. • Les menu contextuels (popup menu) Jacques BAPST Menus affichés en réaction à un événement, généralement une action de la souris (clic-droit), l'activation d'une touche ou un geste. ð Les options du menu qui sont affichées dépendent de l'endroit où l'on a cliqué (contexte). ð [email protected] IHM-1 – FX08 – Jacques BAPST 3 Menu [2] § Les options des menus peuvent elles-mêmes ouvrir d'autres menus. On parle alors de sous-menus qui peuvent s'ouvrir en cascade (on peut avoir plusieurs niveaux de sous-menus). Menus déroulants Menus contextuels Mnémoniques / Accélérateurs IHM-1 – FX08 – Jacques BAPST § Dans la librairie JavaFX, un certain nombre de composants sont dédiés aux menus et sont utilisés pour les construire : • • • • • • • • 2 MenuBar Menu MenuItem CheckMenuItem RadioMenuItem CustomMenuItem SeparatorMenuItem Contextmenu IHM-1 – FX08 – Jacques BAPST 4 Menu [3] Barre de menu [1] § Les différents composants qui interviennent dans la gestion des menus déroulants sont illustrés dans l'exemple suivant : § La barre de menus, représentée par le composant MenuBar, est un conteneur permettant de rassembler les en-têtes des menus. § Le composant MenuBar doit être placé dans un conteneur de l'interface (par exemple en haut d'un BorderPane). Menu § La création d'une barre de menus s'effectue à l'aide des instructions suivantes : MenuBar CheckMenuItem SeparatorMenuItem Menu • Création de la barre de menus : MenuBar mBar = new MenuBar(); • L'ajout des menus (composants de type Menu) dans la barre peut se faire de manière individuelle (en séquence) ou collective : MenuItem mBar.getMenus().add(mnuFile); mBar.getMenus().addAll(mnuFile, mnuOptions, mnuHelp); RadioMenuItem § La propriété useSystemMenuBar permet d'indiquer que le mécanisme standard de menu de la plateforme cible doit être utilisé (par exemple sur MacOS). IHM-1 – FX08 – Jacques BAPST 5 IHM-1 – FX08 – Jacques BAPST Menu [4] Menu déroulant [1] § Pour les menus contextuels, ce sont pratiquement les mêmes composants qui sont utilisés à la simple différence que : § Le composant Menu représente un conteneur d'éléments de menu (options) et peut se présenter "enroulé" (en-tête) ou "déroulé" (fenêtre popup) lorsqu'on clique dessus. • Le composant MenuBar n'est plus nécessaire. • A la place, c'est le composant ContextMenu qui est utilisé pour rassembler les différentes options du menu contextuel. 7 § Il peut être ajouté à une barre de menus (MenuBar) ou être inséré comme sous-menu dans un autre composant de type Menu. § L'affichage du composant sera déclenché par une action de l'utilisateur (en général un clic-droit sur un conteneur ou composant). § Lorsque le composant Menu représente un sous-menu, il est accompagné d'un indicateur visuel (généralement une flèche dirigée vers la droite) qui le distingue d'un élément terminal (option de menu). § Un menu contextuel peut également contenir des sousmenus en cascade. § Création et alimentation d'un objet de type Menu : • Création d'un menu : Menu mnuFile = new Menu("File"); • L'ajout des options de menus (MenuItem, CheckMenuItem, …) dans le menu peut se faire de manière individuelle (en séquence) ou collective : mnuFile.getItems().add(mniSave); mnuFile.getItems().addAll(mniSave, mniSaveAs, mniQuit); IHM-1 – FX08 – Jacques BAPST 6 IHM-1 – FX08 – Jacques BAPST 8 Menu déroulant [2] Options de menu [2] § Un sous-menu est donc simplement créé en ajoutant un composant de type Menu dans un autre composant du même type : § Pour le traitement de l'action déclenchée par le composant MenuItem on peut utiliser la méthode utilitaire setOnAction(…). • Création d'un menu et d'un sous-menu : § Exemple : Menu mnuOptions = new Menu("Options"); Menu mnuPrefs = new Menu("Preferences"); . . . //--- Traitement de l'action des options de menu mniQuit.setOnAction(event -> { Platform.exit(); }); • L'ajout du menu comme sous-menu : mnuOptions.getItems().add(mnuPrefs); § Le nombre de niveaux d'imbrication des sous-menus n'est pas limité mais on ne dépasse généralement pas trois niveaux (selon le contexte applicatif, des exceptions sont possibles). IHM-1 – FX08 9 – Jacques BAPST mniAbout.setOnAction(event -> { System.out.println("About MenuItem Activated"); }); . . . IHM-1 – FX08 – Jacques BAPST Options de menu [1] Options de menu [3] § Un menu déroulant ou contextuel peut contenir différents éléments (appelés options de menu) permettant à l'utilisateur de déclencher une action ou d'activer/désactiver l'élément. § Comme option de menu, on peut aussi utiliser le composant CheckMenuItem qui agit comme une case à cocher placée dans un menu déroulant ou contextuel (l'aspect visuel dépend du thème). § L'option de base est représentée par le composant MenuItem qui agit, du point de vue de l'utilisateur, comme un bouton. § La propriété selected indique si l'option est sélectionnée ou non. § Lors de la création d'une option de menu, on peut lui assigner un texte et optionnellement un composant complémentaire (graphic) qui ne devrait pas dépasser 16x16 pixels. • Création d'une option de menu : MenuItem mniSave = new MenuItem("Save"); ImageView image = new ImageView(. . .); MenuItem mniPrefs = new MenuItem("Preferences", image); 11 § La propriété graphic permet d'ajouter au libellé un composant complémentaire (généralement une icône ou graphique de petite taille). • Création d'une option de menu de type case à cocher : CheckMenuItem cmiSound = new CheckMenuItem("Sound"); ImageView image = new ImageView(. . .); CheckMenuItem cmiNotify = new CheckMenuItem("Notify", image); • Ajout d'un graphique : cmiSound.setGraphic(new Rectangle(8, 8, Color.GREEN)); • Ajout d'un graphique (icône) : mniSave.setGraphic(new Circle(0, 0, 5, Color.RED)); IHM-1 – FX08 – Jacques BAPST 10 IHM-1 – FX08 – Jacques BAPST 12 Options de menu [4] Options de menu [6] § Comme pour le composant MenuItem, on peut traiter l'événement du composant CheckMenuItem en invoquant la méthode utilitaire setOnAction(…). § Le composant RadioMenuItem ne devrait jamais être utilisé seul mais faire partie d'un groupe (comme pour les boutons radios). § Parfois, on ne réagit pas directement au clic sur une telle option mais on prend en compte l'état (sélectionné ou non-sélectionné) dans le traitement d'autres événements (ceux déclenchés par des boutons ou des options de menu de type MenuItem par exemple). § Il est également possible de lier (binding) la propriété selected de ce composant à une propriété du modèle. Ainsi, le modèle de l'application reflète de manière synchrone l'état de cette propriété sans qu'il soit nécessaire de gérer des événements lors des changements dans la sélection. IHM-1 – FX08 – Jacques BAPST 13 § Toutes les options qui font partie du même groupe doivent être placées dans un objet commun de type ToggleGroup (toggleGroup est une propriété du composant RadioMenuItem). ToggleGroup tgGroup = new ToggleGroup(); rmiDock.setToggleGroup(tgGroup); rmiFloat.setToggleGroup(tgGroup); § Pour le traitement des événements, toutes les indications et remarques formulées pour le composant CheckMenuItem (voir pages précédentes) s'appliquent également au composant RadioMenuItem. IHM-1 – FX08 – Jacques BAPST Options de menu [5] Options de menu [7] § Le composant RadioMenuItem qui est une sous-classe de MenuItem peut également être utilisé comme option dans un menu. Il agit comme un bouton radio placé dans un menu déroulant et permet la sélection d'une option parmi plusieurs. § Le composant CustomMenuItem permet de traiter n'importe quel nœud (Node) d'un graphe de scène comme un élément de menu. § Pour avoir ce comportement de sélection mutuellement exclusive, le composant doit être placé dans un ToggleGroup (comme c'est le cas pour le RadioButton). § La propriété selected indique si l'option est sélectionnée ou non. • Création d'une option de menu de type radio : RadioMenuItem rmiDock = new RadioMenuItem("Docking"); image = new ImageView(. . .); RadioMenuItem rmiFloat = new RadioMenuItem("Floating", image); IHM-1 – FX08 – Jacques BAPST § Exemple d'un curseur (Slider) placé dans un menu : Slider slrSpeed = new Slider(0, 100, 30); CustomMenuItem cmiSlider = new CustomMenuItem(slrSpeed); cmiSlider.setHideOnClick(false); mnuOptions.getItems().add(cmiSlider); § La propriété hideOnClick permet d'indiquer si le composant et toutes les autres options visibles doivent être masquées lorsqu'on clique sur cet élément (true par défaut). § La propriété graphic permet d'ajouter au libellé un composant complémentaire (généralement un graphique de petite taille). ImageView 15 § Les événements du composant inséré dans le menu doivent naturellement être gérés si nécessaire. 14 IHM-1 – FX08 – Jacques BAPST 16 Options de menu [8] Menu contextuel [2] § Il est possible de grouper visuellement les options de menu en séparant les différents groupes d'options par une ligne horizontale. § L'association des menus contextuels aux composants peut se faire de deux manières. • Pour les composants qui sont des sous-classes de Control, le plus simple est d'utiliser la propriété contextMenu. § Le composant SeparatorMenuItem (qui est une sous-classe de CustomMenuItem) permet de créer un tel séparateur. § Pour chaque séparateur, il faut créer un nouvel objet (on ne peut pas ajouter plusieurs fois un même séparateur). • Pour les autres composants et notamment les conteneurs (sous-classes de Pane), il faut gérer l'événement provoqué par le clic-droit de la souris et utiliser la méthode show() pour afficher le menu (à la position du clic). private SeparatorMenuItem sep1 = new SeparatorMenuItem(); private SeparatorMenuItem sep1 = new SeparatorMenuItem(); mnuPrefs.getItems().addAll(mniFont, mniSize, sep1, rmiDock, rmiFloat, sep2, . . . ); IHM-1 – FX08 – Jacques BAPST ContextMenu ctxMenuIm = new ContextMenu(); ctxMenuIm.getItems().addAll(mniCopy, mniSave, mniResize); lblImage.setContextMenu(ctxMenuIm); root.setOnMousePressed(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent ev) { if (ev.isSecondaryButtonDown()) { ctxMenuIm.show(root, ev.getScreenX(), ev.getScreenY()); } } }); 17 IHM-1 – FX08 – Jacques BAPST Menu contextuel [1] Raccourcis clavier § Les menus contextuels (popup menu) se construisent de la même manière que les menus déroulants mais les différents options du menu sont assemblées dans un composant ContextMenu (au lieu de créer des en-têtes de menus et de les ajouter dans un MenuBar). § Il est souvent souhaitable que certaines actions que l'on peut déclencher avec la souris puissent être également déclenchées à l'aide du clavier. § Instructions pour gérer un menu contextuel : • Création du menu contextuel : ContextMenu ctxMenuIm § C'est le rôle des raccourcis clavier qui existent sous deux formes principales : • Les mnémoniques = new ContextMenu(); ctxMenuIm.getItems().add(mniWeb); ctxMenuIm.getItems().addAll(mniCopy, mniSave, mniResize); § Des sous-menus peuvent également être créés dans un menu contextuel en imbriquant des composants de type Menu (de la même manière que pour les menus déroulants). – Jacques BAPST Activation du composant en pressant sur une touche spécifique qui dépend de la plateforme (touche Alt sur Windows) associée à un autre caractère (généralement alphanumérique). ð Les mnémoniques sont associés à des composants qui possède un libellé et le caractère associé au mnémonique est généralement souligné. ð • L'ajout des options de menus peut se faire de manière individuelle (en séquence) ou collective : IHM-1 – FX08 19 18 • Les accélérateurs ð Activation de l'action associée à une option de menu par une combinaison unique de touches du clavier. IHM-1 – FX08 – Jacques BAPST 20 Mnémonique Accélérateur [2] § Des mnémoniques peuvent être associés à tous les composants possédant un libellé (sous-classes de Labeled) ainsi qu'aux menus et aux différentes options de menus. § Pour les touches spéciales (Enter, Home, Page_Up, …), la classe KeyCode définit des constantes énumérées représentant les différentes touches du clavier. Le nom de ces constantes peut généralement être utilisé dans la chaîne de caractères passée à la méthode keyCombination(). § Pour ces composants, la propriété booléenne mnemonicParsing indique si le caractère souligné '_' dans le libellé du composant doit être considéré comme l'annonce d'un mnémonique pour le caractère suivant. Menu mnuFile Menu mnuOptions = new Menu("_Options"); Button btnOk § La classe KeyCodeCombination peut également être utilisée pour créer des combinaisons de touches (voir exemple). = new Menu("_File"); § Exemples de création d'accélérateurs sur des options de menu : = new Button("O_K"); mniSave.setAccelerator(KeyCombination.keyCombination("Ctrl+S")); • La propriété mnemonicParsing est à true par défaut pour la plupart des composants. mniAbout.setAccelerator(KeyCombination.keyCombination("Shift+Ctrl+A")); mniQuit.setAccelerator(KeyCombination.keyCombination("Ctrl+Home")); mnuOpt.setAccelerator(KeyCombination.keyCombination("Alt+Shift+Ctrl+O")); mniSize.setAccelerator(new KeyCodeCombination( KeyCode.S, KeyCombination.ALT_DOWN, KeyCombination.SHIFT_DOWN)); IHM-1 – FX08 – Jacques BAPST 21 IHM-1 – FX08 – Jacques BAPST 23 Accélérateur [1] § Des accélérateurs peuvent être associés à tous les composants de type Menu (c'est rarement utile) et aux différentes options de menus MenuItem, CheckMenuItem, … § Pour ces composants, la propriété accelerator, qui est de type KeyCombination indique la combinaison de touches qui permet de déclencher l'action associée. § Il existe différentes manières de créer des objets de type KeyCombination. Une des plus simples est d'utiliser la méthode statique keyCombination("key-pattern") qui prend en paramètre une chaîne de caractères qui sera interprétée. MenuButton / SplitMenuButton ChoiceBox / ComboBox ListView / Spinner • Cette chaîne de caractères comprendra un ou plusieurs modificateurs et un caractère séparés par le symbole plus '+' • Modificateurs possibles : Shift, Ctrl, Alt, Meta, Shortcut • Exemples : "Shift+A", "Ctrl+S", "Alt+Ctrl+M" IHM-1 – FX08 – Jacques BAPST 22 IHM-1 – FX08 – Jacques BAPST 24 MenuButton ChoiceBox [1] § Le composant MenuButton est un bouton qui affiche un menu popup lorsqu'il est cliqué (à la manière d'un menu contextuel). § Le composant ChoiceBox permet de présenter à l'utilisateur une liste d'éléments dans laquelle il peut en sélectionner un. § C'est une sous-classe de ButtonBase et son API est très proche de celle du composant Menu. § Le composant se présente au départ sous la forme d'un bouton et, lorsqu'on clique dessus, les éléments sont affichés sous la forme d'une liste déroulante. § La propriété popupSide permet de définir de quel côté doit s'ouvrir le menu popup. § L'élément actuellement sélectionné est affiché en libellé lorsque la liste est fermée (enroulée). § Ce composant est à réserver pour la sélection de listes relativement courtes car il n'y a pas de scrolling possible. § Exemple (extrait du code) : § Par défaut, il n'y a pas d'élément sélectionné au départ. MenuButton mbnOptions = new MenuButton("Options"); mnuSubOpt.getItems().addAll(mniSkin, mniSize, mniColor); // Sous-menu mbnOptions.setPopupSide(Side.LEFT); bnOptions.getItems().addAll(mniSave, mniQuit, mnuSubOpt, mniFont); IHM-1 – FX08 – Jacques BAPST 25 § Le type du composant est générique ChoiceBox<T>, T étant le type des éléments de la liste. IHM-1 – FX08 – Jacques BAPST SplitMenuButton ChoiceBox [2] § Le composant SplitMenuButton est une sorte de combinaison du composant Button et du composant MenuButton (dont il est une sous-classe). § Quelques propriétés du composant ChoiceBox : Liste des éléments à afficher. Un objet de type ObservableList<T> que l'on peut également passer au items constructeur. La liste des éléments peut être modifiée dynamiquement (ajout, suppression, …). Modèle de sélection des éléments de la liste. Type SingleSelectionModel<T>. selectionModel On ne change généralement pas le modèle de sélection par défaut. value Valeur de l'élément sélectionné. Type <T>. showing Booléen qui indique si la liste des éléments est affichée ou non. Convertisseur qui indique comment l'élément est représenté. Ce convertisseur (StringConverter<T>) est utilisé pour convertir converter les éléments de type T en String (et inversement). Par défaut, c'est la méthode toString() de l'élément qui est utilisée. § La zone du bouton est divisée en 2 parties : • Une zone avec un libellé qui agit comme un bouton ordinaire en déclenchant une action lorsqu'on clique sur cette partie (code associé à la méthode setOnAction(…)). • Une zone de menu qui peut être activée en cliquant dessus ou en utilisant les touches "curseurs" du clavier et qui fonctionne comme le composant MenuButton en ouvrant un menu dans une fenêtre popup. § La dernière sélection qui a été effectuée dans le menu n'est pas automatiquement associée à l'action du bouton. IHM-1 – FX08 – Jacques BAPST 27 26 IHM-1 – FX08 – Jacques BAPST 28 ChoiceBox [3] ComboBox [1] § Exemple avec une liste de chaînes de caractères (choix d'une langue). § Le composant ComboBox<T> est assez similaire au composant ChoiceBox et permet de présenter à l'utilisateur une liste d'éléments (de type T) dans laquelle il pourra en sélectionner un. ChoiceBox<String> cbxLang = new ChoiceBox<String>(); cbxLang.getItems().addAll("Deutsch", "English", "Français", "Italiano"); cbxLang.getSelectionModel().select(1); // ou cbxLang.setValue("English"); root.getChildren().add(cbxLang); § Une des différences réside dans le fait que ComboBox permet de limiter le nombre de choix affichés et offre automatiquement une barre de défilement (ascenseur) pour naviguer dans la liste. § Une autre différence est qu'un ComboBox peut être éditable (l'utilisateur peut saisir une valeur qui ne figure pas dans la liste). § ComboBox est une sous-classe de ComboBoxBase qui offre des propriétés et fonctionnalités de base au composant. § La propriété onAction permet d'enregistrer un gestionnaire d'événement qui sera exécuté lorsque la valeur sélectionnée change. § La fonction de saisie automatique (autocomplétion) n'est pas disponible par défaut et doit être codée si elle est souhaitée. IHM-1 – FX08 – Jacques BAPST 29 IHM-1 – FX08 – Jacques BAPST ChoiceBox [4] ComboBox [2] § Si l'on souhaite effectuer une action lors d'un changement de sélection, il faut enregistrer un gestionnaire d'événement sur le modèle de sélection (propriété selectedItem ou selectedIndex). § Quelques propriétés du composant ComboBox : cbxLang.getSelectionModel().selectedItemProperty() .addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ov, String s1, String s2) { System.out.println(s1+", "+s2); // Display old and new values System.out.println(cbxLang.getSelectionModel().getSelectedIndex()); } }); • Identique mais avec une expression lambda : cbxLang.getSelectionModel().selectedItemProperty() .addListener((ov, s1, s2) -> { System.out.println(s1+", "+s2); // Display old and new values System.out.println(cbxLang.getSelectionModel().getSelectedIndex()); }); IHM-1 – FX08 – Jacques BAPST 30 31 Liste des éléments à afficher. Un objet de type ObservableList<T> que l'on peut également passer au items constructeur. La liste des éléments peut être modifiée dynamiquement (ajout, suppression, …). Modèle de sélection des éléments SingleSelectionModel<T> selectionModel (un seul élément peut être sélectionné). value Valeur de l'élément sélectionné. Type <T>. showing Booléen qui indique si la liste des éléments est affichée ou non. Convertisseur qui indique comment l'élément est représenté et, s'il est éditable, comment traduire la chaîne de caractères en converter objet de type T. Ce convertisseur (StringConverter<T>) est donc utilisé pour convertir les éléments de type T en String et inversement. Booléen qui indique si le ComboBox est éditable, c'est-à-dire que editable l'utilisateur peut saisir une valeur qui n'est pas dans la liste des éléments. IHM-1 – FX08 – Jacques BAPST 32 ComboBox [3] ListView [1] Nombre maximal de lignes affichées dans la liste déroulante visibleRowCount (Integer). Si ce nombre est dépassé, une barre de défilement permet à l'utilisateur de parcourir la liste (scrolling) Traitement de l'événement déclenché lors du changement de la onAction valeur sélectionnée (value). onShowing onShown onHiding onHidden promptText placeholder Traitement des événements associés à l'ouverture et à la fermeture du menu déroulant (juste avant et juste après l'ouverture et la fermeture de la fenêtre popup). Texte affiché si le composant n'est pas éditable et qu'aucun élément n'est sélectionné. Par exemple à l'état initial ou après annulation de la sélection (setValue(null);). Composant (de type Node) à afficher si la liste des éléments est vide. IHM-1 – FX08 – Jacques BAPST 33 ComboBox [4] § Le composant ListView<T> permet d'afficher un ensemble d'éléments (de type T) sous la forme d'une liste scrollable. § Les éléments de la liste peuvent être affichés verticalement (par défaut) ou horizontalement. § L'utilisateur peut sélectionner les éléments (sélection simple par défaut, mais sélection multiple possible) ou interagir avec eux. § Comme pour les ComboBox, les éléments sont enregistrés en interne dans une collection de type ObservableList (modèle du composant) qui informe le composant ListView des changements intervenus. § Les éléments de la liste peuvent potentiellement être édités par l'utilisateur (pour cela, la propriété cellFactory doit avoir été redéfinie). § Le nombre d'éléments affichés dépend de la taille préférée du composant (prefWidth, prefHeight). Si nécessaire des barres de défilement (ascenseurs) apparaîtront pour permettre le scrolling. IHM-1 – FX08 – Jacques BAPST 35 ListView [2] § Le peuplement de la liste peut être effectué en ajoutant les éléments avec les méthodes add() ou addAll() de la propriété items (comme dans les exemples pour ChoiceBox et ComboBox). ComboBox<String> cbbCountry = new ComboBox<String>(); cbbCountry.getItems().addAll("Allemagne", "Angleterre", "Belgique", "Espagne", "France", "Italie", "Pays-Bas", "Portugal", "Suisse"); § Une autre manière de faire (courante) est d'utiliser des méthodes statiques de la classe FXCollections pour créer des objets de type ObservableList<T> qui seront assignés au composant ListView avec la méthode setItems() ou alors passés en paramètre à son constructeur. cbbCountry.setPromptText("Select a Country..."); cbbCountry.setVisibleRowCount(5); // Max 5 éléments visibles cbbCountry.setOnAction(event -> { System.out.println(cbbCountry.getValue()); }); ListView<String> livCountry = new ListView<String>(); ObservableList<String> countries = FXCollections.observableArrayList( "Allemagne", "Angleterre", "Belgique", "Espagne", "France", "Italie", "Pays-Bas", "Portugal", "Suisse" ); livCountry.setItems(countries); IHM-1 – FX08 – Jacques BAPST 34 IHM-1 – FX08 – Jacques BAPST 36 ListView [3] ListView [5] § Quelques propriétés du composant ListView : § Méthodes pour gérer la visibilité des éléments de ListView : Liste des éléments à afficher dans la liste. Un objet de type ObservableList<T> que l'on peut également passer au items constructeur. La liste des éléments peut être modifiée dynamiquement (ajout, suppression, …). Modèle de sélection des éléments MultipleSelectionModel<T>. selectionModel - Unique : setSelectionMode(SelectionMode.SINGLE) - Multiple : setSelectionMode(SelectionMode.MULTIPLE) orientation Orientation de la liste. Type Orientation. Booléen qui indique si la ListView est éditable. Cela implique que editable les éléments (ListCell) doivent également être éditables ce qui nécessite de redéfinir la propriété cellFactory. Composant (de type Node) à afficher si la liste des éléments est placeholder vide. Fait défiler la liste (scroll), si nécessaire, pour que l'élément correspondant à l'indice passé en paramètre soit visible. Fait défiler la liste (scroll), si nécessaire, pour que l'élément scrollTo(object) correspondant à l'objet passé en paramètre soit visible. scrollTo(index) § Les éléments de la liste peuvent être quelconques et sont représentés par des objets de type ListCell. § Il existe quelques sous-classes prédéfinies de ListCell qui permettent de créer facilement des listes éditables dont les éléments correspondent à des composants connus. • CheckBoxListCell : Éléments de type CheckBox • ChoiceBoxListCell : Éléments de type ChoiceBox : Éléments de type ComboBox • TextFieldListCell : Éléments de type TextField • ComboBoxListCell IHM-1 – FX08 – Jacques BAPST 37 ListView [4] fixedCellSize cellFactory focusModel onScrollTo onEditStart onEditCommit onEditCancel IHM-1 – FX08 – Jacques BAPST 39 ListView [6] Valeur Double qui définit une hauteur (ou largeur si horizontal) fixe pour tous les éléments de la liste (calculée sinon pour chacun des éléments). Accélère l'affichage pour les longues listes. Définition d'une fabrique d'éléments (factory) permettant de personnaliser totalement les éléments de la liste. Type Callback<ListView<T>,ListCell<T>> Modèle permettant de connaître l'élément de la liste qui possède le focus (ou son index). Définition de l'action à exécuter lorsque la méthode scrollTo() a été invoquée. Définition des actions à exécuter lors des différentes phases de l'édition des éléments (s'ils sont éditables). § Exemple d'utilisation d'un composant ListView pour permettre à l'utilisateur de sélectionner des pays (sélection multiple) et affichage sur la console de la sélection courante (à chaque changement). ListView<String> livCountry = new ListView<String>(); ObservableList<String> countries = FXCollections.observableArrayList( "Allemagne", "Angleterre", "Belgique", "Espagne", "France", "Italie", "Pays-Bas", "Portugal", "Suisse" ); livCountry.setItems(countries); livCountry.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); livCountry.setFixedCellSize(22); livCountry.setPrefSize(200, 180); livCountry.setOnMouseClicked(event -> { System.out.println(livCountry.getSelectionModel().getSelectedItems()); }); IHM-1 – FX08 – Jacques BAPST 38 IHM-1 – FX08 – Jacques BAPST 40 ListView [7] ListView [9] § Exécution de l'application : § L'utilisateur peut cliquer sur un des éléments de la liste et faire apparaître un composant qui s'apparente à un ComboBox qui lui permet de choisir dans la liste déroulante. • La valeur sélectionnée dans la liste déroulante devient la valeur de l'élément de la ListView. • Affichage sur la console [Angleterre] [Angleterre, Belgique] [Angleterre, Belgique, Italie] [Espagne] IHM-1 – FX08 41 – Jacques BAPST IHM-1 – FX08 – Jacques BAPST 43 ListView [8] Spinner [1] § Exemple d'utilisation d'un composant ListView en plaçant une liste déroulante (ComboBox) comme élément éditable de la liste. § Le composant Spinner<T> permet à l'utilisateur de sélectionner une valeur dans une liste d'éléments ordonnés (de type T). ObservableList<String> candidates = FXCollections.observableArrayList(); ObservableList<String> top10 = FXCollections.observableArrayList(); ListView<String> listView = new ListView<String>(top10); listView.setPrefSize(180, 235); listView.setEditable(true); § Deux boutons permettent à l'utilisateur de faire défiler les valeurs successives de la liste dans l'ordre croissant et décroissant. § Les valeurs sélectionnables sont définies dans le modèle du composant qui est de type SpinnerValueFactory<T>. Éléments de la ComboBox candidates.addAll("Mommy", "Interstallar", "Gone Girl", "Her", "Boyhood", "Le sel de la terre", "Dragon 2", "Night Call", "Les combattants", "The Raid 2", "12 Years a Slave", "Pride", "Timbuktu", "R", "Winter Sleep", "Philomena", "Nebraska" ); for (int i = 1; i <= 10; i++) { top10.add("Select Position " + i + "..."); } • Trois modèles sont proposés par défaut (classes internes) : ð ð ð // Valeurs initiales ComboBox comme ListCell listView.setItems(top10); listView.setCellFactory(ComboBoxListCell.forListView(candidates)); IHM-1 – FX08 § Seule la valeur de l'élément sélectionné est affichée dans un champ texte interne (TextField) nommé éditeur (propriété Editor). – Jacques BAPST 42 IntegerSpinnerValueFactory DoubleSpinnerValueFactory ListSpinnerValueFactory § Il est possible de rendre la zone de texte éditable. L'utilisateur doit alors saisir des valeurs valides (en fonction du modèle interne choisi) sinon la valeur sera adaptée [min, max] ou une exception sera générée. IHM-1 – FX08 – Jacques BAPST 44 Spinner [2] § Le défilement des valeurs peut être rendu circulaire (wrapping). § L'emplacement des boutons de défilement est configurable en changeant le style CSS associé à la classe du composant. • En plus du style par défaut (right vertical), cinq styles sont définis par des constantes de la classe Spinner. Spinner<Integer> Spinner<Double> sprI sprD = new Spinner<>(-20, 20, 0, 5); = new Spinner<>(1.0, 6.0, 4.0, 0.25); sprI.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL); sprD.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL); IHM-1 – FX08 – Jacques BAPST 45 Spinner [3] § Différents constructeurs permettent de créer le composant Spinner en définissant simultanément le type et les valeurs enregistrées dans le modèle interne. § Pour les modèles numériques, les valeurs minimales, maximales, initiales ainsi que les incréments (step) peuvent être définis. § Exemples : Spinner<Integer> sprTemp = new Spinner<>(-20, 20, 0, 5); Spinner<Double> sprGrades = new Spinner<>(1.0, 6.0, 4.0, 0.25); Spinner<String> sprDays = new Spinner<>( FXCollections.observableArrayList( "Monday", "Tuesday", Modèle de type "Wednesday", ListSpinnerValueFactory "Thursday", "Friday", "Saturday", "Sunday" )); IHM-1 – FX08 – Jacques BAPST 46 Panneaux spécialisés [2] Cours IHM-1 JavaFX § Suite des panneaux spécialisés : • Pagination • TitledPane 9 - Panneaux spécialisés • Accordion Scroll-, Split-, Tab-, … Panes • ToolBar : Panneau d'accès à plusieurs 'pages' contenant chacune un composant. Accès direct par des boutons. : Panneau contenant un en-tête (titre) et un composant qui peut être ouvert ou fermé en cliquant sur le titre. : Panneau contenant plusieurs TitledPanes dont un seul peut être ouvert à la fois. : Barre horizontale ou verticale contenant des composants 'outils' (boutons ou autres). Un bouton-menu est affiché si tous les composants ne peuvent pas être affichés. § Ces composants sont sommairement expliqués dans ce chapitre. Il faut cependant consulter la documentation pour connaître l'ensemble de leurs caractéristiques et de leurs fonctionnalités. Jacques BAPST [email protected] IHM-1 – FX09 – Jacques BAPST 3 Panneaux spécialisés [1] ScrollPane [1] § Un certain nombre de composants ne font pas partie de la famille des Layout-Panes (les sous-classes de Pane) mais jouent en partie un rôle de conteneur. § Le composant ScrollPane permet d'afficher tout ou partie d'un autre composant et en offrant la possibilité d'afficher des barres de défilement pour déplacer la partie visible (scrolling) du composant 'observé'. § Parmi ces panneaux spécialisés (une désignation non-officielle), on peut mentionner les composants suivants : • ScrollPane • SplitPane • TabPane IHM-1 – FX09 § C'est donc une sorte de fenêtre (viewport) sur un autre composant. : Panneau permettant le scrolling d'un autre composant à l'aide de barres de défilement (scrollbar) ou à l'aide de la souris (panning). : Panneau divisé en plusieurs volets redimensionnables et contenant chacun un composant. Le panneau peut être divisé horizontalement ou verticalement. : Panneau à onglets. Un composant est assigné à chaque onglet. Chaque onglet possède un titre. Un seul onglet est visible à la fois (il est sélectionnable par l'utilisateur). – Jacques BAPST 2 § Il est également possible de permettre à l'utilisateur de se déplacer avec la souris ou autre dispositif de pointage (panning). § Un seul composant peut être observé au travers d'un ScrollPane, cependant, le composant peut naturellement être un conteneur ou n'importe quel nœud (Node) d'un graphe de scène. § On peut définir les règles d'affichage (policies) des barres de défilement horizontales et verticales ("jamais", "toujours" et "si nécessaire"). IHM-1 – FX09 – Jacques BAPST 4 ScrollPane [2] ScrollPane [4] § Quelques propriétés du composant ScrollPane : § Exemple d'utilisation d'un ScrollPane pour visualiser une image. Le composant 'observé' au travers du scroll-pane (Node). Booléen qui indique si l'utilisateur peut déplacer le pannable composant observé (le viewport) avec la souris. Par défaut : false. Largeur préférée de la fenêtre d'observation (viewport) prefViewportWidth (Double). Hauteur préférée de la fenêtre d'observation (viewport) prefViewportHeight (Double). Valeurs minimales, maximales et courantes de la position hmin, hmax, hvalue des barres de défilement horizontale et verticale (Double). vmin, vmax, vvalue Par défaut min=0, max=1. Règles d'affichage des barres de défilement horizontale et hbarPolicy verticale (ScrollPane.ScrollBarPolicy). Les valeurs vbarPolicy possibles sont : NEVER, ALWAYS, AS_NEEDED. content IHM-1 – FX09 – Jacques BAPST 5 ScrollPane [3] fitToWidth fitToHeight viewportBounds private static final String IMG = "/resources/Monument_Valley.jpg"; private ScrollPane root = new ScrollPane(); private Image image = new Image(getClass().getResourceAsStream(IMG)); private ImageView imagev = new ImageView(image); . . . root.setContent(imagev); root.setPannable(true); root.setPrefViewportWidth(300); root.setPrefViewportHeight(200); primaryStage.setScene(new Scene(root)); primaryStage.show(); . . . L'utilisateur peut aussi déplacer l'image avec la souris (panning) IHM-1 – FX09 – Jacques BAPST 7 SplitPane [1] Si le composant 'observé' est redimensionnable (resizable), et que cette propriété booléenne est true, le composant sera redimensionné pour que sa largeur corresponde à la largeur de la fenêtre d'observation (largeur du viewport). Si le composant 'observé' est redimensionnable (resizable), et que cette propriété booléenne est true, le composant sera redimensionné pour que sa hauteur corresponde à la hauteur de la fenêtre d'observation (hauteur du viewport). Limites actuelles du viewport (Bounds). Caractéristiques du rectangle qui correspond à la fenêtre d'observation du composant. § Le composant SplitPane est un panneau divisé horizontalement ou verticalement en sous-panneaux contenant chacun un composant. § Les barres de division entre les sous-panneaux (dividers) peuvent être déplacées par l'utilisateur ce qui va provoquer (dans certaines limites) le redimensionnement des deux panneaux voisins. § Le nombre de sous-panneaux n'est pas limité. § Pour permettre le déplacement des dividers, la taille des composants placés dans un SplitPane doit donc pouvoir être adaptée. C'est pourquoi on place généralement des conteneurs (layout-panes) dans les sous-panneaux car ils sont redimensionnables. § Les sous-panneaux sont ajoutés en invoquant une des méthodes : • getItems.add(node); • getItems.add(index, node); • getItems.addAll(node1, node2, …); IHM-1 – FX09 – Jacques BAPST 6 IHM-1 – FX09 – Jacques BAPST 8 SplitPane [2] SplitPane [4] § La seule propriété du composant SplitPane est orientation qui indique si les sous-panneaux sont placés horizontalement (orientation par défaut) ou verticalement. § Exemple d'utilisation d'un SplitPane avec trois sous-panneaux. § On utilisera les constantes : • Orientation.HORIZONTAL • Orientation.VERTICAL • getDividerPositions() : Retourne un tableau de double (double[]) root sp1 sp2 sp3 lbl1 btn2 tfd3 = = = = = = = new new new new new new new SplitPane(); StackPane(); StackPane(); StackPane(); Label("A label"); Button("A button"); TextField("A textfield"); root.getItems().addAll(sp1, sp2, sp3); root.setDividerPositions(0.2, 0.6); • setDividerPosition(int dividerIndex, double position) • setDividerPositions(double... positions) – Jacques BAPST SplitPane StackPane StackPane StackPane Label Button TextField sp1.getChildren().add(lbl1); sp2.getChildren().add(btn2); sp3.getChildren().add(tfd3); § Les méthodes suivantes permettent de consulter ou définir la position des barres de séparation (dividers) : IHM-1 – FX09 private private private private private private private primaryStage.setScene(new Scene(root, 250, 80)); 9 IHM-1 – FX09 – Jacques BAPST SplitPane [3] TabPane [1] § Lorsque le composant SplitPane est redimensionné, la taille de tous les sous-panneaux est adaptée proportionnellement (c'est le comportement par défaut). § Le composant TabPane est un panneau avec des onglets (tabs) associés à des sous-panneaux contenant chacun un composant. § Si l'on souhaite qu'un ou plusieurs sous-panneaux gardent leur taille actuelle lors du redimensionnement du SplitPane on peut utiliser la méthode statique : • setResizableWithParent(Node node, Boolean resizable) § Une valeur false n'empêchera pas l'utilisateur de modifier la position des barres de séparation. L'impact est uniquement sur le redimensionnement du SplitPane lui-même (provoqué par exemple par un redimensionnement de la fenêtre principale). 11 § Seul un des onglets est visible à un moment donné, mais l'utilisateur peut basculer entre ces différents sous-panneaux en cliquant sur l'onglet associé. • Les onglets peuvent être positionnés sur un des quatre bords du composant (ils sont placés en haut par défaut). • Si la taille du composant ne permet pas d'afficher tous les onglets, un bouton (MenuButton) est automatiquement ajouté pour permettre à l'utilisateur d'accéder aux onglets masqués. § La classe Tab représente un onglet et associe un composant (Node) avec un texte (et éventuellement un graphique) qui identifie l'onglet. § Par défaut, les onglets peuvent être supprimés par l'utilisateur en cliquant sur une croix affichée à côté de l'onglet actuellement sélectionné (à la manière des onglets des navigateurs web). IHM-1 – FX09 – Jacques BAPST 10 IHM-1 – FX09 – Jacques BAPST 12 TabPane [2] TabPane [4] § Dans un TabPane on ne peut ajouter que des objets de type Tab (on ne peut pas ajouter directement des conteneurs ou composants). § Quelques propriétés de la classe TabPane : § Il faut donc : • Créer des objets de type Tab et y insérer des composants • Créer un objet de type TabPane et y ajouter les onglets (tabs) créés tabClosingPolicy § En code Java : Tab tab1 Tab tab2 rotateGraphic = new Tab("Texte Tab 1"); = new Tab("Texte Tab 2"); tabMinWidth . . . tab1.setContent(node1); tab2.setContent(node2); tabMinHeight // Ajout du contenu à l'onglet 1 // Ajout du contenu à l'onglet 2 tabMaxWidth tabMaxHeight . . . TabPane side tPane = new TabPane(); selectionModel tPane.getTabs().addAll(tab1, tab2, …); IHM-1 – FX09 – Jacques BAPST 13 Détermine de quel côté sont affichés les onglets ( Side). Définit les règles globales de fermeture (suppression) des onglets par l'utilisateur (en cliquant sur le symbole x) (TabPane.TabClosingPolicy). Valeurs possibles : ALL_TABS, SELECTED_TAB, UNAVAILABLE Booléen qui indique si les icônes (graphic) associés aux onglets doivent tourner pour suivre l'orientation des onglets (en fonction de la position des onglets). Largeur minimale des onglets (en-tête) (Double). Hauteur minimale des onglets (en-tête) (Double). Largeur maximale des onglets (en-tête) (Double). Hauteur maximale des onglets (en-tête) (Double). Modèle de sélection des onglets (sélection simple) (SingleSelectionModel<Tab>). IHM-1 – FX09 – Jacques BAPST 15 TabPane [3] TabPane [5] § Quelques propriétés de la classe Tab : § Exemple d'une application comportant trois TabPane créés par trois méthodes distinctes, de manière à décomposer le code. text graphic closable id content selected disable toolTip contextMenu IHM-1 – FX09 Texte affiché dans l'onglet (String). Composant additionnel (généralement une icône ou un graphique) associé à l'onglet (Node). Booléen qui indique si l'onglet peut être fermé (supprimé) par l'utilisateur. Identificateur de l'onglet (indépendant du texte affiché) (String). Le composant associé à cet onglet (contenu affiché dans le panneau) (Node). Booléen qui indique si l'onglet est sélectionné. Inactivation de l'onglet (Boolean). Bulle d'aide associée à l'onglet (Tooltip). Menu contextuel associé à l'onglet (ContextMenu). – Jacques BAPST 14 • Les onglets peuvent être supprimés par l'utilisateur en cliquant sur la croix (x) placée à côté du texte (comportement par défaut). • Un des onglets (About) possède un graphique (rectangle rouge). private TabPane root = new TabPane(); private Tab private Tab private Tab tab1 tab2 tab3 = new Tab("Varia"); = new Tab("Login"); = new Tab("About"); tab1.setContent(createTab1()); tab2.setContent(createTab2()); tab3.setContent(createTab3()); tab3.setGraphic(new Rectangle(20, 10, Color.RED)); root.setRotateGraphic(true); root.getTabs().addAll(tab1, tab2, tab3); IHM-1 – FX09 – Jacques BAPST 16 TabPane [6] TabPane [8] § Méthode de création du composant associé au premier onglet. § Méthode de création du composant associé au troisième onglet. private Pane createTab1() VBox pane; Button btnA = Label lblB = ComboBox<String> cbbC = private Pane createTab3() { BorderPane pane = new BorderPane(); { new Button("Alpha"); new Label("Bravo"); new ComboBox<>(); Label lblTitle = new Label("About TabPane"); lblTitle.setFont(Font.font("SansSerif", FontWeight.BOLD, 18)); lblTitle.setTextFill(Color.DARKBLUE); BorderPane.setAlignment(lblTitle, Pos.CENTER); pane.setTop(lblTitle); pane = new VBox(10); pane.setPadding(new Insets(10)); pane.setStyle("-fx-background-color: #DDFFEE"); pane.setAlignment(Pos.CENTER); TextArea txaText = new TextArea("A very useful app\n" + "from Sella Kellos"); txaText.setPrefColumnCount(10); txaText.setPrefRowCount(3); BorderPane.setMargin(txaText, new Insets(10)); pane.setCenter(txaText); cbbC.getItems().addAll("Charlie", "Delta"); cbbC.getSelectionModel().select(0); pane.getChildren().addAll(btnA, lblB, cbbC); return pane; } return pane; } IHM-1 – FX09 – Jacques BAPST 17 IHM-1 – FX09 – Jacques BAPST TabPane [7] Pagination [1] § Méthode de création du composant associé au deuxième onglet. § Le composant Pagination permet de présenter des informations ou des éléments d'interaction sous forme de 'pages'. Une liste de boutons (ou de puces) permettent à l'utilisateur d'accéder aux pages individuelles (de les afficher). private Pane createTab2() { GridPane pane = new Label lblUser = new Label lblPass = new TextField tfdUser = new PasswordField tfdPass = new Button btnLogin = new GridPane(); Label("Username:"); Label("Password:"); TextField(); PasswordField(); Button("Login"); 19 § Les pages sont constituées de conteneurs ou de composants quelconques (Nodes). § La classe dispose des constructeurs suivants : pane.setHgap(6); pane.setVgap(12); pane.setPadding(new Insets(15)); pane.setStyle("-fx-background-color: #FCF7CC"); pane.add(lblUser, 0, 0); pane.add(tfdUser, 1, 0); pane.add(lblPass, 0, 1); pane.add(tfdPass, 1, 1); pane.add(btnLogin, 0, 2, 2, 1); GridPane.setHalignment(btnLogin, HPos.CENTER); GridPane.setMargin(btnLogin, new Insets(10, 0, 0, 0)); return pane; • Pagination(); • Pagination(int pageCount); • Pagination(int pageCount, int pageIndex); § Les boutons peuvent être remplacés par des puces (bullets) en adaptant le style de l'affichage du composant Pagination : • getStyleClass().add(Pagination.STYLE_CLASS_BULLET); } IHM-1 – FX09 – Jacques BAPST 18 IHM-1 – FX09 – Jacques BAPST 20 Pagination [2] Pagination [4] § Le nombre maximal d'indicateurs de pages (boutons ou puces), fixé par défaut à 10, peut être adapté en modifiant la valeur de la propriété maxPageIndicatorCount : § Méthode de création des pages (liste verticale de boutons). private Node createPage(int pageIndex, int nbPerPage) { VBox pane = new VBox(10); Label lblTitle = new Label("Inbox Messages ("+(pageIndex+1)+")"); • root.setMaxPageIndicatorCount(5); pane.setPadding(new Insets(20)); pane.setAlignment(Pos.CENTER); § Pour créer les pages, il faut définir la propriété pageFactory qui est de type Callback<Integer, Node> (interface fonctionnelle) possédant une unique méthode chargée de créer la page dont l'indice est passé en paramètre : lblTitle.setFont(Font.font("System", FontWeight.BOLD, 18)); lblTitle.setTextFill(Color.TOMATO); pane.getChildren().add(lblTitle); int offset = pageIndex * nbPerPage; • call(Integer pageIndex) : Doit retourner un objet de type Node for (int i=offset; i<(offset+nbPerPage); i++) { if (i>=sList.length) return new Label("Page not found !"); Button btn = new Button("Message from " + sList[i]); btn.setMinWidth(150); pane.getChildren().addAll(btn); } § L'objet Callback peut être créé à l'aide d'une expression lambda (voir l'exemple qui suit), d'une classe anonyme ou en instanciant une classe 'ordinaire'. return pane; } IHM-1 – FX09 – Jacques BAPST 21 IHM-1 – FX09 – Jacques BAPST Pagination [3] TitledPane [1] § Exemple d'une application affichant différentes pages à l'aide du composant Pagination. § Le composant TitledPane est un panneau avec un titre. § Le panneau peut être ouvert ou fermé en cliquant sur le titre qui agit comme un bouton à deux états. • Les pages sont créées par la méthode createPage() § Le panneau peut contenir n'importe quel objet de type Node (généralement un conteneur qui regroupe des composants). private String[] sList = {"Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot","Golf", "Hotel", "India", "Juliet", "Kilo", "Lima" }; private Pagination root = new Pagination(5, 0); § La propriété collapsible détermine si le panneau peut être fermé (comportement par défaut). Sinon, il reste toujours ouvert et le composant est un panneau avec un titre. primaryStage.setTitle("Pagination Test"); root.setPageFactory(index -> { return createPage(index, 4); }); § La propriété expanded indique si le panneau est ouvert ou fermé. primaryStage.setScene(new Scene(root)); primaryStage.show(); IHM-1 – FX09 – Jacques BAPST 23 22 IHM-1 – FX09 – Jacques BAPST 24 TitledPane [2] ToolBar [1] § Le titre du composant TitledPane est une sous-classe de Labeled et peut donc contenir un texte et/ou un graphique : § Le composant ToolBar permet de rassembler (horizontalement ou verticalement) des éléments d'interaction : c'est une barre d'outils. • setText(String title) • setGraphic(Node graphic) § Le contenu du composant (panneau) est géré par la propriété content. Le panneau est généralement représenté par un conteneur (layout-pane) qui rassemble et dispose les composants : • setContent(Node root) § Le composant TitledPane constitue l'élément de base du composant Accordion (voir pages suivantes). § Dans une barre d'outils on y trouve généralement des boutons, des boutons à deux états, des séparateurs mais on peut y placer n'importe quel composant. § Une des caractéristiques du composant ToolBar est que si tous les composants qu'il contient ne peuvent pas être affichés, un boutonmenu apparaît automatiquement pour permettre à l'utilisateur d'accéder aux outils non visibles. § La propriété orientation détermine si la barre d'outils est disposée horizontalement ou verticalement. § Les éléments de la barre d'outils peuvent être insérés soit dans le constructeur new ToolBar(node1, node2, …) soit avec les méthodes getItems().add(node) ou getItems().addAll(node1, node2, …). IHM-1 – FX09 – Jacques BAPST 25 IHM-1 – FX09 – Jacques BAPST 27 Accordion ToolBar [2] § Le composant Accordion rassemble un groupe de TitledPane dont un seul peut être ouvert à la fois. § Le composant Separator peut être inséré entre les éléments de la barre d'outils. Son orientation sera automatiquement adaptée en fonction de l'orientation de la barre d'outils. § La liste des composants TitledPane est retournée par la méthode getPanes(). § Pour créer un composant Accordion, il faut • Créer les composants TitledPane (voir pages précédentes) • Les assembler dans le composant Accordion en les ajoutant à la liste de manière individuelle ou collective : ð ð getPanes().add(tPane) getPanes().addAll(tPane1, tPane2, …) Separator § La propriété expandedPane représente le panneau ouvert (type TitledPane). IHM-1 – FX09 – Jacques BAPST 26 IHM-1 – FX09 – Jacques BAPST 28 Modal / Non-modal § Une boîte de dialogue peut être Cours IHM-1 JavaFX • Modale : L'utilisateur ne peut pas interagir avec la fenêtre dont la boîte de dialogue dépend avant de l'avoir fermée. • Non-modale : L'utilisateur peut interagir avec la boîte de dialogue mais (modeless) aussi avec la fenêtre dont la boîte de dialogue dépend (en laissant la boîte de dialogue ouverte). (modal) 10 - Boîtes de dialogue § On utilisera une boîte de dialogue modale par ex. pour confirmer ou annuler une action critique (suppression de données par exemple). (simples et spécialisées) • La fenêtre principale est 'bloquée' tant que l'utilisateur n'a pas décidé. • La boîte de dialogue se ferme automatiquement dès la décision prise. § Une boîte de dialogue non-modale sera utilisée par exemple pour présenter à l'utilisateur une palette d'outils qu'il pourra sélectionner et appliquer successivement sur la fenêtre principale. Jacques BAPST [email protected] • La boîte de dialogue reste ouverte tant que l'utilisateur ne la ferme pas explicitement. IHM-1 – FX10 – Jacques BAPST 3 Définition [1] § Les boîtes de dialogue sont des éléments d'une interface graphique qui se présentent généralement sous la forme d'une fenêtre affichée par une application (ou éventuellement par le système d'exploitation) dans le but : • d'informer l'utilisateur (texte, mise en garde, graphique, etc.) Boîtes de dialogue simples • d'obtenir une information de l'utilisateur (mot-de-passe, choix, etc.) • ou une combinaison des deux ð Par exemple l'informer d'un événement et lui demander de faire un choix § Une boîte de dialogue dépend d'une autre fenêtre (év. d'une autre boîte de dialogue), c'est ce qui distingue ce composant d'une fenêtre indépendante (la fenêtre principale par exemple). IHM-1 – FX10 – Jacques BAPST 2 IHM-1 – FX10 – Jacques BAPST 4 Types de boîtes de dialogue Boîte de dialogue - Alert [1] § Dans une application, on peut trouver § Boîte de dialogue Alert de type "Information" avec en-tête (header). • des boîtes de dialogue standard avec une mise en page prédéfinie et qui sont utilisées Pour informer l'utilisateur par un simple texte ð Pour demander quittance à l'utilisateur ð Pour demander à l'utilisateur de faire un choix entre plusieurs options ð Pour saisir une information simple (ligne de texte) Alert dialog = new Alert(AlertType.INFORMATION); dialog.setTitle("An information dialog-box"); dialog.setHeaderText("An information dialog with header"); dialog.setContentText("Le message d'information\n" + "(une deuxième ligne)"); dialog.showAndWait(); ð • des boîtes de dialogue spécifiques avec ð ð Une disposition des éléments propre à l'application De nombreux composants pour représenter l'information § La plupart des kits de développement offrent des solutions clés en main pour les boîtes de dialogue simples (standard). § Les boîtes de dialogue spécifiques doivent par contre être conçues et réalisées avec les mêmes efforts que pour tout autre type d'interface (choix des conteneurs et composants, contraintes de disposition, etc.). IHM-1 – FX10 – Jacques BAPST 5 IHM-1 – FX10 – Jacques BAPST Boîtes de dialogue JavaFX Boîte de dialogue - Alert [2] § Les boîtes de dialogue standard n'ont été introduites que très tardivement dans JavaFX (version 8u40, en 2015). § Boîte de dialogue Alert de type "Warning" sans en-tête (header). Alert dialogW = new Alert(AlertType.WARNING); § Avant cette version, il était habituel d'utiliser les composants de type Dialogs de la librairie externe ControlsFX (qui sont désormais signalés comme étant deprecated). § Les pages suivantes décrivent sommairement, et sur la base d'exemples concrets, la manière de coder les boîtes de dialogue en se basant sur les classes et composants disponibles dans JavaFX. IHM-1 – FX10 – Jacques BAPST 7 6 dialogW.setTitle("A warning dialog-box"); dialogW.setHeaderText(null); // No header dialogW.setContentText("Caution : Low Level Battery !"); dialogW.showAndWait(); IHM-1 – FX10 – Jacques BAPST 8 Boîte de dialogue - Alert [3] Boîte de dialogue - Alert [5] § Boîte de dialogue Alert de type "Error". § Boîte de dialogue Alert de type "Confirmation" avec options personnalisées. Alert dialogE = new Alert(AlertType.ERROR); Alert dBox = new Alert(AlertType.CONFIRMATION); dialogE.setTitle("An error dialog-box"); dialogE.setHeaderText("Not Printed"); dialogE.setContentText("Error : Incorrect Paper-Format !"); dialogE.showAndWait(); dBox.setTitle("A confirmation dialog-box with custom actions"); dBox.setHeaderText("Java-Pizza : The Very Best in Town !"); dBox.setContentText("Choose your pizza size :"); ButtonType ButtonType ButtonType ButtonType btnSmall btnMedium btnBig btnCancel = = = = new new new new ButtonType("Small"); ButtonType("Medium"); ButtonType("Big"); ButtonType("Cancel", ButtonData.CANCEL_CLOSE); dBox.getButtonTypes().setAll(btnSmall, btnMedium, btnBig, btnCancel); IHM-1 – FX10 – Jacques BAPST 9 Boîte de dialogue - Alert [4] IHM-1 – FX10 – Jacques BAPST 11 Boîte de dialogue - Alert [6] § Boîte de dialogue Alert de type "Confirmation". Optional<ButtonType> choice = dBox.showAndWait(); Alert dialogC = new Alert(AlertType.CONFIRMATION); if (choice.get() == btnSmall) { System.out.println("User chose Small"); } else if (choice.get() == btnMedium) { System.out.println("User chose Medium"); } else if (choice.get() == btnBig) { System.out.println("User chose Big"); } else { System.out.println("Cancel or Close"); } dialogC.setTitle("A confirmation dialog-box"); dialogC.setHeaderText(null); dialogC.setContentText("Full backup will start immediately"); Optional<ButtonType> answer = dialogC.showAndWait(); if (answer.get() == ButtonType.OK) { System.out.println("User chose OK"); } else { System.out.println("User chose Cancel or closed the dialog-box"); } IHM-1 – FX10 – Jacques BAPST 10 IHM-1 – FX10 – Jacques BAPST 12 Saisie de texte - TextInputDialog [1] Sélection - ChoiceDialog [1] § Boîte de dialogue TextInputDialog pour saisir une ligne de texte. § Boîte de dialogue ChoiceDialog pour saisir un choix (dans une liste). TextInputDialog inDialog = new TextInputDialog("Guest"); inDialog.setTitle("A Text-Input Dialog"); inDialog.setHeaderText("Account Login (or Guest Access)"); inDialog.setContentText("Username :"); String[] Valeur par défaut ChoiceDialog<String> cDial = new ChoiceDialog<>(choices[2], choices); cDial.setTitle("A Choice Dialog"); cDial.setHeaderText("Select Execution Priority"); cDial.setContentText("Priority:"); Optional<String> textIn = inDialog.showAndWait(); //--- Get response value (traditional way) if (textIn.isPresent()) { System.out.println("Login name = " + textIn.get()); } Valeur par défaut Optional<String> selection = cDial.showAndWait(); selection.ifPresent(str -> System.out.println("Selection:" + str)); Expression lambda non exécutée si l'utilisateur presse Cancel ou ferme la boîte de dialogue Retourne false si l'utilisateur presse Cancel ou ferme la boîte de dialogue IHM-1 – FX10 choices = {"Urgent", "High", "Standard", "Low"}; – Jacques BAPST 13 IHM-1 – FX10 – Jacques BAPST Saisie de texte - TextInputDialog [2] Boîte de dialogue personnalisée [1] § Le texte saisi par l'utilisateur peut aussi être récupéré en utilisant la méthode ifPresent() qui prend en paramètre un objet de type Consumer<T> que l'on peut instancier avec une expression lambda. § Il est possible de personnaliser des boîtes de dialogue (custom dialog) en utilisant directement la classe Dialog qui est la classe parente de Alert, TextInputDialog et ChoiceDialog. L'interface fonctionnelle Consumer<T> possède la méthode abstraite void accept(T t) qui effectue une opération avec la valeur t passée en paramètre (pas de valeur de retour, opération par effet de bord). Optional<String> textIn = inDialog.showAndWait(); //--- Get response value (with lambda expression) textIn.ifPresent(txt -> System.out.println("Login name = " + txt)); 15 § En plus du texte principal défini par la propriété contentText (de type String), il est possible, pour toutes les boîtes de dialogue, de définir un contenu étendu (affiché par "Show Details") représenté par la propriété expandableContent (de type Node) de l'objet interne DialogPane. Peut correspondre à § On peut définir ce contenu étendu ainsi : tout un graphe de scène Alert mBox = new Alert(AlertType.ERROR); mBox.getDialogPane().setExpandableContent(node); Expression lambda non exécutée si l'utilisateur presse Cancel ou ferme la boîte de dialogue IHM-1 – FX10 Expandable Content – Jacques BAPST 14 IHM-1 – FX10 – Jacques BAPST 16 Boîte de dialogue personnalisée [2] § Par défaut, toutes les boîtes de dialogues standards (qui héritent de Dialog) sont modales. § Pour créer une boîte de dialogue non-modale, on peut invoquer la méthode initModality() : Boîtes de dialogue spécialisées //--- To create a modeless dialog box Alert mBox = new Alert(AlertType.ERROR); mBox.initModality(Modality.NONE); § L'icône par défaut (placée dans la barre de titre de la fenêtre popup) peut être remplacée par une icône personnalisée : //--- To add a custom title-bar icon Stage dStage = (Stage)(cDial.getDialogPane().getScene().getWindow()); dStage.getIcons().add(new Image("/resources/warn_1.png")); IHM-1 – FX10 17 – Jacques BAPST IHM-1 – FX10 – Jacques BAPST 19 Boîte de dialogue personnalisée [3] FileChooser [1] § Pour changer l'icône principale qui est placée à gauche du contenu de la boîte de dialogue ou dans son en-tête (header), on peut utiliser la méthode setGraphic(). § L'utilitaire FileChooser permet d'ouvrir une boîte de dialogue permettant à l'utilisateur de naviguer dans l'arborescence des fichiers de la machine cible et de sélectionner un ou plusieurs fichiers (pour sélectionner un répertoire, il faut utiliser DirectoryChooser). //--- Add a custom content icon cDial.setGraphic(new ImageView("/resources/warn_1.png")); § Il s'agit d'une boîte de dialogue spécifique au système d'exploitation de la machine cible. Le look & feel est indépendant de JavaFX (il correspond en principe au look & feel natif). § Par défaut, cette icône est déterminée par le type de boîte de dialogue ou par le type d'alerte (AlertType : ERROR, WARNING, …). § Ce composant peut être utilisé pour : • Naviguer et sélectionner un seul fichier ð showOpenDialog() • Naviguer et sélectionner plusieurs fichiers ð showOpenMultipleDialog() • Naviguer et sauvegarder un fichier ð showSaveDialog() Icône § L'invocation de FileChooser est généralement liée à l'action d'un bouton, d'une option de menu ou autre action de l'utilisateur. Icône IHM-1 – FX10 – Jacques BAPST 18 IHM-1 – FX10 – Jacques BAPST 20 FileChooser [2] FileChooser [4] § Exemple d'utilisation de FileChooser avec : § Affichage (sous Windows) : • Définition d'un titre pour la fenêtre popup • Définition d'un répertoire initial (point de départ de la navigation) basé sur des propriétés du système • Définition de filtres basés sur les extensions des fichiers FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("FileChooser Example"); File homeDir = new File(System.getProperty("user.home")); fileChooser.setInitialDirectory(homeDir); fileChooser.getExtensionFilters().addAll( new ExtensionFilter("Text Files" , "*.txt"), new ExtensionFilter("Image Files", "*.png", "*.jpg", "*.gif"), new ExtensionFilter("Audio Files", "*.wav", "*.mp3", "*.aac"), new ExtensionFilter("All Files" , "*.*")); IHM-1 – FX10 – Jacques BAPST 21 IHM-1 – FX10 23 – Jacques BAPST FileChooser [3] FileChooser [5] § La fenêtre popup s'ouvre lors de l'invocation de la méthode show…() qui retourne le fichier sélectionné ou une référence null si l'utilisateur a pressé sur Cancel ou a fermé la fenêtre. § Pour permettre une sélection multiple, il faut utiliser la méthode showOpenMultipleDialog() qui retourne une liste de fichiers ou une référence null si l'utilisateur a pressé sur Cancel ou a fermé la fenêtre. File selectedFile = fileChooser.showOpenDialog(primaryStage); List<File> selectedFiles = fileChooser.showOpenMultipleDialog(stage); if (selectedFile != null) { try { //--- Open the file with associated application Desktop.getDesktop().open(selectedFile); } catch (Exception e) { System.err.println("ERROR: Unable to open the file"); } } if (selectedFiles != null) { for (File f : selectedFiles) { System.out.println(f); } Codage classique Avec expression lambda selectedFiles.forEach(f -> System.out.println(f)); selectedFiles.forEach(System.out::println); } Tente d'ouvrir le fichier avec l'application associée IHM-1 – FX10 – Jacques BAPST Avec référence de méthode 22 IHM-1 – FX10 – Jacques BAPST 24 DirectoryChooser [1] DirectoryChooser [3] § L'utilitaire DirectoryChooser permet d'ouvrir une boîte de dialogue permettant à l'utilisateur de naviguer dans l'arborescence des fichiers de la machine cible et de sélectionner un répertoire. § Affichage (sous Windows) : § Il s'agit d'une boîte de dialogue spécifique au système d'exploitation de la machine cible. Le look & feel est indépendant de JavaFX (il correspond au look & feel natif). § Ce composant ne comporte qu'une seule méthode de sélection showDialog(), qui retourne un objet de type File représentant un répertoire. IHM-1 – FX10 – Jacques BAPST 25 IHM-1 – FX10 – Jacques BAPST DirectoryChooser [2] DatePicker [1] § Exemple : § La classe DatePicker représente un composant qui permet à l'utilisateur de sélectionner une date dans un calendrier qui est affiché en fenêtre popup. DirectoryChooser dirChooser = new DirectoryChooser(); dirChooser.setTitle("DirectoryChooser Example"); dirChooser.setInitialDirectory(new File("D://Temp")); 27 § Le composant représente une forme de ComboBox éditable (il a comme classe parente ComboBoxBase) et la date sélectionnée par l'utilisateur peut être consultée avec la méthode getValue() qui retourne un objet de type LocalDate. File selectedFile = dirChooser.showDialog(primaryStage); if (selectedFile != null) { try { Desktop.getDesktop().open(selectedFile); } catch (Exception e) { e.printStackTrace(); } } Tente d'ouvrir le gestionnaire de fichiers de l'OS (file manager) IHM-1 – FX10 – Jacques BAPST 26 IHM-1 – FX10 – Jacques BAPST 28 DatePicker [2] ColorPicker [2] § Exemple d'utilisation du composant : . . . DatePicker dPicker = new DatePicker(); Ajout du composant dans le graphe de scène root.getChildren().add(dPicker); dPicker.setOnAction(e -> { LocalDate date = dPicker.getValue(); System.out.println("Selected date: " + date); . . . // 1..31 int day = date.getDayOfMonth(); int month = date.getMonthValue(); // 1..12 int year = date.getYear(); . . . }); . . . IHM-1 – FX10 – Jacques BAPST 29 IHM-1 – FX10 ColorPicker [1] ColorPicker [3] § La classe ColorPicker représente un composant qui permet à l'utilisateur de sélectionner une couleur dans une palette ou selon différents espaces de couleur (HSB, RBG, Web, …). § Exemple d'utilisation du composant : § Le composant représente une forme de ComboBox éditable (il a comme classe parente ComboBoxBase) et la couleur sélectionnée par l'utilisateur peut être consultée avec la méthode getValue(). . . . ColorPicker cPicker = new ColorPicker(Color.BLUE); Ajout du composant dans le graphe de scène root.getChildren().add(cPicker); § L'utilisateur peut définir et enregistrer des couleurs personnalisées (custom colors) qui peuvent être récupérées avec la méthode getCustomColors() qui retourne une liste de couleurs. 31 – Jacques BAPST cPicker.setOnAction(e -> { Color c = cPicker.getValue(); . . . //--- Display hexadecimal values (#rrggbbaa) System.out.println("Selected color: " + c); // Display decimal values RGB (0.0...1.0) System.out.println("Selected color = " + c.getRed() + ", " + c.getGreen() + ", " + c.getBlue() ); . . . }); § Une couleur par défaut peut être passée en paramètre au constructeur de la classe (par défaut Color.WHITE). . . . IHM-1 – FX10 – Jacques BAPST 30 IHM-1 – FX10 – Jacques BAPST 32 Feuille de style CSS [2] § Une définition de style est constituée : Cours IHM-1 JavaFX • D'un sélecteur (le nom du style) • D'un ensemble de règles sous la forme de paires "propriété" / "valeur" § Le sélecteur est un identificateur qui servira à définir les éléments sur lesquels s'appliqueront les règles. 11 - Feuilles de style CSS § Les paires propriété / valeur sont constituées du nom de la propriété, du séparateur ":" et de la valeur de cette propriété. § Les paires propriété / valeur sont séparées par des ";" § L'ensemble des paires propriété / valeur (les règles) est entouré par des accolades "{ }" et ce bloc suit le sélecteur. § Exemple : Jacques BAPST [email protected] .button { -fx-background-color: blue; Sélecteur -fx-text-fill: yellow; } Propriété IHM-1 – FX11 Valeur – Jacques BAPST 3 Feuille de style CSS [1] Feuille de style CSS [3] § Une feuille de style en cascade (CSS - Cascading Style Sheet) est un élément permettant de décrire l'aspect visuel des composants d'une interface. § Une feuille de style peut enregistrer plusieurs définitions de style. Chacune est identifiée par son sélecteur (différents types : class, id, …). § A l'origine les CSS ont été élaborées (par le W3C) pour les pages web, afin de pouvoir décrire la présentation des éléments HTML. § Exemple d'une feuille de style : § Un des buts de son utilisation est de séparer le contenu (HTML) de la présentation (CSS) des pages web. .label { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; -fx-text-fill: white; -fx-opacity: 0.6; } § JavaFX a repris le concept et des feuilles de style CSS peuvent être utilisées pour définir l'aspect (le look, le style) des éléments UI d'une application, qu'elle soit développée en mode procédural (API) ou déclaratif (FXML). .label-bright { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; -fx-text-fill: white; -fx-opacity: 1; } § Les CSS sont des éléments textuels, généralement enregistrés dans des fichiers, qui définissent des règles de style. Le codage de ces règles doit naturellement respecter une certaine syntaxe. #welcome-text { -fx-font-size: 32px; -fx-font-family: "Arial Black"; -fx-fill: #818181; } IHM-1 – FX11 – Jacques BAPST 2 IHM-1 – FX11 – Jacques BAPST 4 Sélecteur [1] Sélecteur de classe [1] § Il existe différentes formes de sélecteurs : § La plupart des classes de composants UI (qui héritent de Node) possèdent, par défaut, un nom de classe de style enregistré dans la liste retournée par la méthode getStyleClass(). • Sélecteur de classe (class selector) • Sélecteurs d'Id (id selector) • Sélecteurs d'éléments (element selector) • Les conteneurs ne possèdent pas, par défaut, de noms de classes de style mais on peut sans autre en ajouter explicitement dans la liste. Pas utilisé dans JavaFX § Les sélecteurs de classe sont précédés par "." • Exemple : § Le nom de classe de style correspond au nom de la classe du composant (Node), en minuscules. On ajoute un tiret séparateur "-" si le nom du composant est formé de plusieurs noms (nom-composé). .button { . . . } § Les sélecteurs d'Id sont précédés par "#" • Exemple : § Dans le CSS, le nom du sélecteur de classe correspondra au nom de la classe de style, préfixé avec un point ".". #saveButton { . . . } § Il est possible de grouper les sélecteurs pour appliquer les mêmes règles à plusieurs sélecteurs. La virgule "," est utilisée comme séparateur. § Par exemple : .button, .label { -fx-text-fill: blue; } IHM-1 – FX11 – Jacques BAPST 5 IHM-1 – FX11 Classe du composant Sélecteur de classe Button TextField Hyperlink CheckBox .button .text-field .hyperlink .check-box 7 – Jacques BAPST Sélecteur [2] Sélecteur de classe [2] § Le but des CSS est de définir le style de présentation de l'interface et plus précisément celui de chacun des nœuds du graphe de scène (composant UI, conteneurs, etc.). § Le système assigne automatiquement au nœud qui est à la racine du graphe de scène le nom de la classe de style "root". § Le rôle des sélecteurs est de déterminer à quels nœuds du graphe de scène s'appliquent les règles de style qu'ils représentent. § Chaque objet Node possède différentes variables ou propriétés qui permettent de lui associer un style. § Le JavaFX CSS Reference Guide est disponible à l'adresse : • styleClass : Liste de noms de classes de styles (au sens CSS) utilisés pour appliquer à ce nœud un style basé sur un sélecteur de classe (ObservableList<String>) • id • style IHM-1 – FX11 : Identificateur unique du nœud qui permet de lui associer un style avec un sélecteur d'Id (String) docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.htm Attention : Dans ce contexte, le mot "classe" est utilisé dans deux sens différents, ce qui peut induire une certaine confusion. : Règles de style que l'on peut appliquer directement au nœud considéré (inline, sans sélecteur) (String) – Jacques BAPST § Si l'on veut s'assurer du nom de la classe de style d'un composant JavaFX ou si l'on souhaite connaître d'autres détails associés à la définition des styles CSS, il faut consulter la documentation officielle d'Oracle. 6 IHM-1 – FX11 – Jacques BAPST A insérer dans vos bookmarks ! 8 Sélecteur d'Id [1] Autres sélecteurs [1] § Chaque objet Node possède une propriété baptisée Id dont le but est de donner une identification unique à chacun des nœuds d'un graphe de scène. § L'astérisque ("*") représente le sélecteur universel qui s'applique à tous les nœuds. C'est le sélecteur le moins spécifique. • Attention, c'est au programmeur de garantir l'unicité des Id d'un graphe de scène. Le système n'effectue aucune vérification et aucune erreur ne sera générée en cas de doublon. Cependant, on n'obtiendra peut-être pas les effets souhaités dans ce cas ! § L'Id est un identificateur enregistré sous la forme d'une chaîne de caractères (String). -fx-text-fill: darkgreen; } • Sélecteur de descendant (descendant selector) : ð Button btnOk = new Button("OK"); ð le sélecteur d'Id du composant sera "#AcceptButton". – Jacques BAPST .vbox .button { … } Les règles s'appliquent aux boutons qui se trouvent dans un conteneur de type VBox (pas nécessairement comme enfant direct) • Sélecteur d'enfant (child selector) : btnOk.setId("AcceptButton"); IHM-1 – FX11 * { § Des règles peuvent être définies en prenant en compte la hiérarchie du graphe de scène : Espace § Le nom du sélecteur d'Id correspond au nom de l'Id préfixé avec le caractère dièse "#". § Par exemple, avec le code : • Par exemple, si l'on veut définir les textes de tous les nœuds en vert foncé : 9 .vbox > .button { … } Les règles s'appliquent aux boutons qui sont directement placés dans un conteneur de type VBox (comme enfant direct) IHM-1 – FX11 – Jacques BAPST 11 Sélecteur d'Id [2] Autres sélecteurs [2] § L'Id d'un nœud peut être exploité de différentes manières. § Il est également possible de définir des sélecteurs qui dépendent de l'état du composant (nœud) auquel ils s'appliquent. § Dans le code, on peut, par exemple, rechercher un composant à partir de son sélecteur d'Id. § On parle dans ce cas de state-based selector ou de pseudo-class selector. Node n = scene.lookup("#AcceptButton"); § L'indicateur d'état est un mot-clé qui se place à droite du sélecteur avec ":" comme séparateur. § Dans un fichier CSS, on peut associer des règles de style à un sélecteur d'Id. § Exemple : § Par exemple pour changer la couleur du texte des boutons lorsque le curseur de la souris est placé au-dessus du composant : #AcceptButton { -fx-text-fill: red; } .button:hover { -fx-text-fill: red; } IHM-1 – FX11 – Jacques BAPST 10 IHM-1 – FX11 – Jacques BAPST 12 Pseudo-class Ajout des feuilles de styles [2] § Pour connaître la liste de tous les états (pseudo-class) traités par JavaFX, il faut consulter la documentation (JavaFX CSS Reference Guide). § Le paramètre passé à la méthode add() correspond à l'URL qui référence la feuille de style (le fichier xxxx.css). § On peut indiquer un chemin relatif ou absolu. § Quelques états traités • Le chemin relatif est basé sur la racine du classpath de l'application État (pseudo-class) S'applique à hover Node Lorsque la souris survole le composant focused Node Lorsque le composant possède le focus pressed Node Lorsque le bouton de la souris est pressé sur l'élément disabled Node Lorsque le composant est inactivé show-mnemonic Node Lorsque le mnémonique doit être affiché (souligné) filled Cell Lorsque la cellule est remplie selected Cell CheckBox Lorsque le composant est sélectionné readonly TextInput Control Lorsque le composant n'est pas éditable . . . . . . IHM-1 – FX11 Description ð • Le chemin absolu est de la forme [scheme:][//authority][path] ð "http://www.orion.org/app/dynamiccss/chrome.css" § Une autre manière d'obtenir l'URL d'une ressource et de passer par le class-loader en invoquant la méthode getResource() de la classe. • Un slash initial indique un chemin absolu (depuis racine de l'application) • Sinon le système ajoute le nom du package avant la chaîne de caractères final URL sURL = getClass().getResource("/css/mystyle.css"); scene.getStylesheets().add(sURL.toExternalForm()); ... – Jacques BAPST Dans la chaîne de caractère, le '/' initial est ignoré 13 IHM-1 – FX11 – Jacques BAPST Ajout des feuilles de styles [1] Feuilles de styles par défaut § Il est possible d'ajouter plusieurs feuilles de style à une application JavaFX. § Il est naturellement possible de créer une application JavaFX sans définir explicitement de feuilles de styles. § Des feuilles de style peuvent être ajoutées § Le runtime JavaFX utilise, par défaut, une feuille de style globale appelée Modena.css (représentée par la constante STYLESHEET_MODENA de la classe Application). • A une scène (objet Scene) ð Les styles s'appliquent à la scène et à tous ses descendants • Remarque : JavaFX 2.0 utilisait Caspian.css (constante STYLESHEET_CASPIAN) • Aux nœuds de type Parent (conteneurs, composants, …) ð 15 Les styles s'appliquent au nœud courant et à ses descendants § Pour appliquer ou changer la feuille de style globale utilisée par défaut (appelée aussi user-agent style sheet), on peut invoquer la méthode statique (de la classe Application) : § Tous les objets de type Scene et Parent maintiennent une liste observable (ObservableList<String>) des feuilles de style qui y sont associées. setUserAgentStylesheet(String url) • La méthode getStylesheets() retourne cette liste § Exemple : § Pour ajouter une feuille de style, on invoque la méthode add() de la liste : Application.setUserAgentStylesheet(STYLESHEET_CASPIAN); scene.getStylesheets().add("/resources/greatstyle.css"); IHM-1 – FX11 – Jacques BAPST 14 IHM-1 – FX11 – Jacques BAPST 16 Propriété style (inline style) Priorités [2] § Tous les nœuds (Node) possèdent la propriété style qui permet d'appliquer des règles de style appelées "inline" qui s'appliqueront spécifiquement à ce nœud (sans impact sur d'autres). § Les propriétés d'un nœuds peuvent être définies de plusieurs manières (dans le code Java, dans des feuilles de style CSS ou en appliquant des règles de style inline). § Les règles de styles inline sont données directement dans le code et ne sont pas enregistrées dans une feuille de style externe. § La plateforme JavaFX (runtime) applique les règles de priorités suivantes pour déterminer la valeur finale d'une propriété : 1. 2. 3. 4. 5. § La définition d'un style inline ne comporte pas de sélecteur mais comprend uniquement l'ensemble des règles : propriétés : valeur ; propriété : valeur ; … § Exemple : Règles de style inline [la plus haute priorité] Feuilles de style du nœud Parent Feuilles de style de la Scene Valeurs définies dans le code (API) Feuilles de style User-Agent [la plus basse priorité] Button btnOk = new Button("OK"); btnOk.setStyle("-fx-font-weight: bold; -fx-background-color: green;"); § Les styles inline sont prioritaires sur les autres (précisions plus loin). IHM-1 – FX11 – Jacques BAPST 17 § Par défaut, la feuille de style User-Agent est automatiquement appliquée, mais certaines règles peuvent être ensuite modifiées ou complétées par des règles avec un plus haut niveau de priorité. IHM-1 – FX11 19 – Jacques BAPST Priorités [1] Propriétés : Valeurs [1] § Si plusieurs sélecteurs de style s'appliquent au même nœud d'un graphe de scène, des règles assez complexes déterminent la priorité (specificity) de l'application des styles. § Les règles de style enregistrées dans des feuilles de style CSS ou injectées directement (inline) sont de la forme : Propriété : Valeur § Pour plus de détail, il faut consulter le document : § Dans les CSS JavaFX, les noms de propriétés commencent par "-fx-". www.w3.org/TR/CSS21/cascade.html#specificity § Parmi ces règles, un des points à considérer est que, si un sélecteur de classe et un sélecteur d'Id sont utilisés pour un même nœud, c'est le sélecteur d'Id qui a la plus haute priorité. § Le nombre de nœuds du graphe de scène affectés par une définition de style dépend du nombre de nœuds qui correspondent (match) au sélecteur considéré. IHM-1 – FX11 – Jacques BAPST 18 § Ce préfixe est suivi par un nom de propriété CSS standard ou par le nom de la propriété de l'API JavaFX transformé en minuscules et avec un tiret ("-") pour séparer les mots si la propriété est un mot composé (mixed-case). § Exemples : IHM-1 – FX11 CSS/API Property JavaFX CSS Property font padding -fx-font -fx-padding font-size textAlignment -fx-font-size -fx-text-alignment – Jacques BAPST Std CSS Property 20 Propriétés : Valeurs [2] Héritage de style [2] § Les valeurs associées aux propriétés peuvent être de différents types § Exemple : • boolean, string, number, size, percentage, duration, angle, point, color-stop, URI, effect, font, paint, inherit, … /*--- Parent Node (Container VBox) ---*/ #CmdPanel { -fx-cursor: hand; Hérité par défaut -fx-border-color: blue; -fx-border-width: 5px; } § Les propriétés qui s'appliquent à un composant particulier sont déterminées notamment par : • Les propriétés définies dans le composant lui-même • Les propriétés héritées des classes parentes /*--- Child Node (Save Button) ---*/ #SaveButton { -fx-border-color: red; -fx-border-width: inherit; } § Un certain nombre de propriétés CSS standard peuvent s'ajouter. • Les CSS JavaFX sont essentiellement basées sur la spécification du W3C CSS v2.1 avec quelques ajouts de la version CSS 3 ainsi que quelques extensions qui sont spécifiques à JavaFX. § Pour connaître l'ensemble des propriétés d'un composant ainsi que les valeurs qu'il est possible de leur associer, il faut consulter la documentation (JavaFX CSS Reference Guide). IHM-1 – FX11 Que pensez-vous du résultat final ? 21 – Jacques BAPST Hérité du conteneur parent IHM-1 – FX11 – Jacques BAPST 23 Compilation des feuilles de style § Les styles (valeurs des propriétés) d'un composant peuvent être héritées d'un composant parent (ancêtre) selon la hiérarchie du graphe de scène (à ne pas confondre avec la hiérarchie des classes). § Si les feuilles de styles deviennent volumineuses, il est possible, pour des raisons de performance et pour alléger le déploiement, de compiler les fichiers CSS qui se transformeront alors en fichiers BSS (Binary Style Sheet). > > > Héritage de style [1] • Par exemple, un composant UI peut hériter d'un conteneur parent -fx-cursor, -fx-font-family, -fx-font-style -fx-text-alignment, -fx-font-size, Compléme nt § Par défaut, il n'y a que quelques propriétés CSS qui sont héritées automatiquement : -fx-font, -fx-font-weight, IHM-1 – FX11 – Jacques BAPST < < < § Pour les autres propriétés, il est possible de "forcer" l'héritage en donnant la valeur "inherit" à la propriété dont on veut qu'elle hérite du nœud parent dans le graphe de scène. 22 § Pour compiler les feuilles de style, on peut utiliser l'utilitaire (en ligne de commande) javapackager qui fait partie de la plateforme Java. § Par exemple, pour compiler le fichier dark.css en dark.bss : javapackager -createbss -srcfiles dark.css -outdir . § Après compilation, il n'est pas nécessaire de modifier le code source. • Dans le code, on continuera à charger le fichier "dark.css" mais le système chargera effectivement le fichier "dark.bss". IHM-1 – FX11 – Jacques BAPST 24 Règles de base du threading [1] § Comme la majorité des librairies d'interfaces graphiques, JavaFX utilise un seul thread, nommé JavaFX Application Thread, pour gérer les composants et les événements de l'interface (GUI). Cours IHM-1 JavaFX § Les classes représentant les nœuds du graphe de scène ne sont pas thread-safe. 12 - Gestion de la concurrence Longs traitements • Avantage : Plus rapide, pas de synchronisation (verrou) nécessaire • Inconvénient : Ne doivent être accédées/manipulées que depuis un seul thread pour garantir l'intégrité de leurs données (état, propriétés) § Règle : Tous les composants qui font partie d'une scène active (live scene) ne doivent être accédés que depuis le JavaFX Application Thread. Jacques BAPST • Une live scene est une scène associée à une fenêtre (Stage). • Dans la plupart des cas, une exception est générée si cette règle est violée (IllegalStateException). [email protected] IHM-1 – FX12 – Jacques BAPST 3 Règles de base du threading [2] § Dans une application JavaFX, il y a différents threads à considérer. • Le thread principal (appelé main) qui exécute les instructions de la méthode main() au lancement de l'application. • Le JavaFX Application Thread qui exécute notamment : Le code des méthodes start() et stop() (classe Application) Le code des gestionnaires d'événements (contrôleurs) ð Le code chargé du rendu graphique des éléments de l'interface ð JavaFX Threading ð • Le thread JavaFX-Launcher qui exécute notamment : ð Le code de la méthode init() (classe Application) • Les Worker threads (ou Background threads) qui sont des threads optionnels, créés explicitement dans l'application, et dans lesquels sont exécutées les tâches qui prennent du temps, qui sont en attente d'un signal/résultat ou qui s'exécutent à intervalle régulier. • D'autres threads, qui sont liés à certains composants de l'architecture JavaFX, et dont on a généralement pas à s'occuper explicitement. ð IHM-1 – FX12 – Jacques BAPST 2 §IHM-1 – QuantumRenderer-n par exemple FX12 – Jacques BAPST 4 Règles de base du threading [3] Comment régler le problème [1] § Le JavaFX Application Thread se charge notamment de deux tâches importantes : § La plupart des applications avec interfaces graphiques comportent des tâches dont les temps d'exécution dépassent (potentiellement) les temps qui font qu'une application reste réactive : • Le rendu graphique des composants (graphical rendering) • La gestion des événements (event handling) • • • • • § Cela implique, comme corollaire, les règles suivantes : Le code qui gère les événements (event handler) doit s'exécuter rapidement, sinon l'interface graphique risque d'être figée et ne plus répondre aux actions de l'utilisateur. § Dans certains cas, l'utilisateur ne peut effectivement rien faire d'autre avec l'interface durant l'exécution d'une tâche longue. Toutes les tâches qui prennent du temps doivent être exécutées en dehors du JavaFX Application Thread. § § Quand on parle d'exécution rapide des contrôleurs (gestionnaires d'événements ), le terme "rapide" est toujours à mettre en relation avec les temps de réaction et de perception de l'humain. On est dans l'ordre de grandeur : t < 0.1 s … 0.3 s (selon contexte) IHM-1 – FX12 5 – Jacques BAPST Règles de base du threading [4] Key Press Critical thread Mouse Release Touch Move Time-consuming code will freeze the GUI ! Touch Release Event Handler AT – Jacques BAPST IHM-1 – FX12 – Jacques BAPST 7 § Pour résoudre ce dilemme, la class Platform dispose d'une méthode statique runLater(Runnable runnable) qui permet de soumettre l'exécution du code passé en paramètre, dans le JavaFX Application Thread. § L'objet Runnable (transmis en paramètre) pourra donc légitimement contenir des instructions qui manipulent des éléments de l'interface graphique. GUI Rendering § L'objet Runnable s'exécutera de manière asynchrone par rapport au thread utilisateur qui invoque runLater() (main thread par exemple). JavaFX Application Thread IHM-1 – FX12 Modification du curseur (spinning wheel), barre de progression, … § Cependant, il est fréquent que ces tâches doivent effectuer des mises à jour dans l'interface, … ce qui n'est autorisé que dans le JavaFX Application Thread ! Mouse Move Touch Press ð § Pour résoudre le problème, il faut que les tâches longues s'exécutent dans des threads séparés (et non dans le JavaFX Application Thread). Mouse Press Operating System • Les mécanismes proposés doivent malgré tout être mis en œuvre car sinon, certaines opérations seront bloquées (redimensionnement de la fenêtre, gestion des occlusions, mise à jour des composants, tooltips, etc.) • Un indicateur (feedback) doit signaler à l'utilisateur qu'il doit attendre. Comment régler le problème [2] Event Queue ... Lecture et chargement de fichiers volumineux Communication avec un serveur distant (web service, etc.) Recherche d'informations dans une base de données, dans le cloud, … Calcul ou algorithme complexe ... 6 IHM-1 – FX12 – Jacques BAPST 8 Comment régler le problème [3] § Le code sera donc inséré dans le JavaFX Application Thread et exécuté de manière asynchrone (dans un avenir plus ou moins proche). § La méthode runLater() retourne immédiatement et n'attend pas que l'objet runnable ait été exécuté. JavaFX Worker Framework § Illustration du séquencement des opérations : JavaFX Application Thread 1 2 3 Platform.runLater( this::redCode(); ); Instruction en cours User's Thread IHM-1 – FX12 4 Worker activity – Jacques BAPST 9 IHM-1 – FX12 – Jacques BAPST Comment régler le problème [4] Worker framework [1] § Il est parfois utile de pouvoir tester si le code courant s'exécute dans le JavaFX Application Thread ou non, on peut pour cela faire appel à la méthode statique isFxApplicationThread() qui se trouve dans la classe Platform. § Pour simplifier la gestion des applications qui comportent des activités dont la durée est indéterminée et qui doivent cependant pouvoir manipuler des éléments de l'interface utilisateur, JavaFX offre différents composants (concurrent framework API) dans le package javafx.concurrent. § Cela permet d'écrire du code qui pourra être exécuté sans problème depuis différents threads (méthodes utilitaires par exemple). § Exemple : . . . if (Platform.isFxApplicationThread()) { displayStatus(message, level); } else { Platform.runLater(() -> displayStatus(message, level)); } . . . 11 § A la base de ce framework, il y a l'interface Worker<V> qui représente un objet qui effectue une tâche dont l'activité est déléguée à un ou plusieurs background threads. § Trois classes abstraites implémentent cette interface : • Task<V> : Tâche destinée à n'être utilisée qu'une seule fois par instance • Service<V> : Service permettant l'exécution et la ré-exécution d'une tâche • ScheduledService<V> : Service permettant l'exécution périodique d'une tâche IHM-1 – FX12 – Jacques BAPST 10 IHM-1 – FX12 – Jacques BAPST 12 Worker Framework [2] Worker - Propriétés [1] § Relations entre les éléments du framework : § Les objets de type Worker possèdent différentes propriétés qui peuvent être mises à jour durant le déroulement de la tâche de fond et qui peuvent être observées par des event handlers. § Ces gestionnaires d'événements peuvent, eux, mettre à jour l'interface (car ils s'exécutent dans le JavaFX Application Thread). § Ce mécanisme constitue la base de la communication entre les tâches de fond et les activités de mises à jour de l'interface (GUI). § Principales propriétés des worker (elles sont toutes read-only) : • title : Titre de la tâche (String) § Worker.State est un type énuméré qui répertorie chacun des états • message : Message indiquant l'activité courante de la tâche (String) § WorkerStateEvent représente un événement qui se produit • value : Résultat final de la tâche ou, éventuellement, valeur intermédiaire (<V>) dans lesquels peuvent se trouver les objets worker. lorsqu'un worker change d'état. IHM-1 – FX12 – Jacques BAPST 13 IHM-1 – FX12 – Jacques BAPST Worker Framework [3] Worker - Propriétés [2] § Durant son cycle de vie, un objet worker passe par différents états. § Suite… • La liste des états valides est représentée par les constantes du type énuméré Worker.State. § Les transitions possibles : IHM-1 – FX12 • workDone : Quantité de travail déjà réalisé par la tâche (Double) [-1 si la valeur est non définie] Après la création du worker – Jacques BAPST 15 • totalWork : Quantité totale de travail à réaliser par la tâche (Double) [-1 si la valeur est non définie] 14 • progress : Progression du travail de la tâche [0 … 1] (Double). Correspond au rapport workDone / totalWork [-1 si la valeur est non définie] • running : Vrai si le worker se trouve dans l'état SCHEDULED ou RUNNING, faux sinon (Boolean). • state : État dans lequel se trouve le worker (Worker.State) • exception : Exception éventuellement générée et non catchée par la tâche de fond. Valeur non-nulle seulement si l'état est FAIL (Throwable). IHM-1 – FX12 – Jacques BAPST 16 Task [1] Task [3] § La classe abstraite Task<V> (qui implémente l'interface Worker) définit dans la méthode call() les instructions qui seront exécutées en tâche de fond (dans un thread séparé). § Les propriétés de la tâche que les méthodes updateppp() mettent à jour sont éventuellement agrégées (coalesced) avant d'être injectées dans le JavaFX Application Thread. § Le paramètre générique V représente le type du résultat de la tâche. § Concrètement, cela a comme conséquence que : • La valeur de la propriété n'est pas nécessairement mise à jour immédiatement. Un observateur pourra être informé avec du retard. • Chacun des appels ne conduira pas nécessairement à la mise à jour de la propriété. Certaines valeurs pourront être "perdues". § Si la tâche ne produit aucun résultat on utilisera le type Void. § Une instance de Task ne peut être exécutée qu'une seule fois (elle n'est pas réutilisable). § Exemple d'une tâche qui produit un résultat de type Long : § Si des mises à jours interviennent avec un intervalle très court (modifications en rafale), seule la dernière valeur conduira à une modification de la propriété. public class NextPrimeTask extends Task<Long> { @Override protected Long call() throws Exception { // Implement task code here } . . . § Ce mécanisme a été mis en place pour éviter que le JavaFX Application Thread soit submergé par une avalanche de notifications et de traitements consécutifs à des modifications très rapides des propriétés des workers. } IHM-1 – FX12 – Jacques BAPST 17 IHM-1 – FX12 – Jacques BAPST Task [2] Task [4] § Les propriétés de la tâche (i.e. celles de Worker) peuvent être mises à jour à l'aide des méthodes spéciales suivantes : § La classe Task dispose d'un certain nombre de propriétés qui permettent de réagir aux changements d'état du worker. • • • • • updateTitle(String title) updateMessage(String message) updateProgress(double workDone, double totalWork) updateProgress(long workDone, long totalWork) updateValue(V value) • • • • • onScheduled onRunning onSucceeded onCancelled onFailed 19 Il n'y a pas de propriété onReady car un objet Task ne peut pas revenir à cet état. § Ces méthodes ont la particularité d'effectuer la mise à jour de ces propriétés dans le JavaFX Application Thread. § On peut ainsi enregistrer des gestionnaires d'événements qui seront appelés lors de la transition entre les états du worker. § Ainsi toutes ces propriétés peuvent être observées (lues) dans un contexte apte à mettre à jour de manière sûre l'interface. § Exemple : . . . myTask.setOnRunning(event -> { lblStatus.setText("< Running... >"); lblStatus.setTextFill(Color.GREEN); }); . . . • C'est un des buts principaux de ces propriétés que de transmettre des informations de la tâche de fond vers l'interface graphique. IHM-1 – FX12 – Jacques BAPST 18 IHM-1 – FX12 – Jacques BAPST 20 Task [5] Service [2] § Comme la classe Task implémente les interfaces Runnable et FutureTask, on peut exécuter la tâche en créant un objet Thread ou en passant par un ExecutorService. § Pour créer un service, on sous-classe généralement la classe abstraite Service<V> et on implémente la méthode createTask(). • Il est naturellement aussi possible de créer la sous-classe sous forme de classe locale anonyme. § Exemple 1 : § Création d'un service qui fournit un résultat de type Long : . . . //--- Launch the task by creating a background thread Thread workerThread = new Thread(aTask); workerThread.setDaemon(true); workerThread.start(); . . . public class NextPrimeService extends Service<Long> { . . . @Override protected Task<Long> createTask() { // Create and return a task Task<Long> task = . . . . . . return task; } § Exemple 2 : . . . //--- Use an Executor-Service to launch the task ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(aTask); . . . Non-daemon thread ! Execution could continue after application is closed. IHM-1 – FX12 – Jacques BAPST . . . } 21 IHM-1 – FX12 – Jacques BAPST 23 Service [1] Service [3] § La classe abstraite Service<V> implémente aussi l'interface Worker et permet d'encapsuler une tâche (Task) en la rendant facilement réutilisable (restart). § La classe service dispose des propriétés de Worker (title, message, value, progress, state, …) mais pas des méthodes de mise à jour (updateppp()) qui sont réservées à l'objet Task. § Une fois démarré, la majorité des méthodes de la classe Service sont destinées à être gérées depuis la partie UI de l'application et doivent être exécutées dans le JavaFX Application Thread. § Dans le service, ces propriétés sont liées (bound) aux propriétés correspondantes de la tâche associée. Les changements sont ainsi propagés et peuvent être observés au niveau du service. § Une instance de la classe Service crée et gère un objet Task qui effectuera le travail en tâche de fond. § La propriété executor peut être utilisée pour définir l'objet de type Executor qui sera utilisé pour exécuter le service (et donc la tâche). § La tâche sera créée dans l'unique méthode abstraite de la classe : § Si la propriété executor n'est pas définie, un daemon thread sera créé pour exécuter le service. Task<V> createTask() § La méthode createTask() sera invoquée lorsque le service sera démarré ou redémarré à l'aide des méthodes start() / restart() qui se chargeront également de démarrer la tâche dans un thread séparé (background thread). IHM-1 – FX12 – Jacques BAPST 22 § Le démarrage du service (avec création et exécution de la tâche) est lancé avec la méthode start() (exécutable depuis n'importe quel thread). • Le service doit se trouver dans l'état READY lors de l'invocation de la méthode start(). IHM-1 – FX12 – Jacques BAPST 24 Service [4] Interruption Task / Service [2] § La méthode restart() du service permet d'interrompre une tâche en cours, de réinitialiser le service et de le relancer. Cette méthode doit être invoquée depuis le JavaFX Application Thread. § Remarque : Dans la classe Task, on trouve une deuxième méthode cancel() qui permet de passer en paramètre un booléen qui indique si le flag interrupted du Thread doit être activé ou non. • Son appel correspond à l'invocation en séquence des trois méthodes suivantes : cancel() ð reset() ð ð start() boolean cancel(boolean mayInterruptIfRunning) Fonctionnement expliqué plus loin Remet les propriétés à l'état initial (notamment state=READY) (ne peut pas être invoquée si state=SCHEDULED ou RUNNING) Doit être appelée depuis le JavaFX Application Thread Peut être exécutée depuis n'importe quel thread § Dans la classe service, on trouve également les propriétés qui permettent de réagir aux changements d'état du worker (onRunning, onSuccedded, onFailed, …). En plus des propriétés disponibles dans la classe Task, le service ajoute la propriété onReady (car, contrairement à la tâche, le service peut se retrouver plusieurs fois dans cet état). IHM-1 – FX12 – Jacques BAPST 25 § Dans la classe Task, il est possible de redéfinir la méthode cancelled() qui sera automatiquement invoquée lorsque la tâche passe dans l'état CANCELLED. • Par défaut (dans la classe parente) cette méthode ne fait rien • La méthode est exécutée dans le JavaFX Application Thread et ses instructions peuvent donc agir sur l'interface graphique § Des méthodes similaires existent également pour la transition de la tâche vers les autres états : • scheduled() / running() / succeeded() / failed() IHM-1 – FX12 – Jacques BAPST 27 Interruption Task / Service [1] Interruption Task / Service [3] § Si l'on souhaite interrompre prématurément une tâche exécutée dans un background thread il faut invoquer la méthode cancel() qui existe dans la classe Task et dans la classe Service. § Exemple de détection de l'interruption dans la méthode call() de la classe Task : @Override protected Long call() throws Exception { . . . for (int i=0; i<loopMax; i++) { // Main loop //--- Check for cancellation if (isCancelled()) return null; § Lors de l'invocation de la méthode cancel() • L'état de l'objet Task (propriété state) est mis à CANCELLED • Et le flag interrupted du thread est mis à l'état true. § L'interruption d'une tâche est basée sur la coopération et c'est à celui qui code la tâche de faire en sorte que les états d'interruption soient testés périodiquement. . . . try { Thread.sleep(20); // Can throw InterruptedException } //--- Check for thread interruption catch (InterruptedException interrupted) { if (isCancelled()) return null; } § Ces états peuvent être testés • Avec la méthode isCancelled() de Task • Avec la méthode isInterrupted() de Thread ou si l'exception InterruptedException est levée lors de certains appels bloquants (Thread.sleep(t) ou obj.wait() sont des exemples classiques) IHM-1 – FX12 – Jacques BAPST } . . . } 26 IHM-1 – FX12 – Jacques BAPST 28 ScheduledService [1] ScheduledService [3] § La classe abstraite ScheduledService<V> est une sous-classe de Service. Elle permet de créer un service qui se ré-exécute à intervalle périodique (la tâche redémarre automatiquement). § Parmi les propriétés spécifiques (qui s'ajoutent à celles de Service), on peut mentionner : § Ce type de service est utile dans les applications qui nécessitent la consultation (polling) et la mise à jour périodique (cyclique) de certaines informations, par exemple : • Informations météo, trafic routier • Cours de la bourse, résultats sportifs • État des serveurs, des spoolers d'impression, notifications, etc. • delay : Délai initial entre le lancement du service (start) et le début de l'exécution de la tâche (Duration). Durant cette période le service reste dans l'état SCHEDULED. • period : Temps minimal entre le démarrage de la dernière exécution et le démarrage de la suivante (Duration) • lastValue : Valeur retournée par la dernière exécution réussie de la tâche (<V>). La propriété value est réinitialisée à null à chaque lancement de la tâche et n'est donc généralement pas d'un grand intérêt dans ce contexte. § Comme pour la classe Service la tâche sera créée dans la méthode abstraite createTask(). § Le processus de création est identique à celui de Service. On crée une sous-classe qui hérite de ScheduledService et qui implémente la méthode createTask(). IHM-1 – FX12 – Jacques BAPST 29 • restartOnFailure : Indique si la tâche doit redémarrer en cas d'échec (Boolean). Par défaut, la tâche ne redémarre pas en cas d'échec. IHM-1 – FX12 – Jacques BAPST ScheduledService [2] ScheduledService [4] § Le service est démarré à l'aide de la méthode start() et il sera automatiquement redémarré à la fin de l'exécution de la tâche en fonction de certains paramètres qui sont définis par des propriétés spécifiques (décrites plus loin). § Suite… § Les méthodes cancel(), reset() et restart() fonctionnent essentiellement de la même manière que celles de la classe Service. 31 • maximumFailureCount : Indique le nombre de fois que le service peut échouer avant de passer dans l'état FAILED (Integer). Si l'on redémarre le service (restart), ce compteur est remis à zéro. • currentFailureCount : Indique le nombre de fois que le service a échoué depuis son lancement (start ou restart) (Integer). § Après la fin d'une tâche réussie (SUCCEEDED), le service passera successivement (et automatiquement) par les états READY, SCHEDULED, et RUNNING. • backOffStrategy : Permet de spécifier le temps d'attente à ajouter à period à chaque exécution qui se termine en échec. (Callback<ScheduledService<?>,Duration>). Trois stratégies sont prédéfinies : § Selon la configuration, il peut faire de même après la fin d'une tâche échouée (FAILED). EXPONENTIAL_BACKOFF_STRATEGY LINEAR_BACKOFF_STRATEGY LOGARITHMIC_BACKOFF_STRATEGY IHM-1 – FX12 – Jacques BAPST 30 IHM-1 – FX12 – Jacques BAPST 32 ScheduledService [5] § Suite… • cumulativePeriod : Détermine le temps actuel entre deux exécutions après un échec en tenant compte de la backoff strategy (Duration>). Cette valeur est réinitialisée à la valeur de period après chaque tâche réussie. • maximumCumulativePeriod : Définit un temps maximal pour le temps entre deux exécutions en échec (Duration>). Permet de limiter l'effet de la backoff strategy en définissant un temps d'attente qui ne sera jamais dépassé, même en cas de nombreux échecs successifs. IHM-1 – FX12 – Jacques BAPST 33 ScheduledService [6] § Les valeurs des propriétés delay et period peuvent être modifiées durant l'exécution de la tâche. § Les nouvelles valeurs seront prises en considération au prochain lancement du service (delay) respectivement au prochain cycle de la tâche (period). § Par défaut, les valeurs des propriétés delay et period sont à zéro. • Selon l'activité de la tâche (calcul pur par exemple), s'il y a redémarrage sans attente (period=0), la tâche risque de monopoliser le CPU et ainsi de figer l'interface utilisateur (ce que l'on cherche à éviter !). § On ne peut naturellement pas compter sur ce mécanisme (valeur de la propriété period) pour définir un temps de cycle très précis. • Généralement, plus la valeur de period est petite et plus la précision relative du temps de cycle diminue. IHM-1 – FX12 – Jacques BAPST 34