C.
RECANATI
IHM Java
Swing
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();
}
}