La bibliothèque Swing propose une riche collection de composants répondant aux besoins les plus courants concernant la création d'une interface graphique.
Cependant, certaines applications peuvent avoir besoin de composants spécifiques, qu'il soit basé sur un composant existant (comme une case à cocher à trois états par exemple), ou qu'il ne soit basé sur aucun composant existant.
C'est par exemple le cas d'un feu tricolore utilisé dans un jeu sur la circulation routière, qui sera développé dans ce chapitre par étapes.
Création de la classe.
La première version de la classe ci-dessous définit les couleurs utilisées et l'état courant du feu tricolore.
La méthode statique principale crée une fenêtre pour tester le composant.
Le composant n'est pas visible pour le moment avec cette première version du code.
package org.wikibooks.fr.swing.component;
import java.awt.*;
import javax.swing.*;
 * Composant de feu tricolore.
 * @author fr.wikibooks.org
public class FeuTricolore extends JComponent
 // Couleurs du feu
 private static final Color// R V B
 FEU_ETEINT = new Color( 32, 32, 32),
 FEU_VERT = new Color( 0, 180, 0),
 FEU_ORANGE = new Color(255, 160, 0),
 FEU_ROUGE = new Color(255, 0, 0);
 static enum EtatFeu
 ETEINT,
 VERT,
 ORANGE,
 ROUGE
 private EtatFeu etat_courant = EtatFeu.ETEINT;
 public FeuTricolore()
 Dimension d = new Dimension(40,120);
 setMinimumSize(d);
 setPreferredSize(d);
 setBackground(Color.BLACK);
 setForeground(Color.DARK_GRAY);
 // Méthode main utilisée pour tester le composant dans une fenêtre
 public static void main(String[] args)
 // Créer la fenêtre
 JFrame f = new JFrame("Feu tricolore");
 f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
 // Personaliser le panneau du contenu
 JPanel p_content = new JPanel();
 p_content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
 f.setContentPane(p_content);
 // Ajouter un feu tricolore
 p_content.add(new FeuTricolore());
 f.pack();
 f.setVisible(true);
État du composant.
L'état du composant est représenté par une énumération de type codice_1
static enum EtatFeu
 ETEINT,
 VERT,
 ORANGE,
 ROUGE
private EtatFeu etat_courant = EtatFeu.ETEINT;
On ajoute également des accesseurs permettant de lire et écrire l'état courant du feu :
/**
 * Obtenir l'état courant du feu.
 * @return L'état courant du feu.
public EtatFeu getEtatCourant()
 return etat_courant;
 * Définir l'état courant du feu.
 * @param etat_courant Le nouvel état courant du feu.
public void setEtatCourant(EtatFeu etat_courant)
 this.etat_courant = etat_courant;
 repaint();
La méthode de changement d'état appelle la méthode codice_2 pour mettre à jour l'affichage aussitôt.
Dessin du composant.
Le dessin d'un composant Swing est effectué par la méthode publique codice_3.
Cette méthode délègue le travail à trois autres méthodes protégées appelées dans l'ordre suivant :
Cet ordre assure que les composants contenus soit affichés au-dessus du composant.
Pour définir l'apparence du composant, une sous-classe ne doit redéfinir que la méthode codice_4.
Celle-ci doit éviter de dessiner dans la zone définie par les marges de type codice_6 qui est réservée aux bords (codice_11).
Le code ci-dessous ajoute la méthode de dessin du feu tricolore.
Elle utilise un tableau à deux dimensions définissant la couleur de chaque feu pour chaque état possible.
// Couleur des feux selon l'état courant
private Color[][] COULEUR_FEUX = // [état][feu]
 { FEU_ETEINT, FEU_ETEINT, FEU_ETEINT }, // ETEINT
 { FEU_ETEINT, FEU_ETEINT, FEU_VERT }, // VERT
 { FEU_ETEINT, FEU_ORANGE, FEU_ETEINT }, // ORANGE
 { FEU_ROUGE, FEU_ETEINT, FEU_ETEINT }, // ROUGE
@Override
protected void paintComponent(Graphics g)
 Dimension d = getSize();
 // Le fond
 g.setColor(getBackground());
 g.fillRect(0, 0, d.width, d.height);
 // Dimension d'un feu selon la taille du composant
 int fw = d.width, fh = d.height/3;
 // ...de forme carrée :
 if (fw>fh) fw=fh; else fh = fw;
 int dm = fw*3/4, // Diamètre : 75% de la taille disponible
 ecart = fw-dm, // Décalage de 25% = 100%-75%
 // Position centrée avec décalage
 x = (d.width - fw + ecart)/2,
 y = (d.height - 3*fh + ecart)/2;
 // Les trois feux de haut en bas
 Color[] couleurs = COULEUR_FEUX[etat_courant.ordinal()];
 for(int i=0 ; i<3 ; i++,y+=fh)
 g.setColor(couleurs[i]);
 g.fillOval(x, y, dm, dm);
 g.setColor(getForeground());
 g.drawOval(x, y, dm-1, dm-1); // dm-1 pour ne pas déborder
Gestion des évènements.
Dans l'état actuel du code, le feu affiche l'état courant initial éteint.
Cette section définit le code pour changer l'état courant sur réception d'un évènement de clic de souris.
On ajoute la méthode qui met l'état suivant selon l'état courant :
protected void cycleEtatCourant()
 switch(etat_courant)
 case ETEINT: setEtatCourant(EtatFeu.ROUGE); break;
 case VERT: setEtatCourant(EtatFeu.ORANGE); break;
 case ORANGE: setEtatCourant(EtatFeu.ROUGE); break;
 case ROUGE: setEtatCourant(EtatFeu.VERT); break;
Cette méthode est appelée par le gestionnaire de clic de souris ajouté au composant dans le constructeur de la classe :
addMouseListener(new MouseAdapter()
 @Override
 public void mouseClicked(MouseEvent e)
 cycleEtatCourant();
});
Améliorations.
La classe peut être améliorée sur divers points abordés dans cette section.
Changement d'état.
Afin que le feu ait un comportement plus proche de la réalité, le changement d'état du feu devrait se faire sans intervention de l'utilisateur, au bout d'un certain temps dépendant de l'état courant.
Tout d'abord, afin d'éviter de réafficher le composant trop souvent, la méthode de changement d'état est modifiée comme ci-dessous :
public void setEtatCourant(EtatFeu etat_courant)
 // Seulement si changement d'état
 if (etat_courant != this.etat_courant)
 this.etat_courant = etat_courant;
 repaint();
On supprime donc le changement d'état par clic de souris, c'est à dire le code ajouté dans la section « Gestion des évènements » :
La gestion d'un croisement implique plusieurs feux, affectés à différentes voies de circulation (à double sens ou non).
Il est donc préférable que le changement d'état d'un feu soit géré par une nouvelle classe afin de synchroniser les feux du carrefour.
Il faut donc déplacer l'énumération dans une classe à part :
package org.wikibooks.fr.swing.component;
public enum EtatFeu
 ETEINT,
 VERT,
 ORANGE,
 ROUGE
Ensuite, il faut créer une nouvelle classe qui sera nommée codice_14, qui va gérer une liste de feux par voie de circulation, définir la voie de circulation active (cycle rouge-vert-orange-rouge) les autres étant inactives (feu rouge), et définir un thread mettant à jour le cycle courant :
package org.wikibooks.fr.swing.component;
import java.util.*;
 Gestionnaire de l'état des feux tricolores d'un carrefour.
 <h3>Fonctionnement</h3>
 Une seule voie est active à la fois, les autres
 voies ont le feu au rouge.
 La voie active effectue un cycle où le feu prend
 successivement les couleurs suivantes :<ul>
 <li>rouge pendant marge_duree_rouge cycles élémentaires,</li>
 <li>vert pendant duree_vert cycles élémentaires,</li>
 <li>orange pendant duree_orange cycles élémentaires,</li>
 <li>rouge pendant marge_duree_rouge cycles élémentaires.</li>
 </ul>
 À la fin de ce cycle, la voie devient inactive,
 et la voie suivante devient active.
 * @author fr.wikibooks.org
public class GestionnaireFeu
 private int duree_cycle_elementaire = 1000; // ms
 /** Couleur de feu courante pour la voie active. */
 private EtatFeu etat_voie_active = EtatFeu.ROUGE; // Valeur initiale pour la sécurité
 /** Index de la voie active. */
 private int voie_active = 0;
 /** Liste des feux pour chaque voie. */
 private ArrayList<FeuTricolore>[] feux_par_voie;
 private final int duree_vert, duree_orange, marge_duree_rouge;
 private final int total_cycle, total_period;
 private final Object lock = new Object();
 private void boucleCycleFeux()
 try
 long time = System.currentTimeMillis(), t;
 int index_cycle = 0;
 for(;;)
 // Mise à jour de l'état
 voie_active = index_cycle / total_cycle;
 int n = index_cycle % total_cycle;
 if (n < marge_duree_rouge) etat_voie_active = EtatFeu.ROUGE;
 else
 n -= marge_duree_rouge;
 if (n < duree_vert) etat_voie_active = EtatFeu.VERT;
 else
 n -= duree_vert;
 if (n < duree_orange) etat_voie_active = EtatFeu.ORANGE;
 else etat_voie_active = EtatFeu.ROUGE;
 // Mise à jour des feux
 synchronized(lock)
 for(int voie=0 ; voie<feux_par_voie.length; voie++)
 EtatFeu etat = voie==voie_active ? etat_voie_active : EtatFeu.ROUGE;
 for(FeuTricolore feu : feux_par_voie[voie])
 feu.setEtatCourant(etat);
 // Cycle élémentaire suivant
 index_cycle = (index_cycle+1) % total_period;
 // Attendre jusqu'à l'heure du prochain cycle élémentaire
 time += duree_cycle_elementaire;
 t = System.currentTimeMillis();
 if (t<time) Thread.sleep(time - t);
 catch(InterruptedException ex)
 public void add(int voie, FeuTricolore feu)
 if (voie<0 || voie>=feux_par_voie.length)
 throw new IllegalArgumentException("Voie non valide : "+voie);
 if (feu == null)
 throw new NullPointerException("Pas de feu tricolore spécifié");
 synchronized(lock)
 feux_par_voie[voie].add(feu);
 EtatFeu etat = voie==voie_active ? etat_voie_active : EtatFeu.ROUGE;
 feu.setEtatCourant(etat);
 public GestionnaireFeu()
 this(2);
 public GestionnaireFeu(int nombre_de_voies)
 this(nombre_de_voies, 8, 2, 1);
 * Construire un gestionnaire de feux.
 * @param nombre_de_voies Nombre de voies de circulation.
 * @param duree_vert Durée du feu au vert en nombre de cycles élémentaires (1 seconde par défaut).
 * @param duree_orange Durée du feu au orange en nombre de cycles élémentaires (1 seconde par défaut)
 * @param marge_duree_rouge Marge de durée du feu au rouge en nombre de cycles élémentaires (1 seconde par défaut)
 public GestionnaireFeu(int nombre_de_voies,
 int duree_vert, int duree_orange, int marge_duree_rouge)
 if (nombre_de_voies<2 ||
 duree_vert<1 ||
 duree_orange<1 ||
 marge_duree_rouge<0)
 throw new IllegalArgumentException("Un argument est invalide");
 this.duree_vert = duree_vert;
 this.duree_orange = duree_orange;
 this.marge_duree_rouge = marge_duree_rouge;
 this.total_cycle = 2*marge_duree_rouge + duree_vert + duree_orange;
 this.total_period = this.total_cycle * nombre_de_voies;
 feux_par_voie = new ArrayList[nombre_de_voies];
 for(int i=0 ; i<feux_par_voie.length ; i++)
 feux_par_voie[i] = new ArrayList<FeuTricolore>();
 // Le thread mettant à jour les feux en tâche de fond :
 Thread th = new Thread(new Runnable()
 @Override
 public void run()
 }, "Gestion des feux tricolores");
 th.setDaemon(true); // Ne maintient pas l'application en vie.
 th.start();
La méthode statique de lancement permettant de montrer trois feux sur trois voies de circulation différentes est la suivante :
public static void main(String[] args)
 // Créer la fenêtre
 JFrame f = new JFrame("Feu tricolore");
 f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
 // Personnaliser le panneau du contenu
 JPanel p_content = new JPanel();
 p_content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
 p_content.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 20));
 f.setContentPane(p_content);
 int nb_voies = 3;
 GestionnaireFeu gestionnaire = new GestionnaireFeu(nb_voies);
 // Ajouter un feu tricolore par voie
 for(int i=0 ; i<nb_voies ; i++)
 FeuTricolore feu = new FeuTricolore();
 p_content.add(feu);
 gestionnaire.add(i, feu);