Programmation Java Swing/Dessin d'une étoile
Le dessin d'un composant utilise différente formes de base : lignes, rectangles, polygones. Le but de cette section est de dessiner des étoiles de cette forme :
Afin d'avoir un composant générique permettant d'afficher le texte sous une série d'étoiles correspondant à un niveau d'appréciation, un niveau de qualité, ...
Composant
modifierLa classe DisplayLevel
ci-dessous définit le composant : état courant et couleurs utilisées.
Elle sera complétée progressivement au cours de ce chapitre.
package org.wikibooks.fr.components;
import java.awt.*;
import javax.swing.*;
/**
* Afficher un niveau avec une série d'étoiles.
* @author fr.wikibooks.org
*/
public class DisplayLevel extends JComponent
{
// Couleurs prédéfinies
protected static final Color
C_LINE = Color.BLACK,
C_LINE_OFF = Color.LIGHT_GRAY,
C_TEXT = Color.BLACK,
C_FILL = new Color(240,176,20);
protected int
maxlevel, // Le nombre total d'étoiles.
level; // Le nombre d'étoiles pleines.
protected String
level_name; // Texte de niveau d'appréciation/qualité/...
// Couleurs utilisées
protected Color
c_line = C_LINE,
c_line_off = C_LINE_OFF,
c_text = C_TEXT;
public DisplayLevel()
{
setForeground(C_FILL);
setMinimumSize(new Dimension(400,50));
setPreferredSize(new Dimension(500,50));
setSize(new Dimension(500,50));
}
/**
* Modifier le niveau affiché.
* @param level Niveau affiché.
* @param max Niveau max.
* @param name Texte affiché.
*/
public void setLevel(int level, int max, String name)
{
this.maxlevel = max;
this.level = level;
this.level_name = name;
repaint();
}
// ...
}
Dessin d'une étoile
modifierLes lignes de la forme n'étant pas courbées, le dessin d'un polygone sera utilisé.
Le polygone sera composé de 10 points :
- 5 points pour les pointes, à la distance r du centre du cercle englobant (voir image ci-contre).
- 5 points entre les pointes, à la distance s du centre du cercle englobant (voir image ci-contre).
La distance r représente le rayon du cercle englobant qui déterminera la taille des étoiles dessinées. La distance s est calculable à partir du rayon r par la formule suivante :
Avec
Les points étant radialement espacés régulièrement, l'angle entre deux points est de degrés.
Coordonnées du polygone
modifierLes coordonnées des points du polygone sont pré-calculées et stockées dans un tableau pour un rayon de 10000 pixels. Ces coordonnées de base servent au calcul des coordonnées selon la taille d'étoile voulue, et sont relatives au centre du cercle situé aux coordonnées (0,0).
private static final int[]
SHAPE_X={ 9511, 2245, 0, -2245, -9511, -3633, -5878, 0, 5878, 3633 },
SHAPE_Y={ -3090, -3090, -10000, -3090, -3090, 1180, 8090, 3820, 8090, 1180 };
Un objet de la classe java.awt.Polygon
est utilisé pour représenter le polygone adapté à la taille de l'étoile selon celle du composant.
Les points sont calculés en multipliant les coordonnées pré-calculées des 10 points par le rayon voulu et en divisant le résultat par 10000.
Pour les points intermédiaires, on peut aussi ajouter un coefficient pour changer la forme de l'étoile :
-
Dessin d'une étoile avec coefficient de 80%.
-
Dessin d'une étoile avec coefficient de 100%.
-
Dessin d'une étoile avec coefficient de 150%.
private Polygon shape; // Polygone calculé pour une certaine taille, null signifiant à recalculer.
private static final int SHAPE_NORMAL = 100; // Coefficient nominal pour la forme de l'étoile.
private int
shape_size = -1, // La taille courante pour le polygone shape.
shape_x, // Abscisse du point de référence.
shape_y, // Ordonnée du point de référence.
star_shape = SHAPE_NORMAL; // Facteur de forme de l'étoile.
/**
* Modifier la forme de l'étoile dessinée.
* @param factor_percent Facteur de forme en pourcentage :<ul>
* <li>inférieur à 100 pour une étoile plus fine,</li>
* <li>supérieur à 100 pour une étoile plus grosse.</li>
* </ul>
*/
public void setStarShape(int factor_percent)
{
if (star_shape != factor_percent)
{
star_shape = factor_percent;
shape = null; // Polygone à recalculer
shape_size = -1; // Polygone à recalculer
repaint();
}
}
Calcul du polygone
modifierLe polygone est calculé, si besoin, au moment de dessiner le composant :
/**
* Obtenir le polygone à utiliser pour la taille voulue.
* Si besoin, cette méthode recalcule le polygone.
* @param size Taille d'étoile.
* @return Le polygone à utiliser.
*/
private Polygon getShape(int size)
{
if (shape_size<0 || size!=shape_size)
{
// Polygone à recalculer car la taille est différente :
shape_size = size;
// Les 11 points du poloygone fermé, le dernier étant identique au premier.
int[] xp=new int[11], yp=new int[11];
final int
r = size/2, // Rayon
shpr = r*star_shape, // Coefficient pour les points intermédiaires.
shpmax = 10000*SHAPE_NORMAL; // Diviseur pour les points intermédiaires.
for(int i=0 ; i<10 ; i++)
{
if ((i&1)==0)
{
xp[i] = SHAPE_X[i]*r/10000;
yp[i] = SHAPE_Y[i]*r/10000;
}
else
{
xp[i] = SHAPE_X[i]*shpr/shpmax;
yp[i] = SHAPE_Y[i]*shpr/shpmax;
}
}
xp[10] = xp[0];
yp[10] = yp[0];
shape_x = -r;
shape_y = -r;
shape = null;
shape = new Polygon(xp, yp, 11); // Polygone à onze points.
}
return shape;
}
Les coordonnées dans le polygone shape
sont fixes, mais le composant a besoin de le dessiner plusieurs fois à différents endroits.
La méthode moveShapeTo
ci-dessous déplace les points en mettant à jour le point de référence servant à calculer le vecteur de translation à appliquer :
/**
* Déplacer le polygone au point spécifié par translation géométrique.
* @param x Abscisse du nouveau point de référence.
* @param y Ordonnée du nouveau point de référence.
*/
private void moveShapeTo(int x, int y)
{
if (x!=shape_x || y!=shape_y)
{
shape.translate(x-shape_x, y-shape_y);
shape_x = x;
shape_y = y;
}
}
Dessiner le composant
modifierIl ne reste plus que le dessin du composant, utilisant plusieurs fois le polygone pour afficher les étoiles et affiche également le texte.
@Override
protected void paintComponent(Graphics gg)
{
Dimension d = getSize();
Graphics2D g = (Graphics2D) gg;
if (isOpaque())
{
g.setColor(getBackground());
g.fillRect(0, 0, d.width, d.height);
}
applyHintsTo(g);
final String s = level_name;
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics();
int y = 2, fha = fm.getMaxAscent(), fhb = fm.getMaxDescent(),
hb = d.height-8-(s==null?0:fha+fhb),
x = (d.width+4-(hb+4)*maxlevel)/2;
if (hb>4 && maxlevel>0)
{
Polygon p = getShape(hb);
for(int l=0 ; l<maxlevel ; l++)
{
moveShapeTo(x, y);
if (l<level)
{
g.setColor(getForeground());
g.fillPolygon(p);
g.setColor(c_line);
}
else g.setColor(c_line_off);
g.drawPolygon(p);
x += hb+4;
}
}
if (s!=null)
{
y += hb+4+fha;
g.setColor(c_text);
int tw = fm.stringWidth(s);
x = (d.width-tw)/2;
g.drawString(s, x, y);
}
super.paintComponent(gg);
}
La méthode applyHintsTo
ajuste la configuration pour améliorer la qualité d'affichage :
protected static void applyHintsTo(Graphics2D g)
{
g.addRenderingHints(hm_hints);
}
private static HashMap<Key, Object> hm_hints = new HashMap<Key, Object>();
static
{
hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
hm_hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}
Code source complet et utilisation
modifierLe code source complet de la classe DisplayLevel
est ci-dessous.
package org.wikibooks.fr.components;
import java.awt.*;
import java.awt.RenderingHints.Key;
import java.util.*;
import javax.swing.*;
/**
* Afficher un niveau avec une série d'étoiles.
* @author fr.wikibooks.org
*/
public class DisplayLevel extends JComponent
{
// Couleurs prédéfinies
protected static final Color
C_LINE = Color.BLACK,
C_LINE_OFF = Color.LIGHT_GRAY,
C_TEXT = Color.BLACK,
C_FILL = new Color(240,176,20);
protected int
maxlevel, // Le nombre total d'étoiles.
level; // Le nombre d'étoiles pleines.
protected String
level_name; // Texte de niveau d'appréciation/qualité/...
// Couleurs utilisées
protected Color
c_line = C_LINE,
c_line_off = C_LINE_OFF,
c_text = C_TEXT;
public DisplayLevel()
{
setForeground(C_FILL);
setMinimumSize(new Dimension(400,50));
setPreferredSize(new Dimension(500,50));
setSize(new Dimension(500,50));
}
/**
* Modifier le niveau affiché.
* @param level Niveau affiché.
* @param max Niveau max.
* @param name Texte affiché.
*/
public void setLevel(int level, int max, String name)
{
this.maxlevel = max;
this.level = level;
this.level_name = name;
repaint();
}
private static final int[]
SHAPE_X={ 9511, 2245, 0, -2245, -9511, -3633, -5878, 0, 5878, 3633 },
SHAPE_Y={ -3090, -3090, -10000, -3090, -3090, 1180, 8090, 3820, 8090, 1180 };
private Polygon shape; // Polygone calculé pour une certaine taille, null signifiant à recalculer.
private static final int SHAPE_NORMAL = 100; // Coefficient nominal pour la forme de l'étoile.
private int
shape_size = -1, // La taille courante pour le polygone shape.
shape_x, // Abscisse du point de référence.
shape_y, // Ordonnée du point de référence.
star_shape = SHAPE_NORMAL; // Facteur de forme de l'étoile.
/**
* Modifier la forme de l'étoile dessinée.
* @param factor_percent Facteur de forme en pourcentage :<ul>
* <li>inférieur à 100 pour une étoile plus fine,</li>
* <li>supérieur à 100 pour une étoile plus grosse.</li>
* </ul>
*/
public void setStarShape(int factor_percent)
{
if (star_shape != factor_percent)
{
star_shape = factor_percent;
shape = null; // Polygone à recalculer
shape_size = -1; // Polygone à recalculer
repaint();
}
}
/**
* Obtenir le polygone à utiliser pour la taille voulue.
* Si besoin, cette méthode recalcule le polygone.
* @param size Taille d'étoile.
* @return Le polygone à utiliser.
*/
private Polygon getShape(int size)
{
if (shape_size<0 || size!=shape_size)
{
// Polygone à recalculer car la taille est différente :
shape_size = size;
// Les 11 points du poloygone fermé, le dernier étant identique au premier.
int[] xp=new int[11], yp=new int[11];
final int
r = size/2, // Rayon
shpr = r*star_shape, // Coefficient pour les points intermédiaires.
shpmax = 10000*SHAPE_NORMAL; // Diviseur pour les points intermédiaires.
for(int i=0 ; i<10 ; i++)
{
if ((i&1)==0)
{
xp[i] = SHAPE_X[i]*r/10000;
yp[i] = SHAPE_Y[i]*r/10000;
}
else
{
xp[i] = SHAPE_X[i]*shpr/shpmax;
yp[i] = SHAPE_Y[i]*shpr/shpmax;
}
}
xp[10] = xp[0];
yp[10] = yp[0];
shape_x = -r;
shape_y = -r;
shape = null;
shape = new Polygon(xp, yp, 11); // Polygone à onze points.
}
return shape;
}
/**
* Déplacer le polygone au point spécifié par translation géométrique.
* @param x Abscisse du nouveau point de référence.
* @param y Ordonnée du nouveau point de référence.
*/
private void moveShapeTo(int x, int y)
{
if (x!=shape_x || y!=shape_y)
{
shape.translate(x-shape_x, y-shape_y);
shape_x = x;
shape_y = y;
}
}
@Override
protected void paintComponent(Graphics gg)
{
Dimension d = getSize();
Graphics2D g = (Graphics2D) gg;
if (isOpaque())
{
g.setColor(getBackground());
g.fillRect(0, 0, d.width, d.height);
}
applyHintsTo(g);
final String s = level_name;
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics();
int y = 2, fha = fm.getMaxAscent(), fhb = fm.getMaxDescent(),
hb = d.height-8-(s==null?0:fha+fhb),
x = (d.width+4-(hb+4)*maxlevel)/2;
if (hb>4 && maxlevel>0)
{
Polygon p = getShape(hb);
for(int l=0 ; l<maxlevel ; l++)
{
moveShapeTo(x, y);
if (l<level)
{
g.setColor(getForeground());
g.fillPolygon(p);
g.setColor(c_line);
}
else g.setColor(c_line_off);
g.drawPolygon(p);
x += hb+4;
}
}
if (s!=null)
{
y += hb+4+fha;
g.setColor(c_text);
int tw = fm.stringWidth(s);
x = (d.width-tw)/2;
g.drawString(s, x, y);
}
super.paintComponent(gg);
}
protected static void applyHintsTo(Graphics2D g)
{
g.addRenderingHints(hm_hints);
}
private static HashMap<Key, Object> hm_hints = new HashMap<Key, Object>();
static
{
hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
hm_hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}
}
Le programme ci-dessous illustre comment le composant peut être utilisé.
package org.wikibooks.fr.test;
import org.wikibooks.fr.components.*;
import java.awt.*;
import javax.swing.*;
/**
*
* @author fr.wikibooks.org
*/
public class DisplayTest
{
public static void main(String[] args)
{
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(500, 140);
Container c = f.getContentPane();
DisplayLevel niveau = new DisplayLevel();
niveau.setSize(400, 100);
c.add(niveau);
niveau.setStarShape(120);
niveau.setOpaque(true);
niveau.setBackground(Color.WHITE);
niveau.setFont(new Font("Dialog", Font.BOLD, 16));
niveau.setLevel(3, 5, "Ce chapitre est bien"); // 3 étoiles sur 5.
f.setVisible(true);
}
}