Programmation JavaFX/Version imprimable
Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_JavaFX
Introduction
JavaFX est un framework et une bibliothèque d'interface utilisateur issue du projet OpenJFX, qui permet aux développeurs Java de créer une interface graphique pour des applications de bureau, des applications internet riches et des applications smartphones et tablettes tactiles. Il permet également le rendu de scènes 3D.
JavaFX a été conçu initialement pour remplacer Swing et AWT à long terme.
Utilisation
[modifier | modifier le wikicode]Java 7 à 10
[modifier | modifier le wikicode]Le framework JavaFX est intégré aux versions 7 à 10 du JDK et du JRE (librairie jfxrt.jar). Il n'y a donc aucun logiciel supplémentaire à installer.
<JDKDIR>/jre/lib/ext/jfxrt.jar
Des applications de démonstration sont fournies avec le JDK dans un sous-répertoire
<JDKDIR>/demo/javafx_samples
Lancez en particulier l'application Ensemble8.jar qui donne un large aperçu de toutes les possibilités de JavaFX. Le code source de chaque démonstration est composé souvent d'un seul fichier, disponible par simple clic sur le lien "View source" associé à la démo.
Java 11
[modifier | modifier le wikicode]À partir de Java 11, JavaFX (version 11 également) est un module indépendant à installer séparément[1] depuis le site suivant :
Les packages de JavaFX
[modifier | modifier le wikicode]Le framework JavaFX est composé de classes situées dans les packages commençant par javafx :
- javafx.application
- Les classes pour la gestion de l'application JavaFX.
- javafx.stage
- Les classes pour la gestion des fenêtres applicatives (Stage).
- javafx.scene
- Les classes pour la gestion des scènes qui définissent le contenu des fenêtres. Ce contenu est constitués de nœuds (classe Node).
- javafx.scene.control
- Composants d'interface utilisateur : boutons, bouton radio, case à cocher, champ de texte, ...
- javafx.scene.layout
- Gestion de la taille et du positionnement des nœuds composant un scène.
- javafx.scene.shape
- Formes de base en 2D et 3D : rectangle, cercle, courbe, chemin, polygone, sphère, boîte, cylindre, ...
- javafx.scene.paint
- Couleur et autres objets tels que les dégradés (radial, linéaire, ...) pour le remplissage et le traçage des formes et du texte.
- javafx.scene.image
- Classes pour le chargement et l'affichage des images.
- javafx.scene.text
- Classes pour l'affichage du texte, gestion des polices de caractères.
- javafx.scene.transform
- Transformation 2D et 3D des nœuds : translation, rotation, échelle, étirage. Ces transformations sont aussi accessibles par appel de méthode sur les nœuds.
- javafx.scene.input
- Gestion des interactions avec l'utilisateur par le clavier et la souris.
- javafx.scene.effect
- Effets graphiques par application de filtres.
- javafx.scene.effect.light
- javafx.scene.chart
- Création et gestion de graphiques : courbe de points, graphique à barres, camembert, ...
- javafx.scene.chart.data
- Gestion des données pour les graphiques.
- javafx.scene.chart.part
- .
- javafx.geometry
- Les classes de gestion des dimensions et positions des nœuds (éléments d'une scène) en 2D et 3D.
- javafx.ext.swing
- Adaptateurs pour encapsuler des composants Swing dans une scène. Ces classes sont en général utilisées pour effectuer une transition des composants Swing vers leur équivalents JavaFX.
Utilisation avec Eclipse
[modifier | modifier le wikicode]Après la création d'un nouveau projet Eclipse ou l'import d'un projet existant utilisant un package de JavaFX, il est possible que Eclipse affiche des messages d'erreurs de ce type :
Access restriction: The type 'Application' is not API (restriction on required library jfxrt.jar)
Dans ce cas, il faut ajouter une règle d'accès dans le "Build Path" du projet :
- Cliquer avec le bouton droit sur le projet, "Build Path" > "Configure Build Path...".
- Dans l'onglet "Libraries", déplier le nœud "JRE System Library".
- Cliquer "Access rules", puis le bouton "Edit..." situé à droite.
- Dans la nouvelle fenêtre, cliquer le bouton "Add..." situé à droite.
- Mettre "Accessible" comme valeur pour "Resolution".
- Entrer "
javafx/**
" comme valeur pour "Rule Pattern". - Valider toutes les fenêtres de dialogues ouvertes.
Le chapitre suivant décrits les objets composant une application JavaFX.
Réferences
[modifier | modifier le wikicode]- ↑ https://www.infoworld.com/article/3305073/removed-from-jdk-11-javafx-11-arrives-as-a-standalone-module.html
Composants d'une application
Ce chapitre est important car il présente les classes de bases communes aux applications JavaFX. Un exemple illustre ce chapitre, complété au fur et à mesure des notions introduites dans les sections du chapitre.
Application
[modifier | modifier le wikicode]Application est le nom de la classe Abstraite à implémenter par la sous-classe représentant l'application JavaFX.
Cette sous-classe ne comporte pas obligatoirement une méthode statique main
car la JVM peut lancer directement un JAR contenant une application JavaFX en utilisant certains attributs particuliers du fichier de manifeste.
Elle suit un cycle particulier :
- La méthode statique
launch(Class c, String[] args)
est appelée soit par une méthodemain(String[] args)
, soit directement par la JVM. Le premier argument est la classe implémentant la classe abstraiteApplication
et peut être omis, auquel cas la méthode utilise la classe à partir de laquelle elle a été appelée. Le second argument est un tableau des arguments de la ligne de commande (voir section « Récupération des arguments de la ligne de commande » ci-dessous). - Cette méthode initialise l'environnement JavaFX et appelle ensuite la méthode
init()
. - La méthode
start(Stage stage)
est ensuite appelée pour démarrer l'application : créer l'interface et les scènes à afficher dans la fenêtre (stage) donnée en argument. La sous-classe deApplication
doit implémenter cette méthode. - La méthode
stop()
est appelée pour arrêter l'application, généralement quand la fenêtre de l'application est fermée. L'implémentation par défaut ne fait rien.
Récupération des arguments de la ligne de commande
[modifier | modifier le wikicode]Quel que soit le mode de lancement de l'application, la méthode getParameters()
de la classe Application retourne un objet de classe Parameters
:
Parameters params = getParameters();
Les méthodes de la classe Parameters
permettent de récupérer les arguments de la ligne de commande :
List<String> getRaw()
- Cette méthode retourne une liste contenant tous les arguments de la ligne de commande.
Map<String,String> getNamed()
- Cette méthode retourne un dictionnaire de tous les arguments nommés de la forme suivante :
--name=value
List<String> getUnnamed()
- Cette méthode retourne une liste contenant les autres arguments (non nommés) de la ligne de commande.
Exemple
[modifier | modifier le wikicode]package org.wikibooks.fr.javafx;
import java.util.*;
import javafx.application.Application;
import javafx.stage.Stage;
public class FirstExample extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
@Override
public void start(Stage stage)
{
Group root = new Group();
Scene scene = new Scene(root, 400, 200); // Fenêtre 400x200 pixels
stage.setScene(scene);
stage.setTitle("Exemple de scene vide"); // Titre de la fenêtre
stage.show();
// Afficher tous les arguments passés à l'application
// Exemple: test --limit=20 example --out=file.dat
// Paramètre limit = 20
// Paramètre out = file.dat
// Autre paramètre: test
// Autre paramètre: example
Parameters params = getParameters();
for(Map.Entry<String, String> named_parameter : params.getNamed().entrySet())
System.out.println(" Paramètre "+named_parameter.getKey()+" = "+named_parameter.getValue());
for(String p : params.getUnnamed())
System.out.println(" Autre paramètre: "+p);
}
}
Stage
[modifier | modifier le wikicode]Une instance de la classe Stage
représente une fenêtre d'application JavaFX.
Les principales méthodes sont les suivantes:
setTitle(String title)
- Définir le titre de la fenêtre.
setScene(Scene scene)
- Définir la scène à afficher.
show()
- Afficher la fenêtre.
Il est également possible d'ajouter une ou plusieurs icônes en modifiant la liste observable retournée par la méthode getIcons()
.
Liste observable
[modifier | modifier le wikicode]Une liste observable est une liste générique dont les modifications sont notifiés aux instances des classes implémentant l'interface ListChangeListener enregistrées sur la liste par la méthode addListener
.
Les listes observables sont très utilisées dans l'API JavaFX.
Scene
[modifier | modifier le wikicode]Une scène est composée de nœuds (Node). Une scène comporte des nœuds 2D (composants : boutons, labels...) ou 3D (formes : sphères, boîtes, cylindres...).
package org.wikibooks.fr.javafx;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;
public class First3DScene extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
@Override
public void start(Stage stage)
{
// Create a Box
Box box = new Box(100, 100, 100);
box.setTranslateX(150);
box.setTranslateY(0);
box.setTranslateZ(400);
// Create a Sphere
Sphere sphere = new Sphere(50);
sphere.setTranslateX(300);
sphere.setTranslateY(-5);
sphere.setTranslateZ(400);
// Create a Cylinder
Cylinder cylinder = new Cylinder(40, 120);
cylinder.setTranslateX(500);
cylinder.setTranslateY(-25);
cylinder.setTranslateZ(600);
// Create a Light
PointLight light = new PointLight();
light.setTranslateX(350);
light.setTranslateY(100);
light.setTranslateZ(300);
// Create a Camera to view the 3D Shapes
PerspectiveCamera camera = new PerspectiveCamera(false);
camera.setTranslateX(100);
camera.setTranslateY(-50);
camera.setTranslateZ(300);
// Add the Shapes and the Light to the Group
Group root = new Group(box, sphere, cylinder, light);
// Create a Scene with depth buffer enabled
Scene scene = new Scene(root, 400, 200, true);
// Add the Camera to the Scene
scene.setCamera(camera);
// Add the Scene to the Stage
stage.setScene(scene);
// Set the Title of the Stage
stage.setTitle("An Example with Predefined 3D Shapes");
// Display the Stage
stage.show();
}
}
Classes similaires à AWT et Swing
[modifier | modifier le wikicode]JavaFX a été conçu pour remplacer Swing et AWT à long terme. Le framework est donc indépendant et possède des classes spécifiques, nommées comme celles de Swing et AWT et jouant à peu près le même rôle mais sont différentes. Ce qui veut dire qu'il n'est pas possible d'utiliser directement les classes de JavaFX en Swing/AWT et vice-versa.
La spécificité des classes de JavaFX permet leur utilisation en 2D et en 3D.
- Color
- Représente une couleur, modélisée par les composantes RVB (Rouge, Vert, Bleu) dont les valeurs sont de type
double
et comprises entre 0.0 et 1.0. - Image
- Représente une image.
Nœud (Node)
[modifier | modifier le wikicode]Un nœud représenté un élément affichable dans une scène.
Certaines classes d'objet ne sont pas des nœuds ; leurs instances doivent alors être encapsulées dans une vue (View) gérant le type d'objet :
ImageView
pour les objets de classeImage
;MeshView
pour les objets de classeMesh
(notamment la sous-classeTriangleMesh
) ;- ...
Exemple : afficher une image
[modifier | modifier le wikicode]package org.wikibooks.fr.javafx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ShowImage extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
@Override
public void start(Stage stage)
{
StackPane sp = new StackPane();
Image im = new Image("image.jpg"); // Chemin local relatif
// Image im = new Image("file://///E:/Images/image_8.jpg"); // ou chemin local absolu : URL
// Image im = new Image(getClass().getResourceAsStream("logo.png")); // ou resource du package
// Image im = new Image("http://www.monserveur.com/images/example.png"); // ou URL internet
ImageView im_view = new ImageView(im);
sp.getChildren().add(im_view);
Scene scene = new Scene(sp);
stage.setScene(scene);
stage.setTitle("Afficher une image");
stage.show();
}
}
Classes 2D
Hiérarchie des classes 2D
[modifier | modifier le wikicode]La hiérarchie de classes ci-dessous montrent les classes communes aux application 2D et 3D, vues précédemment et les classes spécifiques à la 2D.
Classes de base
[modifier | modifier le wikicode]La classe de base des composants JavaFX est Node
.
Il s'agit de la classe utilisée pour l'ajout de nœuds dans les conteneurs (par exemple Pane
).
Parmi les sous-classes, il y a la classe Parent
servant de base à tous les composants pouvant contenir d'autres composants (nœuds enfants dans l'arbre).
Il y a également la sous-classe MediaViewer
héritant directement de la classe Node
, ce qui signifie que ce composant ne peut pas contenir d'autres composants, et aussi qu'il peut être ajouté à n'importe quel composant Parent
. Voir le chapitre Jouer une vidéo ou un son pour une utilisation concrète de la classe MediaViewer
.
La classe Region
est la classe de base des composants de type Pane
et définit une composition similaire à celle du modèle de boîte en CSS[source 1] :
marge extérieure (Margin
)
bordure (Border
)
espace intérieur (Padding
)
aire intérieure (ContentArea
)
La similitude avec le modèle de boîte en CSS est due au fait que les composants JavaFX peuvent être stylisés avec des règles CSS (voir le livre Le langage CSS).
Styles CSS sur les composants JavaFX
[modifier | modifier le wikicode]Les composants JavaFX dérivant de la classe Node
peuvent être stylisés en CSS.
Cependant, les styles CSS supportés sont limités[source 2], car basés sur W3C CSS version 2.1 avec quelques ajouts spécifiques à JavaFX.
De plus les propriétés doivent être préfixées par -fx-
.
Exemple de code :
package org.wikibooks.fr;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.stage.*;
public class CssTest extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
@Override
public void start(Stage stage) throws Exception
{
StackPane root = new StackPane();
root.setStyle("-fx-background-color: #00f;"); // Style CSS : fond bleu.
// Création de la scène :
Scene scene = new Scene(root, 500, 500, Color.BLACK);
stage.setScene(scene);
stage.setTitle("CSS test");
stage.show();
}
}
Sources
[modifier | modifier le wikicode]- ↑ JavaFX Region - https://jenkov.com/tutorials/javafx/region.html
- ↑ JavaFX CSS Reference Guide - https://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html
Formes 3D
Une scène en 3D est composée de formes 3D, de source lumineuses et d'une caméra définissant le point de vue de la scène.
Coordonnées et orientation des axes 3D
[modifier | modifier le wikicode]Les coordonnées d'un point ou d'une forme est un ensemble de 3 valeurs de type double
, nommées X, Y et Z.
L'orientation par défaut des axes est le suivant :
- L'axe X est orienté de la gauche vers la droite : une valeur plus élevée de X donne une position relative plus à droite.
- L'axe Y est orienté du haut vers le bas.
- L'axe Z est orienté du plus proche au plus loin.
Classe de base
[modifier | modifier le wikicode]La classe Shape3D
est la classe de base des formes 3D.
Elle définit les méthodes communes à toutes les formes, permettant de les placer/déplacer et de les orienter.
La position initiale à la création d'une forme 3D est (0,0,0).
- setTranslateX(double)
- Incrémente la coordonnée X du nœud.
- setTranslateY(double)
- Incrémente la coordonnée Y du nœud.
- setTranslateZ(double)
- Incrémente la coordonnée Z du nœud.
La classe Shape3D
a 4 implémentations concrètes :
- La classe
Sphere
- La classe
Cylinder
- La classe
Box
représente une boîte ou parallélépipède rectangle ou pavé droit. - La classe
MeshView
permet de définir une forme particulière à partir d'une instance de la classe abstraiteMesh
. La classe abstraiteMesh
possède la sous-classeTriangleMesh
comme implémentation concrète, définissant la forme à partir de triangles.
Les sous-classes présentées ci-dessous ont des caractéristiques communes :
- Par défaut, les formes sont centrées sur le point d'origine (0,0,0).
- Si les dimensions ne sont pas spécifiées (constructeur sans arguments), les dimensions par défaut sont définies par une boîte 2x2x2.
Sphère (classe Sphere)
[modifier | modifier le wikicode]Les paramètres définissant une sphère sont les suivants (même ordre que les constructeurs de la classe) :
double radius
- (Optionnel) Rayon de la sphère. La valeur par défaut est
1.0
. int divisions
- (Optionnel) Nombre de divisions de la circonférence. La valeur par défaut est
64
.
Exemple
[modifier | modifier le wikicode]Sphere sphere = new Sphere(100);
Cylindre (classe Cylinder)
[modifier | modifier le wikicode]Les paramètres définissant un cylindre sont les suivants (même ordre que les constructeurs de la classe) :
double radius
- (Optionnel) Rayon du cylindre. La valeur par défaut est
1.0
. double height
- (Optionnel) Hauteur du cylindre. La valeur par défaut est
2.0
. int divisions
- (Optionnel) Nombre de divisions de la circonférence. La valeur par défaut est
64
.
L'orientation initiale du cylindre suit l'axe Y ; les faces circulaires sont donc perpendiculaire à l'axe Y.
Exemple
[modifier | modifier le wikicode]Cylinder cyl = new Cylinder(40, 220);
cyl.setTranslateY(220);
Boîte (classe Box)
[modifier | modifier le wikicode]Les paramètres définissant une boîte sont les suivants (même ordre que les constructeurs de la classe) :
double width
- (Optionnel) Largeur de la boîte (axe X). La valeur par défaut est
2.0
. double height
- (Optionnel) Hauteur de la boîte (axe Y). La valeur par défaut est
2.0
. double depth
- (Optionnel) Profondeur de la boîte (axe Z). La valeur par défaut est
2.0
.
Exemple
[modifier | modifier le wikicode]Box box = new Box(100, 200, 100);
box.setTranslateX(250);
box.setTranslateY(220);
Forme triangulée
[modifier | modifier le wikicode]Une forme triangulée est définie avec un objet de class TriangleMesh
.
Le seul constructeur à appeler n'a aucun paramètres.
Il faut ensuite remplir les listes de points, de faces triangulaires et les points de texture.
Méthodes de la classe TriangleMesh
:
getTexCoords()
- Retourne la liste modifiable (float) des points (U,V) pour les textures.
getPoints()
- Retourne la liste modifiable (float) de tous les sommets.
getFaces()
- Retourne la liste modifiable (int) des index des sommets de chaque face.
getFaceSmoothingGroups()
- Retourne la liste modifiable (int) des identifiants de face. La même valeur signifiant qu'il s'agit de la même face. Ceci est utilisé pour le rendu continu des faces.
Exemple
[modifier | modifier le wikicode]TriangleMesh pyramid_mesh = new TriangleMesh();
// Pas de texture dans cet exemple, mais un point est nécessaire => (0,0)
pyramid_mesh.getTexCoords().addAll(0, 0); // --> T[]
float h = 150; // Height
float s = 300; // Side
pyramid_mesh.getPoints().addAll( // --> P[]
// X Y Z
0, 0, 0, // Point 0 - Top
0, h, -s/2, // Point 1 - Front
-s/2, h, 0, // Point 2 - Left
0, h, s/2, // Point 3 - Back
s/2, h, 0 ); // Point 4 - Right
pyramid_mesh.getFaces().addAll( // --> F[] = index in P[],T[]
// P,T P,T P,T
0,2, 2,4, 1,3, // Front left face
0,2, 1,4, 4,3, // Front right face
0,2, 4,4, 3,3, // Back right face
0,2, 3,4, 2,3, // Back left face
3,2, 1,3, 2,0, // Bottom rear face
3,2, 4,1, 1,4 ); // Bottom front face
// Regrouper les deux derniers triangles constituant la base carrée de la pyramide
pyramid_mesh.getFaceSmoothingGroups().addAll(0,1,2,3,4,4);
Il faut ensuite encapsuler la triangulation dans une forme :
MeshView pyramid = new MeshView(pyramid_mesh);
pyramid.setDrawMode(DrawMode.FILL);
pyramid.setTranslateY(-150-h);
Sources de lumière
[modifier | modifier le wikicode]Par défaut, la scène n'a aucune source lumineuse. Le rendu serait donc de la couleur de fond de la scène.
Une source lumineuse a une couleur filtrant celle des objets, une intensité, et une position.
JavaFX a deux types de sources lumineuses :
- Lumière ambiante (
AmbientLight
) - Une source lumineuse sans position, se diffusant sur toute la scène avec la même intensité.
- Lumière ponctuelle (
PointLight
) - Une source lumineuse avec position, dont la luminosité décroit avec l'augmentation de la distance.
Il faut également définir pour chaque source lumineuse, la liste des objets éclairés par celle-ci.
Exemple
[modifier | modifier le wikicode]Node[] allnodes = { sphere, cyl, pyramid, box };
Color light_color = Color.LIGHTGREY;
AmbientLight light = new AmbientLight(light_color);
light.setTranslateX(-180);
light.setTranslateY(-90);
light.setTranslateZ(-120);
light.getScope().addAll(allnodes);
PointLight light2 = new PointLight(light_color);
light2.setTranslateX(+180);
light2.setTranslateY(+190);
light2.setTranslateZ(+180);
light2.getScope().addAll(allnodes);
Caméra
[modifier | modifier le wikicode]La caméra (classe Camera
) représente le point de vue d'affichage de la scène : position et orientation.
Pour une représentation en 3D, la sous-classe PerspectiveCamera
est utilisée.
setNearClip(float)
- Définit la distance minimale pour le rendu des objets.
setFarClip(float)
- Définit la distance maximale pour le rendu des objets.
getTransforms()
- Liste modifiable des transformations (translation, rotation).
// Rotation et relocalisation de la caméra
Rotate rotate_x = new Rotate(0, Rotate.X_AXIS);
Rotate rotate_y = new Rotate(0, Rotate.Y_AXIS);
Translate translate = new Translate(0, 0, -1000);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1); // Afficher pour une distance entre 0.1
camera.setFarClip(10000.0); // ... et 10000
camera.getTransforms().addAll(rotate_x, rotate_y, translate);
Construction de la scène
[modifier | modifier le wikicode]La scène est construite en spécifiant le nœud racine regroupant les éléments de la scène.
Exemple
[modifier | modifier le wikicode]Il ne manque que la scène constituée des éléments vus précédemment.
Group root = new Group(allnodes);
root.getChildren().addAll(light, light2);
Scene scene = new Scene(root, 600, 600, true); // true to use depth buffer
scene.setFill(Color.BLACK); // Couleur de fond par défaut
scene.setCamera(camera);
// Affichage de la scène dans la fenêtre de l'application
stage.setTitle("3D JavaFX");
stage.setScene(scene);
stage.show();
Code complet
[modifier | modifier le wikicode]package org.wikibooks.fr.javafx;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.*;
public class ExempleFormes3D extends Application
{
// Transformations pour la caméra
private final Rotate rotate_x = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotate_y = new Rotate(0, Rotate.Y_AXIS);
private final Translate translate = new Translate(0, 0, -1000);
@Override
public void start(Stage stage)
{
// Coordonnées de base
int dx = 0, dy = 0, dz = 0;
// Sphère
Sphere sphere = new Sphere(100);
sphere.setTranslateX(dx);
sphere.setTranslateY(dy);
sphere.setTranslateZ(dz);
sphere.setCursor(Cursor.OPEN_HAND);
// Boîte
Box box = new Box(100, 200, 100);
box.setTranslateX(dx + 250);
box.setTranslateY(dy + 220);
box.setTranslateZ(dz);
box.setCursor(Cursor.OPEN_HAND);
// Cylindre
Cylinder cyl = new Cylinder(40, 220);
cyl.setTranslateX(dx);
cyl.setTranslateY(dy + 220);
cyl.setTranslateZ(dz);
cyl.setCursor(Cursor.OPEN_HAND);
// Pyramide à base carrée : par triangulation
TriangleMesh pyramid_mesh = new TriangleMesh(); // VertexFormat.POINT_TEXCOORD => P,T
pyramid_mesh.getTexCoords().addAll(0, 0); // --> T[]
pyramid_mesh.getTexCoords().addAll(1.0F, 0); // --> T[]
pyramid_mesh.getTexCoords().addAll(0.515F, 0.02F); // --> T[]
pyramid_mesh.getTexCoords().addAll(1.0F, 1.0F); // --> T[]
pyramid_mesh.getTexCoords().addAll(0, 1.0F); // --> T[]
float h = 150; // Height
float s = 300; // Side
pyramid_mesh.getPoints().addAll( // --> P[]
// X Y Z
0, 0, 0, // Point 0 - Top
0, h, -s/2, // Point 1 - Front
-s/2, h, 0, // Point 2 - Left
0, h, s/2, // Point 3 - Back
s/2, h, 0 ); // Point 4 - Right
pyramid_mesh.getFaces().addAll( // --> F[] = index in P[],T[]
// P,T P,T P,T
0,2, 2,4, 1,3, // Front left face
0,2, 1,4, 4,3, // Front right face
0,2, 4,4, 3,3, // Back right face
0,2, 3,4, 2,3, // Back left face
3,2, 1,3, 2,0, // Bottom rear face
3,2, 4,1, 1,4 ); // Bottom front face
pyramid_mesh.getFaceSmoothingGroups().addAll(0,1,2,3,4,4);
MeshView pyramid = new MeshView(pyramid_mesh);
pyramid.setDrawMode(DrawMode.FILL);
pyramid.setTranslateX(dx);
pyramid.setTranslateY(dy-150-h);
pyramid.setTranslateZ(dz);
pyramid.setEffect(new Shadow());
// Tous les éléments à éclairer
Node[] allnodes = { sphere, cyl, pyramid, box };
// Sources lumineuses
Color light_color = Color.LIGHTGREY;
AmbientLight light = new AmbientLight(light_color);
light.setTranslateX(dx - 180);
light.setTranslateY(dy - 90);
light.setTranslateZ(dz - 120);
light.getScope().addAll(allnodes);
PointLight light2 = new PointLight(light_color);
light2.setTranslateX(dx + 180);
light2.setTranslateY(dy + 190);
light2.setTranslateZ(dz + 180);
light2.getScope().addAll(allnodes);
// Nœud racine regroupant tous les éléments
Group root = new Group(allnodes);
root.getChildren().addAll(light, light2); // et les sources lumineuses
Scene scene = new Scene(root, 600, 600, true); // true to use depth buffer
scene.setFill(Color.BLACK);
// Point de vue de la scène
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.getTransforms().addAll(rotate_x, rotate_y, translate);
scene.setCamera(camera);
stage.setTitle("3D JavaFX");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
L'apparence des objets sera améliorée dans le chapitre suivant.
Apparence 3D
L'apparence par défaut des objets de la scène construite au chapitre précédent est plutôt monotone :
Le but de ce chapitre est de montrer comment améliorer l'apparence des objets en leur donnant une couleur ou une texture.
L'apparence d'un objet (couleur, reflets, texture, ...) est défini par une instance de la classe abstraite Material
.
JavaFX fournit une classe concrète nommée PhongMaterial
, nommée d'après le modèle d'ombrage de Phong.
Couleur
[modifier | modifier le wikicode]Les méthodes de la classe PhongMaterial
sont les suivantes :
setDiffuseColor(Color)
- Définit la couleur principale.
setSpecularColor(Color)
- Définit la couleur des reflets des sources lumineuses.
setSpecularPower(float)
- Définit la puissance de réflexion.
Exemple
[modifier | modifier le wikicode]PhongMaterial mat_blue = new PhongMaterial();
mat_blue.setDiffuseColor(Color.BLUE);
mat_blue.setSpecularColor(Color.LIGHTBLUE);
mat_blue.setSpecularPower(10.0);
On peut ensuite assigner l'apparence aux objets :
sphere.setMaterial(mat_blue);
Texture
[modifier | modifier le wikicode]Une texture est définie par une image pouvant définir la couleur de diffusion, de réflexion, d'illumination de l'objet.
Les méthodes de la classe PhongMaterial
concernant les textures sont les suivantes :
setDiffuseMap(Image)
- Définit la texture pour la couleur de diffusion.
setSpecularMap(Image)
- Définit la texture pour la couleur de reflet.
setSelfIlluminationMap(Image)
- Définit la texture pour l'intensité d'illumination.
setBumpMap(Image)
- Définit la texture pour la granularité de l'objet.
Exemple
[modifier | modifier le wikicode]L'exemple ci-dessous utilise l'image Red-brick-wall-texture-1.jpg disponible sur commons.
PhongMaterial mat_wall = new PhongMaterial();
//Image tex_im = new Image("file://///E:/Image/wall.png"); // Chemin local
Image tex_im = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Red-brick-wall-texture-1.jpg/640px-Red-brick-wall-texture-1.jpg");
PhongMaterial mat_wall = new PhongMaterial();
mat_wall.setDiffuseColor(Color.GRAY);
mat_wall.setSpecularColor(Color.WHITE);
mat_wall.setSpecularPower(8.0);
mat_wall.setDiffuseMap(tex_im);
Code complet
[modifier | modifier le wikicode]package org.wikibooks.fr.javafx;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.*;
public class TestApparence3D extends Application
{
private final Rotate rotate_x = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotate_y = new Rotate(0, Rotate.Y_AXIS);
private final Translate translate = new Translate(0, 0, -1000);
@Override
public void start(Stage stage)
{
Image tex_im = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Red-brick-wall-texture-1.jpg/640px-Red-brick-wall-texture-1.jpg");
PhongMaterial mat_wall = new PhongMaterial();
mat_wall.setDiffuseColor(Color.GRAY);
mat_wall.setSpecularColor(Color.WHITE);
mat_wall.setSpecularPower(8.0);
mat_wall.setDiffuseMap(tex_im);
PhongMaterial mat_blue = new PhongMaterial();
mat_blue.setDiffuseColor(Color.BLUE);
mat_blue.setSpecularColor(Color.LIGHTBLUE);
mat_blue.setSpecularPower(10.0);
PhongMaterial mat_red = new PhongMaterial();
mat_red.setDiffuseColor(Color.RED);
mat_red.setSpecularColor(Color.ORANGERED);
mat_red.setSpecularPower(10.0);
int dx = 0, dy = 0, dz = 0;
Sphere sphere = new Sphere(100);
sphere.setTranslateX(dx);
sphere.setTranslateY(dy);
sphere.setTranslateZ(dz);
sphere.setCursor(Cursor.OPEN_HAND);
sphere.setMaterial(mat_blue);
Box box = new Box(100, 200, 100);
box.setTranslateX(dx + 250);
box.setTranslateY(dy + 220);
box.setTranslateZ(dz);
box.setCursor(Cursor.OPEN_HAND);
box.setMaterial(mat_wall);
Cylinder cyl = new Cylinder(40, 220);
cyl.setTranslateX(dx);
cyl.setTranslateY(dy + 220);
cyl.setTranslateZ(dz);
cyl.setCursor(Cursor.OPEN_HAND);
cyl.setMaterial(mat_red);
TriangleMesh pyramid_mesh = new TriangleMesh(); // VertexFormat.POINT_TEXCOORD => P,T
// Texture : coordonnées indépendantes de la taille de la texture
// 0.5 = 50%, 1.0 = 100%, 2.0 = 200% (texture répétée 2 fois), ...
pyramid_mesh.getTexCoords().addAll( 0 , 0 ); // --> T[0]
pyramid_mesh.getTexCoords().addAll( 1.0F, 0 ); // --> T[1]
pyramid_mesh.getTexCoords().addAll( 0.5F, 0.0F ); // --> T[2]
pyramid_mesh.getTexCoords().addAll( 1.0F, 1.0F ); // --> T[3]
pyramid_mesh.getTexCoords().addAll( 0 , 1.0F ); // --> T[4]
float h = 150; // Height
float s = 300; // Side
pyramid_mesh.getPoints().addAll( // --> P[]
// X Y Z
0, 0, 0, // Point 0 - Top
0, h, -s/2, // Point 1 - Front
-s/2, h, 0, // Point 2 - Left
0, h, s/2, // Point 3 - Back
s/2, h, 0 ); // Point 4 - Right
pyramid_mesh.getFaces().addAll( // --> F[] = index in P[],T[]
// P,T P,T P,T
0,2, 2,4, 1,3, // Front left face
0,2, 1,4, 4,3, // Front right face
0,2, 4,4, 3,3, // Back right face
0,2, 3,4, 2,3, // Back left face
3,2, 1,3, 2,0, // Bottom rear face
3,2, 4,1, 1,4 ); // Bottom front face
pyramid_mesh.getFaceSmoothingGroups().addAll(0,1,2,3,4,4);
MeshView pyramid = new MeshView(pyramid_mesh);
pyramid.setDrawMode(DrawMode.FILL);
pyramid.setMaterial(mat_wall);
pyramid.setTranslateX(dx);
pyramid.setTranslateY(dy-150-h);
pyramid.setTranslateZ(dz);
Node[] allnodes = { sphere, cyl, pyramid, box };
Color light_color = Color.LIGHTGREY;
AmbientLight light = new AmbientLight(light_color);
light.setTranslateX(dx - 180);
light.setTranslateY(dy - 90);
light.setTranslateZ(dz - 120);
light.getScope().addAll(allnodes);
PointLight light2 = new PointLight(light_color);
light2.setTranslateX(dx + 180);
light2.setTranslateY(dy + 190);
light2.setTranslateZ(dz + 180);
light2.getScope().addAll(allnodes);
Group root = new Group(allnodes);
root.getChildren().addAll(light, light2);
Scene scene = new Scene(root, 600, 600, true);
scene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.getTransforms().addAll(rotate_x, rotate_y, translate);
scene.setCamera(camera);
stage.setTitle("3D JavaFX");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
Ajustements
[modifier | modifier le wikicode]La texture de la base carrée fait apparaître les deux triangles car les 2 faces adjacentes sont associées à deux triangles qui ne sont pas adjacent dans la texture :
En ajustant la texture comme dans le code ci-dessous, on obtient une texture uniforme pour la base carrée :
pyramid_mesh.getFaces().addAll( // --> F[] = index in P[],T[]
// P,T P,T P,T
0,2, 2,4, 1,3, // Front left face
0,2, 1,4, 4,3, // Front right face
0,2, 4,4, 3,3, // Back right face
0,2, 3,4, 2,3, // Back left face
3,1, 1,4, 2,0, // Bottom rear face
3,1, 4,3, 1,4 ); // Bottom front face
|
Dans cette version, la caméra est fixe. Le chapitre suivant résout le problème en ajoutant la possibilité d'interagir avec la souris pour déplacer la caméra.
Interactions 3D
Ce chapitre explique comment ajouter de l'interaction à une scène 3D.
Le principe est le même avec les composants 2D d'une interface graphique : ajouter un listener à notifier de certains évènements.
Rotations et déplacements
[modifier | modifier le wikicode]Le code des exemples précédents contient déjà les transformations appliquées à la caméra :
private final Rotate rotate_x = new Rotate(0, Rotate.X_AXIS); // axe horizontal
private final Rotate rotate_y = new Rotate(0, Rotate.Y_AXIS); // axe vertical
private final Translate translate = new Translate(0, 0, -1000);
Il suffit d'ajouter des listeners pour modifier les paramètres de ces transformations selon les déplacements de la souris quand le bouton gauche est maintenu appuyé. La modification dépend des touches Shift et Ctrl appuyées lors du déplacement de la souris :
- Si la touche Ctrl est appuyée; la caméra est déplacée dans le plan XY ;
- Si la touche Shift est appuyée; la caméra est déplacée dans le plan ZY ;
- Sinon, l'angle de rotation autour de l'axe horizontal est contrôlé par les mouvements verticaux de la souris et vice-versa.
Les listeners sont ajoutés à la scène.
scene.setOnMousePressed((MouseEvent me) -> {
mouse_x = me.getSceneX();
mouse_y = me.getSceneY();
mouse_x_prev = mouse_x;
mouse_y_prev = mouse_y;
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouse_x = me.getSceneX();
mouse_y = me.getSceneY();
if (me.isControlDown())
{
translate.setX(translate.getX() + (mouse_x - mouse_x_prev));
translate.setY(translate.getY() + (mouse_y - mouse_y_prev));
}
else if (me.isShiftDown())
{
translate.setZ(translate.getZ() + (mouse_x - mouse_x_prev));
translate.setY(translate.getY() + (mouse_y - mouse_y_prev));
}
else
{
rotate_x.setAngle(rotate_x.getAngle() - (mouse_y - mouse_y_prev));
rotate_y.setAngle(rotate_y.getAngle() + (mouse_x - mouse_x_prev));
}
mouse_x_prev = mouse_x;
mouse_y_prev = mouse_y;
});
La rotation est centrée sur la sphère, c'est à dire en (0,0,0), quel que soit la position de la caméra car il s'agit des deux premières transformations ajoutées à la caméra, avant le déplacement :
camera.getTransforms().addAll(rotate_x, rotate_y, translate);
Code complet
[modifier | modifier le wikicode]package org.wikibooks.fr.javafx;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.*;
public class TestCameraMove extends Application
{
private double mouse_x;
private double mouse_y;
private double mouse_x_prev;
private double mouse_y_prev;
private final Rotate rotate_x = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotate_y = new Rotate(0, Rotate.Y_AXIS);
private final Translate translate = new Translate(0, 0, -1000);
@Override
public void start(Stage stage)
{
Image tex_im = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Red-brick-wall-texture-1.jpg/640px-Red-brick-wall-texture-1.jpg");
PhongMaterial mat_wall = new PhongMaterial();
mat_wall.setDiffuseColor(Color.GRAY);
mat_wall.setSpecularColor(Color.WHITE);
mat_wall.setSpecularPower(8.0);
mat_wall.setDiffuseMap(tex_im);
PhongMaterial mat_blue = new PhongMaterial();
mat_blue.setDiffuseColor(Color.BLUE);
mat_blue.setSpecularColor(Color.LIGHTBLUE);
mat_blue.setSpecularPower(10.0);
PhongMaterial mat_red = new PhongMaterial();
mat_red.setDiffuseColor(Color.RED);
mat_red.setSpecularColor(Color.ORANGERED);
mat_red.setSpecularPower(10.0);
int dx = 0, dy = 0, dz = 0;
Sphere sphere = new Sphere(100);
sphere.setTranslateX(dx);
sphere.setTranslateY(dy);
sphere.setTranslateZ(dz);
sphere.setCursor(Cursor.OPEN_HAND);
sphere.setMaterial(mat_blue);
Box box = new Box(100, 200, 100);
box.setTranslateX(dx + 250);
box.setTranslateY(dy + 220);
box.setTranslateZ(dz);
box.setCursor(Cursor.OPEN_HAND);
box.setMaterial(mat_wall);
Cylinder cyl = new Cylinder(40, 220);
cyl.setTranslateX(dx);
cyl.setTranslateY(dy + 220);
cyl.setTranslateZ(dz);
cyl.setCursor(Cursor.OPEN_HAND);
cyl.setMaterial(mat_red);
TriangleMesh pyramid_mesh = new TriangleMesh(); // VertexFormat.POINT_TEXCOORD => P,T
// Texture : coordonées indépendantes de la taille de la texture
// 0.5 = 50%, 1.0 = 100%, 2.0 = 200% (texture répétée 2 fois), ...
pyramid_mesh.getTexCoords().addAll(0, 0); // --> T[]
pyramid_mesh.getTexCoords().addAll(2.0F, 0); // --> T[]
pyramid_mesh.getTexCoords().addAll(1.0F, 0.0F); // --> T[]
pyramid_mesh.getTexCoords().addAll(2.0F, 2.0F); // --> T[]
pyramid_mesh.getTexCoords().addAll(0, 2.0F); // --> T[]
float h = 150; // Height
float s = 300; // Side
pyramid_mesh.getPoints().addAll( // --> P[]
// X Y Z
0, 0, 0, // Point 0 - Top
0, h, -s/2, // Point 1 - Front
-s/2, h, 0, // Point 2 - Left
0, h, s/2, // Point 3 - Back
s/2, h, 0 ); // Point 4 - Right
pyramid_mesh.getFaces().addAll( // --> F[] = index in P[],T[]
// P,T P,T P,T
0,2, 2,4, 1,3, // Front left face
0,2, 1,4, 4,3, // Front right face
0,2, 4,4, 3,3, // Back right face
0,2, 3,4, 2,3, // Back left face
3,1, 1,4, 2,0, // Bottom rear face
3,1, 4,3, 1,4 ); // Bottom front face
pyramid_mesh.getFaceSmoothingGroups().addAll(0,1,2,3,4,4);
MeshView pyramid = new MeshView(pyramid_mesh);
pyramid.setDrawMode(DrawMode.FILL);
pyramid.setMaterial(mat_wall);
pyramid.setTranslateX(dx);
pyramid.setTranslateY(dy-150-h);
pyramid.setTranslateZ(dz);
Node[] allnodes = { sphere, cyl, pyramid, box };
Color light_color = Color.LIGHTGREY;
AmbientLight light = new AmbientLight(light_color);
light.setTranslateX(dx - 180);
light.setTranslateY(dy - 90);
light.setTranslateZ(dz - 120);
light.getScope().addAll(allnodes);
PointLight light2 = new PointLight(light_color);
light2.setTranslateX(dx + 180);
light2.setTranslateY(dy + 190);
light2.setTranslateZ(dz + 180);
light2.getScope().addAll(allnodes);
Group root = new Group(allnodes);
root.getChildren().addAll(light, light2);
Scene scene = new Scene(root, 600, 600, true);
scene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.getTransforms().addAll(rotate_x, rotate_y, translate);
scene.setCamera(camera);
scene.setOnMousePressed((MouseEvent me) -> {
mouse_x = me.getSceneX();
mouse_y = me.getSceneY();
mouse_x_prev = mouse_x;
mouse_y_prev = mouse_y;
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouse_x = me.getSceneX();
mouse_y = me.getSceneY();
if (me.isControlDown())
{
translate.setX(translate.getX() + (mouse_x - mouse_x_prev));
translate.setY(translate.getY() + (mouse_y - mouse_y_prev));
}
else if (me.isShiftDown())
{
translate.setZ(translate.getZ() + (mouse_x - mouse_x_prev));
translate.setY(translate.getY() + (mouse_y - mouse_y_prev));
}
else
{
rotate_x.setAngle(rotate_x.getAngle() - (mouse_y - mouse_y_prev));
rotate_y.setAngle(rotate_y.getAngle() + (mouse_x - mouse_x_prev));
}
mouse_x_prev = mouse_x;
mouse_y_prev = mouse_y;
});
stage.setTitle("3D JavaFX");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
Jouer une vidéo ou un son
JavaFX permet de jouer une vidéo ou un son en utilisant les classes du package javafx.scene.media
.
Jouer un fichier
[modifier | modifier le wikicode]Le code d'exemple ci-dessous montre comment jouer une vidéo ou un son à partir d'un fichier.
- Si le fichier est une vidéo, elle est affichée dans la scène. Initialement en mode plein écran, la taille de la vidéo s'adapte à celle de la scène par binding des propriétés
width
etheight
. - Si le fichier est un son, le son est audible et la scène affiche la couleur de fond.
package org.wikibooks.fr;
import java.io.*;
import javafx.application.*;
import javafx.beans.binding.*;
import javafx.beans.property.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.media.*;
import javafx.scene.paint.*;
import javafx.stage.*;
public class MediaApplication extends Application
{
public static void main(String[] args)
{
Application.launch(args);
}
protected MediaPlayer media_player;
@Override
public void start(Stage stage) throws Exception
{
// .mp3 / .m4a / .mpeg / ...
String path = "/chemin/du/fichier.mp3";
Media media = new Media(new File(path).toURI().toString());
media_player = new MediaPlayer(media);
MediaView viewer = new MediaView(media_player);
// Ajuster automatiquement la vue vidéo à la taille de la scène :
DoubleProperty width = viewer.fitWidthProperty();
DoubleProperty height = viewer.fitHeightProperty();
width.bind(Bindings.selectDouble(viewer.sceneProperty(), "width"));
height.bind(Bindings.selectDouble(viewer.sceneProperty(), "height"));
viewer.setPreserveRatio(true);
StackPane root = new StackPane();
root.getChildren().add(viewer);
// Création de la scène :
Scene scene = new Scene(root, 500, 500, Color.BLACK);
stage.setScene(scene);
stage.setTitle("Vidéo");
stage.setFullScreen(true);
stage.show();
media_player.play();
}
}
Comme le fichier est joué de manière asynchrone en tâche de fond, la référence au MediaPlayer doit être conservée pour que le garbage collector ne supprime pas le media player, sinon cela provoquerait un arrêt du fichier au bout d'un moment aléatoire.
Dans le code précédent, la référence au MediaPlayer est conservée par l'objet viewer
.
Il est possible de se passer d'objet viewer
(instance de la classe MediaView
), pour jouer le son seulement, auquel cas il faut conserver la référence au MediaPlayer dans la classe.
Scene Builder
Scene Builder est un outil interactif de conception d'interface graphique pour JavaFX. Il permet de créer des interfaces utilisateurs rapidement et sans avoir besoin de coder ; il en résulte des fichiers au format FXML qui sont ensuite chargés par le programme pour afficher une interface graphique à l'utilisateur.
Développé initialement par Oracle et sous le nom JavaFX Scene Builder, son code source a été publié en open source à partir de sa version 2.0.
Depuis, le logiciel est principalement développé et soutenu par la société Gluon.
Utilisation
[modifier | modifier le wikicode]Le logiciel s'utilise avec la technique du glisser/déposer : un panneau latéral situé à gauche de la fenêtre permet à l'utilisateur de sélectionner et positionner un composant dans la hiérarchie des composants ou sur la vue centrale affichant le rendu.
Un panneau latéral positionné à droite de la fenêtre permet quant à lui de définir les caractéristiques du composant sélectionné par l'utilisateur.
Les interfaces sont enregistrées dans des fichiers au format FXML, qui sont ensuite lus et chargés en mémoire par le programme, puis affichés à l'écran.
FXML
[modifier | modifier le wikicode]FXML est un format de données textuel, dérivé du format XML, qui permet de décrire une interface utilisateur pour des applications conçues avec JavaFX.
Il s'agit d'une alternative à la conception d'interfaces réalisée par l'écriture de lignes de codes, en découplant l'interface graphique du code source qui le contrôle.
Cette technologie a été intégrée dans JavaFX à partir de sa version 2.0, pour remplacer JavaFX Script.
GFDL | Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture. |