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 ».
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;
/**
* Cette vue contient un graphe construit programmatiquement, en construisant
* 'a la main' les noeuds et les branches.
*
*
@author
A. Bernard
*
*/
public
class
BasicGraphView extends
ViewPart {
/**
* le graphe
*/
private
Graph graph;
@Override
public
void
createPartControl
(
Composite parent) {
// Le Graph contient tous les autres objets
graph =
new
Graph
(
parent, SWT.NONE);
// 1 :Les villes sont les noeuds du graphe
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"
);
// 2 :Construction des branches du graphe.
// Chaque branche affiche la distance kilometrique entre deux villes
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"
);
// 3 :Definition de l'algorithme de rendu du graphe
// Ces layouts seront decrits dans un prochain cours
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 :
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;
/**
* Cette classe represente une ville, c'est-a-dire une entite, ou un noeud, de
* notre graphe.
*
*
@author
A. Bernard
*
*/
public
class
Ville {
/**
* nom de la ville
*/
private
String nom;
/**
* villes auxquelles est reliee cette ville
*/
private
List<
Ville>
connexions;
/**
* Constructeur
*
@param
n
nom de la ville
*/
public
Ville (
String n) {
this
.nom =
n;
this
.connexions =
new
ArrayList<
Ville>(
);
}
/**
* Ajouter une ville reliee a cette ville
*
@param
v
la ville a connecter
*/
public
void
addConnexion
(
Ville v) {
if
(!
connexions.contains
(
v)) {
connexions.add
(
v);
}
}
/**
* Donne la liste des villes connectees a cette ville
*
@return
les villes connectees
*/
public
List<
Ville>
getConnexions
(
) {
return
this
.connexions;
}
/**
* Donne le nom de cette ville
*
@return
le nom de cette ville
*/
public
String getNom
(
) {
return
this
.nom;
}
}
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;
/**
* Cette class represente une route, c'est-a-dire une branche du graphe.
*
*
@author
A. Bernard
*
*/
public
class
Route {
/**
* la ville de depart de la route
*/
private
Ville source;
/**
* la ville d'arrivee de la route
*/
private
Ville destination;
/**
* la longueur de la route
*/
private
int
longueur;
/**
* Constructeur
*
@param
s
la ville de depart
*
@param
d
la ville de destination
*
@param
l
la longueur de la route
*/
public
Route
(
Ville s, Ville d, int
l) {
this
.source =
s;
this
.destination =
d;
this
.longueur =
l;
}
/**
* Donne la ville de depart
*
@return
la ville de depart
*/
public
Ville getSource
(
) {
return
this
.source;
}
/**
* Donne la ville d'arrivee
*
@return
la ville d'arrivee
*/
public
Ville getDestination
(
) {
return
this
.destination;
}
/**
* Donne la longueur de la route
*
@return
la longueur de la route
*/
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.
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;
/**
* Modele de notre graphe. Il contient toutes les villes et toutes les routes,
* et permet d'acceder soit a la liste de toutes les villes, soit a la liste de
* toutes les routes
*
*
@author
A. Bernard
*/
public
enum
Model {
/**
* l'instance unique de notre modele
*/
INSTANCE;
/**
* toutes les villes du graphe
*/
private
List<
Ville>
villes;
/**
* toutes les routes du graphe
*/
private
List<
Route>
routes;
/**
* Constructeur. Initialise le modele.
*/
private
Model
(
) {
villes =
new
ArrayList<
Ville>(
);
routes =
new
ArrayList<
Route>(
);
// Creation de toutes les villes du graphe
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);
// Creation de toutes les routes
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);
}
/**
* Donne la liste de toutes les villes
*
@return
la liste des villes
*/
public
List<
Ville>
getVilles
(
) {
return
villes;
}
/**
* Donne la liste de toutes les routes
*
@return
la liste des routes
*/
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 :
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;
/**
* ContentProvider de notre graphe. Cette implementation prend en donnees
* d'entree la liste des villes.
*
*
@author
A. Bernard
*/
public
class
EntityContentProvider implements
IGraphEntityContentProvider {
@SuppressWarnings
(
"unchecked"
)
@Override
public
Object[] getElements
(
Object input) {
// Cette methode definit les elements d'entree du graphe.
// Dans notre cas ce doit etre une liste de Villes.
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) {
// Pour chaque ville, on retourne la liste des villes connectees a la
// ville courante
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 » :
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;
/**
* LabelProvider du graphe. Determine le texte a afficher selon le type
* d'element.
*
*
@author
A. Bernard
*/
public
class
EntityLabelProvider extends
LabelProvider {
@Override
public
String getText
(
Object element) {
// On retourne la longueur de la route, ou le nom de la ville
// On remarquera que le texte sur la route n'est jamais affiche :
// aucun element de type Route n'est traite.
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.
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;
/**
* Cette vue affiche un graphe construit a partir d'un modele base sur les
* noeuds du graphe (dans notre cas les villes).
*
*
@author
A. Bernard
*/
public
class
EntityGraphView extends
ViewPart {
/**
* le graphe
*/
private
GraphViewer viewer;
@Override
public
void
createPartControl
(
Composite parent) {
viewer =
new
GraphViewer
(
parent, SWT.NONE);
// Traitement du contenu
viewer.setContentProvider
(
new
EntityContentProvider
(
));
// Traitement de l'affichage
viewer.setLabelProvider
(
new
EntityLabelProvider
(
));
// Donnees d'entree du graphe
// Comme pour les autres viewers JFace, la methode setInput doit etre
// appelee APRES la definition des ContentProvider et LabelProvider.
viewer.setInput
(
Model.INSTANCE.getVilles
(
));
// Definition du layout
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 :
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;
/**
* ContentProvider de notre graphe. Cette implementation prend en donnees
* d'entree la liste des routes.
*
*
@author
A. Bernard
*/
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) {
// Donne pour chaque branche du graphe le noeud source de la branche
if
(
rel instanceof
Route) {
return
((
Route)rel).getSource
(
);
}
else
{
return
null
;
}
}
@Override
public
Object getDestination
(
Object rel) {
// Donne pour chaque branche du graphe le noeud destination de la
branche
if
(
rel instanceof
Route) {
return
((
Route)rel).getDestination
(
);
}
else
{
return
null
;
}
}
@SuppressWarnings
(
"unchecked"
)
@Override
public
Object[] getElements
(
Object input) {
// Ici, les donnees d'entree du graphe sont les routes du modele, c'est-
// a-dire les branches du graphe.
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 » :
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;
/**
* LabelProvider du graphe. Determine le texte a afficher selon le type
* d'element.
*
*
@author
A. Bernard
*/
public
class
LinkLabelProvider extends
LabelProvider {
@Override
public
String getText
(
Object element) {
// On retourne la longueur de la route, ou le nom de la ville
// Dans ce cas, les textes sont affiches sur tous les elements du graphe
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 :
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;
/**
* Cette vue affiche un graphe construit a partir d'un modele base sur les
* branches du graphe (dans notre cas les routes).
*
*
@author
A. Bernard
*/
public
class
LinkGraphView extends
ViewPart {
/**
* le graphe
*/
private
GraphViewer viewer;
@Override
public
void
createPartControl
(
Composite parent) {
viewer =
new
GraphViewer
(
parent, SWT.NONE);
// Traitement du contenu
viewer.setContentProvider
(
new
LinkContentProvider
(
));
// Traitement de l'affichage
viewer.setLabelProvider
(
new
LinkLabelProvider
(
));
// Donnees d'entree du graphe
// Comme pour les autres viewers JFace, la methode setInput doit etre
// appelee APRES la definition des ContentProvider et LabelProvider.
viewer.setInput
(
Model.INSTANCE.getRoutes
(
));
// Definition du layout
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.