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 :
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 :
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 :
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 :
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 :
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 :
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.
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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.