DESS MINT
Programmation Orientée Objets, langage Java
2000-2001
Henri Garreta

Versions téléchargeables : archive zip, archive jar
Dernière mise à jour : 21 octobre 2000

 

TP sur les interfaces graphiques en Java :

EdiPol,
un éditeur de lignes polygonales

Les classes du paquet awt (abstract window toolkit)

 


L'objet de ce travail dirigé est la présentation des composants graphiques de Java à travers la construction, étape par étape, d'une application avec une interface-utilisateur graphique.

Les éléments montrés ici appartiennent au paquet awt, la plus ancienne des bibliothèques de composants graphiques de Java. Les versions actuelles du JDK (au-delà de 1.2.2, dite Java 2) offrent également les composants Swing, qui font partie de la bibliothèque JFC (Java Foundation Classes). Pour commencer, nous laisserons de côté ces composants, bien plus complexes.

L'application que nous construisons est un éditeur de lignes polygonales, EdiPol, qui, dans sa version achevée, se présentera sous la forme suivante :


Fig. 1 - EdiPol en état de marche

1. Le moins qu'on puisse faire

La plus "petite" application avec interface graphique que l'on peut écrire en Java comporte juste un cadre (Frame), c'est-à-dire une fenêtre de niveau supérieure avec un bord, une barre de titre, etc. Cela ne fait rien, mais c'est vivant : on peut lui changer la taille, la faire passer devant ou derrière les autres fenêtres, la réduire en icône, etc.


Fig. 2a - Pas grand-chose, mais c'est vivant

Voici un code source minimal (fichier EdiPol.java) :

import java.awt.*;

public class EdiPol {
    public static final int LARGEUR = 400;
    public static final int HAUTEUR = 250;

    public static void main(String args[]) {
        Frame cadre = new Frame("EdiPol (en chantier)");
        cadre.setSize(LARGEUR, HAUTEUR);        // [2]
        cadre.show();                           // [3]
    }
}

Notes

[1] Pour le moment, cette fenêtre est indestructible : la seule manière de terminer cette application consiste à "tuer" la machine Java (faire Ctrl-C dans la fenêtre où l'on a tapé la commande java EdiPol). Ce n'est qu'au n° 7 que nous serons suffisamment armés pour nous attaquer à ce petit point.

[2] Sans cela, la taille du cadre serait minimale : il se réduirait à la barre de titre !

[3] Un cadre ne se rend visible que lorsqu'on lui demande.

A propos des cadres

Le comportement d'un cadre (Frame) est défini par la réunion des comportements hérités de ses classes de base :

Décrivons rapidement le rôle de ces classes.

java.lang.Object
La racine de la hiérarchie des classes. Introduit des éléments fondamentaux du comportement commun à tous les objets, comme les méthodes clone(), equals(Object), toString(), etc.
java.awt.Component
Un composant est un objet qui a une représentation graphique : il peut être affiché sur l'écran et il peut interagir avec l'utilisateur. Exemples : les boutons, les cases à cocher, les ascenseurs, etc
Une caractéristique importante d'un composant est sa capacité à générer des événements qui, le plus souvent, reflètent des actions de l'utilisateur (à l'aide de la souris, du clavier, etc.)
java.awt.Container
Un conteneur est un composant qui peut contenir d'autres composants. Ces derniers sont ajoutés par des appels de la méthode add(Component c), puis maintenus dans une liste qui détermine leur ordre d'empilement.
Un conteneur est associé à un "gestionnaire de disposition" (LayoutManager), chargé de placer les composants dans la surface du conteneur.
java.awt.Window
Une fenêtre est un conteneur qui apparaît comme une fenêtre de niveau supérieur (par opposition à fenêtre fille, une fenêtre qui reste incluse dans, ou du moins subordonnée à, une autre fenêtre) sans bord ni barre de menus. Elle peut servir à l'implémentation d'un menu surgissant (menu contextuel).
Lorsqu'il est actif, un objet fenêtre bloque les entrées des autres applications.
java.awt.Frame
Un cadre est une fenêtre de niveau supérieur avec un titre et une barre de menus.
Chaque application possède un cadre (qui, en général, représente l'application auprès du système d'exploitation sous-jacent).

2. Introduisons notre propre classe Fenêtre

L'essentiel du travail de programmation à faire consiste à enrichir l'aspect et le comportement de notre cadre, qui est la partie visible de notre application. Les choses sont plus claires si on fait cela à travers une classe spécialement définie à cet effet, distincte de la classe de l'application. Le résultat visible sera le même :


Fig. 2b - Toujours pas grand-chose...

Fichier FenetreEdiPol.java (le comparer avec la version précédente de EdiPol.java) :

import java.awt.*;

public class FenetreEdiPol extends Frame {
    public static final int LARGEUR = 400;
    public static final int HAUTEUR = 250;

    public FenetreEdiPol(String titre) {
        super(titre);
        setSize(LARGEUR, HAUTEUR);
        show();
    }
}

Avec cela, le fichier EdiPol.java, qui ne bougera pratiquement plus au cours de ce développement, devient :

public class EdiPol {
    public static void main(String args[]) {
        new FenetreEdiPol("EdiPol (en chantier)");
    }
}  

3. Placement de composants dans la fenêtre

Nous allons commencer à placer des gadgets dans notre fenêtre. Le placement des composants (classe java.awt.Component) dans les conteneurs (classe java.awt.Container) est l'affaire des LayoutManager, qui se chargent : (1) du placement initial des composants, lors de l'appel de la méthode add, (2) le cas échéant, de donner une taille et une forme aux composants, en fonction de leur contenu et leur disposition et (3) du replacement des composants lorsque la taille ou la forme du conteneur change.

Avant de continuer, allez voir ici quelques expériences avec les LayoutManager.

Pour commencer, nous devons nous occuper de la structure générale de notre fenêtre. La plus grande partie, en haut, sera partagée horizontalement en une zone de dessin à gauche et un panneau "de commande" à droite. En bas, une étroite barre d'état affichera les coordonnées du pointeur et un message d'aide.

Pour y voir quelque chose, nous plaçons des composants standard arbitrairement colorés. Par la suite nous les remplacerons par les composants adaptés à nos besoins :


Fig. 12 - Structure générale de notre fenêtre

Code source :

public class FenetreEdiPol extends Frame  { 
    public static final int LARGEUR = 400; 
    public static final int HAUTEUR = 250; 

    private Canvas dessin;
    private Panel commandes, etat;
 
    public FenetreEdiPol(String titre) { 
        super(titre);
        setSize(LARGEUR, HAUTEUR);
        setBackground(Color.lightGray);

        dessin = new Canvas();
        dessin.setBackground(Color.magenta);

        commandes = new Panel();
        commandes.setBackground(Color.yellow);
        commandes.add(new Label("panneau de commande"));

        Panel p = new Panel();
        p.setLayout(new GridLayout(1, 2, 2, 0));
        p.add(dessin);
        p.add(commandes);
        add(p, BorderLayout.CENTER);

        etat = new Panel();
        etat.add(new Label("barre d'etat"));
        etat.setBackground(Color.cyan);
        add(etat, BorderLayout.SOUTH);

        show();
    } 
}

4. La barre d'état

Réglons le sort de notre barre d'état. C'est un panneau (donc une sous-classe de Panel) capable de mémoriser une position et un message d'aide (éventuellement vide), et de les (ré-)afficher dans des Label lorsqu'il y a lieu, c'est-à-dire :


Fig. 13. La barre d'état fonctionne

Voici la nouvelle classe (publique, car elle nous semble largement réutilisable) :

import java.awt.*; 

public class BarreEtat extends Panel { 
    private int x, y;    
    private String texte = "Je vous écoute";  
    private Label position, message; 

    public BarreEtat() { 
        setBackground(Color.lightGray); 
        setLayout(new GridLayout(1, 2)); 
        add(message = new Label()); 
        add(position = new Label()); 
        actualiser(); 
    }
 
    private void actualiser() { 
        position.setText("Position: (" + x + "," + y + ")"); 
        message.setText(texte);    
    }
 
    public void nouvellePosition(int x, int y) { 
        this.x = x; 
        this.y = y; 
        actualiser();    
    }
 
    public void nouveauMessage(String texte) { 
        this.texte = texte; 
        actualiser();    
    } 
}

et voici la partie modifiée de la classe FenêtreEdiPol  :

public class FenetreEdiPol extends Frame {   
    ... 
    private Canvas dessin; 
    private Panel commandes;    
    private BarreEtat etat;
 
    public FenetreEdiPol(String titre) { 
        ...
        etat = new BarreEtat(); 
        add(etat, "South");
        show(); 
    } 
}

5. La partie graphique (début)

Nous allons nous intéresser à la partie graphique de notre application. Pour commencer, nous en faisons une classe séparée (Croquis) avec un constructeur qui définit un fond et un pointeur spécifiques (remarquez comment le curseur change de forme lorsqu'il survole cette partie).

Au fur et à mesure qu'ils deviennent non triviaux, il faut que les composants puissent communiquer les uns avec les autres, pour se transmettre des données et des requêtes. Il faut donc que chaque composant ait des références sur ceux des autres composants avec lesquels il doit communiquer.

Dans notre cas, la FenetreEdiPol "connaît" son Croquis, puisque ce dernier est la valeur de la variable d'instance dessin. Inversement, pour que le Croquis "connaisse" sa FenetreEdiPol, cette dernière passe son adresse au constructeur de Croquis, afin que celui-ci la mémoriser dans une variable d'instance, appelée cadre (ce schéma de "présentations mutuelles" est fréquent).

Le constructeur de Croquis reçoit également la référence de la barre d'état, cela lui permettra de demander à cette dernière d'afficher la position de la souris.


Fig. 14 - Apparition du Croquis, la partie graphique

Code source (fichier Croquis.java) :

import java.awt.*;

public class Croquis extends Canvas {
    private FenetreEdiPol cadre;
    private BarreEtat etat;

    public Croquis(FenetreEdiPol cadre, BarreEtat etat) {
        this.cadre = cadre;
        this.etat = etat;
        setBackground(Color.white);
        setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
    }
}

Nouvelle programmation de la classe FenetreEdiPol :

public class FenetreEdiPol extends Frame {
    public static final int LARGEUR = 400;
    public static final int HAUTEUR = 250;
    private Panel commandes;
    private BarreEtat etat;

    private Croquis dessin;

    public FenetreEdiPol(String titre) {
        super(titre);
        setSize(LARGEUR, HAUTEUR);
        setBackground(Color.darkGray);
        setLayout(new BorderLayout(2, 2));

        etat = new BarreEtat();	 // ATTENTION, il faut avancer cette création
        dessin = new Croquis(this, etat);

        commandes = new Panel();
        commandes.setBackground(Color.yellow);
        commandes.add(new Label("panneau de commande"));

        Panel p = new Panel();
        p.setLayout(new GridLayout(1, 2, 2, 0));
        p.add(dessin);
        p.add(commandes);
        add(p, "Center");
        add(etat, "South");

        show();
    }
}

6. On commence à sentir la souris...

Nous allons faire en sorte que la barre d'état affiche la position de la souris, lorsque cette dernière est dans la partie graphique de notre application :


Fig. 15 - On détecte le mouvement de la souris

Auparavant, allez voir ici quelques explications sur le modèle des événements de Java.

Code source ; seule la classe Croquis change :

import java.awt.*; 
import java.awt.event.*; 

public class Croquis extends Canvas implements MouseMotionListener { 
    private FenetreEdiPol cadre; 
    private BarreEtat etat; 

    public Croquis(FenetreEdiPol cadre, BarreEtat etat) { 
        this.cadre = cadre;    
        this.etat = etat; 
        setBackground(Color.white); 
        setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));    
        addMouseMotionListener(this); 
    }
 
    public void mouseMoved(MouseEvent e) { 
        etat.nouvellePosition(e.getX(), e.getY()); 
    }
 
    public void mouseDragged(MouseEvent e) { 
        etat.nouvellePosition(e.getX(), e.getY()); 
    } 
}

Notez que la dernière ligne du constructeur de Croquis peut aussi s'écrire ainsi :

this.addMouseMotionListener(this);
c'est-à-dire : le composant s'enregistre donc lui-même comme étant l'auditeur des événements souris dont il est la source.

7. Fermeture de la fenêtre

Ayant commencé à nous intéresser aux événements, nous pouvons enfin nous occuper d'une question laissée en attente depuis le début de cet exercice : comment provoquer la terminaison du programme lorsque l'utilisateur clique sur la case de fermeture de la fenêtre, à l'extremité droite de la barre de titre ?

Il faut savoir qu'un tel clic produit l'événement windowClosing, il nous faut donc implémenter l'interface WindowListener. Or, cette dernière déclarant sept événements dont un seul nous intéresse, nous allons utiliser un adaptateur anonyme (au besoin, relisez les explications données ici).

Tout ce qui change se trouve dans le constructeur de FenetreEdiPol. Le voici :

import java.awt.event.*; 

public class FenetreEdiPol extends Frame {
    ...
    public FenetreEdiPol(String titre) { 
        ...
        addWindowListener(new WindowAdapter() { 
public void windowClosing(WindowEvent e) { System.exit(0); } });
show(); } ... }

Le résultat obtenu, la destruction immédiate de l'application, est un peu brutal. Il serait préférable qu'avant une telle destruction, une boîte de message demande à l'utilisateur si telle est vraiment son intention. Or, la bibliothèque Java n'offre pas des boîtes de message prêtes à l'emploi. Si cela vous intéresse, vous pouvez allez voir ici la définition d'un tel outil, et son emploi dans le cadre de notre application.

Attention, ne confondez pas l'événement windowClosing (l'utilisateur a demandé la fermeture de la fenêtre) avec l'événement windowClosed (la fenêtre a été fermée), qui n'a pas de sens sur une fenêtre principale.

8. On dessine (pour le moment, on s'y prend mal)

Puisque nous détectons la souris, nous pouvons envisager de nous en servir pour tracer des segments dans la partie graphique de notre application :


Fig 17 - A coups de clics, on trace une ligne polygonale

Mais où trouver l'opération tracer ? Réponse : dans la classe abstraite Graphics, qui est la super-classe de toutes les classes qui permettent à une application de dessiner sur un composant. Un objet d'une sous-classe de Graphics s'appelle un contexte graphique ; il encapsule des informations comme :

On ne construit jamais directement un contexte graphique ; au lieu de cela :

La classe Graphics offre toute une panoplie d'opérations de dessin : drawLine, drawRect, drawOval, etc. Voici donc une première version dessinatrice de notre application ; seule la classe Croquis est concernée :

public class Croquis extends Canvas implements MouseMotionListener  { 
    private FenetreEdiPol cadre; 
    private BarreEtat etat; 
    private int xPrec = -1, yPrec; 

    public Croquis(FenetreEdiPol cadre, BarreEtat etat) { 
        this.cadre = cadre; 
        this.etat = etat; 
        setBackground(Color.white);
        setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); 
        addMouseMotionListener(this);    
        addMouseListener(new MouseAdapter() { 
            public void mousePressed(MouseEvent e) { 
                clicSouris(e.getX(), e.getY()); 
            } 
        }); 
    } 

    private void clicSouris(int x, int y) { 
        if (xPrec >= 0) { 
            Graphics g = getGraphics(); 
            g.drawLine(xPrec, yPrec, x, y); 
        }
        xPrec = x; 
        yPrec = y; 
    } 
    ...
}

Notre application semble satisfaisante, mais ce n'est qu'une apparence. En effet, nous dessinons "à fond perdu" : nous traçons des segments mais, mis à part le dernier point acquis (nécessaire pour tracer le prochain segment), nous ne mémorisons rien. Si la fenêtre vient à être modifiée ou masquée le tracé sera partiellement ou totalement perdu :


Fig 18a - Un tracé masqué...


Fig 18b - ...est perdu !

9. La bonne manière de dessiner

Nous voyons qu'il ne suffit pas de tracer un segment une seule fois, lorsqu'on en connaît les extrémités ; au contraire, il faut être capable de le redessiner chaque fois que la présentation graphique n'est plus ce qu'elle devrait être. Pour cela, nous allons mémoriser la liste de points constituant la ligne polygonale. Chaque clic ajoutera un point à cette liste. Chaque fois qu'il le faudra, nous pourrons parcourir cette liste en traçant tous les segments.

Très naturellement, nous voyons se séparer les deux éléments fondamentaux d'une application avec une interface graphique :

Les données sont ici la liste de points représentant la ligne polygonale, la vue est le tracé que nous en faisons dans la fenêtre graphique (une application peut offrir plusieurs vues d'un même ensemble de données, ce sera le cas de la nôtre). La liste de points sera représentée par un objet Vector, variable d'instance privée de FenetreEdiPol, la classe qui représente l'application, avec une méthode nouveauPoint pour faire grossir la liste et une méthode parcours qui renvoie une "énumération" pour la parcourir (au besoin, allez voir ici des explications sur les énumérations)  :

import java.util.*;

public class FenetreEdiPol extends Frame {
    ...
    private Vector sommets = new Vector();
    ...
    public void nouveauPoint(int x, int y) {
        sommets.addElement(new Point(x, y));
        dessin.repaint();
    }
    public Enumeration parcours() {
        return sommets.elements();
    }
}

Dans la classe Croquis, la méthode clicSouris devient encore plus simple :

public class Croquis extends Canvas implements MouseMotionListener { ... private void clicSouris(int x, int y) { cadre.nouveauPoint(x, y); } }

Lorsque la vue est invalide, c'est-à-dire lorsqu'elle ne reflète pas les données qu'elle est censée faire voir, il faut la mettre à jour. Or, la vue peut devenir invalide pour des raisons extérieures à l'application, que cette dernière ne peut pas connaître (exemple : elle a été totalement ou partiellement recouverte par la fenêtre d'un autre programme) ou bien pour des raisons internes à l'application (exemple : le nombre ou la position des points ont changé).

Dans le premier cas c'est la machine Java qui détecte l'invalidité de la vue et qui commande la mise à jour des éléments endommagés, en appellant la fonction paint de chacun d'eux. Les composants à l'aspect prédéfini (labels, boutons, listes, etc.) prennent soin d'eux-mêmes, mais nous devons fournir la fonction paint correspondant à la partie graphique de notre application.

Dans le second cas, seule notre application peut savoir que la vue est devenue invalide, c'est donc elle qui doit programmer la réfection du dessin ; pour cela on appelle la méthode repaint qui finira par provoquer indirectement un appel de paint (si cela vous étonne, allez donc lire ici pourquoi on n'appelle pas directement paint) :

public class Croquis extends Canvas implements MouseMotionListener {
    ...
    public void paint(Graphics g) {
        Point a, b;
        Enumeration e = cadre.parcours();
        if (e.hasMoreElements()) {
            a = (Point) e.nextElement();
            while (e.hasMoreElements()) {
                b = (Point) e.nextElement();
                g.drawLine(a.x, a.y, b.x, b.y);
                a = b;
            }
        }
    }
}

Le comportement de notre application est maintenant correct :


Fig. 19a - Maintenant, les parties masquées...


Fig. 19b - ...sont restaurées quand c'est nécessaire

10. Apparition des menus

La partie graphique étant devenue satisfaisante, nous allons ajouter à notre application d'autres moyens d'agir sur les données : menus, boutons, listes, etc. Auparavant, allez donc visiter ici la panoplie de composants offerts dans le paquet awt.

Nous allons définir un menu permettant de choisir la couleur :


Fig. 20a - Un menu pour la couleur

Pour commencer, il faut ajouter la couleur à notre classe FenetreEdiPol :

public class FenetreEdiPol extends Frame {
    ...
    final String sCouleur[] = { "Noir",      "Rouge",     "Vert",      "Bleu" };
    final Color  cCouleur[] = { Color.black, Color.red,   Color.green, Color.blue };
    final int nbrCouleurs = sCouleur.length;
    private int couleurCourante = 0;

    public int quelleCouleur() {
        return couleurCourante;
    }
    public void nouvelleCouleur(int i) {
        couleurCourante = i;
        BoiteMessage.message(this, "Couleur courante : " + sCouleur[couleurCourante], "");
    }
    ...
}

Bien entendu, la boîte de message qui annonce les changements de couleur est provisoire (les boîtes de message sont expliquées ici) :


Fig. 20b - (à titre provisoire)

Il y a aussi un ajout à faire au constructeur de FenetreEdiPol :

    public FenetreEdiPol(String titre) {
        ...
        setMenuBar(new BarreMenuEdiPol(this));
        ...
    }
}
Et voici notre classe BarreMenuEdiPol :
public class BarreMenuEdiPol extends MenuBar implements ActionListener {
    private FenetreEdiPol cadre;
    private MenuItem item[];

    public BarreMenuEdiPol(FenetreEdiPol cadre) {
        this.cadre = cadre;

        Menu m = new Menu("Couleur");
        m.addActionListener(this);
        add(m);

        item = new MenuItem[cadre.nbrCouleurs];
        for (int i = 0; i < cadre.nbrCouleurs; i++) {
            item[i] = new MenuItem(cadre.sCouleur[i]);
            item[i].addActionListener(this);
            m.add(item[i]);
        }
        item[cadre.quelleCouleur()].setEnabled(false);
    }

    public void actionPerformed(ActionEvent e) {
        int i;
        for (i = 0; i < cadre.nbrCouleurs; i++)
            if (cadre.sCouleur[i].equals(e.getActionCommand()))
                break;
        item[cadre.quelleCouleur()].setEnabled(true);
        item[i].setEnabled(false);
        cadre.nouvelleCouleur(i);
    }
}

11. On colorie la ligne polygonale

Décidons que chaque point sera désormais affecté d'une couleur, et que chaque segment composant une ligne polygonale aura la couleur de son extrémité :


Fig 21 - Avec la couleur c'est plus gai !

Voici la classe PointColoré :

public class PointColore extends Point {
    public Color couleur;

    public PointColore(int x, int y, Color couleur) {
        super(x, y);
        this.couleur = couleur;
    }
}
Le nombre d'autres choses à changer dans notre programme est étonamment réduit. Dans la classe FenetreEdiPol, une seule ligne :
public void nouveauPoint(int x, int y) {
    sommets.addElement(new PointColore(x, y, cCouleur[couleurCourante]));
    dessin.repaint();
}
Dans la classe Croquis, guère plus :
public void paint(Graphics g) {
    Enumeration e = cadre.parcours();
    if (e.hasMoreElements()) {
        Point a = (Point) e.nextElement();
        while (e.hasMoreElements()) {
            PointColore b = (PointColore) e.nextElement();
            g.setColor(b.couleur);
            g.drawLine(a.x, a.y, b.x, b.y);
            a = b;
        }
    }
}

12. Rattraper ses erreurs

Pouvoir créer des points, c'est bien. Mais ce serait encore mieux si on pouvait modifier, voire supprimer, les points créés.

Cela tombe bien, car c'est justement le moment de placer des gadgets dans le "panneau des commandes". Commençons par quelque chose de simple : un bouton qui supprime le dernier point placé :


Fig. 22 - Un bouton pour supprimer des points

Voici la nouvelle classe PanneauCommandes :

public class PanneauCommandes extends Panel implements ActionListener {
    private FenetreEdiPol cadre;
    private final String SUPPR = "Suppr.";

    public PanneauCommandes(FenetreEdiPol cadre) {
        this.cadre = cadre;
        setBackground(Color.lightGray);

        Button bouton = new Button(SUPPR);
        bouton.addActionListener(this);
        add(bouton);
    }

    public void actionPerformed(ActionEvent e) {
        cadre.supprimerPremier();
    }
}

Il y a quelques changement à faire dans la classe FenetreEdiPol. De plus, nous faisons en sorte que la barre d'état affiche le nombre de points existants, c'est plus rassurant :

public class FenetreEdiPol extends Frame {
    ...
    public FenetreEdiPol(String titre) {
        ...
        commandes = new PanneauCommandes(this);
        ...
    }

    public void supprimerPremier() {
        if ( ! sommets.isEmpty()) {
            sommets.removeElementAt(0);
            dessin.repaint();
            etat.nouveauMessage("Nombre de points " + sommets.size());
        }
    }

    public void nouveauPoint(int x, int y) {
        ...
        etat.nouveauMessage("Nombre de points " + sommets.size());
    }
}

13. Déplacer les points, I

Notre application aura désormais deux modes. En mode création, la souris servira toujours à créer de nouveaux points ; en mode modification, elle servira à déplacer des points déjà créés.

Pour commencer, il nous faut ajouter deux boutons "radio" (c'est-à-dire mutuellement exclusifs) au panneau de commande :


Fig. 23 - Deux états possibles

Il y a des changements à faire dans le constructeur du panneau de commande :

public class PanneauCommandes extends Panel implements ActionListener {
    private FenetreEdiPol cadre;
    private final String SUPPR = "Suppr.";
    private final String CREAT = "Création";
    private final String MODIF = "Modification";

    public PanneauCommandes(FenetreEdiPol cadre) {
        Checkbox cCreat, cModif;

        this.cadre = cadre;
        setBackground(Color.lightGray);

        Button bouton = new Button(SUPPR);
        bouton.addActionListener(this);

        CheckboxGroup g = new CheckboxGroup();
        cCreat = new Checkbox(CREAT);
        cCreat.setCheckboxGroup(g);
        cModif = new Checkbox(MODIF);
        cModif.setCheckboxGroup(g);
        cCreat.setState(true);

        Panel p, q1, q2;
        p = new Panel();
        add(p);
        p.setLayout(new GridLayout(1, 2));

        p.add(q1 = new Panel());
        q1.add(bouton);

        p.add(q2 = new Panel());
        q2.setLayout(new GridLayout(2, 1));
        q2.add(cCreat);
        q2.add(cModif);
    }
    ...
}

C'est passablement compliqué, essayons de comprendre comment cela marche. Après avoir créé les trois composants visibles, le bouton et les deux boîtes à cocher exclusives (l'objet invisible g, de type CheckBoxGroup, assure l'exclusion mutuelle), on les place en empilant des panneaux (Panel) qui ne sont là que pour supporter des gestionnaires de disposition (LayoutManager) :

14. Déplacer les points, II

Afficher les bonnes cases à cocher ne suffit pas. Il faut aussi gérer les modes de notre application, et recupérer les événements produits par ces nouvelles cases à cocher. De plus, la barre d'état donnera un conseil en accord avec le mode.

Dans la classe PanneauCommandes :

public class PanneauCommandes extends Panel implements ActionListener, ItemListener {
    ...
    private Checkbox cCreat;

    public PanneauCommandes(FenetreEdiPol cadre) {
        Checkbox cModif;
        ...
        cCreat.addItemListener(this);
        cModif.addItemListener(this);
        ...
    }

    public void itemStateChanged(ItemEvent e) {
        cadre.nouveauMode(cCreat.getState());
    }
}
Et dans la classe FenetreEdiPol :
public class FenetreEdiPol extends Frame {
    ...
    private boolean modeCreation;

    public FenetreEdiPol(String titre) {
        ...
        show();
        nouveauMode(true);
    }

    public void nouveauMode(boolean mode) {
        modeCreation = mode;
        if (modeCreation)
            etat.nouveauMessage("Montre la position du point à créer");
        else
            etat.nouveauMessage("Montre le point à déplacer");
    }

    public boolean quelMode() {
        return modeCreation;
    }
    ...
}

15. Déplacer les points, et III

Maintenant qu'on peut mettre notre programme dans un état "modification" nous devons faire en sorte qu'on puisse effectivement modifier des points, par exemple à l'aide de la souris. Pour cela nous aurons besoin d'une variable représentant "le point couramment sélectionné en vue de sa modification". C'est dans la classe FenetreEdiPol que cela se passe :

public class FenetreEdiPol {
    ...
    private Point selection = null;

    public void selection(Point p) {
        selection = p;
    }

    public Point quelleSelection() {
        return selection;
    }
    ...
}

Quelques autres modifications sont à faire dans la classe Croquis. La démarche est la suivante :

ce qui donne :
public class Croquis extends Canvas implements MouseMotionListener {
    ...
    public Croquis(FenetreEdiPol cadre, BarreEtat etat) {
        ...
        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                clicSouris(e.getX(), e.getY());
            }
            public void mouseReleased(MouseEvent e) {
                déclicSouris(e.getX(), e.getY());
            }
        });
        ...
    }

    private void clicSouris(int x, int y) {
        if (cadre.quelMode())
            cadre.nouveauPoint(x, y);
        else {
            Point p = lePlusProche(x, y);
            cadre.selection(p);
            if (p != null)
                etat.nouveauMessage("Traine ce point avec la souris");
        }
    }

    public void mouseDragged(MouseEvent e) {
        etat.nouvellePosition(e.getX(), e.getY());
        Point p = cadre.quelleSelection();
        if (p != null) {
            p.x = e.getX();
            p.y = e.getY();
            repaint();
        }
    }

    private void déclicSouris(int x, int y) {
        if (cadre.quelleSelection() != null)
            cadre.selection(null);
    }

    Point lePlusProche(int x, int y) {
        Point r = null;
        int distance = Integer.MAX_VALUE;
        Enumeration e = cadre.parcours();
        while (e.hasMoreElements()) {
            Point a = (Point) e.nextElement();
            int d = Math.abs(a.x - x) + Math.abs(a.y - y);
            if (d < distance) {
                distance = d;
                r = a;
            }
        }
        return r;
    }
}

16. Une deuxième vue des données

Ajoutons une vue textuelle, sous forme de liste, de notre ligne polygonale :

Les modifications à faire dans PanneauCommandes.java sont à peu près évidentes :

public class PanneauCommandes extends Panel implements ActionListener, ItemListener {
    ...
    private List liste;

    public PanneauCommandes(FenetreEdiPol cadre) {
        ...
        liste = new List(10);
        add(liste);
    }

    public void ajouter(PointColore p) {
        liste.add(p.toString());
    }

    public void enlever() {
        liste.remove(0);
    }

    public void reafficher() {
        liste.removeAll();
        Enumeration e = cadre.parcours();
        while (e.hasMoreElements()) {
            PointColore a = (PointColore) e.nextElement();
            liste.add(a.toString());
        }
    }
}

Ensuite, il faut détécter les endroits d'où appeler les fonctions précédentes. Dans FenetreEdiPol :

public void nouveauPoint(int x, int y) {
    PointColore p = new PointColore(x, y, cCouleur[couleurCourante]);
    sommets.addElement(p);
    ...
    commandes.ajouter(p);
}

public void supprimerPremier() {
    if ( ! sommets.isEmpty()) {
        ...
        commandes.enlever();
    }
}

public void selection(Point p) {
    selection = p;
    if (selection == null)
        commandes.reafficher();
}

(La fonction précédente s'explique par le fait que l'appel selection(null) est fait lorsque l'on vient de déplacer un point ; c'est alors qu'il fait réafficher la liste des coordonnées).

17. Saisie au clavier des coordonnées des points, I

Nous allons permettre de positionner un point au pixel près, en utilisant le clavier. Tout d'abord, ajoutons deux champs de saisie de texte (TextField) au panneau de commande, nous étudiarons leur comportement à la section suivante.

Profitons-en pour rearranger le constructeur de la classe PanneauCommandes, qui est devenu au fil de l'exercice passablement touffu :

public class PanneauCommandes extends Panel implements ActionListener, ItemListener {
    ...
    private TextField texteX, texteY;

    public PanneauCommandes(FenetreEdiPol cadre) {
        Panel haut, milieu, bas, q1, q2;

        this.cadre = cadre;
        setBackground(Color.lightGray);
        setLayout(new BorderLayout());

        add(haut   = new Panel(), BorderLayout.NORTH);
        add(milieu = new Panel(), BorderLayout.CENTER);
        add(bas    = new Panel(), BorderLayout.SOUTH);

                    // Le haut
        Button bouton = new Button(SUPPR);
        bouton.addActionListener(this);

        CheckboxGroup g = new CheckboxGroup();
        cCreat = new Checkbox(CREAT);
        cCreat.setCheckboxGroup(g);
        cCreat.addItemListener(this);
        Checkbox cModif = new Checkbox(MODIF);
        cModif.setCheckboxGroup(g);
        cModif.addItemListener(this);
        cCreat.setState(true);

        haut.add(q1 = new Panel());
        haut.add(q2 = new Panel());
        q1.add(bouton);
        q2.setLayout(new GridLayout(2, 1));
        q2.add(cCreat);
        q2.add(cModif);

                    // Le milieu
        liste = new List(10);

        milieu.add(q1 = new Panel());
        q1.add(liste);

                    // Le bas
        Label s;

        bas.add(q1 = new Panel());
        q1.setLayout(new GridLayout(2, 2));
        (s = new Label("X")).setAlignment(Label.CENTER);
        q1.add(s);
        (s = new Label("Y")).setAlignment(Label.CENTER);
        q1.add(s);
        q1.add(texteX = new TextField());
        texteX.setText("      ");
        q1.add(texteY = new TextField());
        texteY.setText("      ");
    }
    ...
}

18. Saisie au clavier des coordonnées des points, et II

Nous voulons que la sélection d'un point sur la liste des points produise l'affichage de ses coordonnées dans les cases X et Y.

Ensuite, nous voulons que la frappe de nombres dans ces cases produise le déplacement immédiat du point sélectionné (c'est bizarre du point de vue ergonomique, mais intéressant du point de vue de la gestion des événements). Pour mettre à jour la liste affichée, l'utilisateur doit cliquer dans la liste ou dans un autre composant.

Il y a très peu de modifications dans FenetreEdiPol : la liste de sommets devient publique et l'on donne au PanneauCommandes la référence du Croquis :

public class FenetreEdiPol extends Frame {
    public Vector sommets = new Vector(); 
    ...
    public FenetreEdiPol(String titre) {
        ...
        dessin = new Croquis(this, etat);
        commandes = new PanneauCommandes(this, dessin);
        ...
    } 
    ...
}

Les autres modifications sont dans PanneauCommandes :

public class PanneauCommandes extends Panel implements ActionListener, ItemListener {
    ...
    private List liste;
    private TextField texteX, texteY;
    private Croquis dessin;
    private int selectionDansListe;

    public PanneauCommandes(FenetreEdiPol cadre, Croquis dessin) {
        ...
        this.cadre = cadre;
        this.dessin = dessin;
        ...
                   // Le milieu
        liste = new List(10);
        liste.addItemListener(new SelectionListe());
        ...
        q1.add(texteX = new TextField());
        texteX.setText("      ");
        texteX.addTextListener(new ActionSurTexte());
        texteX.addFocusListener(new ActionSurTexte());
        q1.add(texteY = new TextField());
        texteY.setText("      ");
        texteY.addTextListener(new ActionSurTexte());
        texteY.addFocusListener(new ActionSurTexte());
    }

    class SelectionListe implements ItemListener {
        public void itemStateChanged(ItemEvent e) {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                selectionDansListe = ((Integer) e.getItem()).intValue();
                Point p = (Point) cadre.sommets.elementAt(selectionDansListe);
                cadre.selection(p);
                texteX.setText(String.valueOf(p.x));
                texteY.setText(String.valueOf(p.y));
            }
        }
    }

    class ActionSurTexte implements TextListener, FocusListener {
        public void textValueChanged(TextEvent e) {
            if (texteX.getText().length() != 0 && texteY.getText().length() != 0) {
                Point p = cadre.quelleSelection();
                try {
                    p.x = Integer.parseInt(texteX.getText());
                    p.y = Integer.parseInt(texteY.getText());
                }
                catch (Exception w) { }
                dessin.repaint();
            }
        }
        public void focusLost(FocusEvent e) {
            reafficher();
        }
        public void focusGained(FocusEvent e) {
        }
    }
}