C. RECANATI

IHM Java Swing

DESS EID 2004/2005

 

 

 

 

 

TP n¡ 8

 

 

Exercice 1: (suite de l’exercice 1 du TP 7)

On va maintenant transformer le programme Sketcher pour en faire un logiciel de dessin. On va dessiner dans la fenêtre au lieu d’imprimer des infos sur les événements se produisant sur les menus. Le JTextArea va donc être remplacé par un JPanel qui captera les clics de souris et permettra de dessiner interactivement.

 

Pour réaliser ce logiciel, on va implémenter l’architecture Modèle/Vue introduite en cours en définissant les classes SketcherView et SketcherModel (cf. poly pp. 486-91).

 

Un modèle de dessin sera défini par la classe SketcherModel comme une liste d’éléments (des formes colorées) dessinés (cf. poly p. 491). La vue SketcherView gèrera l’affichage du modèle (cf. p. 490), et implémentera aussi la création interactive d’un nouvel élément (élément temporaire) à ajouter au modèle. Cette vue (un JPanel étendu) sera insérée dans le panneau de contenu de la fenêtre de l’application (cf. poly p. 487-88). Dans cet exercice, on va implanter la création interactive d’un nouvel élément de dessin dans la vue.

 

Mais avant d’implanter cette création, on va définir la classe Element permettant de définir les éléments du dessin.

 

 

F  Pour définir un élément, on va utiliser une forme (basée sur l’interface Shape de javax.swing) et une couleur. On va définir une classe abstraite Element et des classes imbriquées statiques implémentant cette classe abstraite dont une classe Ligne, Rectangle, etc. Ces classes concrètes d’éléments Ligne, Rectangle, etc.,  seront donc définies dans la classe abstraite Element comme sous-classes et accessibles en utilisant l’opérateur « . »  par Element.Ligne, Element.Rectangle, etc.

 

Les constructeurs des classes concrètes prendront 3 arguments : début et fin (de type Point), et couleur (de type Color). Il mémorisent la forme concrète créée dans un membre spécifique (Line2D.Double pour une Ligne ici, mais on aurait pu prendre Line2D.Float aussi bien) et cette forme pourra ensuite être récupérée avec getShape().

 

Voici le début du code de la classe abstraite Element :

 

// Element.java

//

 

import java.awt.*;

import java.awt.geom.*;

 

public abstract class Element {

 

    protected Color color; // un élément a une couleur

   

    public Element(Color color) {

        this.color = color;

    }

   

    public Color getColor() {

        return color;

    }

         // un élément a une forme  récupérable par getShape() :

    public abstract Shape getShape();

         // un élément a aussi un plus petit rectangle englobant

         // récupérable par getBounds() :

    public abstract java.awt.Rectangle getBounds();

         // + une méthode modify pour positionner l’élément :

    public abstract void modify(Point debut, Point fin);

 

 

         // on définit une sous-classe « concrète » de Element

         // accessible par Element.Ligne   

    public static class Ligne extends Element {

       

        private Line2D.Double  ligne;    // ça sera sa forme

       

        public Ligne(Point debut, Point fin, Color couleur){

            super(couleur);

            ligne = new Line2D.Double(debut, fin);

        }

       

        public Shape getShape() {

            return ligne;

        }

       

        public java.awt.Rectangle getBounds() {

            return ligne.getBounds();

        }

       

        public void modify(Point debut, Point fin) {

            ligne.x1 = debut.x;

            ligne.y1 = debut.y;

            ligne.x2 = fin.x;

            ligne.y2 = fin.y;

        }

       

    }

 

         //  code implémentant Element.Rectangle

 

         // code implémentant Element.Cercle

 

         // etc. pour tous les élements dessinables

}

 

La classe abstraite Element inclut un membre de type Color (avec un accesseur getColor() et un constructeur qui initialise ce membre) et déclare des méthodes abstract getShape() et abstract getBounds() retournant respectivement la forme correspondant à l’élément  (un rectangle, cercle, etc.) et son (plus petit) rectangle englobant ; le rectangle retourné par getBounds servira, lors de l’affichage d’un élément, à délimiter la zone impliquée par l’affichage (clip).

 

On définit également une méthode abstraite modify(Point debut, Point fin) permettant de modifier un élément en spécifiant ces deux nouvelles extrémités.

 

1) Implémentez la sous-classe Element.Rectangle en vous inspirant du code de Element.Ligne.

 

2) Architecture Modèle/Vue. Mettez en place l’architecture proposée en cours en reprenant Sketcher.java comme suit. Dans un premier temps, on va juste implémenter la création interactive d’un élément dans la vue, et laisser en commentaire ce qui concerne le modèle (cf. correction ci-après).

 

L’utilisateur va dessiner interactivement un élément temporaire avec la souris dans SketcherView.

 

On gèrera avec un MouseHandler qui étend MouseInputAdapter l’enchaînement des événements souris suivants : un premier clic de souris, suivi d’un déplacement de souris bouton enfoncé (drag) suivi du relâchement du bouton. Sur le clic : on enregistre le point de début du dessin (pour un élément temporaire tempElement). Sur le mouvement : 1. on enregistre le point du curseur comme fin de dessin de l’élément temporaire ; 2. on efface l’élément temporaire précédemment dessiné (s’il existe) et 3. on retrace le nouvel élément temporaire. Sur le relâcher, on remettra les variables en place pour une utilisation future et on stockera l’élément tracé dans le model.

 

Implémentez dans l’écouteur de souris de la vue le dessin interactif d’une ligne sans vous soucier dans un premier temps du stockage de l’élément dans le modèle.

 

F  1. Pour dessiner (ou effacer, c’est pareil !), on utilise la méthode générique draw d’un objet graphics2D en mode xor (utiliser setXorMode) . On mémorisera l’élément temporaire dans un membre de la classe MouseHandler ainsi que les points de début et de fin.

         2. Pour faire référence aux constantes qui identifient le type d’élément (LIGNE, RECTANGLE, etc.), on modifiera la classe SketcherView :

class SketcherView extends JPanel implements Observer, Constants ; 

 

3) Pour terminer l’architecture Modèle/Vue, on va maintenant implémenter le modèle et ajouter à SketcherFrame les méthodes getElementType() et getElementColor() permettant de récupérer le type et la couleur de l’élément temporaire à créer dans la vue.

 

Dans l’écouteur de souris de la vue, sur le relâchement du bouton de souris, on ajoute maintenant l’élément temporaire au modèle. (On peut ajouter une méthode createElement(debut, fin) comme méthode privée du MouseHandler).

 

Pour stocker les formes dans le modèle, on peut utiliser une LinkedList. (On dispose alors des procédures add et remove). On définira alors les méthodes remove et add permettant de retirer ou d’ajouter un élement au modèle.

 

A chaque modification du modèle, faire un SetChanged et un notifyObservers (en passant en argument le rectangle modifié pour qu’il serve ensuite de clip dans la méthode update de la vue, cf. poly). Définir également dans le modèle une méthode getIterator() retournant l’iterateur de la liste (faire simplement un getIterator sur la LinkedList) pour pouvoir la parcourir lors de l’affichage du modèle (méthode paintComponent de SketcherView).

 

Implanter également la méthode update de SketcherView.

 

Exercice 2: (AnimatorApplicationTimer.java)

On va créer une application faisant une animation en utilisant un timer. Conformément au squelette vu en cours, la classe de l’application étendra JFrame et implémentera ActionListener. L’animation consistera simplement à afficher un texte dans un JLabel indiquant le nombre d’animations (appels d’actions du timer) ayant eu lieu. Le nombre d’animations devant se produire par seconde (aps) sera passé en argument au programme java et utilisé pour initialiser le timer. Le constructeur du JFrame de l’application prendra en argument ce nombre et une chaîne de titre pour la fenêtre.

 

On spécifiera en outre un écouteur pour les événements sur la fenêtre (utiliser WindowAdapter) qui permettra d’arrêter l’animation quand la fenêtre est iconifiée et de la relancer quand la fenêtre réapparaît.

On quittera l’application si le window manager ferme la fenêtre. En outre, l’utilisateur pourra stopper ou redémarrer l’animation en cliquant sur le label (utiliser un MouseAdapter).

 

Pour gérer les déclenchements et arrêts de l’animation, on utilisera un booléen indiquant si l’animation est gelée ou non et deux procédures stopAnimation() et startAnimation() contrôlant le timer.

 

Exercice 3: (MovingImageTimer.java)

On va créer une application (ou une applet) permettant de bouger une image d’avant-plan représentant une fusée (à récupérer dans "~cathy/images/rocketship.gif") sur une image de fond représentant un ciel étoilé ("~cathy/images/starfield.gif"). Le principe du programme sera le même que celui de l’exercice précédent : utilisation d’un timer, arrêt et reprise de l’animation sur un clic de souris ou sur iconification de la fenêtre.

On simplifie le programme précédent en fixant à 10  le nombre d’animations par seconde.

1) Copier les fichiers indiqués dans un répertoire images situé dans votre répertoire courant.

Version applet. La classe de l’application étend JApplet et implémente ActionListener. On déclarera le nombre d’animations, un booléen pour savoir si l’animation est gelée, le timer et un panneau d’animation. Plus éventuellement des variables pour le nom des fichiers.

         1a) La procédure init() chargera les images d’arrière plan et d’avant-plan (bgImage et fgImage) avec getImage :

Image bgImage = getImage(getCodeBase(),"images/file.gif"));

puis appellera une procédure buildUI

buildUI(Container container, Image bgImage, Image fgImage) pour construire l’interface utilisateur. La procédure buildUI initialisera le timer et construira le panneau d’animation (une extension de JPanel ayant pour membres les deux images et une méthode paintComponent permettant d’afficher les deux images. On affichera d’abord l’image de fond au centre du panneau, puis l’image d’avant-plan en variant l’abscisse d’affichage selon l’indice du nombre d’animations courant).

Le panneau créé sera ajouté au conteneur et gèrera les clics de souris.

         1b) Les procédures start() et stop() de l’applet appelleront comme dans l’exemple du poly des procédures startAnimation() et stopAnimation() qui gèreront le timer.

 

2) Version application (ou version mixte Applet/Application). On garde l’architecture précédente (la classe d’application MyAppletClass étend JApplet) mais on rajoute un main qui initialise les images bgImage et fgImage avec

Image bgImage = Toolkit.getDefaultToolkit().getImage("images/file.gif");

On crée un JFrame et on initialise son contentPane en utilisant la méthode buildUI de notre classe d’application. On a donc quelque chose comme :

JFrame f = new JFrame("MovingImageTimer");

final MyAppletClass controller = new MyAppletClass();

controller.buildUI(f.getContentPane(), bgImage, fgImage);

On enregistre ensuite un écouteur sur le JFrame pour les événements sur les fenêtres, on affiche le JFrame puis on appelle controller.startAnimation().

Le fichier peut maintenant être utilisé soit pour une application, soit pour une applet.

 

Correction

 

//  exercice 2 : AnimatorApplicationTimer.java

// 

import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

 

/*

 * Un moule pour des applications d'animation.

 */

public class AnimatorApplicationTimer extends JFrame implements

                                                                                         ActionListener {

         int AnimationNumber = -1;

         Timer timer;

         boolean frozen = false;

         JLabel label;

 

          AnimatorApplicationTimer(int aps, String windowTitle) {

                  super(windowTitle);

                  int delay = (aps > 0) ? (1000 / aps) : 100;

 

                  //Initialise un timer qui appelle l'action handler de cet objet.

                  timer = new Timer(delay, this);

                  timer.setInitialDelay(0);

                  timer.setCoalesce(true);

 

                  addWindowListener(new WindowAdapter() {

                           public void windowIconified(WindowEvent e) {

                                    stopAnimation();

                           }

                           public void windowDeiconified(WindowEvent e) {

                                    startAnimation();

                           }

                           public void windowClosing(WindowEvent e) {

                                    System.exit(0);

                           } 

                  });

 

                  label = new JLabel("Animation     ", JLabel.CENTER);

                  label.addMouseListener(new MouseAdapter() {

                           public void mousePressed(MouseEvent e) {

                                    if (frozen) {

                                            frozen = false;

                                            startAnimation();

                                    } else {

                                            frozen = true;

                                            stopAnimation();

                                    }

                           }

                  });

 

                  getContentPane().add(label, BorderLayout.CENTER);

         }

 

         //Peut etre invoque par n'importe quel thread (puisque timer est thread-safe)

         public void startAnimation() {

                  if (frozen) {

                           //Ne rien faire.  L'utilisateur demande qu'on

                           //arrete de changer l'image.

                  } else {

                           //Commencer l'animation

                           if (!timer.isRunning()) {

                                    timer.start();

                           }

                  }

         }

 

         //Peut etre invoque par n'importe quel thread (puisque timer est thread-safe)

         public void stopAnimation() {

                  //Arrete le thread d'animation.

                  if (timer.isRunning()) {

                           timer.stop();

                  }

         }

 

         // l'action declenchee par le timer

         public void actionPerformed(ActionEvent e) {

                  //Incremente le nombre d'animations et l'affiche

                  AnimationNumber++;

                  label.setText("Animation " + AnimationNumber);

         }

 

         public static void main(String args[]) {

                  AnimatorApplicationTimer animator = null;

                  int aps = 10;

 

                  //Récupère le nombre d'animation par seconde (aps)

                  // passé sur la ligne de commande en argument

                  if (args.length > 0) {

                           try {

                                    aps = Integer.parseInt(args[0]);

                           } catch (Exception e) {}

                  }

                  animator = new AnimatorApplicationTimer(aps,

                                                                       "Animation avec Timer");

                  animator.setBounds(50,100,400,200);

                  animator.setVisible(true);

 

                  //OK pour commencer l'animation ici puisque

                  //startAnimation peut etre invoquee dans n'importe quel thread.

                  animator.startAnimation();

         }

}

 

 

 

//  exercice 3 : MovingImageTimer.java

//  (version Swing mixte Applet/Application)

// 

 

import javax.swing.*;

import java.awt.*;

import java.awt.event.*;

/*

 * Bouge une image d'avant plan (foreground)

 * devant une image de fond (background).

 */

public class MovingImageTimer extends JApplet implements

                                                                                ActionListener {

         int AnimationNumber = -1;

         boolean frozen = false;

         Timer timer;

         AnimationPane animationPane;

 

         static String fgFile = "images/rocketship.gif";

         static String bgFile = "images/starfield.gif";

 

         //Invoquée seulement quand on tourne en applet.

         public void init() {

          //Recupere les fichiers images dans le repertoire du fichier code.

                  Image bgImage = getImage(getCodeBase(), bgFile);

                  Image fgImage = getImage(getCodeBase(), fgFile);

                  buildUI(getContentPane(), bgImage, fgImage);

         }

       

         void buildUI(Container container,

                                    Image bgImage, Image fgImage) {

                  int aps = 10;

 

                  //Combien de millisecondes entre les animations

                  int delay = (aps > 0) ? (1000 / aps) : 100;

 

                  //Configure un timer qui appelle l'action handler de cet objet.

                  timer = new Timer(delay, this);

                  timer.setInitialDelay(0);

                  timer.setCoalesce(true);

 

                  animationPane = new AnimationPane(bgImage, fgImage);

                  container.add(animationPane, BorderLayout.CENTER);

 

                  animationPane.addMouseListener(new MouseAdapter() {

                           public void mousePressed(MouseEvent e) {

                                    if (frozen) {

                                            frozen = false;

                                            startAnimation();

                                    } else {

                                            frozen = true;

                                            stopAnimation();

                                    }

                           }

                  });

         }

 

         //Invoquée seulement avec un browser.

         public void start() {

                  startAnimation();

         }

 

         //Invoquée seulement avec un browser.

         public void stop() {

                  stopAnimation();

         }

 

         //Peut etre invoquée par n'importe quel thread

         public synchronized void startAnimation() {

                  if (frozen) {

                           // Ne rien faire. L'utilisateur a demandé

                           // d'arrêter de changer l'image.

                  } else {

                           // lancer l'animation

                           if (!timer.isRunning()) {

                                    timer.start();

                           }

                  }

         }

 

         // Peut être invoquée par n'importe quel thread

         public synchronized void stopAnimation() {

                  // Stoppe le thread d'animation.

                  if (timer.isRunning()) {

                           timer.stop();

                  }

         }

 

         public void actionPerformed(ActionEvent e) {

                  // Incrémente le nombre d'animations

                  AnimationNumber++;

 

                   // Réaffiche le pannneau d'animation

                  animationPane.repaint();

         }

 

         class AnimationPane extends JPanel {

                  Image background, foreground;

   

                  public AnimationPane(Image background,

                                                              Image foreground) {

                           this.background = background;

                           this.foreground = foreground;

                  }

   

                  // Peint le cadre d'animation courant.

                  public void paintComponent(Graphics g) {

                           super.paintComponent(g);

 

                           int compWidth = getWidth();

                           int compHeight = getHeight();

                           int imageWidth, imageHeight;

   

                           // Si on a des largeur et hauteur valides pour 

                           // l'image de fond, la dessiner au centre.

                           imageWidth = background.getWidth(this);

                           imageHeight = background.getHeight(this);

                           if ((imageWidth > 0) && (imageHeight > 0)) {

                                    g.drawImage(background,

                                                     (compWidth - imageWidth)/2,

                                                     (compHeight - imageHeight)/2, this);

                           }

   

                           // Si on a des largeur et hauteur valides pour 

                           // l'image d'avant-plan, la dessiner (selon AnimationNumber)

                           imageWidth = foreground.getWidth(this);

                           imageHeight = foreground.getHeight(this);

                           if ((imageWidth > 0) && (imageHeight > 0)) {

                                    g.drawImage(foreground,

                                                     ((AnimationNumber*5)

                                                              % (imageWidth + compWidth))

                                                               - imageWidth,

                                                     (compHeight - imageHeight)/2,

                                                     this);

                           }

                  }

         }

 

         // Invoquée seulement si on tourne comme application

         public static void main(String[] args) {

                  Image bgImage = Toolkit.getDefaultToolkit().getImage(

                                                              MovingImageTimer.bgFile);

                  Image fgImage = Toolkit.getDefaultToolkit().getImage(

                                                              MovingImageTimer.fgFile);

 

                  JFrame f = new JFrame("Timer déplaçant une image");

                  final MovingImageTimer controller = new

                                                                        MovingImageTimer();

                  controller.buildUI(f.getContentPane(), bgImage, fgImage);

 

                  f.addWindowListener(new WindowAdapter() {

                           public void windowIconified(WindowEvent e) {

                                                              controller.stopAnimation();

                           }

                           public void windowDeiconified(WindowEvent e) {

                                    controller.startAnimation();

                           }

                           public void windowClosing(WindowEvent e) {

                                    System.exit(0);

                           }

                  });

                  f.setSize(new Dimension(500, 125)); 

                  f.setVisible(true);

                  controller.startAnimation();

         }

}