Tutoriel sur Java2D pour le développement d'une interface de gestion de maillage 2D

Cet article présente la mise en place en Java d'un panneau graphique personnalisé hérité du composant Swing JPanel.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Nombre de questions reviennent sur les forums afin de savoir comment dessiner un graphe en Java, quelle API utiliser… Ce tutoriel permet de poser les bases de conception d'un composant graphique de type JPanel personnalisé. Personnalisé au sens où tout l'affichage est à définir par le programmeur afin d'avoir le rendu désiré par l'utilisateur : couleurs, éléments à afficher, interaction utilisateur…

Le composant graphique sera tout d'abord créé par héritage de la classe Swing JPanel. Un gestionnaire d'éléments graphiques sera ensuite défini afin de gérer l'affichage des différents éléments. Les éléments graphiques seront alors définis dans différentes classes (modèle, point, ligne…). Un exemple d'application final permettra d'observer le comportement de notre composant graphique en situation d'utilisation.

Ce tutoriel s'intègre dans une série d'articles basés sur le développement d'un logiciel de modélisation éléments finis 2D nommé PFEM2D. Les différentes classes porteront donc ce préfixe dans la suite de l'article.

Dans le cadre de PFEM2D, l'interface visuelle nécessite l'affichage du modèle en 2D, avec un quadrillage vertical et horizontal afin de positionner les différents éléments. Une interaction avec la souris est également nécessaire afin de déplacer et zoomer l'affichage.

L'image suivante montre un rendu du composant graphique personnalisé, intégré dans l'application PFEM2D :

Image non disponible

Dans la suite de cet article, le code source est présenté au plus proche de la description technique des diverses fonctionnalités du code final. Le code affiché peut donc n'être qu'un extrait partiel d'une classe particulière.
Le code source complet de l'article est disponible en téléchargement au chapitre VIConclusion.

II. Définition du panneau graphique

II-A. Surcharge de JPanel : la classe PFEM2DGuiPanel

La méthode de base pour créer un composant graphique est de s'appuyer sur celui qui s'en rapproche le plus dans la bibliothèque de composants Swing, et de surcharger sa méthode paintComponents.

Dans notre cas, il s'agit du composant JPanel. Le composant graphique créé portera le nom de PFEM2DGuiPanel.

La méthode paintComponents est surchargée afin de dessiner :

  • le fond en noir ;
  • les axes X et Y du repère 2D en trait plein vert ;
  • les quadrillages vertical et horizontal en trait pointillé vert ;
  • les éléments graphiques, avec leur affichage personnalisé.

L'interaction avec la souris est ajoutée au composant PFEM2DGuiPanel en ajoutant des listeners MouseListener et MouseMotionListener.

Afin d'améliorer l'expérience utilisateur, deux méthodes seront ajoutées :

  • une méthode permettant d'effectuer une capture d'écran de l'affichage du composant PFEM2DGuiPanel (pratique pour créer les images de cet article !) ;
  • une méthode permettant de centrer l'affichage sur les éléments à afficher.

La trame de base de la classe PFEM2DGuiPanel est présentée ci-après :

Classe PFEM2DGuiPanel
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
package plegatfem2d;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

/**
 * Classe définissant un JPanel personnalisé.
 * Ce JPanel permet l'affichage d'une grille 2D (axes X et Y, quadrillages horizontal et vertical)
 * ainsi que d'objets graphiques 2D définis par le programmeur.
 * L'affichage est interactif via la souris, l'utilisateur peut ainsi déplacer la vue, ainsi que zoomer
 * sur une zone particulière.
 * 
 * @author Jean-Michel BORLOT
 */
public class PFEM2DGuiPanel extends JPanel implements MouseListener, MouseMotionListener {

    /**
     * Constructeur.
     */
    public PFEM2DGuiPanel() {
        super();

        this.mode = VIEW;

        this.offsetX = 0;
        this.offsetY = 0;

        this.echelle = ECHELLE_BASE;

        this.addMouseListener(this);
        this.addMouseMotionListener(this);
    }
    
    private PFEM2DObjectManager pom;            // gestionnaire d'objets graphiques
    private int xold, yold;                     // coordonnées du point local précédent
    private int xstart, ystart;                 // coordonnées du point local initial
    private int mode;                           // mode d'interaction : VIEW, DRAG ou SCALE
    private double offsetX, offsetY;            // décalage de la vue
    private double echelle;                     // échelle de la vue
    private static int VIEW = 0;        
    private static int DRAG = 1;
    private static int SCALE = 2;
    private static double ECHELLE_BASE = 100.;  // échelle de base
    private static double stepX=100, stepY=100; // pas des quadrillages sens X et sens Y

    /**
     * Renvoie le gestionnaire d'objets graphiques
     * @return le gestionnaire d'objets graphiques
     */
    public PFEM2DObjectManager getPom() {
        return pom;
    }

    /**
     * Définit le gestionnaire d'objets graphiques
     * @param pom le gestionnaire d'objets graphiques
     */
    public void setPom(PFEM2DObjectManager pom) {
        this.pom = pom;
    }
}

Dans ce code, les propriétés ECHELLE_BASE, stepX et stepY sont définies « en dur », et ne font pas l'objet de modifications ultérieures. Dans le cas où l'utilisateur doit avoir la possibilité de modifier ces valeurs, il conviendra d'intégrer les getter et setter nécessaires.

II-A-1. Conversion coordonnées réelles - coordonnées locales

La fonction primordiale de la classe PFEM2DGuiPanel est d'afficher à l'écran les objets que l'on souhaite dessiner. Pour cela, il est nécessaire de faire une conversion entre les coordonnées réelles des objets, vers les coordonnées de la zone d'affichage sur l'écran (ou « locales »), et réciproquement. Quatre méthodes permettent de faire ces conversions :

  • getLocalCoordX, pour la conversion d'une abscisse réelle vers l'abscisse locale ;
  • getLocalCoordY, pour la conversion d'une ordonnée réelle vers l'ordonnée locale ;
  • getRealCoordX, pour la conversion d'une abscisse locale vers l'abscisse réelle ;
  • getRealCoordY, pour la conversion d'une ordonnée locale vers l'ordonnée réelle.

Afin de mieux comprendre le processus de conversion, observons le schéma suivant :

Image non disponible

Le rectangle bleu représente la zone qui est affichée à l'écran. Son origine est localisée au coin haut/gauche, l'axe X local positif est orienté vers la droite, l'axe Y local positif vers le bas.

Son centre est décalé par rapport à l'origine du repère réel de offsetX suivant l'axe X, et de offsetY suivant l'axe Y.

Le signe des valeurs offsetX et offsetY est défini relativement au repère local, c'est-à-dire qu'une valeur positive décale la zone bleue respectivement vers la gauche et vers le bas.

La procédure de conversion « repère réel vers repère local » est très simple :

  • on part de la valeur de coordonnée dans le repère réel ;
  • on multiplie par un facteur échelle/ECHELLE_BASE ;
  • on multiplie par un facteur -1 si on calcule une valeur Y, afin de réorienter l'axe vers le bas ;
  • on ajoute respectivement W/2 ou H/2 suivant que l'on calcule une valeur X ou Y, afin de positionner l'origine au centre de la zone d'affichage ;
  • on ajoute respectivement le décalage offsetX ou offsetY suivant que l'on calcule une valeur X ou Y, afin de décaler l'origine à sa position réelle dans le repère local.

La procédure de conversion inverse, « repère local vers repère réel », reprend la procédure ci-dessus, mais en sens inverse.

Les quatre méthodes permettant de faire ces conversions sont présentées ci-après :

Conversion de coordonnées
Sélectionnez
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
    /**
     * Convertit une abscisse réelle en abscisse locale (repère JPanel)
     * @param x l'abscisse réelle
     * @return l'abscisse locale
     */
    public int getLocalCoordX(double x) {

        return (int) Math.round(this.getWidth() / 2 + this.offsetX + x * this.echelle / ECHELLE_BASE);

    }

    /**
     * Convertit une ordonnée réelle en ordonnée locale (repère JPanel)
     * @param y l'ordonnée réelle
     * @return l'ordonnée locale
     */
    public int getLocalCoordY(double y) {

        return (int) Math.round(this.getHeight() / 2 + this.offsetY - y * this.echelle / ECHELLE_BASE);

    }

    /**
     * Convertit une abscisse locale (repère JPanel) en abscisse réelle
     * @param x l'abscisse locale
     * @return l'abscisse réelle
     */
    public double getRealCoordX(double x) {

        return (x - (this.getWidth() / 2 + this.offsetX)) * ECHELLE_BASE / this.echelle;

    }

    /**
     * Convertit une ordonnée locale (repère JPanel) en ordonnée réelle
     * @param y l'ordonnée locale
     * @return l'ordonnée réelle
     */
    public double getRealCoordY(double y) {

        return (y - (this.getHeight() / 2 + this.offsetY)) * -ECHELLE_BASE / this.echelle;

    }

II-A-2. Surcharge de paintComponents

La méthode à modifier pour personnaliser l'affichage de notre JPanel est la méthode paintComponents. Mais avant de la modifier, il faut savoir ce que nous voulons lui faire dessiner !

Dans notre cas, l'affichage se compose :

  • d'un fond noir ;
  • des axes X et Y en trait plein vert ;
  • des axes de quadrillage sens X et sens Y, en trait pointillé vert foncé (codage RGB=60/85/60), avec un pas stepX en sens X et stepY en sens Y ;
  • des éléments graphiques qui seront dessinés par le gestionnaire d'éléments graphiques.

Ceci se traduit dans le code Java par les blocs suivants :

  • dessin avec fillRect d'un rectangle plein noir occupant toute la zone d'affichage ;
  • changement de couleur de dessin avec setColor, et dessin avec drawLine d'une ligne horizontale (à l'ordonnée y=0 locale) et d'une ligne verticale (à l'abscisse x=0 locale) ;
  • définition du motif des pointillés avec setStroke et BasicStroke, puis dessin des quadrillages horizontal et vertical ;
  • appel de la méthode de dessin du gestionnaire d'éléments graphiques.

Le code complet de la méthode paintComponents est présenté ci-après :

Méthode paintComponents
Sélectionnez
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
    // surcharge de la méthode paintComponent afin de dessiner notre vue personnalisée
    @Override
    public void paintComponent(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        int h = this.getHeight();
        int w = this.getWidth();

        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);

        //tracé des axes et du quadrillage

        int roundOffsetX = (int) Math.round(this.offsetX);
        int roundOffsetY = (int) Math.round(this.offsetY);
        int localStepX = (int) Math.round(this.echelle / ECHELLE_BASE * stepX);
        int localStepY = (int) Math.round(this.echelle / ECHELLE_BASE * stepY);

        g.setColor(Color.GREEN);
        g.drawLine(w / 2 + roundOffsetX, 0, w / 2 + roundOffsetX, h);
        g.drawLine(0, h / 2 + roundOffsetY, w, h / 2 + roundOffsetY);

        int offsetStrokeX = 7 - roundOffsetX % 7;
        int offsetStrokeY = 7 - roundOffsetY % 7;

        float[] dash = {2, 5};
        BasicStroke bsX = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 10, dash, offsetStrokeX);
        BasicStroke bsY = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 10, dash, offsetStrokeY);

        g.setColor(new Color(60, 85, 60));

        g2.setStroke(bsY);

        int nbxp = (w - (w / 2 + roundOffsetX)) / localStepX + 1;
        int nbxm = (w / 2 + roundOffsetX) / localStepX + 1;

        for (int i = 1; i < nbxp; i++) {
            int xline = w / 2 + roundOffsetX + (int) Math.round(i * stepX * this.echelle / ECHELLE_BASE);
            g.drawLine(xline, 0, xline, h);

        }
        for (int i = 1; i < nbxm; i++) {
            int xline = w / 2 + roundOffsetX - (int) Math.round(i * stepX * this.echelle / ECHELLE_BASE);
            g.drawLine(xline, 0, xline, h);
        }

        g2.setStroke(bsX);

        int nbym = (h - (h / 2 + roundOffsetY)) / localStepY + 1;
        int nbyp = (h / 2 + roundOffsetY) / localStepY + 1;

        for (int i = 1; i < nbyp; i++) {
            int yline = h / 2 + roundOffsetY - (int) Math.round(i * stepY * this.echelle / ECHELLE_BASE);
            g.drawLine(0, yline, w, yline);
        }
        for (int i = 1; i < nbym; i++) {
            int yline = h / 2 + roundOffsetY + (int) Math.round(i * stepY * this.echelle / ECHELLE_BASE);
            g.drawLine(0, yline, w, yline);
        }

        g2.setStroke(new BasicStroke());

        if (this.pom != null) {
            this.pom.draw(g, this);
        }
    }

II-A-3. Interaction avec la souris

La classe PFEM2DGuiPanel étendant les interfaces MouseListener et MouseMotionListener, les événements suivants sont gérés par la classe, avec les actions associées ci-dessous :

  • mouseDragged : suivant le mode courant, et en fonction du déplacement de la souris :
    • en mode « view » : pas d'effet,
    • en mode « drag » : déplace la zone affichée,
    • en mode « scale » : modifie le facteur de zoom ;
  • mouseMoved : pas d'action associée ;
  • mouseClicked : pas d'action associée ;
  • mousePressed : changement de mode suivant le bouton de la souris pressé :
    • bouton gauche : bascule en mode « drag » (déplacement),
    • bouton milieu : bascule en mode « scale » (zoom) ;
  • mouseReleased : passage en mode « view » (visualisation) ;
  • mouseEntered : pas d'action associée ;
  • mouseExited : pas d'action associée.

La méthode mousePressed est relativement simple, elle modifie l'état de la propriété mode suivant le bouton souris pressé et initialise les propriétés xOld et yOld (ainsi que les propriétés xstart et ystart en cas de mode « scale » activé) avec les coordonnées de la souris.

La méthode mouseDragged est un peu plus complexe, étant donné qu'elle gère l'influence du déplacement de la souris pour les deux modes principaux d'interaction :

  • en mode « drag » (déplacement de la zone d'affichage), le déplacement de la souris résulte en un déplacement similaire de la zone d'affichage. C'est-à-dire que si la souris se déplace de 10 pixels vers la droite, la zone d'affichage se déplace également de 10 pixels vers la droite.
    Pour cela, on ajoute aux propriétés offsetX et offsetY (qui gèrent le décalage de la zone d'affichage) l'écart de position entre la position actuelle de la souris et la position précédente, gardée en mémoire dans les propriétés xOld et yOld. Les valeurs de xOld et yOld sont finalement modifiées pour prendre celles de la position actuelle de la souris ;
  • en mode « scale » (zoom de la zone d'affichage), le déplacement de la souris résulte en une modification du facteur d'échelle de la zone d'affichage. Afin d'avoir une interaction « intuitive », le centre du zoom est positionné à l'endroit où l'utilisateur presse le bouton milieu de la souris (événement géré par la méthode mousePressed, qui place les coordonnées de ce point dans les propriétés xstart et ystart). Cette fonctionnalité complique un peu le calcul, dans la mesure où une modification de l'échelle uniquement va zoomer la zone d'affichage en prenant l'origine du repère réel comme centre de zoom.
    Afin de positionner le centre de zoom là où l'utilisateur a initialement cliqué, il nous faut modifier simultanément les valeurs de l'échelle et des décalages offsetX et offsetY. Ces modifications sont présentées dans le code complet des méthodes de gestion des événements en lignes 171 à 173.
    De la même manière qu'en mode « drag », les valeurs de xOld et yOld sont finalement modifiées pour prendre celles de la position actuelle de la souris.

Les formules de calcul des décalages pour le zoom s'appuient sur le fait que le centre de zoom doit garder les mêmes coordonnées en repère local, quelle que soit la valeur de l'échelle.

Le code complet des méthodes de gestion des événements souris est présenté ci-après :

Interaction avec la souris
Sélectionnez
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
    // surcharge de la méthode mouseDragged. Gestion du déplacement souris avec bouton appuyé
    @Override
    public void mouseDragged(MouseEvent e) {
        if (this.mode == DRAG) {
            int x = e.getX();
            int y = e.getY();

            this.offsetX = this.offsetX + (x - this.xold);
            this.offsetY = this.offsetY + (y - this.yold);

            this.xold = x;
            this.yold = y;

            this.repaint();
        } else if (this.mode == SCALE) {
            int x = e.getX();
            int y = e.getY();

            int dx = x - this.xold;
            int dy = y - this.yold;

            int delta;
            if (Math.abs(dx) > Math.abs(dy)) {
                delta = dx;
            } else {
                delta = dy;
            }

            double newEchelle = Math.max(1, this.echelle * Math.pow(1.01, delta));
            double newOffsetX = this.offsetX + this.getRealCoordX(this.xstart) / ECHELLE_BASE * (this.echelle - newEchelle);
            double newOffsetY = this.offsetY - this.getRealCoordY(this.ystart) / ECHELLE_BASE * (this.echelle - newEchelle);

            this.offsetX = newOffsetX;
            this.offsetY = newOffsetY;
            this.echelle = newEchelle;

            this.xold = x;
            this.yold = y;

            this.repaint();
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    // surcharge de la méthode mousePressed. Gestion du clic souris.
    @Override
    public void mousePressed(MouseEvent e) {
        this.xold = e.getX();
        this.yold = e.getY();

        if (e.getButton() == MouseEvent.BUTTON1) {
            this.mode = DRAG;
        } else if (e.getButton() == MouseEvent.BUTTON2) {
            this.mode = SCALE;
            this.xstart = e.getX();
            this.ystart = e.getY();
        } else {
            this.mode = VIEW;
        }
    }

    // surcharge de la méthode mouseReleased. Gestion du lâcher de clic souris.
    @Override
    public void mouseReleased(MouseEvent e) {
        this.mode = VIEW;
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

II-B. Capture d'écran

La première méthode additionnelle effectue une capture d'écran de la zone d'affichage du composant PFEM2DGuiPanel.

Un JfFileChooser permet de sélectionner le fichier de sortie dans lequel sera enregistrée l'image. Seuls deux formats sont autorisés dans cette méthode, le png et le jpg. Après vérification de l'extension du fichier, l'image est dessinée dans une BufferedImage, puis enregistrée dans le fichier choisi.

Méthode takeScreenshot
Sélectionnez
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
     /**
     * Crée une capture d'écran de l'affichage courant.
     * Le fichier de sortie est défini par sélection du fichier via un JFileChooser.
     * Deux formats possibles en sortie: png ou jpg
     */
    public void takeScreenshot() {

        JFileChooser fc = new JFileChooser();

        int returnVal = fc.showOpenDialog(this);

        if (returnVal == JFileChooser.APPROVE_OPTION) {

            File fichier = fc.getSelectedFile();
            String nomFichier = fichier.getName().toLowerCase();

            if ((nomFichier.endsWith("png")) || (nomFichier.endsWith("jpg"))) {
                try {
                    BufferedImage bufImage = new BufferedImage(this.getSize().width, this.getSize().height, BufferedImage.TYPE_INT_RGB);
                    this.paint(bufImage.createGraphics());

                    String extension = "png";
                    if (nomFichier.endsWith("jpg")) {
                        extension = "jpg";
                    }

                    ImageIO.write(bufImage, extension, fichier);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            } else {
                JOptionPane.showMessageDialog(this, "Veuillez préciser l'extension du fichier image (png ou jpg)",
                        "Extension fichier manquante", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

Les formats choisis ici (png et jpg) ne sont pas une limitation de Java mais uniquement un choix personnel. Les formats bmp et gif peuvent également être utilisés.

II-C. Recadrage de l'affichage

La seconde méthode additionnelle recentre l'affichage sur les éléments graphiques à afficher :

Méthode centreView
Sélectionnez
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
     /**
     * Centre la vue sur les éléments à afficher. 
     * @param boundingBox tableau de 4 double: Xmin, Xmax, Ymin,Ymax, en coordonnées réelles
     */
    public void centreView(double[] boundingBox) {

        if ((boundingBox[1] - boundingBox[0] != Double.NEGATIVE_INFINITY) && (boundingBox[3] - boundingBox[2] != Double.NEGATIVE_INFINITY)) {

            double echelleX = (this.getWidth() - 50) * ECHELLE_BASE / (boundingBox[1] - boundingBox[0]);
            double echelleY = (this.getHeight() - 50) * ECHELLE_BASE / (boundingBox[3] - boundingBox[2]);

            this.echelle = Math.min(echelleX, echelleY);
            
            this.offsetX = -(this.getLocalCoordX((boundingBox[1] + boundingBox[0]) / 2) - this.getLocalCoordX(0));
            this.offsetY = -(this.getLocalCoordY((boundingBox[3] + boundingBox[2]) / 2) - this.getLocalCoordY(0));

            this.repaint();
        }
    }

Cette méthode nécessite le passage en paramètre des coordonnées de la « bounding box », ou boîte englobante en français, afin de pouvoir recadrer au mieux la vue sur les éléments à afficher.

Une vérification sur la taille de la « bounding box » est effectuée, de manière à ne pas recentrer la vue s'il n'y a rien à afficher !

Une marge de 50 pixels est ajoutée autour de la zone d'affichage afin d'améliorer le rendu.

Les échelles sens X et Y sont calculées, l'échelle finale étant le minimum de ces deux échelles.

Les décalages sens X et Y sont calculés comme étant les distances du centre de la « bounding box » au centre de l'affichage (point (0,0) local).

Finalement, un rafraîchissement de l'affichage est appelé.

III. Le gestionnaire d'éléments graphiques

Nous allons utiliser un gestionnaire d'éléments graphiques pour gérer l'affichage dans notre PFEM2DGuiPanel. Il sera chargé de gérer l'affichage de tous les éléments graphiques que nous allons créer dans notre espace d'affichage. Les éléments graphiques seront simplement ajoutés ou supprimés du gestionnaire, charge à lui de gérer l'affichage correctement.

Le gestionnaire d'éléments graphiques est décrit par la classe PFEM2DObjectManager. Les propriétés de l'objet PFEM2DObjectManager sont :

  • la liste objects des éléments graphiques gérés (de type IPFEM2DDrawableObject, voir chapitre IV.AL'interface IPFEM2DDrawableObject) ;
  • et l'identité idCurrentPoint du point courant. Cette propriété est utilisée à titre d'exemple dans cet article pour illustrer la méthode de gestion automatique des identités des éléments graphiques.

Les méthodes de la classe PFEM2DObjectManager sont les suivantes :

  • le constructeur, qui initialise la liste des éléments graphiques ;
  • la méthode init, qui permet d'initialiser la liste des éléments graphiques ;
  • la méthode addObject, qui permet d'ajouter un élément graphique à la liste ;
  • deux méthodes removeObject, permettant de supprimer un élément de la liste soit par son rang dans la liste, soit en passant directement l'objet à supprimer en paramètre ;
  • la méthode draw, qui parcourt la liste des éléments graphiques et provoque leur affichage par appel à leur méthode draw ;
  • la méthode updateId, qui met à jour la propriété idCurrentPoint. Le point ayant l'identité la plus élevée est recherché, cette valeur est incrémentée de un pour être affectée à idCurrentPoint. La propriété idCurrentPoint peut ainsi être utilisée directement afin d'affecter automatiquement la valeur d'identité du prochain point créé ;
  • les méthodes getIdCurrentPoint, incrementIdCurrentPoint et setIdCurrentPoint permettent respectivement d'obtenir, incrémenter et définir la valeur de la propriété idCurrentPoint ;
  • la méthode getBoundingBox est similaire à celle de l'interface IPFEM2DDrawableObject. Elle parcourt la liste des éléments graphiques, récupère leur « bounding box », et calcule à partir de ces informations la « bounding box » globale englobant tous les éléments gérés par le gestionnaire d'éléments graphiques.

Le code de la classe est présenté ci-après :

Classe PFEM2DObjectManager
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
package plegatfem2d;

import java.awt.Graphics;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import plegatfem2d.objects.IPFEM2DDrawableObject;
import plegatfem2d.objects.PFEM2DPoint;

/**
 * Classe définissant le gestionnaire d'objets graphiques.
 * 
 * @author Jean-Michel BORLOT
 */
public class PFEM2DObjectManager {

    /**
     * Constructeur
     */
    public PFEM2DObjectManager() {
        this.objects = new ArrayList<>();
    }

    /**
     * Méthode d'initialisation de la liste des objets gérés
     */
    public void init() {
        this.objects.clear();
    }

    /**
     * Ajoute un objet graphique au gestionnaire
     * @param obj l'objet graphique
     */
    public void addObject(IPFEM2DDrawableObject obj) {
        this.objects.add(obj);
    }

    /**
     * Supprime un objet graphique du gestionnaire
     * @param rank le rang de l'objet graphique
     */
    public void removeObject(int rank) {
        this.objects.remove(rank);
    }

    /**
     * Supprime un objet graphique du gestionnaire
     * @param obj l'objet graphique
     */
    public void removeObject(IPFEM2DDrawableObject obj) {
        this.objects.remove(obj);
    }

    /**
     * Affiche les objets graphiques gérés par le gestionnaire
     * @param g l'objet Graphics sur lequel dessiner
     * @param panel l'objet PFEM2DGuiPanel dans lequel dessiner
     */
    public void draw(Graphics g, PFEM2DGuiPanel panel) {
        
        for (IPFEM2DDrawableObject current : this.objects) {
            current.draw(g,panel);
        }
    }

    /**
     * Méthode de mise à jour des identités des objets graphiques de type PFEM2DPoint
     */
    public void updateId() {

        this.idCurrentPoint = 1;

        for (int i = 0; i < this.objects.size(); i++) {

            IPFEM2DDrawableObject obj = this.objects.get(i);

            if (obj instanceof IPFEM2DDrawableObject) {

                PFEM2DPoint[] pts = obj.getPoints();

                if (pts != null) {

                    for (int j = 0; j < pts.length; j++) {
                        long id = pts[j].getNumId();

                        if (id > this.idCurrentPoint) {
                            idCurrentPoint = id;
                        }
                    }
                }
            }
        }
    }
    
    private List<IPFEM2DDrawableObject> objects;    // liste des objets graphiques
    private long idCurrentPoint;                    // identité courante pour les objets de type PFEM2DPoint

    /**
     * Renvoie l'identité courante des objets de type PFEM2DPoint
     * @return l'identité courante
     */
    public long getIdCurrentPoint() {
        idCurrentPoint++;
        return idCurrentPoint;
    }

    /**
     * Incrémente l'identité courante des objets de type PFEM2DPoint
     */
    public void incrementIdCurrentPoint() {
        idCurrentPoint++;
    }

    /**
     * Définit l'identité courante des objets de type PFEM2DPoint
     * @param idCurrentPoint
     */
    public void setIdCurrentPoint(long idCurrentPoint) {
        this.idCurrentPoint = idCurrentPoint;
    }

    /**
     * Renvoie les valeurs Xmin/max et Ymin/max de la boite englobante des objets graphiques
     * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax
     */
    public double[] getBoundingBox() {

        double data[] = new double[4];

        data[0] = Double.POSITIVE_INFINITY;     // x min
        data[1] = Double.NEGATIVE_INFINITY;     // x max
        data[2] = Double.POSITIVE_INFINITY;     // y min
        data[3] = Double.NEGATIVE_INFINITY;     // y max

        for (IPFEM2DDrawableObject current : this.objects) {
            
            double[] bb = current.getBoundingBox();

            if (bb[0] < data[0]) {
                data[0] = bb[0];
            }
            if (bb[1] > data[1]) {
                data[1] = bb[1];
            }
            if (bb[2] < data[2]) {
                data[2] = bb[2];
            }
            if (bb[3] > data[3]) {
                data[3] = bb[3];
            }
        }

        return data;
    }
}

En complément à la définition de la classe PFEM2DObjectManager, une méthode d'initialisation de la liste des éléments est définie. Celle-ci nous va nous permettre d'importer des éléments graphiques à partir d'un fichier de script Jython. L'avantage de cette méthode est qu'elle va ainsi nous éviter de coder la description des éléments graphiques à afficher dans le code de notre application.

Afin de pouvoir utiliser les scripts Jython, nous supposerons que celui-ci est installé sur votre machine. Si ce n'est pas le cas, je vous renvoie vers le site officiel Jythonsite officiel Jython.

Pourquoi Jython ?
En fait, vous pouvez utiliser n'importe quel moteur de script compatible avec la machine virtuelle Java (Jython, Groovy, Rhino…) selon vos souhaits, besoins et habitudes de programmation.
Dans ce tutoriel, nous avons choisi d'utiliser Jython.

La méthode openFile initialise la liste des éléments graphiques. Elle cherche ensuite à créer le moteur de script Jython, et définit une variable « pom », qui n'est autre que notre gestionnaire d'éléments graphiques. Cette variable « pom » sera utilisée dans le script Jython pour récupérer les éléments graphiques, et faire le lien entre le script Jython et notre application. Le moteur de script Jython évalue finalement le fichier passé en paramètre.

Le code de la méthode openFile est présenté ci-après :

Méthode openFile
Sélectionnez
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
    /**
     * Ouvre un fichier de script Jython permettant de définir le contenu du gestionnaire d'objets
     * graphiques.
     * @param fichier le fichier script Jython
     * @return true si l'importation s'est déroulée correctement, false sinon
     */
    public boolean openFile(File fichier) {

        this.init();

        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine moteur = manager.getEngineByName("jython");

        if (moteur == null) {
            System.out.println("Impossible de trouver le moteur de scripting recherché.");
            return false;
        } else {
            try {
                Bindings bindings = moteur.getBindings(ScriptContext.ENGINE_SCOPE);
                bindings.clear();
                bindings.put("pom", this);

                moteur.eval(new BufferedReader(new FileReader(fichier)));

                System.out.println("end of file importation");
            } catch (FileNotFoundException | ScriptException ex) {
                ex.printStackTrace();
                return false;
            }
        }

        return true;
    }

IV. Les éléments graphiques

IV-A. L'interface IPFEM2DDrawableObject

Cette interface définit les méthodes des objets graphiques qui seront affichables à l'écran :

  • draw, qui dessine l'objet graphique ;
  • setVisible, qui définit la visibilité de l'objet graphique ;
  • isVisible, qui renvoie l'état de visibilité de l'objet graphique ;
  • getPoints, qui renvoie la liste des points définissant l'objet graphique ;
  • getId, qui renvoie l'identité de l'objet graphique ;
  • getBoundingBox, qui renvoie la boîte englobante de l'objet graphique.

Le code de l'interface est présenté ci-après :

Classe IPFEM2DDrawableObject
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
package plegatfem2d.objects;

import java.awt.Graphics;
import plegatfem2d.PFEM2DGuiPanel;

/**
 * Interface définissant les objets graphiques
 * @author Jean-Michel BORLOT
 */
public interface IPFEM2DDrawableObject {

    /**
     * Dessine l'objet graphique
     * @param g l'objet Graphics sur lequel dessiner
     * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner
     */
    public void draw(Graphics g, PFEM2DGuiPanel panel);

    /**
     * Définit la visibilité de l'objet graphique
     * @param flag true si l'objet est visible, false s'il ne l'est pas
     */
    public void setVisible(boolean flag);

    /**
     * Renvoie l'état de visibilité de l'objet graphique
     * @return true si l'objet est visible, false s'il ne l'est pas
     */
    public boolean isVisible();
    
    /**
     * Renvoie la liste des points définissant l'objet graphique
     * @return la liste des points
     */
    public PFEM2DPoint[] getPoints();
    
    /**
     * Renvoie l'identité de l'objet graphique
     * @return l'identité de l'objet graphique
     */
    public String getId();
    
    /**
     * Renvoie la boite englobante de l'objet graphique
     * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax
     */
    public double[] getBoundingBox();
    
}

IV-B. L'interface IPFEM2DCurve

Cette interface définit les méthodes des objets 2D similaires à des courbes : segment, cercle, arc de cercle… :

  • getStartPoint, qui renvoie le point de départ de la courbe ;
  • getEndPoint, qui renvoie le point d'arrivée de la courbe.

Le code de l'interface est présenté ci-après :

Interface IPFEM2DCurve
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
package plegatfem2d.objects;

/**
 * Interface de définition des objets de type "courbe"
 * @author Jean-Michel BORLOT
 */
public interface IPFEM2DCurve {
   
    /**
     * Renvoie le point de départ de la courbe
     * @return le point de départ de la courbe
     */
    public PFEM2DPoint getStartPoint();

    /**
     * Renvoie le point d'arrivée de la courbe
     * @return le point d'arrivée de la courbe
     */
    public PFEM2DPoint getEndPoint();    
}

IV-C. La classe PFEM2DModel

La forme à dessiner dans la zone graphique va être encapsulée dans un objet PFEM2DModel, qui va contenir tous les éléments individuels à afficher. Cela permet d'avoir un accès unique à tous les composants d'un objet graphique complexe, la gestion de l'affichage étant alors prise en charge par l'objet PFEM2DModel. L'ensemble des éléments individuels forme ainsi un objet complexe que nous appellerons un « modèle » (ou « model » en anglais).

Cette classe implémente l'interface IPFEM2DDrawableObject. On retrouve donc les méthodes qui ont été décrites au chapitre IV.AL'interface IPFEM2DDrawableObject.

À ces méthodes se rajoutent les méthodes spécifiques à l'objet PFEM2DModel :

  • son constructeur, qui initialise la liste des objets composant le modèle ;
  • addObject, permettant d'ajouter un objet individuel ;
  • removeObject, permettant de supprimer un objet individuel.

Le code de la classe PFEM2DModel est présenté ci-après :

Classe PFEM2DModel
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
package plegatfem2d.objects;

import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import plegatfem2d.PFEM2DGuiPanel;

/**
 * Classe définissant l'objet "racine", composé d'un assemblage d'objets
 * graphiques
 *
 * @author Jean-Michel BORLOT
 */
public class PFEM2DModel implements IPFEM2DDrawableObject {

    private List<IPFEM2DDrawableObject> objects = new ArrayList<>();   // liste des objets constituant l'objet "racine"
    private boolean visible = true;                                         // état de visibilité de l'objet

    /**
     * Constructeur
     */
    public PFEM2DModel() {
        this.objects.clear();
    }

    /**
     * Ajoute un objet graphique à l'objet racine
     *
     * @param obj l'objet graphique
     */
    public void addObject(IPFEM2DDrawableObject obj) {
        this.objects.add(obj);
    }

    /**
     * Supprime un objet graphique de l'objet racine
     *
     * @param obj l'objet graphique
     */
    public void removeObject(IPFEM2DDrawableObject obj) {
        this.objects.remove(obj);
    }

    /**
     * Dessine l'objet graphique
     *
     * @param g l'objet Graphics sur lequel dessiner
     * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner
     */
    @Override
    public void draw(Graphics g, PFEM2DGuiPanel panel) {

        for (IPFEM2DDrawableObject current : this.objects) {
            if (current.isVisible()) {
                current.draw(g, panel);
            }
        }
    }

    /**
     * Définit la visibilité de l'objet graphique
     *
     * @param flag true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public void setVisible(boolean flag) {
        this.visible = flag;
    }

    /**
     * Renvoie l'état de visibilité de l'objet graphique
     *
     * @return true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public boolean isVisible() {
        return this.visible;
    }

    /**
     * Renvoie l'identité de l'objet graphique
     *
     * @return l'identité de l'objet graphique
     */
    @Override
    public String getId() {
        return "Root";
    }

    /**
     * Renvoie la boite englobante de l'objet graphique
     *
     * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax
     */
    @Override
    public double[] getBoundingBox() {

        double data[] = new double[4];

        data[0] = Double.POSITIVE_INFINITY;
        data[1] = Double.NEGATIVE_INFINITY;
        data[2] = Double.POSITIVE_INFINITY;
        data[3] = Double.NEGATIVE_INFINITY;

        for (IPFEM2DDrawableObject current : this.objects) {

            double bb[] = current.getBoundingBox();

            if (bb[0] < data[0]) {
                data[0] = bb[0];
            }
            if (bb[1] > data[1]) {
                data[1] = bb[1];
            }
            if (bb[2] < data[2]) {
                data[2] = bb[2];
            }
            if (bb[3] > data[3]) {
                data[3] = bb[3];
            }
        }

        return data;
    }

    /**
     * Renvoie la liste des points définissant l'objet graphique
     *
     * @return la liste des points
     */
    @Override
    public PFEM2DPoint[] getPoints() {
        ArrayList<PFEM2DPoint> listPts = new ArrayList<>();

        for (IPFEM2DDrawableObject current : this.objects) {
            PFEM2DPoint[] pts = current.getPoints();
            listPts.addAll(Arrays.asList(pts));
        }

        return listPts.toArray(new PFEM2DPoint[listPts.size()]);
    }
}

IV-D. La classe PFEM2DPoint

Maintenant que nous avons un « conteneur » pour décrire les objets que nous voulons afficher, il nous faut définir les éléments individuels qui vont nous permettre de construire nos modèles par assemblage.

Le premier de ces éléments individuels est un élément de base : le point. Il est décrit par la classe PFEM2DPoint. Comme nous travaillons en 2D, un point est décrit par un couple de valeurs, x et y, qui définissent sa position dans l'espace 2D (attention de ne pas confondre la position du point dans l'espace 2D, et sa position dans le panneau graphique !).

Cette classe implémente l'interface IPFEM2DDrawableObject. On retrouve donc les méthodes qui ont été décrites au chapitre IV.AL'interface IPFEM2DDrawableObject.

À ces méthodes se rajoutent les méthodes spécifiques à l'objet PFEM2DPoint :

  • deux constructeurs :
    • un constructeur par défaut, initialisant le point à l'origine du repère 2D,
    • un constructeur prenant en paramètres les coordonnées du point, ainsi que son identité ;
  • les getters et setters pour l'identité et les coordonnées du point (le getter pour l'identité du point fait partie des méthodes de l'interface IPFEM2DDrawableObject) ;
  • la méthode getNumId, qui retourne l'identité du point sous forme numérique et non pas littérale comme le fait la méthode getId de l'interface IPFEM2DDrawableObject ;
  • la méthode getDistanceTo, qui permet d'obtenir la distance du point courant à un autre point passé en paramètre ;
  • la méthode getRotated, qui permet de créer un point par rotation du point courant ;
  • et la surcharge de la méthode toString, donnant les informations relatives au point sous forme littérale.

La méthode draw affiche un carré de 10 pixels centré sur la position du point.

Le code de la classe est présenté ci-après :

Classe PFEM2DPoint
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
package plegatfem2d.objects;

import java.awt.Color;
import java.awt.Graphics;
import plegatfem2d.PFEM2DGuiPanel;

/**
 * Classe définissant un objet graphique de type "point"
 * @author Jean-Michel BORLOT
 */
public class PFEM2DPoint implements IPFEM2DDrawableObject {

    private double x, y;
    private long id;
    private boolean visible;
    
    /**
     * Constructeur
     * @param id l'identité du point
     * @param x l'abscisse du point
     * @param y l'ordonnée du point
     */
    public PFEM2DPoint(long id, double x, double y) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.visible = true;
    }

    /**
     * Constructeur par défaut
     */
    public PFEM2DPoint() {
        this.id = 0;
        this.x = 0;
        this.y = 0;
        this.visible = false;
    }

    /**
     * Définit l'identité du point
     * @param id l'identité du point
     */
    public void setId(long id) {
        this.id = id;
    }

    /**
     * Renvoie l'abscisse du point
     * @return l'abscisse du point
     */
    public double getX() {
        return x;
    }

    /**
     * Définit l'abscisse du point
     * @param x l'abscisse du point
     */
    public void setX(double x) {
        this.x = x;
    }

    /**
     * Renvoie l'ordonnée du point
     * @return l'ordonnée du point
     */
    public double getY() {
        return y;
    }

    /**
     * Définit l'ordonnée du point
     * @param y l'ordonnée du point
     */
    public void setY(double y) {
        this.y = y;
    }

    /**
     * Dessine l'objet graphique
     * @param g l'objet Graphics sur lequel dessiner
     * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner
     */
    @Override
    public void draw(Graphics g, PFEM2DGuiPanel panel) {

        if (this.isVisible()) {
            int xloc = panel.getLocalCoordX(this.x);
            int yloc = panel.getLocalCoordY(this.y);

            int[] xPoints = {xloc + 5, xloc - 5, xloc - 5, xloc + 5, xloc + 5};
            int[] yPoints = {yloc + 5, yloc + 5, yloc - 5, yloc - 5, yloc + 5};

            g.setColor(Color.cyan);
            g.drawPolyline(xPoints, yPoints, 5);

            // dessin id
            
            g.drawString("P" + this.id, xloc + 10, yloc + 10);
        }

    }

    /**
     * Définit la visibilité de l'objet graphique
     * @param flag true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public void setVisible(boolean flag) {
        this.visible = flag;
    }

    /**
     * Renvoie l'état de visibilité de l'objet graphique
     * @return true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public boolean isVisible() {
        return this.visible;
    }

    /**
     * Renvoie l'identité de l'objet graphique
     * @return l'identité de l'objet graphique
     */
    @Override
    public String getId() {
        return "Point " + this.id;
    }

    /**
     * Renvoie l'identité sous format numérique de l'objet graphique
     * @return l'identité sous format numérique de l'objet graphique
     */
    public long getNumId() {
        return this.id;
    }

    /**
     * Renvoie la distance du point à un autre point
     * @param pt l'autre point
     * @return la distance
     */
    public double getDistanceTo(PFEM2DPoint pt) {
        return Math.sqrt(Math.pow(pt.getX() - this.getX(), 2) + Math.pow(pt.getY() - this.getY(), 2));
    }

    /**
     * Constructeur par rotation du point courant 
     * @param centre le centre de la rotation
     * @param angle l'angle de la rotation
     * @return le nouveau point créé
     */
    public PFEM2DPoint getRotated(PFEM2DPoint centre, double angle) {

        double dx = this.getX() - centre.getX();
        double dy = this.getY() - centre.getY();

        double angleBase = Math.atan2(dy, dx);
        double radius = centre.getDistanceTo(this);

        double xRot = centre.getX() + radius * Math.cos(angleBase + angle / 180. * Math.PI);
        double yRot = centre.getY() + radius * Math.sin(angleBase + angle / 180. * Math.PI);

        return new PFEM2DPoint(0, xRot, yRot);
    }

    // méthode toString
    @Override
    public String toString() {
        
        return "Point #"+this.id+", x/y="+this.x+"/"+this.y;
        
    }

    /**
     * Renvoie la boite englobante de l'objet graphique
     * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax
     */
    @Override
    public double[] getBoundingBox() {
        return new double[]{this.x,this.x,this.y,this.y};
    }

    /**
     * Renvoie la liste des points définissant l'objet graphique
     * @return la liste des points
     */
    @Override
    public PFEM2DPoint[] getPoints() {
        return new PFEM2DPoint[]{this};
    }
    
}

IV-E. La classe PFEM2DLine

Le second élément individuel que nous utiliserons est le segment, qui est défini par deux points. Il est décrit par la classe PFEM2DLine.

Cette classe implémente les interfaces IPFEM2DDrawableObject et IPFEM2DCurve. On retrouve donc les méthodes qui ont été décrites aux chapitres IV.AL'interface IPFEM2DDrawableObject et IV.BL'interface IPFEM2DCurve.

À ces méthodes se rajoutent les méthodes spécifiques à l'objet PFEM2DPoint :

  • un constructeur, prenant en paramètres les deux points définissant le segment et l'identité du segment ;
  • les getters et setters pour l'identité du segment et les deux points le définissant (le getter pour l'identité du segment fait partie des méthodes de l'interface IPFEM2DDrawableObject).

La méthode draw affiche simplement un trait entre les deux points de définition du segment.

Le code de la classe est présenté ci-après :

Classe PFEM2DLine
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
package plegatfem2d.objects;

import java.awt.Color;
import java.awt.Graphics;
import plegatfem2d.PFEM2DGuiPanel;

/**
 * Classe définissant un objet graphique de type "ligne", reliant deux points.
 * @author Jean-Michel BORLOT
 */
public class PFEM2DLine implements IPFEM2DDrawableObject, IPFEM2DCurve {

    private PFEM2DPoint pt1, pt2;   // les deux points définissant la ligne 
    private int id;                 // l'identité de la ligne
    private boolean visible;        // l'état de visibilité de la ligne
    
    /**
     * Constructeur
     * @param id identité
     * @param pt1 point n°1
     * @param pt2 point n°2
     */
    public PFEM2DLine(int id, PFEM2DPoint pt1, PFEM2DPoint pt2) {
        this.pt1 = pt1;
        this.pt2 = pt2;
        this.id = id;
        this.visible = true;
    }

    /**
     * Définit l'identité de la ligne
     * @param id l'identité de la ligne
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * Renvoie le premier point
     * @return le premier point
     */
    public PFEM2DPoint getPt1() {
        return pt1;
    }

    /**
     * Définit le premier point
     * @param pt1 le premier point
     */
    public void setPt1(PFEM2DPoint pt1) {
        this.pt1 = pt1;
    }

    /**
     * Renvoie le second point
     * @return le second point
     */
    public PFEM2DPoint getPt2() {
        return pt2;
    }

    /**
     * Définit le second point
     * @param pt2 le second point
     */
    public void setPt2(PFEM2DPoint pt2) {
        this.pt2 = pt2;
    }

    /**
     * Dessine l'objet graphique
     * @param g l'objet Graphics sur lequel dessiner
     * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner
     */
    @Override
    public void draw(Graphics g, PFEM2DGuiPanel panel) {

        if (this.isVisible()) {
            int xloc1 = panel.getLocalCoordX(this.pt1.getX());
            int yloc1 = panel.getLocalCoordY(this.pt1.getY());

            int xloc2 = panel.getLocalCoordX(this.pt2.getX());
            int yloc2 = panel.getLocalCoordY(this.pt2.getY());

            g.setColor(Color.yellow);
            g.drawLine(xloc1, yloc1, xloc2, yloc2);
        }
    }

    /**
     * Définit la visibilité de l'objet graphique
     * @param flag true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public void setVisible(boolean flag) {
        this.visible = flag;
    }

    /**
     * Renvoie l'état de visibilité de l'objet graphique
     * @return true si l'objet est visible, false s'il ne l'est pas
     */
    @Override
    public boolean isVisible() {
        return this.visible;
    }

    /**
     * Renvoie l'identité de l'objet graphique
     * @return l'identité de l'objet graphique
     */
    @Override
    public String getId() {
        return "Line " + this.id;
    }

    /**
     * Renvoie le point de départ de la courbe
     * @return le point de départ de la courbe
     */
    @Override
    public PFEM2DPoint getStartPoint() {
        return this.pt1;
    }

    /**
     * Renvoie le point d'arrivée de la courbe
     * @return le point d'arrivée de la courbe
     */
    @Override
    public PFEM2DPoint getEndPoint() {
        return this.pt2;
    }

    /**
     * Renvoie la boite englobante de l'objet graphique
     * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax
     */
    @Override
    public double[] getBoundingBox() {
        
        double data[]=new double[4];
        
        data[0]=Math.min(this.pt1.getX(),this.pt2.getX());
        data[1]=Math.max(this.pt1.getX(),this.pt2.getX());
        data[2]=Math.min(this.pt1.getY(),this.pt2.getY());
        data[3]=Math.max(this.pt1.getY(),this.pt2.getY());
        
        return data;
    }

    /**
     * Renvoie la liste des points définissant l'objet graphique
     * @return la liste des points
     */
    @Override
    public PFEM2DPoint[] getPoints() {
        return new PFEM2DPoint[]{this.pt1,this.pt2};
    }
}

V. Exemple d'application

Nous allons utiliser toutes les classes et interfaces que nous venons de décrire dans un exemple d'utilisation. Nous allons créer une fenêtre, incluant un panneau PFEM2DGuiPanel, ainsi qu'une barre d'outils de quatre boutons qui nous permettront :

  • d'ouvrir un fichier script Jython ;
  • de faire une impression d'écran de notre zone d'affichage ;
  • de recadrer l'affichage sur les éléments à afficher ;
  • de fermer notre application de démonstration.

Le code de cette classe est relativement simple et ne présente pas de difficultés particulières. Il est présenté ci-après :

Classe PlegatFem2D_GuiPanel
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
package plegatfem2d;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JSeparator;
import javax.swing.JToolBar;

/**
 * Classe de démonstration du panneau personnalisé PFEM2DGuiPanel. Crée une
 * fenêtre incorporant une barre d'outil simple (ouvrir un fichier, capture
 * d'écran, recentrer la vue, et fermer la fenêtre) et un élément
 * PFEM2DGuiPanel.
 *
 * @author Jean-Michel BORLOT
 */
public class PlegatFem2D_GuiPanel extends JFrame {

    /**
     * Constructeur.
     *
     * @param frameTitle le titre apparaissant dans la barre de la fenêtre
     */
    public PlegatFem2D_GuiPanel(String frameTitle) {
        super(frameTitle);
        this.initComponents();
    }

    private void initComponents() {

        final PFEM2DObjectManager pom = new PFEM2DObjectManager();

        this.pfguipanel = new PFEM2DGuiPanel();
        this.pfguipanel.setPreferredSize(new Dimension(800, 600));
        this.add(this.pfguipanel, BorderLayout.CENTER);

        JToolBar toolbar = new JToolBar();
        toolbar.setPreferredSize(new Dimension(100, 40));
        toolbar.setFloatable(false);

        // bouton open

        JButton jbOpen = new JButton("open", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/add.png")));
        jbOpen.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {

                JFileChooser fc = new JFileChooser();
                int returnVal = fc.showOpenDialog(pfguipanel.getParent());

                if (returnVal == JFileChooser.APPROVE_OPTION) {

                    File fichier = fc.getSelectedFile();
                    boolean flag = pom.openFile(fichier);

                    if (flag) {
                        pfguipanel.repaint();
                    }
                }
            }
        });
        toolbar.add(jbOpen);

        // bouton screenshot

        JButton jbScreen = new JButton("screenshot", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/snapshot.png")));
        jbScreen.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pfguipanel.takeScreenshot();
            }
        });
        toolbar.add(jbScreen);

        // bouton resize

        JButton jbResize = new JButton("resize", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/resize_picture.png")));
        jbResize.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pfguipanel.centreView(pom.getBoundingBox());
            }
        });
        toolbar.add(jbResize);


        toolbar.add(new JSeparator(JSeparator.VERTICAL));

        // bouton close

        JButton jbClose = new JButton("close", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/cross.png")));
        jbClose.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                dispose();
            }
        });
        toolbar.add(jbClose);

        this.add(toolbar, BorderLayout.NORTH);
        this.pack();

        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        this.pfguipanel.setPom(pom);

    }
    private PFEM2DGuiPanel pfguipanel;

    /**
     * Main class.
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new PlegatFem2D_GuiPanel("Demo PFEM - GUI Panel").setVisible(true);
            }
        });
    }
}

Les fichiers image pour les icônes de la barre d'outils sont disponibles dans les sources de cet article. Ils sont issus du site http://www.fatcow.com/free-iconshttp://www.fatcow.com/free-icons.

Il ne reste plus qu'à définir le fichier script Jython pour décrire les éléments graphiques que nous allons afficher.

Celui-ci crée quatre points, et les relie par quatre segments. Ces éléments sont ensuite ajoutés à un objet PFEM2DModel, qui est lui-même ajouté à notre gestionnaire d'objets graphiques représenté par la variable « pom » (voir chapitre IIILe gestionnaire d'éléments graphiques).

Le code du script Jython est présenté ci-après :

Fichier script Jython
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
from plegatfem2d import * 
from plegatfem2d.objects import * 

# définition du modèle 

pt1 = PFEM2DPoint(1, -60, -60); 
pt2 = PFEM2DPoint(2, 60, -60); 
pt3 = PFEM2DPoint(3, 60, 60); 
pt4 = PFEM2DPoint(4, -60, 60); 

line1=PFEM2DLine(1,pt1,pt2); 
line2=PFEM2DLine(2,pt2,pt3); 
line3=PFEM2DLine(3,pt3,pt4); 
line4=PFEM2DLine(4,pt4,pt1); 

model = PFEM2DModel(); 
model.addObject(line1); 
model.addObject(line2); 
model.addObject(line3); 
model.addObject(line4); 

pom.addObject(model);

Après avoir lancé l'application, le script Jython peut être importé en cliquant sur le bouton « open » et en sélectionnant le fichier script. L'affichage doit ressembler à la figure suivante :

Image non disponible

Une copie écran peut ensuite être sauvegardée grâce au bouton « screenshot ».

Si la forme géométrique sort de l'écran suite à une manipulation de souris trop rapide, et n'est plus visible, l'affichage peut être recadré sur celle-ci en appuyant sur le bouton « resize ».

Enfin, l'application peut être fermée avec le bouton « close ».

VI. Conclusion

Ce tutoriel nous a expliqué comment créer un JPanel personnalisé, nous permettant d'afficher des formes simples telles que des points et des lignes. Plus qu'une simple adaptation de la classe JPanel, c'est une miniapplication graphique qui est définie dans cet article, que vous pouvez compléter et adapter afin de répondre à vos besoins d'affichage !

Les sources de cet article sont disponibles en suivant ce lien : src_PlegatFem2D_GUIPanel.zipsrc_PlegatFem2D_GUIPanel.zip (miroirmiroir).

Les icônes utilisées dans l'application exemple sont incluses dans les sources. Elles peuvent également être téléchargées sur le site www.fatcow.com (sous licence CC).

Remerciements

Merci à keulkeulkeulkeul pour ses commentaires, à ClaudeLELOUP et f-leb pour leur relecture orthographique, ainsi qu'à djibrildjibril pour son aide sur le KitOOoDVP.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 JM BORLOT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.