TP de Programmation sur le dessin avec un Canvas JavaFX

Dans ce TP, nous allons voir comment il est possible de dessiner assez simplement en JavaFX à l'aide d'un composant appelé Canvas. Nous verrons aussi comment adapter le dessin au nombre de pixels de la fenêtre et comment gérer simplement le redimensionnement.

0 - Remarque pour les utilisateurs d'éclipse

Attention, par défaut Eclipse se place dans une configuration dans laquelle JavaFX n'est pas accessible. Pour résoudre le problème, deux possibilités :

1 - Affichage et rafraîchissment

L'interface graphique d'une application est évidemment construite et affichée par l'application elle-même. Dans un système graphique classique, le contenu d'une fenêtre n'est pas sauvegardé par le système. Cela signifie que lorsqu'une fenêtre est redimensionnée, masquée/démasquée par une autre fenêtre ou iconifiée/déiconifiée, il faut que le système graphique puisse avertir l'application qu'elle doit rafraîchir son affichage. Il faut aussi que l'utilisateur ait écrit une méthode capable de redessiner toute la partie de l'affichage qui doit être rafraichie lorsque le système graphique le demande. Ce n'est pas le cas en JavaFX. Pour que la programmation de l'interface soit plus simple, JavaFX masque le processus de rafraichissement et utilisant essentiellement deux techniques :

La seconde technique, l'utilisation d'un Canvas est généralement moins efficace : lorsque le nombre de pixels constituant un canvas est grand, le calcul des lignes tracées par les primitives de dessin et la recopie d'une portion du Canvas dans la fenêtre affichée à l'écran sont lents.

Malgré ses inconvénients, nous utiliserons la technique à base de Canvas pour dessiner l'ensemble de notre jeu à l'écran. La motivation principale est qu'en utilisant un Canvas, nous pourrons écrire un programme dont le style est très proche d'un moteur de rendu classique de jeu vidéo. En effet, un jeu vidéo est, classiquement, articulé autour d'une boucle qui répète les étapes suivantes :

Nous avons donc besoin d'un moyen de redessiner l'ensemble de ce qui doit être affiché à intervalles réguliers (idéalement toutes les 16,5ms, durée en dessous de laquelle la plupart des personnes ne perçoivent plus les saccades). C'est ce qu'offrent les bibliothèques de rendu à bas niveau comme OpenGl, DirectX, SDL ou LWJGL. Nous aurions pu envisager d'utiliser LWJGL qui est (beaucoup) plus efficace que JavaFX, mais nous l'avons écarté pour deux raisons :

Remarque culturelle : avec les progrès technologiques, les cartes graphiques modernes disposent de suffisamment de mémoire pour stocker en permanence le contenu de plusieurs fenêtres (par exemple sur une carte bas de gamme disposant de 1Go de mémoire, on peut stocker environ 128 fenêtres de 1920x1080 pixels). Cette mémoire est exploitée par JavaFX, elle permet à la fois d'accélérer le rendu et d'ajouter facilement des effets, comme la transparence, au contenu des fenêtres. Cela est d'ailleurs l'un des arguments de vente de JavaFX par rapport à Swing (l'interface traditionnelle de java).

Le programme ExempleJavaFX.java contient un petit exemple d'utilisation de JavaFX pour tracer des formes à l'écran. Vous pouvez y trouver les composants suivants :

En outre, ce programme d'exemple contient quelques appels de méthodes permettant d'effectuer des taches communes dans une application graphique : Si vous jouez un peu avec l'application, vous pourrez constater qu'en redimensionnant la fenêtre pour la rendre plus petite que le dessin qu'elle contient, vous perdez une partie de ce dessin. En effet, le contenu d'un canvas est stocké comme une image qui est découpée à la taille de celui-ci, si nous voulons que le contenu s'adapte, il faudra le redessiner après un redimensionnement.

Exercices Si vous suivez correctement toutes les indications précédentes, vous devriez avoir un dessin minuscule du niveau dans un coin de la fenêtre.

2 - Mise à l'échelle avec un décorateur

Nous avons conclu la partie précédente avec un problème de mise à l'échelle de notre dessin. Ce problème n'est pas simple à résoudre :

Pour résoudre notre problème, nous allons ajouter une fonctionnalité de zoom qui sera générique à tous nos composants graphiques.

Nous voulons donc un composant graphique redimensionable par homotétie par rapport à l'origine du repère. Cela correspondrait à une classe, que nous appellerons Etendeur, capable de stocker un facteur d'échelle et héritant de ComposantGraphique : ses dimensions sont alors les dimensions après application du facteur d'échelle. Nos composants, idéalement, doivent alors hériter de cet Etendeur, mais cela n'est pas très propre : ce sont eux qui implémentent les méthodes l et h mais il faudrait qu'ils appellent l'implémentation de l'Etendeur sur la valeur qu'ils retournent pour avoir un résultat mis à l'échelle. Le code n'est alors pas très bien encapsulé.

Pour résoudre ce problème, nous allons avoir recours à un design pattern appelé décorateur. Un décorateur permet d'ajouter des fonctionnalités à un objet existant sans passer par la spécialisation directe de la classe de cet objet. Un décorateur hérite bien de la classe de l'objet qu'il décore, mais il constitue un objet distinct, il est construit à partir d'une référence à l'objet qu'il décore. Pour implémenter les méthodes de la classe dont hérite le décorateur, il lui suffit d'appeler chacune des méthodes sur la référence à l'objet qu'il a reçu à sa construction : on dit qu'il délègue le traitement à un autre objet. L'avantage de passer par une délégation est qu'il est possible d'appliquer un traitement au résultat fourni par le délégué. Ici nous multiplierons les valeurs par le facteur d'échelle.

Exercices

3 - Pour finir ...

Complétez le dessin :