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. package plegatfem2d; 
  2.  
  3. import java.awt.BasicStroke; 
  4. import java.awt.Color; 
  5. import java.awt.Graphics; 
  6. import java.awt.Graphics2D; 
  7. import java.awt.event.MouseEvent; 
  8. import java.awt.event.MouseListener; 
  9. import java.awt.event.MouseMotionListener; 
  10. import java.awt.image.BufferedImage; 
  11. import java.io.File; 
  12. import java.io.IOException; 
  13. import javax.imageio.ImageIO; 
  14. import javax.swing.JFileChooser; 
  15. import javax.swing.JOptionPane; 
  16. import javax.swing.JPanel; 
  17.  
  18. /** 
  19.  * Classe définissant un JPanel personnalisé. 
  20.  * Ce JPanel permet l'affichage d'une grille 2D (axes X et Y, quadrillages horizontal et vertical) 
  21.  * ainsi que d'objets graphiques 2D définis par le programmeur. 
  22.  * L'affichage est interactif via la souris, l'utilisateur peut ainsi déplacer la vue, ainsi que zoomer 
  23.  * sur une zone particulière. 
  24.  *  
  25.  * @author Jean-Michel BORLOT 
  26.  */ 
  27. public class PFEM2DGuiPanel extends JPanel implements MouseListener, MouseMotionListener { 
  28.  
  29.     /** 
  30.      * Constructeur. 
  31.      */ 
  32.     public PFEM2DGuiPanel() { 
  33.         super(); 
  34.  
  35.         this.mode = VIEW; 
  36.  
  37.         this.offsetX = 0; 
  38.         this.offsetY = 0; 
  39.  
  40.         this.echelle = ECHELLE_BASE; 
  41.  
  42.         this.addMouseListener(this); 
  43.         this.addMouseMotionListener(this); 
  44.     } 
  45.      
  46.     private PFEM2DObjectManager pom;            // gestionnaire d'objets graphiques 
  47.     private int xold, yold;                     // coordonnées du point local précédent 
  48.     private int xstart, ystart;                 // coordonnées du point local initial 
  49.     private int mode;                           // mode d'interaction : VIEW, DRAG ou SCALE 
  50.     private double offsetX, offsetY;            // décalage de la vue 
  51.     private double echelle;                     // échelle de la vue 
  52.     private static int VIEW = 0;         
  53.     private static int DRAG = 1; 
  54.     private static int SCALE = 2; 
  55.     private static double ECHELLE_BASE = 100.;  // échelle de base 
  56.     private static double stepX=100, stepY=100; // pas des quadrillages sens X et sens Y 
  57.  
  58.     /** 
  59.      * Renvoie le gestionnaire d'objets graphiques 
  60.      * @return le gestionnaire d'objets graphiques 
  61.      */ 
  62.     public PFEM2DObjectManager getPom() { 
  63.         return pom; 
  64.     } 
  65.  
  66.     /** 
  67.      * Définit le gestionnaire d'objets graphiques 
  68.      * @param pom le gestionnaire d'objets graphiques 
  69.      */ 
  70.     public void setPom(PFEM2DObjectManager pom) { 
  71.         this.pom = pom; 
  72.     } 
  73. } 

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
  1.     /** 
  2.      * Convertit une abscisse réelle en abscisse locale (repère JPanel) 
  3.      * @param x l'abscisse réelle 
  4.      * @return l'abscisse locale 
  5.      */ 
  6.     public int getLocalCoordX(double x) { 
  7.  
  8.         return (int) Math.round(this.getWidth() / 2 + this.offsetX + x * this.echelle / ECHELLE_BASE); 
  9.  
  10.     } 
  11.  
  12.     /** 
  13.      * Convertit une ordonnée réelle en ordonnée locale (repère JPanel) 
  14.      * @param y l'ordonnée réelle 
  15.      * @return l'ordonnée locale 
  16.      */ 
  17.     public int getLocalCoordY(double y) { 
  18.  
  19.         return (int) Math.round(this.getHeight() / 2 + this.offsetY - y * this.echelle / ECHELLE_BASE); 
  20.  
  21.     } 
  22.  
  23.     /** 
  24.      * Convertit une abscisse locale (repère JPanel) en abscisse réelle 
  25.      * @param x l'abscisse locale 
  26.      * @return l'abscisse réelle 
  27.      */ 
  28.     public double getRealCoordX(double x) { 
  29.  
  30.         return (x - (this.getWidth() / 2 + this.offsetX)) * ECHELLE_BASE / this.echelle; 
  31.  
  32.     } 
  33.  
  34.     /** 
  35.      * Convertit une ordonnée locale (repère JPanel) en ordonnée réelle 
  36.      * @param y l'ordonnée locale 
  37.      * @return l'ordonnée réelle 
  38.      */ 
  39.     public double getRealCoordY(double y) { 
  40.  
  41.         return (y - (this.getHeight() / 2 + this.offsetY)) * -ECHELLE_BASE / this.echelle; 
  42.  
  43.     } 

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
  1.     // surcharge de la méthode paintComponent afin de dessiner notre vue personnalisée 
  2.     @Override 
  3.     public void paintComponent(Graphics g) { 
  4.  
  5.         Graphics2D g2 = (Graphics2D) g; 
  6.  
  7.         int h = this.getHeight(); 
  8.         int w = this.getWidth(); 
  9.  
  10.         g.setColor(Color.BLACK); 
  11.         g.fillRect(0, 0, w, h); 
  12.  
  13.         //tracé des axes et du quadrillage 
  14.  
  15.         int roundOffsetX = (int) Math.round(this.offsetX); 
  16.         int roundOffsetY = (int) Math.round(this.offsetY); 
  17.         int localStepX = (int) Math.round(this.echelle / ECHELLE_BASE * stepX); 
  18.         int localStepY = (int) Math.round(this.echelle / ECHELLE_BASE * stepY); 
  19.  
  20.         g.setColor(Color.GREEN); 
  21.         g.drawLine(w / 2 + roundOffsetX, 0, w / 2 + roundOffsetX, h); 
  22.         g.drawLine(0, h / 2 + roundOffsetY, w, h / 2 + roundOffsetY); 
  23.  
  24.         int offsetStrokeX = 7 - roundOffsetX % 7; 
  25.         int offsetStrokeY = 7 - roundOffsetY % 7; 
  26.  
  27.         float[] dash = {2, 5}; 
  28.         BasicStroke bsX = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 10, dash, offsetStrokeX); 
  29.         BasicStroke bsY = new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 10, dash, offsetStrokeY); 
  30.  
  31.         g.setColor(new Color(60, 85, 60)); 
  32.  
  33.         g2.setStroke(bsY); 
  34.  
  35.         int nbxp = (w - (w / 2 + roundOffsetX)) / localStepX + 1; 
  36.         int nbxm = (w / 2 + roundOffsetX) / localStepX + 1; 
  37.  
  38.         for (int i = 1; i < nbxp; i++) { 
  39.             int xline = w / 2 + roundOffsetX + (int) Math.round(i * stepX * this.echelle / ECHELLE_BASE); 
  40.             g.drawLine(xline, 0, xline, h); 
  41.  
  42.         } 
  43.         for (int i = 1; i < nbxm; i++) { 
  44.             int xline = w / 2 + roundOffsetX - (int) Math.round(i * stepX * this.echelle / ECHELLE_BASE); 
  45.             g.drawLine(xline, 0, xline, h); 
  46.         } 
  47.  
  48.         g2.setStroke(bsX); 
  49.  
  50.         int nbym = (h - (h / 2 + roundOffsetY)) / localStepY + 1; 
  51.         int nbyp = (h / 2 + roundOffsetY) / localStepY + 1; 
  52.  
  53.         for (int i = 1; i < nbyp; i++) { 
  54.             int yline = h / 2 + roundOffsetY - (int) Math.round(i * stepY * this.echelle / ECHELLE_BASE); 
  55.             g.drawLine(0, yline, w, yline); 
  56.         } 
  57.         for (int i = 1; i < nbym; i++) { 
  58.             int yline = h / 2 + roundOffsetY + (int) Math.round(i * stepY * this.echelle / ECHELLE_BASE); 
  59.             g.drawLine(0, yline, w, yline); 
  60.         } 
  61.  
  62.         g2.setStroke(new BasicStroke()); 
  63.  
  64.         if (this.pom != null) { 
  65.             this.pom.draw(g, this); 
  66.         } 
  67.     } 

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
  1.     // surcharge de la méthode mouseDragged. Gestion du déplacement souris avec bouton appuyé 
  2.     @Override 
  3.     public void mouseDragged(MouseEvent e) { 
  4.         if (this.mode == DRAG) { 
  5.             int x = e.getX(); 
  6.             int y = e.getY(); 
  7.  
  8.             this.offsetX = this.offsetX + (x - this.xold); 
  9.             this.offsetY = this.offsetY + (y - this.yold); 
  10.  
  11.             this.xold = x; 
  12.             this.yold = y; 
  13.  
  14.             this.repaint(); 
  15.         } else if (this.mode == SCALE) { 
  16.             int x = e.getX(); 
  17.             int y = e.getY(); 
  18.  
  19.             int dx = x - this.xold; 
  20.             int dy = y - this.yold; 
  21.  
  22.             int delta; 
  23.             if (Math.abs(dx) > Math.abs(dy)) { 
  24.                 delta = dx; 
  25.             } else { 
  26.                 delta = dy; 
  27.             } 
  28.  
  29.             double newEchelle = Math.max(1, this.echelle * Math.pow(1.01, delta)); 
  30.             double newOffsetX = this.offsetX + this.getRealCoordX(this.xstart) / ECHELLE_BASE * (this.echelle - newEchelle); 
  31.             double newOffsetY = this.offsetY - this.getRealCoordY(this.ystart) / ECHELLE_BASE * (this.echelle - newEchelle); 
  32.  
  33.             this.offsetX = newOffsetX; 
  34.             this.offsetY = newOffsetY; 
  35.             this.echelle = newEchelle; 
  36.  
  37.             this.xold = x; 
  38.             this.yold = y; 
  39.  
  40.             this.repaint(); 
  41.         } 
  42.     } 
  43.  
  44.     @Override 
  45.     public void mouseMoved(MouseEvent e) { 
  46.     } 
  47.  
  48.     @Override 
  49.     public void mouseClicked(MouseEvent e) { 
  50.     } 
  51.  
  52.     // surcharge de la méthode mousePressed. Gestion du clic souris. 
  53.     @Override 
  54.     public void mousePressed(MouseEvent e) { 
  55.         this.xold = e.getX(); 
  56.         this.yold = e.getY(); 
  57.  
  58.         if (e.getButton() == MouseEvent.BUTTON1) { 
  59.             this.mode = DRAG; 
  60.         } else if (e.getButton() == MouseEvent.BUTTON2) { 
  61.             this.mode = SCALE; 
  62.             this.xstart = e.getX(); 
  63.             this.ystart = e.getY(); 
  64.         } else { 
  65.             this.mode = VIEW; 
  66.         } 
  67.     } 
  68.  
  69.     // surcharge de la méthode mouseReleased. Gestion du lâcher de clic souris. 
  70.     @Override 
  71.     public void mouseReleased(MouseEvent e) { 
  72.         this.mode = VIEW; 
  73.     } 
  74.  
  75.     @Override 
  76.     public void mouseEntered(MouseEvent e) { 
  77.     } 
  78.  
  79.     @Override 
  80.     public void mouseExited(MouseEvent e) { 
  81.     } 

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
  1.      /** 
  2.      * Crée une capture d'écran de l'affichage courant. 
  3.      * Le fichier de sortie est défini par sélection du fichier via un JFileChooser. 
  4.      * Deux formats possibles en sortie: png ou jpg 
  5.      */ 
  6.     public void takeScreenshot() { 
  7.  
  8.         JFileChooser fc = new JFileChooser(); 
  9.  
  10.         int returnVal = fc.showOpenDialog(this); 
  11.  
  12.         if (returnVal == JFileChooser.APPROVE_OPTION) { 
  13.  
  14.             File fichier = fc.getSelectedFile(); 
  15.             String nomFichier = fichier.getName().toLowerCase(); 
  16.  
  17.             if ((nomFichier.endsWith("png")) || (nomFichier.endsWith("jpg"))) { 
  18.                 try { 
  19.                     BufferedImage bufImage = new BufferedImage(this.getSize().width, this.getSize().height, BufferedImage.TYPE_INT_RGB); 
  20.                     this.paint(bufImage.createGraphics()); 
  21.  
  22.                     String extension = "png"; 
  23.                     if (nomFichier.endsWith("jpg")) { 
  24.                         extension = "jpg"; 
  25.                     } 
  26.  
  27.                     ImageIO.write(bufImage, extension, fichier); 
  28.                 } catch (IOException ex) { 
  29.                     ex.printStackTrace(); 
  30.                 } 
  31.             } else { 
  32.                 JOptionPane.showMessageDialog(this, "Veuillez préciser l'extension du fichier image (png ou jpg)", 
  33.                         "Extension fichier manquante", JOptionPane.ERROR_MESSAGE); 
  34.             } 
  35.         } 
  36.     } 

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
  1.      /** 
  2.      * Centre la vue sur les éléments à afficher.  
  3.      * @param boundingBox tableau de 4 double: Xmin, Xmax, Ymin,Ymax, en coordonnées réelles 
  4.      */ 
  5.     public void centreView(double[] boundingBox) { 
  6.  
  7.         if ((boundingBox[1] - boundingBox[0] != Double.NEGATIVE_INFINITY) && (boundingBox[3] - boundingBox[2] != Double.NEGATIVE_INFINITY)) { 
  8.  
  9.             double echelleX = (this.getWidth() - 50) * ECHELLE_BASE / (boundingBox[1] - boundingBox[0]); 
  10.             double echelleY = (this.getHeight() - 50) * ECHELLE_BASE / (boundingBox[3] - boundingBox[2]); 
  11.  
  12.             this.echelle = Math.min(echelleX, echelleY); 
  13.              
  14.             this.offsetX = -(this.getLocalCoordX((boundingBox[1] + boundingBox[0]) / 2) - this.getLocalCoordX(0)); 
  15.             this.offsetY = -(this.getLocalCoordY((boundingBox[3] + boundingBox[2]) / 2) - this.getLocalCoordY(0)); 
  16.  
  17.             this.repaint(); 
  18.         } 
  19.     } 

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. package plegatfem2d; 
  2.  
  3. import java.awt.Graphics; 
  4. import java.io.BufferedReader; 
  5. import java.io.File; 
  6. import java.io.FileNotFoundException; 
  7. import java.io.FileReader; 
  8. import java.util.ArrayList; 
  9. import java.util.List; 
  10. import javax.script.Bindings; 
  11. import javax.script.ScriptContext; 
  12. import javax.script.ScriptEngine; 
  13. import javax.script.ScriptEngineManager; 
  14. import javax.script.ScriptException; 
  15. import plegatfem2d.objects.IPFEM2DDrawableObject; 
  16. import plegatfem2d.objects.PFEM2DPoint; 
  17.  
  18. /** 
  19.  * Classe définissant le gestionnaire d'objets graphiques. 
  20.  *  
  21.  * @author Jean-Michel BORLOT 
  22.  */ 
  23. public class PFEM2DObjectManager { 
  24.  
  25.     /** 
  26.      * Constructeur 
  27.      */ 
  28.     public PFEM2DObjectManager() { 
  29.         this.objects = new ArrayList<>(); 
  30.     } 
  31.  
  32.     /** 
  33.      * Méthode d'initialisation de la liste des objets gérés 
  34.      */ 
  35.     public void init() { 
  36.         this.objects.clear(); 
  37.     } 
  38.  
  39.     /** 
  40.      * Ajoute un objet graphique au gestionnaire 
  41.      * @param obj l'objet graphique 
  42.      */ 
  43.     public void addObject(IPFEM2DDrawableObject obj) { 
  44.         this.objects.add(obj); 
  45.     } 
  46.  
  47.     /** 
  48.      * Supprime un objet graphique du gestionnaire 
  49.      * @param rank le rang de l'objet graphique 
  50.      */ 
  51.     public void removeObject(int rank) { 
  52.         this.objects.remove(rank); 
  53.     } 
  54.  
  55.     /** 
  56.      * Supprime un objet graphique du gestionnaire 
  57.      * @param obj l'objet graphique 
  58.      */ 
  59.     public void removeObject(IPFEM2DDrawableObject obj) { 
  60.         this.objects.remove(obj); 
  61.     } 
  62.  
  63.     /** 
  64.      * Affiche les objets graphiques gérés par le gestionnaire 
  65.      * @param g l'objet Graphics sur lequel dessiner 
  66.      * @param panel l'objet PFEM2DGuiPanel dans lequel dessiner 
  67.      */ 
  68.     public void draw(Graphics g, PFEM2DGuiPanel panel) { 
  69.          
  70.         for (IPFEM2DDrawableObject current : this.objects) { 
  71.             current.draw(g,panel); 
  72.         } 
  73.     } 
  74.  
  75.     /** 
  76.      * Méthode de mise à jour des identités des objets graphiques de type PFEM2DPoint 
  77.      */ 
  78.     public void updateId() { 
  79.  
  80.         this.idCurrentPoint = 1; 
  81.  
  82.         for (int i = 0; i < this.objects.size(); i++) { 
  83.  
  84.             IPFEM2DDrawableObject obj = this.objects.get(i); 
  85.  
  86.             if (obj instanceof IPFEM2DDrawableObject) { 
  87.  
  88.                 PFEM2DPoint[] pts = obj.getPoints(); 
  89.  
  90.                 if (pts != null) { 
  91.  
  92.                     for (int j = 0; j < pts.length; j++) { 
  93.                         long id = pts[j].getNumId(); 
  94.  
  95.                         if (id > this.idCurrentPoint) { 
  96.                             idCurrentPoint = id; 
  97.                         } 
  98.                     } 
  99.                 } 
  100.             } 
  101.         } 
  102.     } 
  103.      
  104.     private List<IPFEM2DDrawableObject> objects;    // liste des objets graphiques 
  105.     private long idCurrentPoint;                    // identité courante pour les objets de type PFEM2DPoint 
  106.  
  107.     /** 
  108.      * Renvoie l'identité courante des objets de type PFEM2DPoint 
  109.      * @return l'identité courante 
  110.      */ 
  111.     public long getIdCurrentPoint() { 
  112.         idCurrentPoint++; 
  113.         return idCurrentPoint; 
  114.     } 
  115.  
  116.     /** 
  117.      * Incrémente l'identité courante des objets de type PFEM2DPoint 
  118.      */ 
  119.     public void incrementIdCurrentPoint() { 
  120.         idCurrentPoint++; 
  121.     } 
  122.  
  123.     /** 
  124.      * Définit l'identité courante des objets de type PFEM2DPoint 
  125.      * @param idCurrentPoint 
  126.      */ 
  127.     public void setIdCurrentPoint(long idCurrentPoint) { 
  128.         this.idCurrentPoint = idCurrentPoint; 
  129.     } 
  130.  
  131.     /** 
  132.      * Renvoie les valeurs Xmin/max et Ymin/max de la boite englobante des objets graphiques 
  133.      * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax 
  134.      */ 
  135.     public double[] getBoundingBox() { 
  136.  
  137.         double data[] = new double[4]; 
  138.  
  139.         data[0] = Double.POSITIVE_INFINITY;     // x min 
  140.         data[1] = Double.NEGATIVE_INFINITY;     // x max 
  141.         data[2] = Double.POSITIVE_INFINITY;     // y min 
  142.         data[3] = Double.NEGATIVE_INFINITY;     // y max 
  143.  
  144.         for (IPFEM2DDrawableObject current : this.objects) { 
  145.              
  146.             double[] bb = current.getBoundingBox(); 
  147.  
  148.             if (bb[0] < data[0]) { 
  149.                 data[0] = bb[0]; 
  150.             } 
  151.             if (bb[1] > data[1]) { 
  152.                 data[1] = bb[1]; 
  153.             } 
  154.             if (bb[2] < data[2]) { 
  155.                 data[2] = bb[2]; 
  156.             } 
  157.             if (bb[3] > data[3]) { 
  158.                 data[3] = bb[3]; 
  159.             } 
  160.         } 
  161.  
  162.         return data; 
  163.     } 
  164. } 

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
  1.     /** 
  2.      * Ouvre un fichier de script Jython permettant de définir le contenu du gestionnaire d'objets 
  3.      * graphiques. 
  4.      * @param fichier le fichier script Jython 
  5.      * @return true si l'importation s'est déroulée correctement, false sinon 
  6.      */ 
  7.     public boolean openFile(File fichier) { 
  8.  
  9.         this.init(); 
  10.  
  11.         ScriptEngineManager manager = new ScriptEngineManager(); 
  12.         ScriptEngine moteur = manager.getEngineByName("jython"); 
  13.  
  14.         if (moteur == null) { 
  15.             System.out.println("Impossible de trouver le moteur de scripting recherché."); 
  16.             return false; 
  17.         } else { 
  18.             try { 
  19.                 Bindings bindings = moteur.getBindings(ScriptContext.ENGINE_SCOPE); 
  20.                 bindings.clear(); 
  21.                 bindings.put("pom", this); 
  22.  
  23.                 moteur.eval(new BufferedReader(new FileReader(fichier))); 
  24.  
  25.                 System.out.println("end of file importation"); 
  26.             } catch (FileNotFoundException | ScriptException ex) { 
  27.                 ex.printStackTrace(); 
  28.                 return false; 
  29.             } 
  30.         } 
  31.  
  32.         return true; 
  33.     } 

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. package plegatfem2d.objects; 
  2.  
  3. import java.awt.Graphics; 
  4. import plegatfem2d.PFEM2DGuiPanel; 
  5.  
  6. /** 
  7.  * Interface définissant les objets graphiques 
  8.  * @author Jean-Michel BORLOT 
  9.  */ 
  10. public interface IPFEM2DDrawableObject { 
  11.  
  12.     /** 
  13.      * Dessine l'objet graphique 
  14.      * @param g l'objet Graphics sur lequel dessiner 
  15.      * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner 
  16.      */ 
  17.     public void draw(Graphics g, PFEM2DGuiPanel panel); 
  18.  
  19.     /** 
  20.      * Définit la visibilité de l'objet graphique 
  21.      * @param flag true si l'objet est visible, false s'il ne l'est pas 
  22.      */ 
  23.     public void setVisible(boolean flag); 
  24.  
  25.     /** 
  26.      * Renvoie l'état de visibilité de l'objet graphique 
  27.      * @return true si l'objet est visible, false s'il ne l'est pas 
  28.      */ 
  29.     public boolean isVisible(); 
  30.      
  31.     /** 
  32.      * Renvoie la liste des points définissant l'objet graphique 
  33.      * @return la liste des points 
  34.      */ 
  35.     public PFEM2DPoint[] getPoints(); 
  36.      
  37.     /** 
  38.      * Renvoie l'identité de l'objet graphique 
  39.      * @return l'identité de l'objet graphique 
  40.      */ 
  41.     public String getId(); 
  42.      
  43.     /** 
  44.      * Renvoie la boite englobante de l'objet graphique 
  45.      * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax 
  46.      */ 
  47.     public double[] getBoundingBox(); 
  48.      
  49. } 

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. package plegatfem2d.objects; 
  2.  
  3. /** 
  4.  * Interface de définition des objets de type "courbe" 
  5.  * @author Jean-Michel BORLOT 
  6.  */ 
  7. public interface IPFEM2DCurve { 
  8.     
  9.     /** 
  10.      * Renvoie le point de départ de la courbe 
  11.      * @return le point de départ de la courbe 
  12.      */ 
  13.     public PFEM2DPoint getStartPoint(); 
  14.  
  15.     /** 
  16.      * Renvoie le point d'arrivée de la courbe 
  17.      * @return le point d'arrivée de la courbe 
  18.      */ 
  19.     public PFEM2DPoint getEndPoint();     
  20. } 

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. package plegatfem2d.objects; 
  2.  
  3. import java.awt.Graphics; 
  4. import java.util.ArrayList; 
  5. import java.util.Arrays; 
  6. import java.util.List; 
  7. import plegatfem2d.PFEM2DGuiPanel; 
  8.  
  9. /** 
  10.  * Classe définissant l'objet "racine", composé d'un assemblage d'objets 
  11.  * graphiques 
  12.  * 
  13.  * @author Jean-Michel BORLOT 
  14.  */ 
  15. public class PFEM2DModel implements IPFEM2DDrawableObject { 
  16.  
  17.     private List<IPFEM2DDrawableObject> objects = new ArrayList<>();   // liste des objets constituant l'objet "racine" 
  18.     private boolean visible = true;                                         // état de visibilité de l'objet 
  19.  
  20.     /** 
  21.      * Constructeur 
  22.      */ 
  23.     public PFEM2DModel() { 
  24.         this.objects.clear(); 
  25.     } 
  26.  
  27.     /** 
  28.      * Ajoute un objet graphique à l'objet racine 
  29.      * 
  30.      * @param obj l'objet graphique 
  31.      */ 
  32.     public void addObject(IPFEM2DDrawableObject obj) { 
  33.         this.objects.add(obj); 
  34.     } 
  35.  
  36.     /** 
  37.      * Supprime un objet graphique de l'objet racine 
  38.      * 
  39.      * @param obj l'objet graphique 
  40.      */ 
  41.     public void removeObject(IPFEM2DDrawableObject obj) { 
  42.         this.objects.remove(obj); 
  43.     } 
  44.  
  45.     /** 
  46.      * Dessine l'objet graphique 
  47.      * 
  48.      * @param g l'objet Graphics sur lequel dessiner 
  49.      * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner 
  50.      */ 
  51.     @Override 
  52.     public void draw(Graphics g, PFEM2DGuiPanel panel) { 
  53.  
  54.         for (IPFEM2DDrawableObject current : this.objects) { 
  55.             if (current.isVisible()) { 
  56.                 current.draw(g, panel); 
  57.             } 
  58.         } 
  59.     } 
  60.  
  61.     /** 
  62.      * Définit la visibilité de l'objet graphique 
  63.      * 
  64.      * @param flag true si l'objet est visible, false s'il ne l'est pas 
  65.      */ 
  66.     @Override 
  67.     public void setVisible(boolean flag) { 
  68.         this.visible = flag; 
  69.     } 
  70.  
  71.     /** 
  72.      * Renvoie l'état de visibilité de l'objet graphique 
  73.      * 
  74.      * @return true si l'objet est visible, false s'il ne l'est pas 
  75.      */ 
  76.     @Override 
  77.     public boolean isVisible() { 
  78.         return this.visible; 
  79.     } 
  80.  
  81.     /** 
  82.      * Renvoie l'identité de l'objet graphique 
  83.      * 
  84.      * @return l'identité de l'objet graphique 
  85.      */ 
  86.     @Override 
  87.     public String getId() { 
  88.         return "Root"; 
  89.     } 
  90.  
  91.     /** 
  92.      * Renvoie la boite englobante de l'objet graphique 
  93.      * 
  94.      * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax 
  95.      */ 
  96.     @Override 
  97.     public double[] getBoundingBox() { 
  98.  
  99.         double data[] = new double[4]; 
  100.  
  101.         data[0] = Double.POSITIVE_INFINITY; 
  102.         data[1] = Double.NEGATIVE_INFINITY; 
  103.         data[2] = Double.POSITIVE_INFINITY; 
  104.         data[3] = Double.NEGATIVE_INFINITY; 
  105.  
  106.         for (IPFEM2DDrawableObject current : this.objects) { 
  107.  
  108.             double bb[] = current.getBoundingBox(); 
  109.  
  110.             if (bb[0] < data[0]) { 
  111.                 data[0] = bb[0]; 
  112.             } 
  113.             if (bb[1] > data[1]) { 
  114.                 data[1] = bb[1]; 
  115.             } 
  116.             if (bb[2] < data[2]) { 
  117.                 data[2] = bb[2]; 
  118.             } 
  119.             if (bb[3] > data[3]) { 
  120.                 data[3] = bb[3]; 
  121.             } 
  122.         } 
  123.  
  124.         return data; 
  125.     } 
  126.  
  127.     /** 
  128.      * Renvoie la liste des points définissant l'objet graphique 
  129.      * 
  130.      * @return la liste des points 
  131.      */ 
  132.     @Override 
  133.     public PFEM2DPoint[] getPoints() { 
  134.         ArrayList<PFEM2DPoint> listPts = new ArrayList<>(); 
  135.  
  136.         for (IPFEM2DDrawableObject current : this.objects) { 
  137.             PFEM2DPoint[] pts = current.getPoints(); 
  138.             listPts.addAll(Arrays.asList(pts)); 
  139.         } 
  140.  
  141.         return listPts.toArray(new PFEM2DPoint[listPts.size()]); 
  142.     } 
  143. } 

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. package plegatfem2d.objects; 
  2.  
  3. import java.awt.Color; 
  4. import java.awt.Graphics; 
  5. import plegatfem2d.PFEM2DGuiPanel; 
  6.  
  7. /** 
  8.  * Classe définissant un objet graphique de type "point" 
  9.  * @author Jean-Michel BORLOT 
  10.  */ 
  11. public class PFEM2DPoint implements IPFEM2DDrawableObject { 
  12.  
  13.     private double x, y; 
  14.     private long id; 
  15.     private boolean visible; 
  16.      
  17.     /** 
  18.      * Constructeur 
  19.      * @param id l'identité du point 
  20.      * @param x l'abscisse du point 
  21.      * @param y l'ordonnée du point 
  22.      */ 
  23.     public PFEM2DPoint(long id, double x, double y) { 
  24.         this.id = id; 
  25.         this.x = x; 
  26.         this.y = y; 
  27.         this.visible = true; 
  28.     } 
  29.  
  30.     /** 
  31.      * Constructeur par défaut 
  32.      */ 
  33.     public PFEM2DPoint() { 
  34.         this.id = 0; 
  35.         this.x = 0; 
  36.         this.y = 0; 
  37.         this.visible = false; 
  38.     } 
  39.  
  40.     /** 
  41.      * Définit l'identité du point 
  42.      * @param id l'identité du point 
  43.      */ 
  44.     public void setId(long id) { 
  45.         this.id = id; 
  46.     } 
  47.  
  48.     /** 
  49.      * Renvoie l'abscisse du point 
  50.      * @return l'abscisse du point 
  51.      */ 
  52.     public double getX() { 
  53.         return x; 
  54.     } 
  55.  
  56.     /** 
  57.      * Définit l'abscisse du point 
  58.      * @param x l'abscisse du point 
  59.      */ 
  60.     public void setX(double x) { 
  61.         this.x = x; 
  62.     } 
  63.  
  64.     /** 
  65.      * Renvoie l'ordonnée du point 
  66.      * @return l'ordonnée du point 
  67.      */ 
  68.     public double getY() { 
  69.         return y; 
  70.     } 
  71.  
  72.     /** 
  73.      * Définit l'ordonnée du point 
  74.      * @param y l'ordonnée du point 
  75.      */ 
  76.     public void setY(double y) { 
  77.         this.y = y; 
  78.     } 
  79.  
  80.     /** 
  81.      * Dessine l'objet graphique 
  82.      * @param g l'objet Graphics sur lequel dessiner 
  83.      * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner 
  84.      */ 
  85.     @Override 
  86.     public void draw(Graphics g, PFEM2DGuiPanel panel) { 
  87.  
  88.         if (this.isVisible()) { 
  89.             int xloc = panel.getLocalCoordX(this.x); 
  90.             int yloc = panel.getLocalCoordY(this.y); 
  91.  
  92.             int[] xPoints = {xloc + 5, xloc - 5, xloc - 5, xloc + 5, xloc + 5}; 
  93.             int[] yPoints = {yloc + 5, yloc + 5, yloc - 5, yloc - 5, yloc + 5}; 
  94.  
  95.             g.setColor(Color.cyan); 
  96.             g.drawPolyline(xPoints, yPoints, 5); 
  97.  
  98.             // dessin id 
  99.              
  100.             g.drawString("P" + this.id, xloc + 10, yloc + 10); 
  101.         } 
  102.  
  103.     } 
  104.  
  105.     /** 
  106.      * Définit la visibilité de l'objet graphique 
  107.      * @param flag true si l'objet est visible, false s'il ne l'est pas 
  108.      */ 
  109.     @Override 
  110.     public void setVisible(boolean flag) { 
  111.         this.visible = flag; 
  112.     } 
  113.  
  114.     /** 
  115.      * Renvoie l'état de visibilité de l'objet graphique 
  116.      * @return true si l'objet est visible, false s'il ne l'est pas 
  117.      */ 
  118.     @Override 
  119.     public boolean isVisible() { 
  120.         return this.visible; 
  121.     } 
  122.  
  123.     /** 
  124.      * Renvoie l'identité de l'objet graphique 
  125.      * @return l'identité de l'objet graphique 
  126.      */ 
  127.     @Override 
  128.     public String getId() { 
  129.         return "Point " + this.id; 
  130.     } 
  131.  
  132.     /** 
  133.      * Renvoie l'identité sous format numérique de l'objet graphique 
  134.      * @return l'identité sous format numérique de l'objet graphique 
  135.      */ 
  136.     public long getNumId() { 
  137.         return this.id; 
  138.     } 
  139.  
  140.     /** 
  141.      * Renvoie la distance du point à un autre point 
  142.      * @param pt l'autre point 
  143.      * @return la distance 
  144.      */ 
  145.     public double getDistanceTo(PFEM2DPoint pt) { 
  146.         return Math.sqrt(Math.pow(pt.getX() - this.getX(), 2) + Math.pow(pt.getY() - this.getY(), 2)); 
  147.     } 
  148.  
  149.     /** 
  150.      * Constructeur par rotation du point courant  
  151.      * @param centre le centre de la rotation 
  152.      * @param angle l'angle de la rotation 
  153.      * @return le nouveau point créé 
  154.      */ 
  155.     public PFEM2DPoint getRotated(PFEM2DPoint centre, double angle) { 
  156.  
  157.         double dx = this.getX() - centre.getX(); 
  158.         double dy = this.getY() - centre.getY(); 
  159.  
  160.         double angleBase = Math.atan2(dy, dx); 
  161.         double radius = centre.getDistanceTo(this); 
  162.  
  163.         double xRot = centre.getX() + radius * Math.cos(angleBase + angle / 180. * Math.PI); 
  164.         double yRot = centre.getY() + radius * Math.sin(angleBase + angle / 180. * Math.PI); 
  165.  
  166.         return new PFEM2DPoint(0, xRot, yRot); 
  167.     } 
  168.  
  169.     // méthode toString 
  170.     @Override 
  171.     public String toString() { 
  172.          
  173.         return "Point #"+this.id+", x/y="+this.x+"/"+this.y; 
  174.          
  175.     } 
  176.  
  177.     /** 
  178.      * Renvoie la boite englobante de l'objet graphique 
  179.      * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax 
  180.      */ 
  181.     @Override 
  182.     public double[] getBoundingBox() { 
  183.         return new double[]{this.x,this.x,this.y,this.y}; 
  184.     } 
  185.  
  186.     /** 
  187.      * Renvoie la liste des points définissant l'objet graphique 
  188.      * @return la liste des points 
  189.      */ 
  190.     @Override 
  191.     public PFEM2DPoint[] getPoints() { 
  192.         return new PFEM2DPoint[]{this}; 
  193.     } 
  194.      
  195. } 

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. package plegatfem2d.objects; 
  2.  
  3. import java.awt.Color; 
  4. import java.awt.Graphics; 
  5. import plegatfem2d.PFEM2DGuiPanel; 
  6.  
  7. /** 
  8.  * Classe définissant un objet graphique de type "ligne", reliant deux points. 
  9.  * @author Jean-Michel BORLOT 
  10.  */ 
  11. public class PFEM2DLine implements IPFEM2DDrawableObject, IPFEM2DCurve { 
  12.  
  13.     private PFEM2DPoint pt1, pt2;   // les deux points définissant la ligne  
  14.     private int id;                 // l'identité de la ligne 
  15.     private boolean visible;        // l'état de visibilité de la ligne 
  16.      
  17.     /** 
  18.      * Constructeur 
  19.      * @param id identité 
  20.      * @param pt1 point n°1 
  21.      * @param pt2 point n°2 
  22.      */ 
  23.     public PFEM2DLine(int id, PFEM2DPoint pt1, PFEM2DPoint pt2) { 
  24.         this.pt1 = pt1; 
  25.         this.pt2 = pt2; 
  26.         this.id = id; 
  27.         this.visible = true; 
  28.     } 
  29.  
  30.     /** 
  31.      * Définit l'identité de la ligne 
  32.      * @param id l'identité de la ligne 
  33.      */ 
  34.     public void setId(int id) { 
  35.         this.id = id; 
  36.     } 
  37.  
  38.     /** 
  39.      * Renvoie le premier point 
  40.      * @return le premier point 
  41.      */ 
  42.     public PFEM2DPoint getPt1() { 
  43.         return pt1; 
  44.     } 
  45.  
  46.     /** 
  47.      * Définit le premier point 
  48.      * @param pt1 le premier point 
  49.      */ 
  50.     public void setPt1(PFEM2DPoint pt1) { 
  51.         this.pt1 = pt1; 
  52.     } 
  53.  
  54.     /** 
  55.      * Renvoie le second point 
  56.      * @return le second point 
  57.      */ 
  58.     public PFEM2DPoint getPt2() { 
  59.         return pt2; 
  60.     } 
  61.  
  62.     /** 
  63.      * Définit le second point 
  64.      * @param pt2 le second point 
  65.      */ 
  66.     public void setPt2(PFEM2DPoint pt2) { 
  67.         this.pt2 = pt2; 
  68.     } 
  69.  
  70.     /** 
  71.      * Dessine l'objet graphique 
  72.      * @param g l'objet Graphics sur lequel dessiner 
  73.      * @param panel l'objet PFEM2DGuiPanel sur lequel dessiner 
  74.      */ 
  75.     @Override 
  76.     public void draw(Graphics g, PFEM2DGuiPanel panel) { 
  77.  
  78.         if (this.isVisible()) { 
  79.             int xloc1 = panel.getLocalCoordX(this.pt1.getX()); 
  80.             int yloc1 = panel.getLocalCoordY(this.pt1.getY()); 
  81.  
  82.             int xloc2 = panel.getLocalCoordX(this.pt2.getX()); 
  83.             int yloc2 = panel.getLocalCoordY(this.pt2.getY()); 
  84.  
  85.             g.setColor(Color.yellow); 
  86.             g.drawLine(xloc1, yloc1, xloc2, yloc2); 
  87.         } 
  88.     } 
  89.  
  90.     /** 
  91.      * Définit la visibilité de l'objet graphique 
  92.      * @param flag true si l'objet est visible, false s'il ne l'est pas 
  93.      */ 
  94.     @Override 
  95.     public void setVisible(boolean flag) { 
  96.         this.visible = flag; 
  97.     } 
  98.  
  99.     /** 
  100.      * Renvoie l'état de visibilité de l'objet graphique 
  101.      * @return true si l'objet est visible, false s'il ne l'est pas 
  102.      */ 
  103.     @Override 
  104.     public boolean isVisible() { 
  105.         return this.visible; 
  106.     } 
  107.  
  108.     /** 
  109.      * Renvoie l'identité de l'objet graphique 
  110.      * @return l'identité de l'objet graphique 
  111.      */ 
  112.     @Override 
  113.     public String getId() { 
  114.         return "Line " + this.id; 
  115.     } 
  116.  
  117.     /** 
  118.      * Renvoie le point de départ de la courbe 
  119.      * @return le point de départ de la courbe 
  120.      */ 
  121.     @Override 
  122.     public PFEM2DPoint getStartPoint() { 
  123.         return this.pt1; 
  124.     } 
  125.  
  126.     /** 
  127.      * Renvoie le point d'arrivée de la courbe 
  128.      * @return le point d'arrivée de la courbe 
  129.      */ 
  130.     @Override 
  131.     public PFEM2DPoint getEndPoint() { 
  132.         return this.pt2; 
  133.     } 
  134.  
  135.     /** 
  136.      * Renvoie la boite englobante de l'objet graphique 
  137.      * @return un tableau de 4 double: Xmin, Xmax, Ymin, Ymax 
  138.      */ 
  139.     @Override 
  140.     public double[] getBoundingBox() { 
  141.          
  142.         double data[]=new double[4]; 
  143.          
  144.         data[0]=Math.min(this.pt1.getX(),this.pt2.getX()); 
  145.         data[1]=Math.max(this.pt1.getX(),this.pt2.getX()); 
  146.         data[2]=Math.min(this.pt1.getY(),this.pt2.getY()); 
  147.         data[3]=Math.max(this.pt1.getY(),this.pt2.getY()); 
  148.          
  149.         return data; 
  150.     } 
  151.  
  152.     /** 
  153.      * Renvoie la liste des points définissant l'objet graphique 
  154.      * @return la liste des points 
  155.      */ 
  156.     @Override 
  157.     public PFEM2DPoint[] getPoints() { 
  158.         return new PFEM2DPoint[]{this.pt1,this.pt2}; 
  159.     } 
  160. } 

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. package plegatfem2d; 
  2.  
  3. import java.awt.BorderLayout; 
  4. import java.awt.Dimension; 
  5. import java.awt.event.ActionEvent; 
  6. import java.awt.event.ActionListener; 
  7. import java.io.File; 
  8. import javax.swing.JButton; 
  9. import javax.swing.JFileChooser; 
  10. import javax.swing.JFrame; 
  11. import javax.swing.JSeparator; 
  12. import javax.swing.JToolBar; 
  13.  
  14. /** 
  15.  * Classe de démonstration du panneau personnalisé PFEM2DGuiPanel. Crée une 
  16.  * fenêtre incorporant une barre d'outil simple (ouvrir un fichier, capture 
  17.  * d'écran, recentrer la vue, et fermer la fenêtre) et un élément 
  18.  * PFEM2DGuiPanel. 
  19.  * 
  20.  * @author Jean-Michel BORLOT 
  21.  */ 
  22. public class PlegatFem2D_GuiPanel extends JFrame { 
  23.  
  24.     /** 
  25.      * Constructeur. 
  26.      * 
  27.      * @param frameTitle le titre apparaissant dans la barre de la fenêtre 
  28.      */ 
  29.     public PlegatFem2D_GuiPanel(String frameTitle) { 
  30.         super(frameTitle); 
  31.         this.initComponents(); 
  32.     } 
  33.  
  34.     private void initComponents() { 
  35.  
  36.         final PFEM2DObjectManager pom = new PFEM2DObjectManager(); 
  37.  
  38.         this.pfguipanel = new PFEM2DGuiPanel(); 
  39.         this.pfguipanel.setPreferredSize(new Dimension(800, 600)); 
  40.         this.add(this.pfguipanel, BorderLayout.CENTER); 
  41.  
  42.         JToolBar toolbar = new JToolBar(); 
  43.         toolbar.setPreferredSize(new Dimension(100, 40)); 
  44.         toolbar.setFloatable(false); 
  45.  
  46.         // bouton open 
  47.  
  48.         JButton jbOpen = new JButton("open", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/add.png"))); 
  49.         jbOpen.addActionListener(new ActionListener() { 
  50.             @Override 
  51.             public void actionPerformed(ActionEvent e) { 
  52.  
  53.                 JFileChooser fc = new JFileChooser(); 
  54.                 int returnVal = fc.showOpenDialog(pfguipanel.getParent()); 
  55.  
  56.                 if (returnVal == JFileChooser.APPROVE_OPTION) { 
  57.  
  58.                     File fichier = fc.getSelectedFile(); 
  59.                     boolean flag = pom.openFile(fichier); 
  60.  
  61.                     if (flag) { 
  62.                         pfguipanel.repaint(); 
  63.                     } 
  64.                 } 
  65.             } 
  66.         }); 
  67.         toolbar.add(jbOpen); 
  68.  
  69.         // bouton screenshot 
  70.  
  71.         JButton jbScreen = new JButton("screenshot", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/snapshot.png"))); 
  72.         jbScreen.addActionListener(new ActionListener() { 
  73.             @Override 
  74.             public void actionPerformed(ActionEvent e) { 
  75.                 pfguipanel.takeScreenshot(); 
  76.             } 
  77.         }); 
  78.         toolbar.add(jbScreen); 
  79.  
  80.         // bouton resize 
  81.  
  82.         JButton jbResize = new JButton("resize", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/resize_picture.png"))); 
  83.         jbResize.addActionListener(new ActionListener() { 
  84.             @Override 
  85.             public void actionPerformed(ActionEvent e) { 
  86.                 pfguipanel.centreView(pom.getBoundingBox()); 
  87.             } 
  88.         }); 
  89.         toolbar.add(jbResize); 
  90.  
  91.  
  92.         toolbar.add(new JSeparator(JSeparator.VERTICAL)); 
  93.  
  94.         // bouton close 
  95.  
  96.         JButton jbClose = new JButton("close", new javax.swing.ImageIcon(getClass().getResource("/ressources/icons/cross.png"))); 
  97.         jbClose.addActionListener(new ActionListener() { 
  98.             @Override 
  99.             public void actionPerformed(ActionEvent e) { 
  100.                 dispose(); 
  101.             } 
  102.         }); 
  103.         toolbar.add(jbClose); 
  104.  
  105.         this.add(toolbar, BorderLayout.NORTH); 
  106.         this.pack(); 
  107.  
  108.         this.setDefaultCloseOperation(EXIT_ON_CLOSE); 
  109.  
  110.         this.pfguipanel.setPom(pom); 
  111.  
  112.     } 
  113.     private PFEM2DGuiPanel pfguipanel; 
  114.  
  115.     /** 
  116.      * Main class. 
  117.      * 
  118.      * @param args the command line arguments 
  119.      */ 
  120.     public static void main(String[] args) { 
  121.  
  122.         java.awt.EventQueue.invokeLater(new Runnable() { 
  123.             @Override 
  124.             public void run() { 
  125.                 new PlegatFem2D_GuiPanel("Demo PFEM - GUI Panel").setVisible(true); 
  126.             } 
  127.         }); 
  128.     } 
  129. } 

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. from plegatfem2d import *  
  2. from plegatfem2d.objects import *  
  3.  
  4. # définition du modèle  
  5.  
  6. pt1 = PFEM2DPoint(1, -60, -60);  
  7. pt2 = PFEM2DPoint(2, 60, -60);  
  8. pt3 = PFEM2DPoint(3, 60, 60);  
  9. pt4 = PFEM2DPoint(4, -60, 60);  
  10.  
  11. line1=PFEM2DLine(1,pt1,pt2);  
  12. line2=PFEM2DLine(2,pt2,pt3);  
  13. line3=PFEM2DLine(3,pt3,pt4);  
  14. line4=PFEM2DLine(4,pt4,pt1);  
  15.  
  16. model = PFEM2DModel();  
  17. model.addObject(line1);  
  18. model.addObject(line2);  
  19. model.addObject(line3);  
  20. model.addObject(line4);  
  21.  
  22. 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.