Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

JavaFX est le nom d'un ensemble de technologies développées par Sun autour de la technologie Java, et destinées à faciliter l'écriture d'applications riches graphiquement pour une grande variété de cibles (ordinateur, téléphones portables, etc.).

Une des nouveautés les plus visibles dans JavaFX est l'introduction d'un nouveau langage, JavaFX Script. Celui-ci utilise une syntaxe déclarative pour décrire les différents éléments graphiques de l'application. Cet article va vous en présenter les grandes lignes.

Un point de vocabulaire

JavaFX est le nom de la technologie. JavaFX Script le nom du langage. Dans la pratique, on trouve presque systématiquement, JavaFX – ou une variation comme Java FX ou JFX.

Installer JavaFX

Avant de pouvoir utiliser JavaFX, il faut d'abord l'installer. Malheureusement, lors de la rédaction de cet article, il n'y avait pas de version de JavaFX pour Linux. Mais en rusant un peu, il est possible de s'en sortir avec le SDK pour Mac OS X. Voici donc la transcription des commandes shell que j'ai utilisées pour installer le SDK sur ma machine Debian/Etch à partir de l'image disque pour MacOS X.

Note:

Les détails sont dans l'article Récupérer sous Linux des données d'une image disque MacOS X.

sh# cd /usr/local/
sh# mkdir javafx-1.1.1
sh# ln -s javafx-1.1.1 javafx
sh# cd javafx
sh# bunzip2 /path/to/javafx_sdk-1_1_1-macosx-universal.dmg
bunzip2: Can't guess original name for /path/to/javafx_sdk-1_1_1-macosx-universal.dmg -- using /path/to/javafx_sdk-1_1_1-macosx-universal.dmg.out

bunzip2: /path/to/javafx_sdk-1_1_1-macosx-universal.dmg: trailing garbage after EOF ignored
sh# losetup  -o 17408 -s -f /path/to/javafx_sdk-1_1_1-macosx-universal.dmg.out
sh# mkdir mount-point
sh# mount -o loop -t hfsplus /dev/loop0 mount-point
sh# cp 'mount-point/JavaFX 1.1 SDK.mpkg/Contents/Packages/javafxsdk.pkg/Contents/Archive.pax.gz' .
sh# gunzip Archive.pax.gz
sh# cpio -id < Archive.pax
sh# umount mount-point/
sh# rmdir mount-point/
sh# rm Archive.pax
sh# ls
bin             lib          README.html  src.zip
COPYRIGHT.html  LICENSE.txt  samples      THIRDPARTYLICENSEREADME.txt
docs            profiles     servicetag   timestamp

Maintenant, pour faciliter l'utilisation de l'outil du SDK, il suffit d'ajouter le sous-répertoire bin à votre PATH. Cela peut se faire aussi simplement qu'en une ligne dans votre shell:

sh$ export JAVAFX_HOME=/usr/local/javafx
sh$ export PATH="${PATH}:${JAVAFX_HOME}/bin"

Par contre, vous devrez re-déclarer ces variables dans chaque nouveau shell. A l'inverse, sous Debian, vous pouvez aussi ajouter le fichier local-javafx.sh suivant dans le répertoire /etc/profile.d. Il sera alors disponible à tout nouveau shell ouvert:

sh$ cat /etc/profile.d/local-javafx.sh
#
# JavaFX configuration
#

if [ -z "${JAVAFX_HOME}" ]
then
  export JAVAFX_HOME=/usr/local/javafx
  export PATH="${PATH}:${JAVAFX_HOME}/bin"
fi
sh$  . /etc/profile.d/local-javafx.sh {{Commentaire shell|Juste pour initialiser les variables d'environnement dans le shell courant
sh$ echo $JAVAFX_HOME
/usr/local/javafx

Note:

Dans d'autres distributions, si le répertoire /etc/profile.d n'existe pas, vous pouvez directement modifier le fichier /etc/profile.

Hello JavaFX

Puisque c'est la tradition, nous allons sacrifier au sempiternel "Hello World". Cela va au moins nous permettre de vérifier que l'installation précédente s'est bien passée.

Commencez donc par créer le fichier Hello.fx contenant le code suivant:

import javafx.scene.Scene;
import javafx.scene.text.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
 
Stage {
   title: "Hello World JavaFX"
    scene: Scene {
        content: Text {
            content:     "Hello World!"
            font:        Font { size: 30 }
            translateX: 114
            translateY: 45
        }
    }
    width:400 height:100
}

Si vous programmez déjà en Java, ce code doit vous sembler un rien familier. De même si vous êtes un habitué de JavaScript ou Groovy. Mais JavaFX Script reste bel et bien un langage à part entière!

Tentons de compiler. Dans la pure tradition, le compilateur JavaFX est l'outil javafxc:

sh$ javafxc Hello.fx

Piège:

Faites bien attention à l'extention de votre fichier. Les sources JavaFX doivent se terminer avec l'extension .fx. Impérativement. Sinon le compilateur ne reconnaît pas le fichier comme un script JavaFX et affiche un message d'erreur sibyllin. Ainsi, par exemple, s'il vous avait pris la fantaisie d'utiliser l'extension .javafx voici ce que vous auriez obtenu:

sh$  javafxc Hello.javafx
javafxc: invalid flag: Hello.javafx
Usage: javafxc <options> <source files>
use -help for a list of possible options

Toujours par analogie avec Java, la commande pour exécuter les programmes JavaFX est javafx.

sh$ javafx Hello

Ici, vous aurez peut-être la surprise de voir apparaître l'écran ci-contre: en fait – et contrairement à ce que peut laisser penser le texte du message – il s'agit d'accepter la licence d'utilisation de JavaFX. Rien n'a besoin d'être installé puisque nous avons tout mis en place au début de cet article. Acceptez donc la licence pour que nous puissions continuer.

Une fois la licence acceptée, votre première fenêtre avec JavaFX apparaît:

JavaFX primer

La structure de base d'un script JavaFX est toujours la même. Un élément Stage représente une fenêtre de l'application. A l'intérieur de chaque fenêtre le graphe des objets à dessiner est décrit dans l'élément Scene.

Si je vous dis aussi qu'en plus des structures purement déclaratives, JavaFX contient aussi des instruction de répétition. En clair, la boucle for. Muni de toutes ces informations, vous comprenez que le programme suivant ouvre 2 fenêtres:

sh$ cat DeuxFenêtres.fx
import javafx.stage.Stage;

for(s in [0..<2]) { Stage {
} }

Si vous lancez ce programme, vous verrez que les deux fenêtres sont minuscules et apparaissent superposées (sous Gnome, en plein centre de l'écran). Essayons de faire un peu mieux en fixant la taille de ces fenêtres:

import javafx.stage.Stage;
 
for(s in [0..<2]) { Stage {
    width: 200
    height: 100
} }

Note:

En fait, par défaut, la fenêtre est créée de telle sorte qu'elle soit suffisamment grande pour afficher la scène qu'elle contient. Mais comme ici il n'y a aucune scène à afficher, la taille se réduit au minimum possible.

Les propriétés width et height permettent de fixer la taille de la fenêtre indépendamment de la taille de la scène à afficher.

Bon, les deux fenêtres sont créés, mais elles sont superposées. Encore une fois, il est possible de faire mieux sans trop de problème. Avez-vous remarqué la boucle for:

for(s in [0..<2]) {
...
}

Cette structure de contrôle répète ce qui est entre accolade un certain nombre de fois. Ici 2 fois. Et à chaque répétition la valeur de la variable s change. A la première itération (au premier tour), s vaut 0, au second 1. Dans notre exemple, la boucle commence avec s à 0, et tourne tant que s est <2.

Tout ça pour dire que puisque l'on peut savoir "dans quel tour on est" en examinant la variable s. Du coup, il est possible d'utiliser cette variable pour placer différemment les deux fenêtres:

import javafx.stage.Stage;
 
for(s in [0..<2]) { Stage {
    x: s*100     // s "multiplié par" 100
    y: s*100     // s "multiplié par" 100
 
    width: 200
    height: 100
}}

Si vous n'êtes pas familier avec la notion de boucle, n'hésitez pas à jouer un peu avec ce petit programme en changeant les bornes inférieures et supérieures pour voir ce que ça donne.

Note:

Si vous manquez d'imagination, essayez les boucles suivantes:

for(s in [0..2]) { ... }
for(s in [1..<3]) { ... }
for(s in [-1..<2]) { ... }

Backstage

Bien, nous avons joué avec les Stages. Mais créer plusieurs (pleins?) de fenêtres vides n'est guère excitant. Nous allons donc maintenant nous intéresser plus spécialement au contenu à afficher dans une de ces fenêtres. Formellement, dans le vocabulaire de JavaFX, à la scène.

En fait, la scène pour JavaFX est juste un container qui contient le graph des objets à dessiner dans la fenêtre. C'est un des points clés de JavaFX: en fait, nous n'allons pas directement dessiner dans la fenêtre. Mais créer dans la scène l'ensemble des objets à y faire figurer. Ces objets seront (re-)dessinés au besoin. C'est à dire aussi bien lorsque la fenêtre apparaît initialement, que lorsqu'elle doit être rafraîchie (sous parce qu'une partie cachée de la fenêtre vient d'apparaître, ou encore parce que la scène a changé).

La fenêtre

Pour cette introduction, nous allons rester sur des formes géométriques simples. Le programme de départ ressemble fortement à ce que nous avons vu précédemment, et pour l'instant se contente d'afficher une fenêtre (sans spécification de taille ou de position). L'exécution de ce programme donne la mini-fenêtre ci contre. Mais rassurez-vous, d'ici quelques minutes nous allons la remplir.

sh$ cat Dessin.fx
import javafx.stage.Stage;

Stage {
}
sh$ javafxc Dessin.fx
sh$ javafx Dessin

Ajouter la scène

Nous allons y aller tout doucement. Ainsi, même si vous n'êtes pas familier de Java – ou de la programmation en général – vous ne serez pas noyés par de multiples modifications simultanées.

Donc commençons par ajouter une scène vide:

sh$ cat Dessin.fx
import javafx.stage.Stage;
import javafx.scene.Scene;

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    // La scène:
    scene: Scene {
    }
}

Tout d'abord, vous vous en doutez certainement, le bloc entre /* ... */ est un commentaire. C'est un moyen de donner des informations sur le programme à une personne lisant votre code source. Vous pouvez aussi ajouter un commentaire avec la notation //. Dans ce cas, le commentaire s'étend jusqu'à la fin de la ligne.

La seule modification fonctionnelle du code source est l'ajout des lignes:

    scene: Scene {
    }

Celle ci initialise la propriété scene (en minuscule) de la fenêtre avec un nouvel objet de la classe Scene (avec une initiale en majuscule).

Quand à la ligne import ..., celle-ci permet juste au compilateur de savoir dans quel paquet se trouve la classe Scene que nous utilisons dans ce programme. Si vous êtes familier de Java, vous avez déjà entendu parler de paquet (package). Dans tous les cas, il vous suffit de savoir que c'est juste un moyen de regrouper différentes classes. Accessoirement, cela permet aussi lors de l'exécution de retrouver ces classes qui sont souvent dans des fichiers séparés.

Si vous compilez et testez ce programme vous verrez que c'est toujours la même mini-fenêtre qui apparaît. Évidemment: il y a bien maintenant une scène dans notre fenêtre, mais la scène est vide. Nous allons tout de suite y remédier.

Du contenu

Bon, il est plus que temps de dessiner quelque chose (Enfin!). Je vous livre tel quel le code source. Son exécution devrait vous mener au résultat ci-contre:

sh$ cat Dessin.fx
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    scene: Scene {
        content: Circle {
            radius: 100
            translateX: 100
            translateY: 100
        }
    }
}
sh$ javafxc Dessin.fx
sh$ javafx Dessin

Vous remarquez la même structure que ce que nous avons détaillée dans la partie précédente: la propriété content de la scène est initialisée avec un nouvel objet de la classe Circle. Ce dernier objet a aussi un certain nombre de propriétés initialisées (si vous ne voyez pas de façon intuitive à qui servent ces propriétés, n'hésitez pas à essayer le programme en changeant ces valeurs).

Enfin, comme toujours, puisque j'utilise une nouvelle classe dans mon programme, j'indique au compilateur par une directive import que la classe Circle se trouve dans le paquet javafx.scene.shape.

Un peu de couleur

Comme il est triste ce cercle. Donnons lui un peu de peps en lui ajoutant un peu de couleur. La couleur de remplissage d'une forme est sa propriété fill. Et les couleurs de base sont pré-définies dans la classe Color du paquet javafx.scene.paint. Muni de ces informations, vous ne devriez avoir aucun mal à comprendre les modifications apportées à cette nouvelle version de notre programme:

sh$ cat Dessin.fx
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    scene: Scene {
        content: Circle {
            radius: 100
            translateX: 100
            translateY: 100
            fill: Color.YELLOW;
        }
    }
}

Un peu de variété

Rajoutons maintenant un peu de variété à notre scène:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    scene: Scene {
       content: [
           Circle {
               radius: 100
               translateX: 100
               translateY: 100
               fill: Color.YELLOW;
           },
           Circle {
               radius: 10
               translateX: 100
               translateY: 165
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 55;
               translateY: 45
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 120;
               translateY: 45
           }
       ]
    }
}

L'aspect important dans les modifications apportées au programme ne sont pas forcément l'ajout de deux objets de la classe Rectangle. A ce stade, cela devrait vous être compréhensible.

Par contre, ce qui est notable, c'est que jusqu'à présent le contenu de notre scène était composé d'un seul objet. Maintenant, il y en a plusieurs. Une solution dans JavaFX pour résoudre ce problème est d'utiliser une séquence. Vous voyez que nous utilisons une séquence ici, à la notation [ ... , ... ]. Formellement, une séquence commence par un crochet ouvrant et se termine par un crochet fermant. Et les éléments sont séparés par une virgule.

Note:

Les séquences ressemblent beaucoup à la notion de liste ou de tableau que l'on trouve dans d'autres langages. Mais en plus souple. Vous souvenez-vous de l'exemple – un peu plus haut – où nous nous amusions à créer plusieurs fenêtres? Et plus précisément du passage:

for(s in [0..<2]) {
    /* ... */
}

La notation [0..<2] est aussi une séquence. Celle des entiers entre 0 (inclus) et 2 (exclu).

Move it baby!

Variables

Donc nous avons vu comment dessiner avec JavaFX. Mais il est aussi possible d'avoir non seulement des images fixes, mais aussi des animations.

Dans cet exemple, nous allons faire écarquiller (d'excitation?) les yeux de notre smiley. Tout d'abord, nous allons faire un peu de mathématiques. Juste un tout petit peu. En effet, si je veux que les yeux de mon smiley s'écartent chacun de leur côté, et en même temps, il faut que je trouve une formule qui permet de calculer la position de chaque oeil à un instant donné.

Ici, le problème est simple:

Bon la formule est simple:

Et le décalage variera pendant l'animation. En fait, ce décalage sera ce que l'on appelle une variable en informatique. Nous allons tout de suite mettre en place cela dans le code de notre programme:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;

/*
 * Décalage des yeux
 */
var decalage: Number = 0; // définit la variable décalage qui vaut 0

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    scene: Scene {
       content: [
           Circle {
               radius: 100
               translateX: 100
               translateY: 100
               fill: Color.YELLOW
           },
           Circle {
               radius: 10
               translateX: 100
               translateY: 165
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 55 - decalage
               translateY: 45
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 120 + decalage
               translateY: 45
           }
       ]
    }
}

Si vous compilez et exécutez ce programme, le dessin ne devrait pas avoir changé d'un pixel. Maintenant, amusez-vous à changer la déclaration de la variable decalage pour l'initialiser à 20:

var decalage: Number = 20;

Et relancez le programme. Normalement, votre smiley écarquille les yeux. Maintenant, il faudrait un peu automatiser le processus pour avoir une véritable animation!

Timeline et KeyFrame

Le principe dans JavaFX est de définir une timeline. C'est à dire un repère temporel qui indique ce qui doit se passer à des instants précis. Si vous avez déjà fait du montage vidéo – ou travaillé avec une technologie d'animation comme Flash – le concept doit vous être familier.

L'autre concept clé quand ont parle d'animations en JavaFX est celui d'image-clé (keyframe). Si la timeline représente un intervalle de temps, un keyframe représente un instant. Tout l'intérêt est qu'il va être possible de dire ce qui se passe dans une animation à cet instant particulier. Un exemple vaut mieux de longs discours, donc voici le code que vous pourrez ajouter à la fin de votre programme:

Timeline {
    keyFrames: [
        KeyFrame {
            time: 1s
            values: [
                decalage => 20
            ]
        }
        KeyFrame {
            time: 2s
            values: [
                decalage => 0
            ]
        },
    ]
}.play()

Globalement, vous devriez reconnaître dans ce code une structure similaire à celle vue précédemment. Il s'agit de la création d'un objet de la classe TimeLine qui contient une séquence (délimitée par les [ ... , ... ]) d'objets de la classe KeyFrame.

Par contre deux nouvelles notations sont introduites ici. Tout d'abord la flèche =>:

        KeyFrame {
            time: 1s
            values: [
                decalage => 20
            ]
        }

Celle-ci peut se lire comme "voit sa valeur atteindre". Autrement dit, l'image-clé ci-dessus signifie qu'à 1 seconde du début de l'animation, decalage "verra sa valeur atteindre" 20.

        KeyFrame {
            time: 2s
            values: [
                decalage => 0
            ]
        }

De la même manière, à 2 secondes du début de l'animation decalage "verra sa valeur atteindre" 0. Or souvenez-vous que decalage conditionne l'écartement des yeux de notre smiley. Vous comprenez donc qu'en faisant évoluer cette variable au cours du temps, les yeux de notre personnage s'animeront.

Pourquoi pas de KeyFrame à time=0?

J'aurais pu rajouter l'image clé suivante:

       KeyFrame {
           time: 0s
           values: [
               decalage => 0
           ]
       }

Celle-ci servirait à initialiser la variable decalage à 0 au début de l'animation. Mais c'est inutile. En effet, cette variable a déjà été initialisée lors de sa déclaration:

 var decalage: Number = 0;

Remarquez aussi que les temps sont exprimés "depuis le début de l'animation". La question étant de savoir quand démarrer l'animation. Pour ce programme, nous allons faire simple, en faisant démarrer l'animation dès le programme lancé. C'est ce que fait l'appel de méthode suivant:

Timeline {
    /* ... */
}.play()

Le fragment de code précédent pourrait se lire: "créé un objet de la classe Timeline, et lance tout de suite l'animation qu'il contient".

N'oublie pas non plus que qui dit nouvelles classes, dit nouveaux imports. Au final, le code complet du programme est le suivant:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;

/*
 * Décalage des yeux
 */
var decalage: Number = 0;

/*
 * Démonstration des fonctionnalités de
 * dessin de JavaFX
 */
Stage {
    scene: Scene {
       content: [
           Circle {
               radius: 100
               translateX: 100
               translateY: 100
               fill: Color.YELLOW
           },
           Circle {
               radius: 10
               translateX: 100
               translateY: 165
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 55 - decalage
               translateY: 45
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: 120 + decalage
               translateY: 45
           }
       ]
    }
}

Timeline {
    keyFrames: [
       KeyFrame {
           time: 1s
           values: [
               decalage => 20
           ]
       },
       KeyFrame {
           time: 2s
           values: [
               decalage => 0
           ]
       }
    ]
}.play()

Aller, vous en mourrez d'envie: compilez et testez:

sh$ javafxc Dessin.fx
sh$ javafx Dessin

Et vous constaterez ... que rien ne s'anime! Pourquoi? La raison est dans la manière dont sont créés les rectangles qui forment les yeux. Examinons-en un:

Dans cette forme d'initialisation, JavaFX utilise les valeurs. Une fois le calcul effectué, les données sont indépendantes.
           Rectangle {
               height: 25;
               width: 25;
               translateX: 55 - decalage
               translateY: 45
           },

En fait, ce code dit à JavaFX de créer un rectangle, et de lui appliquer une translation selon l'axe X de (55-decalage) pixels. Autrement dit de 55-0 soit 55 pixels. Puisque la variable decalage a été initialisée à 0 au début du programme.

Ce qui se passe, c'est que la valeur contenue dans décalage a été utilisée pour calculer cette translation quand le rectangle a été créé. Après, vous pouvez modifier autant que vous voulez le contenu de decalage: le rectangle restera en place! Or nous voulons exactement le contraire: que les deux valeurs restent synchronisées.

Dans cette forme d'initialisation, JavaFX garde une référence sur les objets utilisés pour le calcul. Les valeurs sont liées.

JavaFX le permet à condition de le préciser explicitement à l'aide du mot -clé bind. Changez simplement les deux créations de rectangles ainsi:

           Rectangle {
               height: 25;
               width: 25;
               translateX: bind 55 - decalage
               translateY: 45
           },
           Rectangle {
               height: 25;
               width: 25;
               translateX: bind 120 + decalage
               translateY: 45
           }

Après cette modification, la position horizontale des yeux restera synchronisée avec la valeur de la variable decalage. Ou autrement dit, quand decalage changera, les rectangles verront leur position modifiée en conséquence.

Sky is the limit

Nous avons fait dans cet article un rapide tour d'horizon des concepts de base de JavaFX. N'hésitez pas à expérimenter un peu avec ce programme. Pour vous familiariser autant avec la syntaxe du langage, qu'avec ces concepts.

Par exemple, vous pourriez changer le diamètre de la bouche de notre smiley en même temps qu'il écarquille les yeux (Ooooh!):

Stage {
    scene: Scene {
       content: [
           /* ... */
           Circle {
               radius: bind 10+decalage
               translateX: 100
               translateY: 165
           },
           /* ... */

Ou encore, expérimenter d'autres formes de binding. Avec les couleurs – histoire de faire rougir notre smiley

/* ... */
var couleur: Color = Color.YELLOW;
/* ... */
Stage {
    scene: Scene {
       content: [
           Circle {
               radius: 100
               translateX: 100
               translateY: 100
               fill: bind couleur
           },
           /* ... */
       ]
    }
}

Timeline {
    keyFrames: [
       /* ... */
       KeyFrame {
           time: 2s
           values: [
               decalage => 0,
               couleur => Color.RED
           ]
       },
       /* ... */

Comme vous le voyez, rien qu'avec les informations données dans cet article, il y a déjà de quoi s'amuser. Et nous n'avons fait qu'effleurer ce que peut faire JavaFX!