Flame Maker – Etape 7 - Bâtisseur Flame observable, interface de modification affine

Introduction

Cette étape a deux buts :

  1. Rendre le bâtisseur de fractale observable, afin que l'interface graphique soit correctement mise à jour lorsqu'il est modifié.
  2. Mettre en page la totalité de l'interface qui permettra de modifier la partie affine de la transformation Flame actuellement sélectionnée. A noter que cette interface est pour le moment inactive, dans le sens où les clics sur les boutons sont sans effet puisqu'aucun auditeur ne leur est attaché. L'ajout de ces auditeurs sera le sujet de la prochaine étape.

Au terme de cette étape, la fenêtre principale de votre programme devrait ressembler à la copie d'écran ci-dessous.

images/stage7.png

L'interface graphique de cette étape

Bâtisseur de fractale observable

Le bâtisseur de fractale Flame (Flame.Builder) n'est actuellement pas observable. Cela pose problème pour l'interface graphique, car le composant qui affiche la fractale et celui qui affiche les transformations affines ne peuvent pas savoir quand se redessiner. Le premier but de cette étape est de corriger ce problème.

A noter qu'il ne serait pas suffisant d'observer les boutons qui ajoutent et suppriment des transformations Flame à la fractale, car la modification de cette dernière peut aussi se faire via l'interface de transformation affine décrite plus bas. Dès lors, il importe de rendre le bâtisseur de fractale lui-même observable.

Pour ce faire, une solution serait de modifier directement la classe Flame.Builder. Toutefois, afin de mieux séparer les parties du programme qui concernent l'interface graphique des autres, il vous est suggéré de procéder autrement.

La solution proposée est d'appliquer le patron Decorator pour offrir une version observable du bâtisseur. En d'autres termes, plutôt que de modifier directement la classe Flame.Builder, il faut définir une nouvelle classe, nommée p.ex. ObservableFlameBuilder, équipée des mêmes méthodes que Flame.Builder mais qui soit de plus observable.

Comme d'habitude, cela implique de définir une interface pour les observateurs, qui pourra être imbriquée dans ObservableFlameBuilder, puis de fournir des méthodes permettant l'ajout et la suppression d'un observateur. Finalement, il faut définir dans ObservableFlameBuilder les mêmes méthodes que dans Flame.Builder, en prenant garde à informer les observateurs à chaque changement du bâtisseur.

Notez que Flame.Builder et ObservableFlameBuilder n'ont pas de super-type commun (Object mis à part), contrairement à ce qui se passe normalement avec le patron Decorator. Cela ne pose pas de problème ici, car aucune partie du code n'a besoin d'accepter un objet qui soit de l'un de deux types.

Le bâtisseur observable défini, il faut encore l'utiliser en lieu et place de la version non-observable utilisée jusqu'ici. Une fois cela fait, le composant affichant la fractale et celui affichant les transformations affines peuvent ajouter des observateurs au bâtisseur. Ces observateurs ne font rien d'autre que demander le redessin du composant, au moyen de la méthode repaint.

Interface de modification affine

Pour modifier la partie affine de la transformation sélectionnée, on désire offrir la possibilité de la composer avec différentes transformations affines de base : translation, rotation, dilatation et transvection. L'interface permettant ces compositions se nomme l'interface de modification affine.

Le second but de cette étape est de faire la mise en page de cette interface relativement complexe. Dans un premier temps, aucune action n'est attachée aux auditeurs, et l'interface est donc inerte.

La copie d'écran ci-dessous présente l'interface en question. Elle est composée de quatre lignes, une par type de transformation. Chacune de ces lignes est formée des mêmes éléments qui sont, de gauche à droite :

  1. Une étiquette donnant le nom de la transformation (translation, rotation, etc.).
  2. Un champ textuel permettant de paramétrer la transformation.
  3. Une suite de 2 (pour la rotation) ou 4 (pour les autres transformations) boutons permettant de modifier la transformation Flame actuellement sélectionnée, en composant sa partie affine avec la transformation décrite par l'interface.

images/affine-editor.png

L'interface de modification affine

Hiérarchie des conteneurs

Comme toujours, l'ajout de nouveaux composants graphiques implique l'ajout de conteneurs à la hiérarchie. La figure ci-dessous montre la hiérarchie suggérée pour cette étape, et comme d'habitude les parties réalisées lors d'étapes précédentes sont grisées ou omises pour alléger la présentation. La totalité des composants constituant l'interface de modification affine est placée dans le panneau d'édition de la partie affine, au sommet de l'image.

images/containment_hierarchy.png

Hiérarchie des conteneurs de cette étape avec leur gestionnaire de mise en page

Comme d'habitude, certains panneaux semblent inutiles à ce stade. En particulier, il y a redondance entre le panneau d'édition de la partie affine et le panneau d'édition de la transformation Flame sélectionnée. Encore une fois, cette situation est temporaire et les deux panneaux prendront tout leur sens dès l'interface complétée.

Etiquettes et boutons

Les étiquettes (JLabel) et les boutons (JButton) Swing ont déjà été décrits lors d'étapes précédentes auxquelles vous vous référerez en cas de problème.

Pour étiqueter les boutons, il est conseillé d'utiliser les caractères Unicode représentant différents types de flèches. Pour faciliter votre travail, ces caractères sont regroupés ci-dessous et il ne vous reste plus qu'à les copier/coller dans votre projet :

← → ↑ ↓ ⟲ ⟳ ↔ ↕

Champs textuels

Swing offre le composant JTextField pour modéliser les champs textuels simples. Le contenu de tels champs est une chaîne de caractères de type String. Cela ne convient pas tout à fait à l'interface de modification affine, puisque tous ses champs contiennent des nombres réels. (Pour être précis, les champs contiennent la représentation textuelle de nombres réels.)

Heureusement, Swing offre un autre type de champs textuel, nommé champ textuel formaté et modélisé par la classe JFormattedTextField. Ces champs peuvent contenir une valeur quelconque qui est automatiquement transformée en texte (et inversément) au moyen d'un objet de formatage attaché au champ. Dans certains cas, il peut être nécessaire de spécifier l'objet de formatage à utiliser, mais ce n'est pas le cas lorsqu'on stocke des nombres dans le champ comme dans ce projet.

La valeur contenue dans un champ formaté peut lui être passée lors de sa création ou via la méthode setValue. Cette valeur peut ensuite être extraite au moyen de la méthode getValue. Notez que, pour des raisons historiques, la classe JFormattedTextField n'est pas générique comme elle devrait l'être, et les méthodes getValue et setValue manipulent donc des valeurs de type Object. Cela implique de faire des transtypages dans certains cas.

Les champs textuels alignent leur contenu à gauche par défaut. Il est généralement préférable d'aligner les nombres à droite, ce qui peut se faire en appelant la méthode setHorizontalAlignment à laquelle on passe la constante SwingConstants.RIGHT.

Référence : How to Use Formatted Text Fields

Mise en page

La mise en page des composants de l'interface de modification affine est relativement complexe. En effet, ces composants doivent être alignés entre-eux sans avoir la même taille, ce qui exclut l'utilisation du gestionnaire GridLayout. Il est donc nécessaire d'utiliser un gestionnaire de mise en page plus sophistiqué—et complexe—que ceux examinés jusqu'à présent, nommé GroupLayout.

Ce gestionnaire aligne les composants par groupes horizontaux et verticaux, qui peuvent être imbriqués. La figure ci-dessous illustre les groupes à utiliser pour l'interface de modification affine. Les sept groupes dont l'étiquette est ou commence par H sont ce que GroupLayout nomme des groupes horizontaux (sic) tandis que les cinq groupes dont l'étiquette est ou commence par V sont des groupes verticaux (sic).

Les groupes H1 à H6 sont englobés dans un plus grand groupe étiqueté H, et il en va de même des groupes V1 à V4, englobés dans V.

images/alignment.png

Groupement des composants de l'interface de modification affine

Tout composant doit absolument appartenir à la fois à un groupe horizontal et à un groupe vertical. Ainsi, dans la figure ci-dessus, le champ textuel formaté paramétrant la rotation—et contenant la valeur 15—appartient à la fois au groupe vertical V2 et au groupe horizontal H2.

Un groupe, indépendamment de son orientation, est soit séquentiel soit parallèle. Les éléments appartenant à un groupe séquentiel sont mis en page l'un après l'autre, avec un petit espacement entre chacun, tandis que les éléments appartenant à un groupe parallèle occupent la même position. Dans l'image ci-dessus, les groupes étiquetés H1 à H6 et V1 à V4 sont tous parallèles, tandis que les groupes H et V qui les englobent sont séquentiels. Cela peut sembler contre-intuitif, mais est cohérent avec l'orientation—elle aussi contre-intuitive—des groupes.

Par exemple, le groupe V1 est un groupe vertical parallèle. Cela signifie que tous les éléments qu'il contient, à savoir les composants relatifs à la translation, occupent la même position verticale. Ces mêmes composants font partie chacun d'un groupe horizontal différent (H1 à H6), eux-mêmes regroupés dans le groupe séquentiel H. Donc ils occupent des positions horizontales successives.

Les éléments d'un groupe parallèle peuvent être alignés à gauche/en haut (par défaut), à droite/en bas, au centre/au milieu ou sur ce que l'on nomme en typographie la ligne de base (baseline en anglais), c-à-d la ligne sur laquelle la plupart des lettres reposent. Dans l'interface de modification affine, les groupes V1 à V4 sont alignés sur la ligne de base, le groupe H1 est aligné à droite et tous les autres sont alignés à gauche/en haut.

Une fois les concepts ci-dessus assimilés, il est relativement simple d'utiliser le gestionnaire GroupLayout. Pour le configurer, il faut lui spécifier un groupe horizontal et un groupe vertical de base à utiliser, au moyen des méthodes setHorizontalGroup et setVerticalGroup. Pour l'interface de modification affine, ces groupes sont les deux groupes H et V qui contiennent tous les autres.

A noter qu'avec le gestionnaire GroupLayout, il n'est pas nécessaire d'ajouter explicitement les composants au conteneur via la méthode add! Cela se fait implicitement lorsque ces composants sont ajoutés aux groupes horizontaux et verticaux de base.

La construction des groupes eux-mêmes se fait au moyen des méthodes createSequentialGroup et createParallelGroup. Cette dernière méthode possède une variante acceptant une contrainte d'alignement, qui peut être TRAILING (à droite/en bas), BASELINE (ligne de base) ou d'autres valeurs non utilisées ici.

L'ajout de composants à un groupe se fait au moyen de la méthode addComponent, tandis qu'un sous-groupe peut être ajouté au moyen de la méthode addGroup. Ces méthodes retournent le groupe auquel on les a appliquées, ce qui permet de chaîner les appels. Pour un exemple, consultez les références ci-dessous.

Référence : How to Use GroupLayout et A GroupLayout Example

Résumé

Pour cette étape, vous devez :

  1. Créer un décorateur qui rende le bâtisseur de fractale Flame observable et l'utiliser en lieu et place du bâtisseur actuel.
  2. Faire en sorte que les composants affichant la fractale et les parties affines des transformations observent ce nouveau bâtisseur et se redessinent lorsqu'il change d'une manière ou d'une autre—en particulier lorsqu'une transformation est ajoutée ou supprimée de la fractale au moyen des boutons ajoutés lors de l'étape précédente.
  3. Modifier la hiérarchie de conteneurs pour y ajouter le panneau d'édition de la transformation Flame sélectionnée et le panneau d'édition de la partie affine de cette transformation.
  4. Créer les composants de l'interface de transformation affine et les mettre en page au moyen d'un gestionnaire de type GroupLayout.
Michel Schinz – 2013-04-29 17:47