Introduction à Zest
Date de publication : 20/01/2012. Date de mise à jour : 09/02/2012.
Par
Alain BERNARD (Page perso)
Ce tutoriel est une introduction à la boîte à
outils de visualisation de graphes sous Eclipse Zest. Au travers d'un
exemple simple, nous montrerons comment construire un graphe en utilisant
cette boîte à outils.
Les sources de l'exemple sont disponibles
à l'adresse suivante :
FTP ou
HTTP.
Pour toute remarque ou question sur ce tutoriel, profitez de
cette discussion :
Commentez 
I. Introduction
I-A. Qu'est-ce que Zest ?
I-B. Présentation de notre fil conducteur
II. Notre premier graphe
II-A. Installation des outils
II-B. Composants de Zest
II-C. Création de notre premier graphe
III. Utilisation de l'abstraction JFace
III-A. Mise en place du modèle
III-B. Approche orientée « nœuds »
III-C. Approche orientée « branches »
IV. Conclusion
V. Liens utiles
VI. Remerciements
I. Introduction
I-A. Qu'est-ce que Zest ?
Eclipse Zest est une boîte à outils de
visualisation de graphes, basée sur SWT et Draw2D. Elle permet
de construire et visualiser des graphes, soit directement par
création des branches et des nœuds, soit par l'utilisation
de l'abstraction JFace. Cette abstraction permet une approche par
modèle, en s'affranchissant de la construction du graphe
proprement dite.
Dans ce tutoriel, je vous propose d'utiliser Zest afin de
représenter un graphe simple, soit directement par la
construction des nœuds et branches du graphe, soit par
l'utilisation de l'abstraction JFace à partir d'un modèle
de données.
Ce tutoriel suppose que vous disposiez des connaissances de
base sur les technologies suivantes :
I-B. Présentation de notre fil conducteur
Notre fil conducteur dans cet article sera la
représentation d'un réseau routier entre villes. Chaque
ville est un nœud du graphe, et chaque route est une branche du
graphe. Vous avez peut-être entendu parler de ces graphes dans
le problème très connu du
voyageur de commerce. Nous n'entrerons pas
dans ce tutoriel dans des considérations sur la théorie des
graphes.
II. Notre premier graphe
II-A. Installation des outils
Pour installer Zest, ouvrez l'assistant d'installation de
nouveaux plugins d'Eclipse et dans la liste sélectionnez
« Graphical Editing Framework Zest Visualization Toolkit
SDK », dans la section
« Modeling ».
II-B. Composants de Zest
Zest est basé sur les composants suivants :
- GraphNode : un nœud du graphe ;
- GraphConnection : une branche du graphe, qui peut
être orientée ou non ;
- GraphContainer : utilisé pour un graphe au sein
d'un autre graphe ;
- Graph : élément de base de la boîte
à outils, il contient les éléments
précités.
D'autre part, Zest agence les composants du graphe en
utilisant différents types de rendu, appelés
« layouts ». Ces layouts seront
détaillés dans un prochain article. Zest peut aussi filtrer
les éléments du graphe, comme peut le faire un arbre ou une
table JFace.
II-C. Création de notre premier graphe
Nous allons construire notre premier graphe. Pour cela,
créez une nouvelle application Eclipse RCP, en utilisant le
template « RCP application with a view ». Ajoutez
dans les dépendances de l'application les plugins
« org.eclipse.zest.core » et
« org.eclipse.zest.layouts ».
Remplacez le contenu de la vue générée par
l'assistant par le code ci-dessous. Notez que nous renommons la vue
en « BasicGraphView ».
| BasicGraphView.java |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. | 61. | 62. | 63. | 64. | 65. |
| package com.abernard.zest;
|
| import org.eclipse.swt.SWT;
| import org.eclipse.swt.widgets.Composite;
| import org.eclipse.ui.part.ViewPart;
| import org.eclipse.zest.core.widgets.Graph;
| import org.eclipse.zest.core.widgets.GraphConnection;
| import org.eclipse.zest.core.widgets.GraphNode;
| import org.eclipse.zest.layouts.LayoutStyles;
| import org.eclipse.zest.layouts.algorithms.SpringLayoutAlgorithm;
|
|
|
|
|
| @author
|
|
| public class BasicGraphView extends ViewPart {
|
|
|
|
| private Graph graph;
|
| @Override
| public void createPartControl(Composite parent) {
|
| graph = new Graph(parent, SWT.NONE);
|
|
| GraphNode paris = new GraphNode(graph, SWT.NONE, "Paris");
| GraphNode bordeaux = new GraphNode(graph, SWT.NONE, "Bordeaux");
| GraphNode poitiers = new GraphNode(graph, SWT.NONE, "Poitiers");
| GraphNode toulouse = new GraphNode(graph, SWT.NONE, "Toulouse");
|
|
|
| GraphConnection parisToulouse = new GraphConnection(graph, SWT.NONE,
| toulouse, paris);
| parisToulouse.setText("678km");
|
| GraphConnection parisPoitiers = new GraphConnection(graph, SWT.NONE,
| paris, poitiers);
| parisPoitiers.setText("338km");
|
| GraphConnection poitiersBordeaux = new GraphConnection(graph, SWT.NONE,
| bordeaux, poitiers);
| poitiersBordeaux.setText("235km");
|
| GraphConnection bordeauxToulouse = new GraphConnection(graph, SWT.NONE,
| toulouse, bordeaux);
| bordeauxToulouse.setText("244km");
|
|
|
| graph.setLayoutAlgorithm(new SpringLayoutAlgorithm(
| LayoutStyles.NO_LAYOUT_NODE_RESIZING), true);
| }
|
| @Override
| public void setFocus() {
|
| }
| }
|
|
|
Les éléments de type GraphNode (bloc 1) sont les
nœuds du graphe. Les éléments de type GraphConnection
(bloc 2) servent à définir les branches du graphe. Enfin,
on définit un layout pour le graphe (bloc 3).
Lancez l'application pour observer le
résultat :
Vous pouvez cliquer sur les éléments du graphe
pour les réagencer. Voilà un graphe simple obtenu
facilement et représentatif du problème. Néanmoins, la
méthode de construction est lourde, et va vite devenir
problématique au fur et à mesure que notre modèle va
s'enrichir avec de nouveaux éléments.
Nous allons donc utiliser l'abstraction JFace pour
représenter des modèles plus complexes dans notre graphe.
C'est l'objet de la partie suivante.
III. Utilisation de l'abstraction JFace
III-A. Mise en place du modèle
Afin de construire notre graphe, nous avons besoin dans un
premier temps de construire notre modèle. Celui-ci est basé
sur les classes Ville et Route qui sont définies comme
suit :
| Classe Ville |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. |
| package com.abernard.zest.model;
|
| import java.util.ArrayList;
| import java.util.List;
|
|
|
|
|
| @author
|
|
| public class Ville {
|
|
|
|
| private String nom;
|
|
|
|
| private List<Ville> connexions;
|
|
|
| @param
|
| public Ville (String n) {
| this.nom = n;
| this.connexions = new ArrayList<Ville>();
| }
|
|
|
| @param
|
| public void addConnexion(Ville v) {
| if (!connexions.contains(v)) {
| connexions.add(v);
| }
| }
|
|
|
| @return
|
| public List<Ville> getConnexions() {
| return this.connexions;
| }
|
|
|
| @return
|
| public String getNom() {
| return this.nom;
| }
|
| }
|
|
|
| Classe Route |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. | 61. | 62. |
| package com.abernard.zest.model;
|
|
|
|
| @author
|
|
| public class Route {
|
|
|
|
| private Ville source;
|
|
|
|
| private Ville destination;
|
|
|
|
| private int longueur;
|
|
|
| @param
| @param
| @param
|
| public Route(Ville s, Ville d, int l) {
| this.source = s;
| this.destination = d;
| this.longueur = l;
| }
|
|
|
| @return
|
| public Ville getSource() {
| return this.source;
| }
|
|
|
| @return
|
| public Ville getDestination() {
| return this.destination;
| }
|
|
|
| @return
|
| public int getLongueur() {
| return this.longueur;
| }
|
| }
|
|
|
La troisième classe du modèle est un singleton, et
construit le modèle de deux manières différentes. La
première approche permet d'accéder à la liste de
toutes les villes, qui donnent à chaque fois à quelles
autres villes elles sont reliées. La deuxième approche
permet d'accéder à la liste des routes, qui donnent à
chaque fois la ville de départ et celle de destination.
Dans la pratique, un modèle ne permet pas toujours
cette double approche. Dans notre cas c'est uniquement pour explorer
les possibilités de Zest.
| Classe Model |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. | 61. | 62. | 63. | 64. | 65. | 66. | 67. | 68. | 69. | 70. | 71. | 72. | 73. | 74. | 75. | 76. | 77. | 78. | 79. | 80. | 81. | 82. | 83. | 84. | 85. | 86. |
| package com.abernard.zest.model;
|
| import java.util.ArrayList;
| import java.util.List;
|
|
|
|
|
|
| @author
|
| public enum Model {
|
|
|
|
| INSTANCE;
|
|
|
|
| private List<Ville> villes;
|
|
|
|
| private List<Route> routes;
|
|
|
|
| private Model() {
| villes = new ArrayList<Ville>();
| routes = new ArrayList<Route>();
|
|
| Ville paris = new Ville("Paris");
| villes.add(paris);
| Ville toulouse = new Ville("Toulouse");
| villes.add(toulouse);
| Ville bordeaux = new Ville("Bordeaux");
| villes.add(bordeaux);
| Ville poitiers = new Ville("Poitiers");
| villes.add(poitiers);
| Ville metz = new Ville("Metz");
| villes.add(metz);
|
|
| Route r1 = new Route(paris, toulouse, 678);
| routes.add(r1);
| paris.addConnexion(toulouse);
|
| r1 = new Route(paris, poitiers, 338);
| routes.add(r1);
| poitiers.addConnexion(paris);
|
| r1 = new Route(poitiers, bordeaux, 235);
| routes.add(r1);
| bordeaux.addConnexion(poitiers);
|
| r1 = new Route(bordeaux, toulouse, 244);
| routes.add(r1);
| toulouse.addConnexion(bordeaux);
|
| r1 = new Route(paris, metz, 333);
| routes.add(r1);
| paris.addConnexion(metz);
| }
|
|
|
| @return
|
| public List<Ville> getVilles() {
| return villes;
| }
|
|
|
| @return
|
| public List<Route> getRoutes() {
| return routes;
| }
| }
|
|
|
Notre modèle étant mis en place, nous pouvons
maintenant créer nos graphes.
III-B. Approche orientée « nœuds »
Pour construire un graphe à partir des nœuds, Zest
fournit l'interface
« IGraphEntityContentProvider ». Créez la
classe « EntityContentProvider » définie
comme suit :
| EntityContentProvider.java |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. |
| package
| com.abernard.zest.viewer;
|
| import java.util.List;
|
| import org.eclipse.jface.viewers.Viewer;
| import org.eclipse.zest.core.viewers.IGraphEntityContentProvider;
|
| import com.abernard.zest.model.Ville;
|
|
|
|
|
| @author
|
| public class EntityContentProvider implements IGraphEntityContentProvider {
|
|
| @SuppressWarnings("unchecked")
| @Override
| public Object[] getElements(Object input) {
|
|
| return ((List<Ville>)input).toArray();
| }
|
| @Override
| public void dispose() {
|
| }
|
| @Override
| public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
|
| }
|
| @Override
| public Object[] getConnectedTo(Object entity) {
|
|
| if (entity instanceof Ville) {
| return ((Ville)entity).getConnexions().toArray();
| } else {
| return null;
| }
| }
| }
|
|
|
Comme tout viewer JFace, il nous faut aussi définir un
« LabelProvider ». Dans un premier temps, nos
LabelProvider se contenteront d'étendre la classe
« LabelProvider » de JFace. Nous verrons dans le
cours « Compléments sur Zest » que l'on peut
définir bien d'autres choses grâce à des interfaces
particulières. Créez donc la classe
« EntityLabelProvider » :
| EntityLabelProvider.java |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. |
| package
| com.abernard.zest.viewer;
|
| import org.eclipse.jface.viewers.LabelProvider;
|
| import com.abernard.zest.model.Route;
| import com.abernard.zest.model.Ville;
|
|
|
|
|
| @author
|
| public class EntityLabelProvider extends LabelProvider {
|
| @Override
| public String getText(Object element) {
|
|
|
| if (element instanceof Route) {
| return ((Route)element).getLongueur() + "km";
| } else if (element instanceof Ville) {
| return ((Ville)element).getNom();
| } else {
| return null;
| }
| }
|
| }
|
|
|
Créez maintenant la vue
« LinkGraphView », qui affichera un graphe
« orienté nœuds ». Les données
d'entrée du graphe sont données via la méthode
« setInput », et sont donc les villes de notre
modèle.
| EntityGraphView.java |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. |
| package com.abernard.zest;
|
| import org.eclipse.swt.SWT;
| import org.eclipse.swt.widgets.Composite;
| import org.eclipse.ui.part.ViewPart;
| import org.eclipse.zest.core.viewers.GraphViewer;
| import org.eclipse.zest.layouts.LayoutStyles;
| import org.eclipse.zest.layouts.algorithms.SpringLayoutAlgorithm;
|
| import com.abernard.zest.model.Model;
| import com.abernard.zest.viewer.EntityContentProvider;
| import com.abernard.zest.viewer.EntityLabelProvider;
|
|
|
|
|
| @author
|
| public class EntityGraphView extends ViewPart {
|
|
|
|
| private GraphViewer viewer;
|
| @Override
| public void createPartControl(Composite parent) {
| viewer = new GraphViewer(parent, SWT.NONE);
|
| viewer.setContentProvider(new EntityContentProvider());
|
| viewer.setLabelProvider(new EntityLabelProvider());
|
|
|
|
| viewer.setInput(Model.INSTANCE.getVilles());
|
|
| viewer.setLayoutAlgorithm(new
| SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));
| viewer.applyLayout();
|
| }
|
| @Override
| public void setFocus() {
|
| }
| }
|
|
|
Le graphe est donc construit correctement, et ceci de
manière très simple. On constate une chose : même
si le LabelProvider fournit le texte à afficher pour des
éléments de type « Route », aucun texte
n'est affiché. On le verra par la suite, le fonctionnement est
différent dans une approche orientée
« branches ».
III-C. Approche orientée « branches »
Construisons maintenant notre graphe à partir des liens
entre nœuds. Pour cela, Zest met à notre disposition
l'interface « IGraphContentProvider ». Créez
la classe « LinkContentProvider » comme
suit :
| LinkContentProvider.java |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. |
| package
| com.abernard.zest.viewer;
|
| import java.util.ArrayList;
|
| import org.eclipse.jface.viewers.Viewer;
| import org.eclipse.zest.core.viewers.IGraphContentProvider;
|
| import com.abernard.zest.model.Route;
|
|
|
|
|
| @author
|
| public class LinkContentProvider implements IGraphContentProvider {
|
| @Override
| public void dispose() {
|
| }
|
| @Override
| public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
|
| }
|
| @Override
| public Object getSource(Object rel) {
|
| if (rel instanceof Route) {
| return ((Route)rel).getSource();
| } else {
| return null;
| }
| }
|
| @Override
| public Object getDestination(Object rel) {
|
| branche
| if (rel instanceof Route) {
| return ((Route)rel).getDestination();
| } else {
| return null;
| }
| }
|
| @SuppressWarnings("unchecked")
| @Override
| public Object[] getElements(Object input) {
|
|
| return ((ArrayList<Route>)input).toArray();
| }
|
|
|
| }
|
|
|
Là aussi, nous devons définir un LabelProvider.
Notez que dans ce cas présent, nous pourrions réutiliser
celui que nous avions défini dans la partie
précédente. Cependant, en prévision du cours suivant,
je vous invite à créer la classe
« LinkLabelProvider » :
| LinkLabelProvider.java |
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. |
| package
| com.abernard.zest.viewer;
|
| import org.eclipse.jface.viewers.LabelProvider;
|
| import com.abernard.zest.model.Route;
| import com.abernard.zest.model.Ville;
|
|
|
|
|
| @author
|
| public class LinkLabelProvider extends LabelProvider {
|
| @Override
| public String getText(Object element) {
|
|
| if (element instanceof Route) {
| return ((Route)element).getLongueur() + "km";
| } else if (element instanceof Ville) {
| return ((Ville)element).getNom();
| } else {
| return null;
| }
| }
|
| }
|
|
|
Enfin, nous pouvons créer la vue qui va afficher le
graphe :
| LinkGraphView.java |
 1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. |
| package com.abernard.zest;
|
| import org.eclipse.swt.SWT;
| import org.eclipse.swt.widgets.Composite;
| import org.eclipse.ui.part.ViewPart;
| import org.eclipse.zest.core.viewers.GraphViewer;
| import org.eclipse.zest.layouts.LayoutStyles;
| import org.eclipse.zest.layouts.algorithms.SpringLayoutAlgorithm;
|
| import com.abernard.zest.model.Model;
| import com.abernard.zest.viewer.LinkContentProvider;
| import com.abernard.zest.viewer.LinkLabelProvider;
|
|
|
|
|
| @author
|
| public class LinkGraphView extends ViewPart {
|
|
|
|
| private GraphViewer viewer;
|
| @Override
| public void createPartControl(Composite parent) {
| viewer = new GraphViewer(parent, SWT.NONE);
|
| viewer.setContentProvider(new LinkContentProvider());
|
| viewer.setLabelProvider(new LinkLabelProvider());
|
|
|
|
| viewer.setInput(Model.INSTANCE.getRoutes());
|
|
| viewer.setLayoutAlgorithm(new
| SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING));
| viewer.applyLayout();
|
| }
|
| @Override
| public void setFocus() {
|
| }
| }
|
|
|
Dans le cas présent, le texte sur les routes est
affiché, ainsi que celui sur les villes.
IV. Conclusion
Dans ce tutoriel nous avons appris comment afficher un graphe,
soit sans modèle de données associé, soit depuis un
modèle en utilisant des concepts classiques de JFace et donc
aisés à mettre en place. Notre graphe est pour l'instant
basique, mais les possibilités d'amélioration sont nombreuses
et ce sont ces possibilités que je vous montrerai dans la
deuxième partie de ce cours sur Zest :
« Compléments sur Zest ».
V. Liens utiles
Pour aller plus loin, voici quelques liens
utiles :
VI. Remerciements
Je tiens à remercier pour cet article Mickaël BARON,
qui m'a poussé à me lancer dans la rédaction d'articles
sur developpez.com, ainsi que les membres de la communauté Java
pour leurs remarques et leurs conseils. Enfin, un grand merci à
Claude Leloup et Maxime Gault pour leur relecture attentive.


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 Alain BERNARD. 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.
Cette page est déposée.