C. Recanati
IG Java
MI1, 2009

TP 10

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 deux classes SketcherView et SketcherModel (cf. poly).
Un modèle de feuille de dessin sera défini par la classe SketcherModel comme une liste d’éléments (des formes colorées). La vue SketcherView gèrera l’affichage du modèle mais aussi l’interaction de l’utilisateur avec le logiciel (la partie « contrôleur »). On va donc implanter dans SketcherView la création d’un nouvel élément (temporaire du point de vue interactif), puis son ajout au modèle à la fin de l’interaction de l’utilisateur avec la souris. Cette « vue » (ici un JPanel étendu) sera insérée dans le panneau du cadre de l’application, comme indiqué dans le poly.

Exercice 1:
On veut implanter ici la « création » interactive d’un nouvel élément de dessin dans la vue. Mais avant d’implanter cette partie interactive, on va définir la classe abstraite Element permettant de définir les différentes formes d’éléments d’une feuille de dessin. 

Pour définir un élément, on a besoin de deux champs : une forme (basée sur l’interface Shape de javax.swing) et une couleur. On va ici définir une classe abstraite Element, avec des sous-classes internes statiques qui implémentent cette classe abstraite (une sous-classe Ligne, une sous-classe Rectangle, etc.). Ces classes « concrètes » d’éléments (Ligne, Rectangle, etc.) -  seront donc définies dans la classe abstraite Element comme classes internes, et accessibles dans le code avec l’opérateur point « . ». On aura ainsi accès à la classe Ligne en écrivant Element.Ligne, à la classe Rectangle avec Element.Rectangle, etc. 

Les constructeurs de ces classes d’éléments prendront 3 arguments : un début et une fin (de type Point) pour positionner la forme, et un argument couleur (de type Color). Ces constructeurs « mémoriseront » la forme concrète créée dans un membre privé avec un type spécifique (ex : Line2D.Double pour une Ligne). Un objet de type Shape pourra néanmoins ensuite être retourné par l’accesseur getShape(), en « castant » la valeur de ce champ privé.

// Element.java

import java.awt.*;
import java.awt.geom.*;

public abstract class Element {

    protected Color color;	// un élément a tjs 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 Shape
        
        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;
        }
        
    }

	//  vient ensuite le code implémentant Element.Rectangle

	// vient ensuite le code implémentant Element.Cercle

	// etc. pour tous les types d’élements 
}

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 rectangle englobant. Ce rectangle retourné par getBounds servira, lors de l’affichage d’un élément, à limiter la zone impliquée par l’affichage. 

On définit également une méthode abstraite modify(Point debut, Point fin) permettant de positionner 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.

Exercice 2: Dans cet exercice, on va implémenter la création interactive d’un élément dans la vue (SketcherView.java ), et laisser en commentaire la partie concernant le modèle. L’utilisateur va ici dessiner interactivement un élément temporaire avec la souris dans SketcherView. 

On gèrera avec un MouseHandler qui étend MouseInputAdapter la réaction du logiciel à l’enchaînement des événements souris permettant de dessiner: un premier clic de souris, suivi d’un déplacement de souris bouton enfoncé (drag) suivi du relâchement du bouton. Ces événements vont permettre à l’utilisateur de dessiner interactivement un nouvel élément (ligne, rectangle, etc. selon le mode sélectionné). 
- Sur le clic : on enregistre le point de début du dessin (pour un élément temporaire tempElement). 
- Sur le mouvement : a. on efface l’élément temporaire précédemment dessiné; b. on enregistre le point du curseur courant comme extrémité du nouveau dessin; et c. on trace le nouvel élément temporaire. 
- Sur le relâcher, on réinitialise les variables pour une nouvelle utilisation (dessin d’un autre élément) et on stocke(ra) l’élément tracé dans le modèle.

  1. Pour dessiner (ou effacer, c’est pareil !), on utilise la méthode générique draw d’un objet Shape  de 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 pour pouvoir continuer le dessin d’événement souris en événement souris. 
	2. Pour faire référence aux constantes qui identifient le type d’élément (LIGNE, RECTANGLE, etc.), on modifiera la classe SketcherView pour qu’elle implémente l’interface  Constants.

Elements de correction
// SketcherView.java (exercice 2)
import javax.swing.*;
import javax.swing.event.*; 	
import java.util.*;		
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;

class SketcherView extends JPanel implements Observer, Constants {
	private Sketcher theApp;	// Objet Application

	public SketcherView(Sketcher theApp) {
		this.theApp = theApp;

             setBackground(Color.white); setOpaque(true);
                setBorder(BorderFactory.createLineBorder(Color.black));

                MouseHandler handler = new MouseHandler();
                addMouseListener(handler);
                addMouseMotionListener(handler);

	}
	public void update(Observable o, Object rectangle) {
		// code correspondant aux modifs du modèle
                if (rectangle == null)
                    repaint();
                else repaint( (Rectangle) rectangle);
	}
        public void paintComponent(Graphics g) {
        
            super.paintComponent(g); 
            Graphics2D g2D = (Graphics2D) g;
            Iterator elements = theApp.getModel().getIterator();
            Element element;
            
            while (elements.hasNext()) {
                element = (Element) elements.next();
                g2D.setPaint(element.getColor());
                g2D.draw(element.getShape());
            }
        }
        
        class MouseHandler extends MouseInputAdapter {
            private Point debut;	// position au clic
            private Point dernier;	// position en drag
            private Element tempElement ; 
			private Graphics2D g2D;	
            
            public void mousePressed (MouseEvent e) {
                // clic du bouton de souris
                debut = e.getPoint();
                System.out.println("premier clic");
                
                g2D = (Graphics2D) getGraphics();
                g2D.setPaint(theApp.getWindow().getElementColor());
                g2D.setXORMode(Color.white); 
			}
            
            public void mouseDragged (MouseEvent e) {
                // deplacement de souris
                dernier = e.getPoint();
                if (tempElement == null) {
                    tempElement = createElement(debut, dernier);
                }
                else { 
                    g2D.draw(tempElement.getShape());	
// effacer le precedent
                    tempElement.modify(debut, dernier); 
// modifier l'element
                }
                g2D.draw(tempElement.getShape()); 
// dessiner le nouveau
            }
            
            public void mouseReleased (MouseEvent e) {
                // relachement de la souris
                System.out.println("souris relachee");

                if (tempElement != null) {
                    theApp.getModel().add(tempElement);
                    tempElement = null;
                }
                if (g2D != null) {
                    g2D.dispose();
                    g2D = null;
                }
                debut = dernier = null;	
            }
            
            private Element createElement(Point debut, Point fin) {
                switch(theApp.getWindow().getElementType()) {
                    case LIGNE: 
					return new Element.Ligne(debut, fin, theApp.getWindow().getElementColor());
                    case RECTANGLE: 
					return new Element.Rectangle(debut, fin, theApp.getWindow().getElementColor());
                    case CERCLE: 
				return new Element.Cercle(debut, fin, theApp.getWindow().getElementColor());
                    case COURBE: 
				return new Element.Courbe(debut, fin, theApp.getWindow().getElementColor());
                }
                return null;
            }}}