Implémenter le patron de conception Data Model et dessiner des graphes en Swing

Thierry Leriche-Dessirier

Ce TP vous montre comment utiliser les classes incontournables de Swing. Il vous fait utiliser des tables modèles pas à pas. Il vous fait dessiner des graphes avec JFreeChart et bien plus encore… Commentez ♪

- Titre : Implémenter le patron de conception Data Model et dessiner des graphes en Swing

- Auteur : Thierry Leriche-Dessirier

- Parution : 10 janvier 2013

- Mise à jour : 28 décembre 2016

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. À propos

- I-B. Versions des logiciels et bibliothèques utilisés

- I-C. Mises à jour

- II. Préparation

- II-A. Télécharger le projet d'exemple

- II-B. Import dans Eclipse (projet Maven)

- II-C. Les objets du domaine

- II-D. DAO

- III. Data model

- III-A. Interface épurée

- III-B. Données dans un tableau

- III-C. Table model

- III-D. Un peu de dynamisme

- III-E. Les vraies données

- III-F. Renderer

- III-G. Des tris

- III-H. Des petits boutons

- IV. Graphes

- IV-A. Maven

- IV-B. Bouton et popup

- IV-C. Graphes

- V. Conclusion

- VI. Remerciements

- VII. Annexes

- VII-A. Liens

- VII-B. Pour aller plus loin

- VII-C. Solution possible

I - Introduction

Durant ce TP, vous allez approfondir les connaissances, déjà abordées en cours de POO, sur les interfaces graphiques avec Swing. En particulier, vous mettrez en place des tableaux. Ce composant pose souvent des problèmes lorsqu'on débute avec Swing. Vous allez donc vous limiter aux notions simples, mais qui couvrent la plupart des besoins. Vous créerez ensuite des graphes à l'aide de la bibliothèque « JFreeChart », ce qui sera relativement amusant.

Ce TP est la suite du TP « tp-chien-dao » dans lequel vous avez découvert (ou redécouvert) Maven, Eclipse, les logs, les tests, les fichiers CSV et quelques bibliothèques utiles. Je vous conseille donc très vivement de le lire avant de poursuivre avec ce nouveau TP.

Des pistes et/ou des solutions sont proposées en annexes. Essayez de résoudre les problèmes vous-mêmes avant de regarder les réponses.

Demandez l'aide du professeur ou de son assistant si vous restez bloqué trop longtemps. La FAQ en annexe du TP « tp-chien-dao » contient les réponses qu'on me pose le plus souvent. Merci de consulter cette FAQ avant d'appeler le professeur.

I-A - À propos

Ce document est la retranscription d'un TP de Génie Logiciel que je donne à mes élèves de l'ESIEA. Je l'offre (gratuitement) à la communauté, en particulier aux autres professeurs qui peuvent le proposer à leurs propres étudiants.

En contrepartie, je vous demande simplement de ne pas modifier ce document. Au contraire, je vous invite à en communiquer directement l'adresse (sur Developpez.com) et de profiter ainsi des futures mises à jour. N'oubliez pas de me le faire savoir, le cas échéant. Par avance, je vous remercie pour votre participation.

I-B - Versions des logiciels et bibliothèques utilisés

Pour écrire ce document, j'ai utilisé les versions suivantes :

- Java JDK 1.8 ;

- Eclipse Indigo 3.7 JEE 64b ;

- Maven 3.0.3 ;

- Log4J 1.2.13 ;

- CSV Engine 1.3.5 ;

- JFreeChart 1.0.14.

I-C - Mises à jour

1er janvier 2013 : Création.

1er novembre 2016 : Remplacement de tout le chapitre dédié à l'import dans Eclipse et à la préparation Maven.

II - Préparation

Avant d'entrer dans le vif du sujet, vous devez suivre quelques étapes simples (mais intéressantes) pour préparer le travail.

II-A - Télécharger le projet d'exemple

Durée estimée : 1 minute.

Pour commencer, s'il ne vous a pas été fourni par votre professeur, téléchargez le fichier tp-chien-ihm-01.zip, contenant un projet Java-Maven d'exemple.

Décompressez le fichier « tp-chien-ihm-01.zip ».

Vous devez normalement avoir un dossier avec les éléments suivants :

- le dossier « src » ";

- le fichier « pom.xml »;

- les fichiers « build.bat » et « build.sh »;

- les fichiers « license.txt » et « README.txt » (ces deux derniers fichiers ne sont pas importants pour la suite).

II-B - Import dans Eclipse (projet Maven)

Durée estimée : 2 minutes.

Importez le projet directement dans Eclipse car cet IDE sait traiter les projets Maven. Pour cela, lancez donc Eclipse si ce n'est pas déjà fait. Utilisez le menu « File/Import ». Dans la popup il faut prendre « Maven/Existing Maven projet ». Dans la seconde popup, utilisez le bouton « Browse » pour aller jusqu'au dossier que vous avez dézippé et où se trouve le fichier « pom.xml ». Enfin cliquez sur le bouton « Finish ». Eclipse devrait mouliner quelques instants.

II-C - Les objets du domaine

Durée estimée : 5 minutes.

Les objets que vous allez utiliser sont exactement les mêmes que ceux du TP « tp-chien-dao ».

Pour rappel, le cœur du domaine est constitué par l'interface Chien, qui définit quelques getters simples :

Chien.java

public interface Chien extends Serializable {

String getNom();

String getNomComplet();

Sexe getSexe();

RaceDeChien getRace();

List<String> getCouleurs();

Double getPoids();

}

La classe SimpleChien est l'implémentation (par défaut) de Chien.

SimpleChien.java

public class SimpleChien implements Chien {

private String nom;
private String nomComplet;
private Sexe sexe;
private RaceDeChien race;
private List<String> couleurs;
private Double poids;

public SimpleChien() {
// rien...
}

public SimpleChien(final String nom) {
this.nom = nom;
}

public SimpleChien(final String nom, final RaceDeChien race, final Sexe sexe) {
this(nom);
this.race = race;
this.sexe = sexe;
}

@Override
public String toString() {
return "SimpleChien [nom=" + nom + "]";
}

// Getters et setters
...

}

La classe « SimpleChien » (et « Chien ») définit des attributs vers les enums « Sexe » et « RaceDeChien », dont l'utilité ne devrait pas nécessiter d'explication.

Sexe.java

public enum Sexe {

FEMALE(2),
MALE(1);

private final int code;

Sexe(final int code) {
this.code = code;
}

public static Sexe valueOfByCode(final int code) {
switch (code) {
case 2:    return FEMALE;
case 1:    return MALE;
default: throw new IllegalArgumentException("Le sexe demande n'existe pas.");
}
}

public boolean isMale() {
return this == MALE;
}

...
}

RaceDeChien.java

public enum RaceDeChien {

BASSET_ALPES("Basset des Alpes", "basset_alp"),
BERGER_ALLEMAND("Berger allemand", "berger_all"),
CANICHE("Caniche", "caniche"),
HARRIER("Harrier", "harrier"),
GOLDEN("Golden retriever", "golden_ret"),
...
ROTTWEILER("Rottweiler", "rottweiler"),
WESTIE("Westie", "westie");

private final String label;
private final String code;

RaceDeChien(final String label, final String code) {
this.label = label;
this.code = code;
}

public static RaceDeChien valueOfByCode(final String code) {

...

for (RaceDeChien race : values()) {
if (race.code.equalsIgnoreCase(code)) {
return race;
}
}

throw new IllegalArgumentException("La race de chien demandee n'existe pas.");
}

...
}

II-D - DAO

Le programme sait lire des données depuis des fichiers CSV et/ou une base de données. Ceci était l'objet du précédent TP et ne sera donc pas réexpliqué ici. Dans la suite, considérez que les méthodes définies dans l'interface « CsvChienDao » fonctionnent parfaitement et qu'elles ont déjà été testées.

III - Data model

Un JTable est un composant Swing permettant d'afficher un tableau. En plus des lignes de contenu, il a également une ligne d'entête avec un titre pour chaque colonne. Ce composant a donc, d'un côté, des données et, de l'autre, des données d'en-tête. On peut voir les données comme un tableau à deux dimensions dans lequel chaque valeur correspond à une cellule du tableau. Quant aux entêtes, on peut les voir comme un tableau de String.

Le composant JTable utilise différents concepts de Swing :

- un modèle pour stocker les données. Un JTable utilise une classe implémentant TableModel. Nous verrons plus loin comment spécifier un modèle pour un JTable ;

- un renderer pour le rendu des cellules. On peut spécifier un TableCellRenderer pour chaque classe de données. Nous verrons plus loin ce que ça signifie exactement ;

- un éditeur pour l'édition du contenu d'une cellule. On peut spécifier un TableCellEditor pour chaque classe de données.

III-A - Interface épurée

Durée estimée : 2 minutes.

Vous allez maintenant créer une IHM simplifiée, pour accueillir le tableau avec la liste des chiens. Pour cela, créez la classe « ChienJFrame1 » dans le package « com.icauda.tp.chien.ihm » en vous inspirant du code suivant :

ChienJFrame1.java

public class ChienJFrame1 extends JFrame {

public ChienJFrame1() {
setTitle("Liste des chiens");
setPreferredSize(new Dimension(500, 400));
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
}
}
}

Complétez ensuite la classe « LauncherIHM » pour qu'elle crée et ouvre l'IHM en vous inspirant du code suivant :

LauncherIHM.java

public class LauncherIHM {

...

public static void main(String[] args) {

final ChienJFrame1 f = new ChienJFrame1();
f.setVisible(true);

...
}
}
}

Lancez l'exécution de « LauncherIHM ». Ça doit vous ouvrir une fenêtre vide, à la bonne dimension et avec juste un titre, comme sur la capture d'écran suivante :

Fenêtre vide

Cette IHM sera suffisante pour ce TD. Maintenant que tout est en place, nous allons pouvoir entrer dans le vif du sujet : les tableaux.

LauncherIHM.java

public class LauncherIHM {

...

public static void main(String[] args) {

final ChienJFrame1 f = new ChienJFrame1();
f.setVisible(true);

...
}
}
}

III-B - Données dans un tableau

Durée estimée : 10 minutes.

La façon la plus simple, mais pas forcément la meilleure, d'alimenter un JTable est de lui passer directement un tableau de données.

Complétez le code de la fenêtre en vous inspirant du code suivant :

ChienJFrame1.java

public class ChienJFrame1 extends JFrame {
...

public ChienJFrame1() {
...

final Object[][] donnees = {
{ "Pluto", "Pluto le chien Disney", Sexe.MALE, RaceDeChien.GOLDEN, "jaune", 24.0 },
{ "Lassie", "Lassie des alpes", Sexe.FEMALE, RaceDeChien.BERGER_ALLEMAND, "blanc,noir", 32.3 },
{ "Volt", "Volt le super chien", Sexe.MALE, RaceDeChien.CANICHE, "blanc,noir", 14.0 },
{ "Medor", "Medor", Sexe.MALE, RaceDeChien.ROTTWEILER, "gris", 32.0 },
{ "King", "Kiking", Sexe.MALE, RaceDeChien.DANOIS, "noir", 46.5 },
{ "Chocolat", "Chocolat mon amour", Sexe.MALE, RaceDeChien.GOLDEN, "marron", 31.0 },
{ "Vanille", "Vanille mon amour", Sexe.FEMALE, RaceDeChien.GOLDEN, "blanc", 37.0 }
};

final String[] entetes = { "Nom", "Nom complet", "Sexe", "Race", "Couleurs", "Poids" };

final JTable tableau = new JTable(donnees, entetes);
getContentPane().add(tableau.getTableHeader(), NORTH);
getContentPane().add(tableau, CENTER);

pack();
}
}

On utilise le constructeur « JTable(Object[][] data, Object[] title) » pour gérer les données et les entêtes. Pour ajouter le « JTable » dans un « JPanel », il faut ajouter séparément le header du tableau et le tableau en lui-même.

Vous pouvez ajouter d'autres chiens dans la liste. Dans ce cas, je vous conseille de recopier les chiens déjà présents dans le fichier « chiens.csv ».

Relancez le programme pour voir le tableau.

Tableau dans la fenêtre

Demandez à Eclipse de faire les imports statiques des sexes et des races de chien à l'aide du raccourci [Ctrl] + [Maj] + [M] pour rendre le code plus lisible. Profitez-en pour formater le code.

Avec très peu de code, vous avez donc un tableau fonctionnel. Néanmoins, cette première implémentation souffre de plusieurs défauts :

- on ne peut pas afficher plus de lignes qu'il n'y a d'espace disponible sur la fenêtre (réduisez la taille de la fenêtre avec la souris pour le constater et/ou ajoutez des chiens dans le tableau) ;

- les données sont statiques et immuables ;

- on ne peut pas gérer la façon dont seront affichées les données ;

- aucune distinction entre les données et la vue ;

- les colonnes « Sexe » et « Race » ne sont pas très esthétiques.

Le premier est très trivial ; la bonne façon d'ajouter un « JTable » dans un « JPanel » est de passer par un « JScrollPane » qui permettra d'afficher plus de lignes que la fenêtre ne le permet, à l'aide d'un ascenseur si besoin.

Modifiez le code en vous inspirant de la ligne suivante :

ChienJFrame1.java

...
//getContentPane().add(tableau, CENTER);
getContentPane().add(new JScrollPane(tableau), CENTER);
...
}

Vous devriez obtenir la même fenêtre, mais avec un ascenseur.

Ascenseur

III-C - Table model

Durée estimée : 15 minutes.

Une chose indispensable, lorsqu'on utilise des « JTable », est d'utiliser un modèle de tableau pour stocker les données. Pour cela il faut créer une classe étendant « TableModel ». En pratique, on implémente rarement directement « TableModel », mais on hérite plutôt de « AbstractTableModel » et l'on redéfinit les méthodes nécessaires. Pour commencer, voici les méthodes qu'il faudra redéfinir pour notre modèle de données statiques :

- int getRowCount() : doit retourner le nombre de lignes du tableau ;

- int getColumnCount() : doit retourner le nombre de colonnes du tableau ;

- Object getValueAt(int rowIndex, int columnIndex) : doit retourner la valeur du tableau à la colonne et la ligne spécifiées ;

- String getColumnName(int columnIndex) : doit retourner l'entête de la colonne spécifiée.

Créez un modèle en reprenant les données dans un tableau à deux dimensions et en vous inspirant du code suivant :

ModeleStatique.java

public class ModeleStatique extends AbstractTableModel {

private final String[] entetes;
private final Object[][] donnees;

public ModeleStatique() {

donnees = new Object[][] {
{"Pluto","Pluto le chien Disney",MALE, GOLDEN,"jaune", 24.0 },
{"Lassie","Lassie des alpes", FEMALE, BERGER_ALLEMAND,"blanc,noir", 32.3 },
{"Volt","Volt le super chien",MALE, CANICHE,"blanc,noir", 14.0 },
...
{"Vanille","Vanille mon amour",FEMALE, GOLDEN,"blanc",37.0 }

};

entetes = new String[] {"Nom","Nom complet","Sexe","Race","Couleurs","Poids"};
;
}

@Override
public String getColumnName(int columnIndex) {
return entetes[columnIndex];
}

@Override
public int getRowCount() {
// TODO
}

@Override
public int getColumnCount() {
// TODO
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {
// TODO
}
}

Remplacez-les « TODO » par du code intelligent…

Bien entendu il faut modifier la « JTable » pour prendre en compte ce modèle.

ChienJFrame2.java

public class ChienJFrame2 extends JFrame {

public ChienJFrame2() {
super();
setTitle("Liste des chiens (v2)");
setPreferredSize(new Dimension(500, 400));
setDefaultCloseOperation(EXIT_ON_CLOSE);

final JTable tableau = new JTable(new ModeleStatique());
getContentPane().add(tableau.getTableHeader(), NORTH);
getContentPane().add(new JScrollPane(tableau), CENTER);

pack();
}
}

Ici j'ai créé la classe « ChienJFrame2 » pour m'y retrouver plus facilement entre les différentes versions. Il faut penser à utiliser les nouvelles versions dans le launcher.

On a donc créé une classe héritant de « AbstractTableModel » et redéfinissant les méthodes indispensables. Les données sont toujours stockées de la même manière, mais cette solution est plus souple et beaucoup plus propre. Si l'on regarde la « JFrame », on peut voir qu'il n'y a plus aucune donnée dans cette classe, ce qui est donc beaucoup plus propre en termes de découplage. En outre, on est maintenant maître de nos données et de la façon dont elles sont stockées. Cela dit, il n'y a aucune différence visible à l'affichage.

En vrai, il est extrêmement rare de voir des données sous la forme de tableaux à deux dimensions. Java étant un langage orienté objet, on manipule des objets. On va maintenant pouvoir utiliser la classe « Chien » directement.

Pour que ce soit plus « lisible », vous devez ajouter un constructeur dans « SimpleChien » :

SimpleChien.java

public class SimpleChien implements Chien {
...

public SimpleChien(String nom, String nomComplet, Sexe sexe, RaceDeChien race, List<String> couleurs, Double poids) {
this(nom, race, sexe);
this.nomComplet = nomComplet;
this.couleurs = couleurs;
this.poids = poids;
}

public SimpleChien(String nom, String nomComplet, Sexe sexe, RaceDeChien race, String[] couleurs, Double poids) {
this(nom, nomComplet, sexe, race, tabToList(couleurs), poids);

}

private static List<String> tabToList(String[] couleurs) {
List<String> couleurList = new ArrayList<String>();
for (String couleur : couleurs) {
couleurList.add(couleur);
}
return couleurList;
}

...
}

Modifiez le modèle statique en vous inspirant du code suivant :

ModeleStatique2.java

public class ModeleStatique2 extends AbstractTableModel {

...

private final String[] entetes;
//private final Object[][] donnees;
private final SimpleChien[] chiens;

public ModeleStatique2() {
super();

chiens = new SimpleChien[] {
new SimpleChien("Pluto","Pluto le chien Disney",MALE, GOLDEN, new String[] { "jaune"}, 24.0),
new SimpleChien("Lassie","Lassie des alpes", FEMALE, BERGER_ALLEMAND, new String[] { "blanc","noir"}, 32.3),
new SimpleChien("Volt","Volt le super chien", MALE, CANICHE, new String[] {"blanc","noir"}, 14.0),
new SimpleChien("Medor","Medor", MALE, ROTTWEILER, new String[] {"gris"}, 32.0),
new SimpleChien("King","Kiking",MALE, DANOIS, new String[] {"noir"}, 46.5),
...
new SimpleChien("Chocolat","Chocolat mon amour",MALE, GOLDEN, new String[] {",marron"}, 31.0),
new SimpleChien("Vanille","Vanille mon amour", FEMALE, GOLDEN, new String[] {"blanc"}, 37.0)
};

entetes = new String[] {"Nom","Nom complet","Sexe","Race","Couleurs","Poids"};
;
}

...
}

Adaptez les autres méthodes de la classe pour que tout compile et renvoie les bons résultats.

Le code devient intéressant. C'est là qu'on commence à comprendre l'utilité d'utiliser un modèle et non pas directement le constructeur de « JTable ». Si maintenant par exemple, on veut inverser deux colonnes et mettre « nom » après « race », il suffit d'inverser les deux colonnes dans la liste des colonnes et d'inverser les deux « return » de la méthode « getValueAt() », alors que ceci aurait été beaucoup plus difficile et contraignant avec un tableau d'objets.

III-D - Un peu de dynamisme

Durée estimée : 10 minutes.

Créez un nouveau « table model », nommé « ModeleDynamique », inspiré de « ModeleStatique », mais utilisant une liste à la place d'un tableau. Vous pouvez vous inspirer du code suivant, mais n'oubliez pas de compléter les méthodes manquantes.

ModeleStatique2.java

public class ModeleDynamique extends AbstractTableModel {

private final String[] entetes;

// private final SimpleChien[] chiens;
private final List<SimpleChien> chiens;

public ModeleDynamique() {
super();

chiens = new ArrayList<SimpleChien>();
chiens.add(new SimpleChien("Pluto","Pluto le chien Disney",MALE, GOLDEN, new String[] {"jaune"}, 24.0));
chiens.add(new SimpleChien("Lassie","Lassie des alpes",FEMALE, BERGER_ALLEMAND, new String[] {"blanc","noir"}, 32.3));
chiens.add(new SimpleChien("Volt","Volt le super chien",MALE, CANICHE, new String[] {"blanc","noir"}, 14.0));
chiens.add(new SimpleChien("Medor","Medor",MALE, ROTTWEILER, new String[] {"gris"}, 32.0));
chiens.add(new SimpleChien("King","Kiking",MALE, DANOIS, new String[] {"noir"}, 46.5));
...
chiens.add(new SimpleChien("Chocolat","Chocolat mon amour", MALE, GOLDEN, new String[] {"marron"}, 31.0));
chiens.add(new SimpleChien("Vanille","Vanille mon amour", FEMALE, GOLDEN, new String[] {"blanc"}, 37.0));

entetes = new String[] {"Nom","Nom complet","Sexe","Race","Couleurs","Poids"};
}

...
}

N'oubliez pas de modifier la fenêtre pour qu'elle utilise le modèle dynamique :

ChienJFrame4.java

public class ChienJFrame4 extends JFrame {
...

public ChienJFrame4() {
...

//final JTable tableau = new JTable(new ModeleStatique2());
final JTable tableau = new JTable(new ModeleDynamique());
...
}

Visuellement on ne voit toujours pas de différence.

III-E - Les vraies données

Durée estimée : 10 minutes.

Maintenant que vous avez compris le principe, vous avez très certainement envie de jouer avec des vraies données et plus particulièrement avec celles contenues dans le fichier CSV. Pour cela, vous allez utiliser la classe de service « ChienService » qui se présente comme un « singleton ».

Vous allez ajouter l'utilisation d'une des implémentations du DAO dans ce service, par exemple « EngineCsvChienDao » qui est la dernière que vous avez programmée dans le TP précédent. Pour cela, inspirez-vous du code suivant :

ChienService.java

public class ChienService {
...

private CsvChienDao csvChienDao;

...

private ChienService() {
LOGGER.debug("Constructeur");
csvChienDao = new EngineCsvChienDao();
}

...

public List<Chien> findAllChiens(final String fileName) {
final File file = new File(fileName);
csvChienDao.init(file);
return csvChienDao.findAllChiens();
}

...
}

Vous constatez que la méthode « findAllChiens(..) » relance l'initialisation du DAO à chaque fois. Dans le cadre de ce TP, on dira que ce n'est pas grave. Vous pouvez toutefois trouver une manière plus élégante de le faire.

Vous devez également adapter le modèle pour qu'il prenne en charge ce service, en vous inspirant du code suivant :

ModeleDynamique2.java

public class ModeleDynamique2 extends AbstractTableModel {
...
private final String[] entetes;
private List<Chien> chiens;

final private static String CHIENS_FILE_NAME ="src/main/resources/chiens.csv";
private ChienService chienService = ChienService.getInstance();

...

public ModeleDynamique2() {
super();

chiens = chienService.findAllChiens(CHIENS_FILE_NAME);
entetes = new String[] {"Nom","Nom complet","Sexe","Race","Couleurs","Poids"};
}

...
}

Relancez le programme pour voir les données du fichier.

Les données du fichier dans le tableau

III-F - Renderer

Durée estimée : 10 minutes.

Vous allez maintenant passer à la personnalisation de l'affichage des cellules pour que ce soit un peu plus beau. Commencez par spécifier (dans le modèle) à quelle classe correspond chacune colonne (on ne peut configurer des « renderers » que par colonne). À la suite de quoi, vous configurerez les « renderers » pour chaque classe de colonne au niveau de la « JTable ».

La première chose à faire est de redéfinir la méthode getColumnClass() dans le modèle, en vous inspirant du code suivant :

ModeleDynamique2.java

public class ModeleDynamique2 extends AbstractTableModel {
...

@Override
public Class<?> getColumnClass(int columnIndex) {
switch (columnIndex) {
case 0:
case 1:
return String.class;

case 2:
return Sexe.class;

case 3:
return RaceDeChien.class;

default:
return Object.class;
}
}
...
}

Ne relancez pas tout de suite l'IHM, faute de quoi vous aurez une exception.

Un « renderer » est une classe implémentant l'interface « TableCellRenderer », cette dernière ne contenant qu'une seule méthode retournant un composant Swing. En pratique, on hérite généralement de « DefaultCellRenderer » qui représente un « JLabel » comme « renderer ».

Il faut éviter dans la mesure du possible de renvoyer un nouvel objet dans un « renderer » si on a beaucoup d'éléments dans la « JTable ». Cela voudrait dire qu'il faudrait créer un objet pour chaque ligne, et à chaque fois qu'on redessine la « JTable », ce qui peut dégrader très fortement les performances.

Créez un « renderer » pour afficher le label des enum de « Sexe » à la place du nom de l'enum en vous inspirant du code suivant :

SexeCellRenderer.java

public class SexeCellRenderer extends DefaultTableCellRenderer {

@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

final Sexe sexe = (Sexe) value;

if (sexe.isMale()) {
setText("M");
setBackground(Color.BLUE);
} else {
setText("F");
setBackground(Color.PINK);
}
return this;
}
...
}

Puis spécifiez que vous souhaitez utiliser ce « renderer » dans le tableau.

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

final JTable tableau = new JTable(new ModeleDynamique2());
...

tableau.setDefaultRenderer(Sexe.class, new SexeCellRenderer());

pack();
}
}

Créez un « renderer » sur le même principe pour la race.

Quand vous relancez le programme, vous devriez constater que la colonne du sexe est maintenant en couleurs et que la race est devenue lisible.

Colonnes sexe et race jolies

La colonne des couleurs n'est toujours pas très jolie. Créez donc un « renderer » pour elle aussi. Par contre, vous allez directement indiquer au tableau sur quel numéro de colonne l'appliquer en vous inspirant du code suivant.

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

final JTable tableau = new JTable(new ModeleDynamique2());
...

tableau.getColumnModel().getColumn(4).setCellRenderer(new ListeCouleursCellRenderer());

pack();
}
}

III-G - Des tris

Durée estimée : 10 minutes.

Vous allez maintenant activer les capacités de tri des colonnes du tableau. Cela est fait au moyen d'un objet « RowSorter ».

Activez-le « sorter » par défaut du tableau en vous inspirant du code suivant :

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

final JTable tableau = new JTable(new ModeleDynamique2());
...

tableau.setAutoCreateRowSorter(true);
}

Tri par défaut sur le sexe

Dans la plupart des cas, c'est suffisant, mais on peut tout de même personnaliser le « sorter ». On peut évidemment créer notre propre « RowSorter », mais il est plus pratique d'utiliser la classe « TableRowSorter » et de la personnaliser pour effectuer des changements plutôt que de redéfinir une nouvelle implémentation ce qui peut s'avérer assez lourde.

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

final JTable tableau = new JTable(new ModeleDynamique2());
...

tableau.setAutoCreateRowSorter(true);
final TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(tableau.getModel());
tableau.setRowSorter(sorter);
}

Une première chose que vous pouvez faire est de spécifier une colonne comme « non triable » via la méthode « setSortable » :

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

sorter.setSortable(0, false); // colonne "Nom" pas triable
}

Relancez l'application et essayez de trier la colonne « Nom ». Vous devriez constater que c'est désormais impossible, alors qu'on peut le faire sur les autres colonnes.

Vous pouvez également créer des comparateurs spécifiques, par exemple lorsque vous travaillez avec des objets complexes. Comme ce n'est pas le cas dans l'application, amusez-vous à trier les noms complets, non pas par ordre alphabétique, mais par le nombre de caractères qui les composent.

StringSizeComparator.java

public class StringSizeComparator implements Comparator<String> {

@Override
public int compare(String s1, String s2) {
final Integer size1 = s1.length();
final Integer size2 = s2.length();
return size1.compareTo(size2);
}
}

Comme vous l'avez sûrement compris, la méthode de comparaison renvoie une valeur positive ou négative en fonction du paramètre le plus grand. Dans ce cas précis, puisque vous travaillez déjà sur des entiers (des tailles), vous auriez donc pu utiliser directement un code ressemblant au suivant :

StringSizeComparator.java

public class StringSizeComparator implements Comparator<String> {

@Override
public int compare(final String s1, final String s2) {
return s1.length() - s2.length();
}
}

Puis utilisez-le en vous inspirant du code suivant :

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

public ChienJFrame5() {
...

sorter.setSortable(0, false); // colonne "Nom" pas triable
sorter.setComparator(1, new StringSizeComparator()); // Tri sur le nb de lettres
}

Si vous faites attention, vous constaterez que la colonne du poids est triée par ordre alphabétique et non par ordre numérique. Vous devez donc y remédier.

III-H - Des petits boutons

Durée estimée : 10 minutes.

Vous allez maintenant donner à l'utilisateur la possibilité de modifier le contenu du tableau. Dans ce TD, vous vous contenterez de supprimer ou d'ajouter des lignes toutes prêtes.

Ajoutez des boutons dans l'IHM en vous inspirant du code suivant :

ChienJFrame5.java

public class ChienJFrame5 extends JFrame {

private static final Logger LOGGER = Logger.getLogger(ChienJFrame5.class);
...

public ChienJFrame5() {
...

final JTable tableau = new JTable(new ModeleDynamique2());
//getContentPane().add(tableau.getTableHeader(), NORTH);
getContentPane().add(new JScrollPane(tableau), CENTER);

final JPanel boutons = new JPanel();
boutons.add(new JButton(new AjouterLigneAction()));
getContentPane().add(boutons, SOUTH);

...

pack();
}

...

private class AjouterLigneAction extends AbstractAction {

private AjouterLigneAction() {
super("Ajouter");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("Click sur le bouton ajouter");
}
}
}
}

Relancez le programme. Quand vous cliquez sur le bouton, ça doit écrire dans la console.

Le bouton écrit dans la console

Modifiez le code de la fenêtre pour que le modèle se retrouve en variable de classe.

Ajoutez une méthode dans le modèle, pour pouvoir ajouter un nouveau chien, en vous inspirant du code suivant :

ModeleDynamique2.java

public class ModeleDynamique2 extends AbstractTableModel {
...

public void ajouterChien(final Chien chien) {
LOGGER.debug("ajouterChien");

...
}
}
}

Puis demandez au bouton d'appeler cette méthode. Pour simplifier ce TP, on va dire qu'on ajoute toujours un labrador blanc nommé « Idefix ». Dans une vraie application, on aurait plutôt ouvert une fenêtre avec un formulaire.

ChienJFrame6.java

public class ChienJFrame6 extends JFrame {

...
private ModeleDynamique2 modele;

...

private class AjouterLigneAction extends AbstractAction {
...

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("Click sur le bouton ajouter");

final Chien idefix = new SimpleChien("Idefix", "Idefix", MALE, LABRADOR, new String[]{"blanc"}, 25.0);
modele.ajouterChien(idefix);
}
}
}
}

Si vous relancez le programme, vous verrez que ça ne fait rien. Il faut revenir au modèle pour lui dire quoi faire.

ModeleDynamique2.java

public class ModeleDynamique2 extends AbstractTableModel {
...

public void ajouterChien(final Chien chien) {
LOGGER.debug("ajouterChien");

// Ajout dans la liste, a la fin.
chiens.add(chien);

final int position = chiens.size() - 1;
fireTableRowsInserted(position, position);
}
}
}

Cherchez ce que fait la méthode « fireTableRowsInserted » dans la Javadoc.

Après un clic sur le bouton, la nouvelle ligne doit apparaître en bas du tableau. En fonction de la taille donnée à la fenêtre, il faudra peut-être scroller pour voir Idefix.

Idefix en bas de liste

Pour supprimer des lignes, il faut employer le même principe.

ModeleDynamique2.java

public class ModeleDynamique2 extends AbstractTableModel {
...

public void supprimerChien(final int rowIndex) {
LOGGER.debug("supprimerChien");

chiens.remove(rowIndex);
fireTableRowsDeleted(rowIndex, rowIndex);
}
}
}

ChienJFrame6.java

public class ChienJFrame6 extends JFrame {
...
private JTable tableau;
private ModeleDynamique2 modele;

...

private class SupprimerLigneAction extends AbstractAction {
private SupprimerLigneAction() {
super("Supprimer");
}

@Override
public void actionPerformed(ActionEvent e) {
final int[] selection = tableau.getSelectedRows();
for (int i = selection.length - 1; i >= 0; i--) {
modele.supprimerChien(selection[i]);
}
}
}
}
}

Devinez pourquoi on décrémente l'index dans la boucle, au lieu de parcourir les éléments comme ils viennent.

Vérifiez que vous pouvez supprimer une ou plusieurs lignes du tableau et que les chiens correspondants disparaissent bien.

Avec plein de chiens en moins

Ici, on se contente d'ajouter ou de supprimer les chiens en mémoire. Quand on ferme le programme, les modifications sont perdues. Dans une vraie application, on aurait donc aussi un menu « Sauver » pour enregistrer les modifications en base ou dans le fichier CSV. Dans le cadre de ce TP, vous pouvez en rester là pour simplifier, mais rien ne vous empêche de coder la fonction de sauvegarde à la maison.

L'action pour ajouter un chien ne fait rien de bien spécial et est plutôt triviale. Par contre, il y a quelques petites subtilités pour la suppression. Tout d'abord, il faut savoir qu'une « JTable » peut fonctionner selon plusieurs modes de sélection :

- SINGLE_SELECTION : permet de sélectionner une seule ligne ;

- SINGLE_INTERVAL_SELECTION : permet de sélectionner un intervalle de ligne ;

- MULTIPLE_INTERVAL_SELECTION : permet de sélectionner de multiples intervalles. C'est la valeur par défaut.

Il faut donc comprendre que le tableau de lignes renvoyé par la méthode « getSelectedRows » peut retourner plusieurs intervalles. Les résultats sont retournés dans l'ordre ascendant. Il nous faut donc les supprimer depuis la fin, sinon on fausserait les résultats.

Faites (à la maison) quelques recherches sur les « table model », car vous aurez au moins une question sur le sujet durant l'évaluation de fin de module.

Le code du programme à ce stade est disponible en ligne : tp-chien-ihm-02.zip

IV - Graphes

Vous arrivez (enfin) à la partie sympa de ce TP. Vous allez maintenant dessiner des graphes pour avoir une représentation visuelle plus parlante de la liste des chiens.

Comme ça commence à être du « connu », la suite va aller plus vite.

IV-A - Maven

Durée estimée : 5 minutes.

Sans surprise, puisque vous allez utiliser la bibliothèque JFreeChart, vous ajouter une dépendance dans le fichier « pom.xml » :

pom.xml

<project ...>

...

<properties>
<!-- JFreeChart -->
<jfreechart.version>1.0.14</jfreechart.version>
</properties>

<dependencies>
<!-- JFreeChart -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>${jfreechart.version}</version>
</dependency>
</dependencies>

...
}

Lancez une installation Maven, puis faites un « refresh » dans Eclipse comme vous l'avez fait précédemment. Vérifiez que la bibliothèque JFreeChart est bien arrivée dans la liste de vos dépendances.

IV-B - Bouton et popup

Durée estimée : 5 minutes.

Ajoutez un nouveau bouton pour afficher un graphe en camembert en vous inspirant du code suivant :

ChienJFrame7.java

public class ChienJFrame7 extends JFrame {
...

private class CamembertMaleFemaleAction extends AbstractAction {

private CamembertMaleFemaleAction() {
super("Camembert M/F");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");
}
}
}
}

Pour l'instant, le bouton ne fait rien d'autre que de s'afficher et d'écrire dans la console, ce qui est un bon début ;-)

Ajout du bouton Camembert

Maintenant, en plus d'écrire dans la console, vous allez ouvrir une fenêtre :

ChienJFrame7.java

public class ChienJFrame7 extends JFrame {
...

private JDialog ratioMaleFemaleJdialog;
...

private class CamembertMaleFemaleAction extends AbstractAction {

...

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

ratioMaleFemaleJdialog = new JDialog();
ratioMaleFemaleJdialog.setTitle("Ratio H/F");

ratioMaleFemaleJdialog.pack();
ratioMaleFemaleJdialog.setVisible(true);
}
}
}
}

Relancez le programme et vérifiez qu'une fenêtre s'ouvre bien quand vous cliquez sur le bouton, même si elle est toute petite (vide).

IV-C - Graphes

Durée estimée : 10 minutes.

Maintenant que tout est prêt, vous pouvez passer à l'affichage du graphe. Cela se fait en deux temps : vous devez calculer les valeurs à afficher, puis vous devez indiquer comment les afficher.

ChienJFrame7.java

public class ChienJFrame7 extends JFrame {
...

private class CamembertMaleFemaleAction extends AbstractAction {

...

@Override
public void actionPerformed(ActionEvent e) {
...

final List<Chien> chiens = modele.getChiens();
int nbMale = 0;
for (Chien chien : chiens) {
if (chien.getSexe() == Sexe.MALE) {
nbMale++;
}
}
final int nbFemele = chiens.size() - nbMale;
}
}
}
}

Ce calcul ne devrait pas vous poser de difficulté. L'affichage va nécessiter un peu plus d'attention de votre part.

ChienJFrame7.java

public class ChienJFrame7 extends JFrame {
...

private class CamembertMaleFemaleAction extends AbstractAction {

...

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

// Calcul
final List<Chien> chiens = modele.getChiens();
int nbMale = 0;
for (Chien chien : chiens) {
if (chien.getSexe() == Sexe.MALE) {
nbMale++;
}
}
final int nbFemele = chiens.size() - nbMale;

ratioMaleFemaleJdialog = new JDialog();
ratioMaleFemaleJdialog.setTitle("Ratio H/F");

// Affichage
final DefaultPieDataset pieDataset = new DefaultPieDataset();

pieDataset.setValue("Female", nbFemele);
pieDataset.setValue("Male", nbMale);

final JFreeChart pieChart = ChartFactory.createPieChart("Ratio M/F", pieDataset, true, false, false);
final ChartPanel cPanel = new ChartPanel(pieChart);

ratioMaleFemaleJdialog.getContentPane().add(cPanel, CENTER);

ratioMaleFemaleJdialog.pack();
ratioMaleFemaleJdialog.setVisible(true);

}
}
}
}

Relancez de nouveau le programme et testez le résultat.

Camembert

Ce graphe en camembert était vraiment facile à programmer. Vous allez maintenant dessiner un graphe de répartition des chiens par poids, en les classant par tranches de cing kg :

- 0-5kg ;

- 6-10 kg ;

- 11-15 kg ;

- 16-20 kg;

- 21-25 kg ;

- 26-30 kg ;

- plus de 31 kg.

Pour le début, c'est la même chose. Il faut ajouter un bouton et récupérer la liste des chiens. Vous devriez être en mesure de réaliser cette étape sans aide.

Parcourez la liste des chiens pour calculer les tranches. Vous pouvez regarder une proposition de solution en annexe si vous ne trouvez pas.

Dans la suite, nous dirons que les tranches ont été mises dans une « Map », dont les clés seraient « 0-5 », « 6-10 », etc. :

Map

Map<String, Integer> map = new HashMap<String, Integer>();

Puis vous pouvez dessiner le graphe.

ChienJFrame7.java

public class ChienJFrame7 extends JFrame {
...

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

Map<String, Integer> map = new HashMap<String, Integer>();
...

final List<Chien> chiens = modele.getChiens();

for (Chien chien : chiens) {

...
}

poidsJdialog = new JDialog();
poidsJdialog.setTitle("Poids");

final DefaultCategoryDataset dataset = new DefaultCategoryDataset();

for(String tranche : map.keySet()) {
final Integer nb = map.get(tranche);
dataset.addValue(nb, "Poids", tranche);
}

final JFreeChart barChart = ChartFactory.createBarChart("Poids","Tranches","Nombre", /**/
dataset, PlotOrientation.VERTICAL, true, true, false);

final ChartPanel cPanel = new ChartPanel(barChart);

poidsJdialog.getContentPane().add(cPanel, CENTER);

poidsJdialog.pack();
poidsJdialog.setVisible(true);
}
}
}
}

Relancez le programme et vérifiez qu'il n'y a bien aucun chien dans la tranche « 26-30 ».

Tranches

Calculez et affichez les poids par sexe et par tranche en même temps, ce qui devrait ressembler à la capture suivante.

Poids M/F

Si, comme sur les captures précédentes, les tranches ne sont pas organisées dans l'ordre logique, c'est que vous devez améliorer votre algorithme. Ça devrait normalement ressembler à la capture suivante.

Tranches dans le bon ordre

Le code du programme à ce stade est disponible en ligne : tp-chien-ihm-03.zip

V - Conclusion

Ce TD est relativement complet, bien qu'on puisse aller encore plus loin. Il vous fait découvrir de nombreuses technologies, qui vous seront utiles au quotidien et dont vous devez maîtriser les grands principes : data model, design patterns, etc.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez

Ce TP est utilisé par de nombreux étudiants. N'hésitez donc pas à m'envoyer directement des idées d'amélioration, des propositions d'exercices supplémentaires, des pistes intéressantes, etc.

VI - Remerciements

Je tiens à remercier, en tant qu'auteur de ce TP, toutes les personnes qui m'ont aidé et soutenu. Je pense tout d'abord à mes collègues qui subissent mes questions au quotidien, mais aussi à mes contacts et amis du Web, dans le domaine de l'informatique ou non, qui m'ont fait part de leurs remarques et critiques. Bien entendu, je n'oublie pas l'équipe de Developpez.com qui m'a guidé dans la rédaction de cet article et m'a aidé à le corriger et le faire évoluer, principalement sur le forum.

Plus particulièrement j'adresse mes remerciements à Mickael BARON (keulkeul), Yann D'Isanto (le Y@m's), et zoom61.

VII - Annexes

VII-A - Liens

Maven : http://maven.apache.org/

Eclipse : http://eclipse.org/

JFreeChart : http://www.jfree.org/jfreechart ou http://sourceforge.net/projects/jfreechart/ si le premier lien ne marche pas.

VII-B - Pour aller plus loin

TP « tp-chien-dao » que vous devez lire en premier :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/tp-coder-et-tester-dao/

Article « Importer un projet Maven dans Eclipse en 5 minutes » :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/importer-projet-maven-dans-eclipse-5-min/

Article « Utiliser Maven 2 » :
https://matthieu-lux.developpez.com/tutoriels/java/maven/

Article « Afficher un tableau avec un Table Model Swing en 5 minutes » :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/afficher-tableau-avec-tablemodel-5-min/

Article « JTables - Un autre regard » de Nicolas Zozol :
https://nicolas-zozol.developpez.com/tutoriel/java/jtable

Article « Création interface graphique avec Swing : les tableaux (JTable) » de Baptiste Wicht :
https://baptiste-wicht.developpez.com/tutoriels/java/swing/jtable

Article « Afficher un graphe jfreechart en 5 minutes » :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/afficher-graphe-jfreechart-5-min/

Article « Threads et performance avec Swing » :
https://gfx.developpez.com/tutoriel/java/swing/swing-threading/
Le toolkit Swing permet aux développeurs Java de réaliser des applications graphiques très complexes. Sa complexité rend malheureusement aisée la réalisation d'interfaces présentant de piètres performances.

Retrouvez ma page et mes autres articles sur Developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articles

QR Code vers mes articles

Ajoutez-moi à vos contacts à l'aide du QR Code suivant :

QR Code contenant ma vCard

Suivez-moi sur Twitter : @thierryleriche

@thierryleriche

VII-C - Solution possible

Voici quelques-uns des fichiers que j'ai utilisés pour rédiger ce TP. Vous trouverez l'intégralité des fichiers et leurs versions successives, dans le Zip. Notez bien qu'il y a plusieurs solutions possibles. Celle que je propose est l'une des plus simples.

LauncherIHM.java

package com.icauda.tp.chien;

import javax.swing.JFrame;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.ihm.ChienJFrame7;

public class LauncherIHM {

private static final Logger LOGGER = Logger.getLogger(LauncherIHM.class);

public static void main(String[] args) {
LOGGER.debug("TP des chiens : DEBUT");

// final ChienJFrame1 f = new ChienJFrame1();
// final ChienJFrame2 f = new ChienJFrame2();
// final JFrame f = new ChienJFrame3();
// final JFrame f = new ChienJFrame4();
// final JFrame f = new ChienJFrame5();
//final JFrame f = new ChienJFrame6();
final JFrame f = new ChienJFrame7();
f.setVisible(true);

LOGGER.debug("TP des chiens : FIN");

}
}

ModeleStatique2.java

package com.icauda.tp.chien.ihm;

import static com.icauda.tp.chien.domain.RaceDeChien.BERGER_ALLEMAND;
import static com.icauda.tp.chien.domain.RaceDeChien.CANICHE;
import static com.icauda.tp.chien.domain.RaceDeChien.DANOIS;
import static com.icauda.tp.chien.domain.RaceDeChien.GOLDEN;
import static com.icauda.tp.chien.domain.RaceDeChien.ROTTWEILER;
import static com.icauda.tp.chien.domain.Sexe.FEMALE;
import static com.icauda.tp.chien.domain.Sexe.MALE;

import javax.swing.table.AbstractTableModel;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.SimpleChien;

public class ModeleStatique2 extends AbstractTableModel {

private static final long serialVersionUID = 2253643022432829283L;

private final String[] entetes;

private final SimpleChien[] chiens;

public ModeleStatique2() {
super();

chiens = new SimpleChien[] { /**/
new SimpleChien("Pluto","Pluto le chien Disney",MALE, GOLDEN, new String[] {"jaune"}, 24.0), /**/
new SimpleChien("Lassie","Lassie des alpes", FEMALE, BERGER_ALLEMAND, new String[] {"blanc","noir"}, 32.3), /**/
new SimpleChien("Volt","Volt le super chien",MALE, CANICHE, new String[] {"blanc","noir"}, 14.0), /**/
new SimpleChien("Medor","Medor",MALE, ROTTWEILER, new String[] {"gris"}, 32.0), /**/
new SimpleChien("King","Kiking", MALE, DANOIS, new String[] {"noir"}, 46.5), /**/
new SimpleChien("Chocolat","Chocolat mon amour", MALE, GOLDEN, new String[] {"marron"}, 31.0), /**/
new SimpleChien("Vanille","Vanille mon amour", FEMALE, GOLDEN, new String[] {"blanc"}, 37.0), /**/
new SimpleChien("Pluto","Pluto le chien Disney", MALE, GOLDEN, new String[] {"jaune"}, 24.0), /**/
new SimpleChien("Lassie","Lassie des alpes", FEMALE, BERGER_ALLEMAND, new String[] {"blanc","noir"}, 32.3), /**/
new SimpleChien("Volt","Volt le super chien", MALE, CANICHE, new String[] {"blanc","noir"}, 14.0), /**/
new SimpleChien("Medor","Medor",MALE, ROTTWEILER, new String[] {"gris"}, 32.0), /**/
new SimpleChien("King", "Kiking", MALE, DANOIS, new String[] {"noir"}, 46.5), /**/
new SimpleChien("Chocolat", "Chocolat mon amour", MALE, GOLDEN, new String[] {"marron"}, 31.0), /**/
new SimpleChien("Vanille","Vanille mon amour",FEMALE, GOLDEN, new String[] { " blanc" }, 37.0), /**/
new SimpleChien("Pluto","Pluto le chien Disney",MALE, GOLDEN, new String[] {" jaune" }, 24.0), /**/
new SimpleChien("Lassie","Lassie des alpes",FEMALE, BERGER_ALLEMAND, new String[] {" blanc","noir" }, 32.3), /**/
new SimpleChien("Volt","Volt le super chien",MALE, CANICHE, new String[] {" blanc","noir" }, 14.0), /**/
new SimpleChien("Medor","Medor",MALE, ROTTWEILER, new String[] {" gris" }, 32.0), /**/
new SimpleChien("King","Kiking",MALE, DANOIS, new String[] {" noir" }, 46.5), /**/
new SimpleChien("Chocolat","Chocolat mon amour",MALE, GOLDEN, new String[] {" marron" }, 31.0), /**/
new SimpleChien("Vanille","Vanille mon amour",FEMALE, GOLDEN, new String[] { " blanc" }, 37.0) /**/
};

entetes = new String[] { " Nom","Nom complet","Sexe","Race","Couleurs","Poids" };
}

@Override
public String getColumnName(int columnIndex) {
return entetes[columnIndex];
}

@Override
public int getRowCount() {
return chiens.length;
}

@Override
public int getColumnCount() {
return entetes.length;
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {

final Chien chien = chiens[rowIndex];

// Ordre :" Nom","Nom complet","Sexe","Race","Couleurs","Poids"
switch (columnIndex) {
case 0:
return chien.getNom();
case 1:
return chien.getNomComplet();
case 2:
return chien.getSexe();
case 3:
return chien.getRace();
case 4:
return chien.getCouleurs();
case 5:
return chien.getPoids();
default:
throw new IllegalArgumentException("Le numero de colonne indique n'est pas valide.");

}

}
}

ModeleDynamique2.java

package com.icauda.tp.chien.ihm;

import java.util.List;

import javax.swing.table.AbstractTableModel;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.RaceDeChien;
import com.icauda.tp.chien.domain.Sexe;
import com.icauda.tp.chien.service.ChienService;

public class ModeleDynamique2 extends AbstractTableModel {

private static final long serialVersionUID = -139320534128196933L;

private static final Logger LOGGER = Logger.getLogger(ModeleDynamique2.class);

private final String[] entetes;
private List<Chien> chiens;

final private static String CHIENS_FILE_NAME =" src/main/resources/chiens.csv";
private ChienService chienService = ChienService.getInstance();

public ModeleDynamique2() {
super();

chiens = chienService.findAllChiens(CHIENS_FILE_NAME);
entetes = new String[] {" Nom","Nom complet","Sexe","Race","Couleurs","Poids" };
}

@Override
public String getColumnName(int columnIndex) {
return entetes[columnIndex];
}

@Override
public int getRowCount() {
return chiens.size();
}

@Override
public int getColumnCount() {
return entetes.length;
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {

final Chien chien = chiens.get(rowIndex);

// Ordre :" Nom","Nom complet","Sexe","Race","Couleurs","Poids"
switch (columnIndex) {
case 0:
return chien.getNom();
case 1:
return chien.getNomComplet();
case 2:
return chien.getSexe();
case 3:
return chien.getRace();
case 4:
return chien.getCouleurs();
case 5:
return chien.getPoids();
default:
throw new IllegalArgumentException("Le numero de colonne indique n'est pas valide.");

}

}

@Override
public Class<?> getColumnClass(int columnIndex) {
switch (columnIndex) {
case 0:
case 1:
return String.class;

case 2:
return Sexe.class;

case 3:
return RaceDeChien.class;

// case 4:
// return List.class;
//
// case 5:
// return Double.class;

default:
return Object.class;
}
}

public void ajouterChien(final Chien chien) {
LOGGER.debug("ajouterChien");

chiens.add(chien);

final int position = chiens.size() - 1;
fireTableRowsInserted(position, position);
}

public void supprimerChien(final int rowIndex) {
LOGGER.debug("supprimerChien");

chiens.remove(rowIndex);
fireTableRowsDeleted(rowIndex, rowIndex);
}

public List<Chien> getChiens() {
return chiens;
}
}

RaceCellRenderer.java

package com.icauda.tp.chien.ihm;

import java.awt.Component;

import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;

import com.icauda.tp.chien.domain.RaceDeChien;

public class RaceCellRenderer extends DefaultTableCellRenderer {

private static final long serialVersionUID = 5826273340420630286L;

@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

final RaceDeChien race = (RaceDeChien) value;

setText(race.getLabel());

return this;
}
}

StringSizeComparator.java

package com.icauda.tp.chien.ihm;

import java.util.Comparator;

public class StringSizeComparator implements Comparator<String> {

@Override
public int compare(String s1, String s2) {
final Integer size1 = s1.length();
final Integer size2 = s2.length();
return size1.compareTo(size2);
}
}

ListeCouleursCellRenderer.java

package com.icauda.tp.chien.ihm;

import java.awt.Component;
import java.util.List;

import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;

public class ListeCouleursCellRenderer extends DefaultTableCellRenderer {

private static final long serialVersionUID = -844387322539021290L;

@Override
@SuppressWarnings(value =" unchecked")
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

final List<String> colors = (List<String>) value;

final StringBuilder sb = new StringBuilder();
boolean first = true;
for (String color : colors) {
if (!first) {
sb.append(",");
}
sb.append(color);
first = false;
}
setText(sb.toString());

return this;
}
}

ChienService.java

package com.icauda.tp.chien.service;

import java.io.File;
import java.util.List;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.dao.csv.CsvChienDao;
import com.icauda.tp.chien.dao.csv.EngineCsvChienDao;
import com.icauda.tp.chien.domain.Chien;

public class ChienService {

private static final Logger LOGGER = Logger.getLogger(ChienService.class);

private CsvChienDao csvChienDao;

/**
* Instance de la classe, pour le singleton.
*/
private static ChienService instance;

/**
* Constructeur prive.
*/
private ChienService() {
LOGGER.debug("Constructeur");
csvChienDao = new EngineCsvChienDao();
}

/**
* Singleton classique, synchro, avec creation sur demande.
*
* @return
*/
public static synchronized ChienService getInstance() {
if (instance == null) {
instance = new ChienService();
}

return instance;
}

public List<Chien> findAllChiens(final String fileName) {
final File file = new File(fileName);
csvChienDao.init(file);
return csvChienDao.findAllChiens();
}
}

ChienJFrame7.java

package com.icauda.tp.chien.ihm;

import static com.icauda.tp.chien.domain.RaceDeChien.LABRADOR;
import static com.icauda.tp.chien.domain.Sexe.MALE;
import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.NORTH;
import static java.awt.BorderLayout.SOUTH;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

import org.apache.log4j.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.RaceDeChien;
import com.icauda.tp.chien.domain.Sexe;
import com.icauda.tp.chien.domain.SimpleChien;

public class ChienJFrame7 extends JFrame {

private static final long serialVersionUID = -638731145561555723L;

private static final Logger LOGGER = Logger.getLogger(ChienJFrame7.class);

private JTable tableau;
private ModeleDynamique2 modele;

private JDialog ratioMaleFemaleJdialog;
private JDialog poidsJdialog;
private JDialog poids2Jdialog;

public ChienJFrame7() {
super();
setTitle("Liste des chiens (v7)");
setPreferredSize(new Dimension(500, 400));
setDefaultCloseOperation(EXIT_ON_CLOSE);

modele = new ModeleDynamique2();
tableau = new JTable(modele);
//getContentPane().add(tableau.getTableHeader(), NORTH);
getContentPane().add(new JScrollPane(tableau), CENTER);

final JPanel boutons = new JPanel();
boutons.add(new JButton(new AjouterLigneAction()));
boutons.add(new JButton(new SupprimerLigneAction()));
boutons.add(new JButton(new CamembertMaleFemaleAction()));
boutons.add(new JButton(new PoidsAction()));
boutons.add(new JButton(new Poids2Action()));
getContentPane().add(boutons, SOUTH);

tableau.setDefaultRenderer(Sexe.class, new SexeCellRenderer());
tableau.setDefaultRenderer(RaceDeChien.class, new RaceCellRenderer());

tableau.getColumnModel().getColumn(4).setCellRenderer(new ListeCouleursCellRenderer());

tableau.setAutoCreateRowSorter(true);

final TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(tableau.getModel());
tableau.setRowSorter(sorter);

sorter.setSortable(0, false); // colonne Nom non triable
sorter.setComparator(1, new StringSizeComparator()); // Tri sur le nb de
// lettres

pack();
}

private class AjouterLigneAction extends AbstractAction {

private static final long serialVersionUID = 7183768497443802311L;

private AjouterLigneAction() {
super("Ajouter");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("Click sur le bouton ajouter");

final Chien idefix = new SimpleChien("Idefix","Idefix",MALE, LABRADOR, new String[] { " blanc" }, 25.0);
modele.ajouterChien(idefix);
}
}

private class SupprimerLigneAction extends AbstractAction {

private static final long serialVersionUID = -5556554884674073716L;

private SupprimerLigneAction() {
super("Supprimer");
}

@Override
public void actionPerformed(ActionEvent e) {
final int[] selection = tableau.getSelectedRows();
for (int i = selection.length - 1; i >= 0; i--) {
modele.supprimerChien(selection[i]);
}
}
}

private class CamembertMaleFemaleAction extends AbstractAction {

private static final long serialVersionUID = 4515460004284651581L;

private CamembertMaleFemaleAction() {
super("Camembert M/F");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

final List<Chien> chiens = modele.getChiens();
int nbMale = 0;
for (Chien chien : chiens) {
if (chien.getSexe() == Sexe.MALE) {
nbMale++;
}
}
final int nbFemele = chiens.size() - nbMale;

ratioMaleFemaleJdialog = new JDialog();
ratioMaleFemaleJdialog.setTitle("Ratio H/F");

final DefaultPieDataset pieDataset = new DefaultPieDataset();

pieDataset.setValue("Female",nbFemele);
pieDataset.setValue("Male",nbMale);

final JFreeChart pieChart = ChartFactory.createPieChart("Ratio M/F",pieDataset, true, false, false);
final ChartPanel cPanel = new ChartPanel(pieChart);

ratioMaleFemaleJdialog.getContentPane().add(cPanel, CENTER);

ratioMaleFemaleJdialog.pack();
ratioMaleFemaleJdialog.setVisible(true);

}
}

private class PoidsAction extends AbstractAction {

private static final long serialVersionUID = -2621678352128531798L;

private PoidsAction() {
super("Poids");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

String[] trancheNames = {" 0-5","6-10","11-15","16-20","21-25","26-30","31+"};

Map<String, Integer> map = new HashMap<String, Integer>();
for(String trancheName:trancheNames) {
map.put(trancheName, 0);
}

final List<Chien> chiens = modele.getChiens();

for (Chien chien : chiens) {

String tranche = " 0-5";
if (6 <= chien.getPoids()) {
tranche =" 6-10";
}
if (11 <= chien.getPoids()) {
tranche =" 11-15";
}
if (16 <= chien.getPoids()) {
tranche =" 16-20";
}
if (21 <= chien.getPoids()) {
tranche =" 21-25";
}
if (26 <= chien.getPoids()) {
tranche =" 26-30";
}
if (31 <= chien.getPoids()) {
tranche =" 31+";
}

Integer value = map.get(tranche);
value++;
map.put(tranche, value);
}

poidsJdialog = new JDialog();
poidsJdialog.setTitle("Poids");

final DefaultCategoryDataset dataset = new DefaultCategoryDataset();

for (String tranche : trancheNames) {
final Integer nb = map.get(tranche);
dataset.addValue(nb,"Poids",tranche);
}

final JFreeChart barChart = ChartFactory.createBarChart("Poids","Tranches","Nombre",/**/
dataset, PlotOrientation.VERTICAL, true, true, false);

final ChartPanel cPanel = new ChartPanel(barChart);

poidsJdialog.getContentPane().add(cPanel, CENTER);

poidsJdialog.pack();
poidsJdialog.setVisible(true);
}
}

private class Poids2Action extends AbstractAction {

private static final long serialVersionUID = -2621678352128531798L;

private Poids2Action() {
super("Poids M/F");
}

@Override
public void actionPerformed(ActionEvent e) {
LOGGER.debug("cliv clic");

String[] trancheNames = {" 0-5","6-10","11-15","16-20","21-25","26-30","31+" };

Map<String, Integer> maleMap = new HashMap<String, Integer>();
Map<String, Integer> femaleMap = new HashMap<String, Integer>();
for(String trancheName:trancheNames) {
maleMap.put(trancheName, 0);
femaleMap.put(trancheName, 0);
}

final List<Chien> chiens = modele.getChiens();

for (Chien chien : chiens) {

String tranche =" 0-5";
if (6 <= chien.getPoids()) {
tranche =" 6-10";
}
if (11 <= chien.getPoids()) {
tranche =" 11-15";
}
if (16 <= chien.getPoids()) {
tranche =" 16-20";
}
if (21 <= chien.getPoids()) {
tranche =" 21-25";
}
if (26 <= chien.getPoids()) {
tranche =" 26-30";
}
if (31 <= chien.getPoids()) {
tranche =" 31+";
}

final Map<String, Integer> map = (chien.getSexe() == Sexe.MALE) ? maleMap : femaleMap;

Integer value = map.get(tranche);
value++;
map.put(tranche, value);
}

poids2Jdialog = new JDialog();
poids2Jdialog.setTitle("Poids M/F");

final DefaultCategoryDataset dataset = new DefaultCategoryDataset();

for (String tranche : trancheNames) {
final Integer nb = maleMap.get(tranche);
dataset.addValue(nb," Male",tranche);
}

for (String tranche : trancheNames) {
final Integer nb = femaleMap.get(tranche);
dataset.addValue(nb," Female",tranche);
}

final JFreeChart barChart = ChartFactory.createBarChart("Poids","Tranches","Nombre",/**/
dataset, PlotOrientation.VERTICAL, true, true, false);

final ChartPanel cPanel = new ChartPanel(barChart);

poids2Jdialog.getContentPane().add(cPanel, CENTER);

poids2Jdialog.pack();
poids2Jdialog.setVisible(true);
}
}
}