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é, ...

Appréciation représentée par le dessin d'étoiles.
Appréciation représentée par le dessin d'étoiles.

Composant

modifier

La 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

modifier
 

Les 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

modifier

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

	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

modifier

Le 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

modifier

Il 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

modifier

Le 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);
	}
}