I. Introduction▲
Dans l'article précédent, Introduction à ZestIntroduction à Zest, nous
avons vu comment construire un graphe, que ce soit à partir d'un
modèle en utilisant les mêmes mécanismes que JFace, ou
directement en créant nœuds et branches. Cet article reprend
l'exemple de l'article précédent, et se propose de montrer
comment enrichir le graphe de diverses manières : soit par le
biais de styles personnalisés, de menus contextuels, ou encore par
la mise en place d'un zoom, ou de layouts différents.
Ce tutoriel suppose que vous disposiez des connaissances de
base sur les technologies suivantes :
- développement de plugins avec EclipseTutoriels sur Eclipse RCP ;
- utilisation de JFaceTutoriels sur JFace.
Par ailleurs, nous repartons sur le graphe orienté "branches" de l'article d'introduction, contenu dans la vue "LinkView". Les sources sont disponibles ici (FTP)src-intro-ftp ou ici (HTTP)src-intro-http.
II. Changer l'algorithme d'agencement▲
La boîte à outils Zest possède plusieurs algorithmes d'agencement pour un graphe, qui modifient la disposition initiale du graphe lors de sa construction. Ces algorithmes sont :
- GridLayoutAlgorithm : dispose les éléments sur une grille, comme le ferait un GridLayout Swing classique ;
- HorizontalLayoutAlgorithm : dispose les éléments horizontalement, sur une seule ligne ;
- HorizontalShift : décale les éléments qui sont hors de la zone d'affichage du graphe vers la droite ;
- TreeLayoutAlgorithm : dispose les éléments selon un arbre vertical ;
- HorizontalTreeLayoutAlgorithm : effectue la même chose que le Tree LayoutAlgorithm, mais horizontalement ;
- RadialLayoutAlgorithm : place le nœud "racine" au centre et dispose les autres nœuds tout autour ;
- SpringLayoutAlgorithm : combine les éléments de manière à ce que les connexions aient approximativement la même taille et que le minimum de nœuds "dépassent" du graphe ;
- VerticalLayoutAlgorithm : dispose les éléments sur une droite verticale ;
- CompositeLayoutAlgorithm : combine différents algorithmes choisis à la construction pour le rendu du graphe. Par exemple, on peut utiliser le SpringLayout, puis HorizontalShift pour décaler les éléments qui seraient encore hors du graphe.
La plupart de ces layouts peuvent s'utiliser sans
paramètres dans le constructeur, néanmoins, on peut utiliser
l'option "LayoutStyles.NO_LAYOUT_NODE_RESIZING" afin que le layout ne
redimensionne pas les nœuds du graphe. De même, l'option
"LayoutStyles.ENFORCE_BOUNDS" force à contenir le graphe dans la
zone d'affichage, sans dépassement.
Nous allons mettre en œuvre ces différents
algorithmes : pour cela complétez votre vue "LinkGraph"
en ajoutant un menu localisé, qui permettra de choisir
le layout à utiliser. Complétez le contenu de la
méthode "createPartControl()" en y ajoutant le code
suivant :
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.
@Override
public
void
createPartControl
(
Composite parent) {
//(...)
//Creation de la liste des layouts disponibles
layouts =
new
HashMap<
String, LayoutAlgorithm>(
);
layouts.put
(
"Grid Layout"
, new
GridLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Horizontal Shift"
, new
HorizontalShift
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Horizontal Layout"
, new
HorizontalLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Horizontal Tree Layout"
, new
HorizontalTreeLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Radial Layout"
, new
RadialLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Spring Layout"
, new
SpringLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Tree Layout"
, new
TreeLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Vertical Layout"
, new
SpringLayoutAlgorithm
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING));
layouts.put
(
"Composite Layout"
, new
CompositeLayoutAlgorithm
(
new
LayoutAlgorithm[] {
new
SpringLayoutAlgorithm
(
),
new
HorizontalShift
(
LayoutStyles.NO_LAYOUT_NODE_RESIZING)}
));
//Creation du menu de selection des layouts
IMenuManager menuManager =
this
.getViewSite
(
).getActionBars
(
)
.getMenuManager
(
);
MenuManager layoutsMenu =
new
MenuManager
(
"Layouts"
);
menuManager.add
(
layoutsMenu); // le menu de selection apparait dans le menu
localise
Action a;
for
(
final
String s : layouts.keySet
(
)) {
//pour chaque layout on cree un element de menu permettant de le
selectionner
a =
new
Action
(
s, IAction.AS_RADIO_BUTTON) {
@Override
public
void
run
(
) {
viewer.setLayoutAlgorithm
(
layouts.get
(
s), true
);
}
}
;
layoutsMenu.add
(
a); //l'item est ajoute au menu des layouts
//par defaut, on selectionne le Spring Layout
if
(
s.equals
(
"Spring Layout"
)) {
a.run
(
);
a.setChecked
(
true
);
}
}
// (...)
}
La vue dispose maintenant d'un menu localisé qui permet de sélectionner le layout à utiliser. Bien évidemment, suivant le type de données représentées par le graphe, il sera plus judicieux d'utiliser tel ou tel layout.
III. Mettre en place un zoom▲
Zest donne la possibilité de mettre en place très facilement un système de zoom sur le graphe. Pour cela, il faut que la vue (ou l'éditeur) implémente l'interface IZoomableWorkbenchPart. D'autre part, il faut aussi définir un menu, qui permettra à l'utilisateur de choisir son niveau de zoom. Reprenez le code de la vue "LinkGraph" et implémentez l'interface IZoomableWorkbenchPart. La méthode "getZoomableViewer()" doit retourner l'objet GraphViewer. D'autre part, ajoutez un menu dans le menu localisé de la vue, 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.
public
class
LinkGraphView extends
ViewPart implements
IZoomableWorkbenchPart {
// (...)
@Override
public
void
createPartControl
(
Composite parent) {
// (...)
//Creation d'un menu pour zoomer sur le graphe
//ce menu sera affiche dans le menu localise de la vue
IMenuManager menuManager =
this
.getViewSite
(
).getActionBars
(
)
.getMenuManager
(
);
MenuManager zoomMenu =
new
MenuManager
(
"Zoom"
);
ZoomContributionViewItem zoomItem =
new
ZoomContributionViewItem
(
this
);
zoomMenu.add
(
zoomItem);
menuManager.add
(
zoomMenu);
// (...)
}
@Override
public
AbstractZoomableViewer getZoomableViewer
(
) {
return
viewer;
}
}
IV. Interagir avec l'utilisateur▲
Au-delà de la visualisation, il peut être intéressant de proposer à l'utilisateur une interaction avec le graphe, par exemple pour modifier le modèle. Dans notre cas, nous aimerions que l'utilisateur puisse ouvrir et fermer des routes. L'utilisateur doit pouvoir choisir le statut qu'il veut affecter à la route à partir d'un menu contextuel accessible depuis les branches du graphe. La première étape consiste à modifier le modèle pour rajouter dans la classe Route un attribut booléen "ouverte" et les getter/setter associés. Il faut ensuite créer le menu contextuel, au sein de notre classe LinkGraphView.java. Rajoutons le code suivant dans la méthode "createPartControl" :
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.
@Override
public
void
createPartControl
(
Composite parent) {
// (...)
//Creation du menu contextuel
final
MenuManager mgr =
new
MenuManager
(
);
viewer.getControl
(
).setMenu
(
mgr.createContextMenu
(
viewer.getControl
(
)));
mgr.setRemoveAllWhenShown
(
true
); //le menu est recree a chaque affichage
mgr.addMenuListener
(
new
IMenuListener
(
) {
@Override
public
void
menuAboutToShow
(
IMenuManager manager) {
//le contenu du menu depend de la selection courante
IStructuredSelection selection =
(
IStructuredSelection)
viewer.getSelection
(
);
if
(!
selection.isEmpty
(
) &&
selection.getFirstElement
(
) !=
null
) {
Object first =
selection.getFirstElement
(
);
if
(
first instanceof
Route) {
manager.add
(
new
OpenRoadAction
((
Route)first));
}
}
}
}
);
// (...)
}
Nous créons dans le menu une instance de "OpenRoadAction", qui permet de définir le comportement à adopter lorsque l'utilisateur la sélectionne. Cette classe est 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.
/**
* Cette action permet a l'utilisateur d'ouvrir/fermer les routes
* directement depuis le graphe.
*
@author
A. Bernard
*/
public
class
OpenRoadAction extends
Action {
/**
* la route geree par l'action
*/
private
Route route;
/**
* Constructeur.
* Definit le texte de l'action affiche.
*
@param
r
la route selectionnee
*/
public
OpenRoadAction
(
Route r) {
super
(
);
this
.route =
r;
}
@Override
public
String getText
(
) {
if
(
route.isOuverte
(
)) {
return
"Fermer la route"
;
}
else
{
return
"Ouvrir la route"
;
}
}
@Override
public
void
run
(
) {
if
(
route.isOuverte
(
)) {
route.setOuverte
(
false
);
}
else
{
route.setOuverte
(
true
);
}
//ne pas oublier de rafraichir le graphe
viewer.refresh
(
);
}
}
On peut alors observer le résultat lors d'un clic droit
sur le graphe :
Notre graphe est maintenant interactif, mais rien ne permet de
voir directement si une route est ouverte ou fermée. Nous allons
donc dans la partie suivante étudier des interfaces fournies par
Zest qui vont nous permettre d'afficher ces informations, en mettant en
forme notre graphe.
V. Décorer son graphe▲
Afin de mettre en forme le graphe, il faut utiliser différentes interfaces, selon l'élément à modifier :
- l'interface "IConnectionStyleProvider" permet de modifier l'apparence des connexions ;
- l'interface "IEntityStyleProvider" permet de modifier l'apparence des entités ;
- l'interface "IEntityConnectionStyleProvider" permet de modifier l'apparence des connexions dans un graphe construit à partir de ses nœuds. En effet, rappelons-nous que dans ce cas, nous n'avons pas accès directement aux connexions.
Nous détaillons l'utilisation de ces interfaces dans les paragraphes suivants.
V-A. Mettre en forme les connexions▲
Dans un premier temps, nous allons retravailler sur la vue LinkGraphView : on se propose d'afficher en rouge les routes fermées et en vert les routes ouvertes. Reprenez le code de la classe LinkLabelProvider.java, et implémentez l'interface IConnectionStyleProvider. Complétez les nouvelles méthodes 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.
61.
62.
63.
64.
65.
66.
public
class
LinkLabelProvider extends
LabelProvider implements
IConnectionStyleProvider {
// (...)
/* **************************************************
* Methodes de l'interface IConnectionStyleProvider
* ************************************************** */
@Override
public
int
getConnectionStyle
(
Object rel) {
return
ZestStyles.CONNECTIONS_SOLID; //valeur par defaut
//Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche
// source -> destination
}
@Override
public
Color getColor
(
Object rel) {
//Donne la couleur de base de la connexion
if
(
rel instanceof
Route) {
Route r =
(
Route) rel;
if
(
r.isOuverte
(
)) {
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_GREEN);
}
else
{
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_RED);
}
}
else
{
return
null
; //valeur par defaut
}
}
@Override
public
Color getHighlightColor
(
Object rel) {
//Donne la couleur de la connexion lorsque cette derniere est
selectionnee
if
(
rel instanceof
Route) {
Route r =
(
Route) rel;
if
(
r.isOuverte
(
)) {
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_GREEN);
}
else
{
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_RED);
}
}
else
{
return
null
; //valeur par defaut
}
}
@Override
public
int
getLineWidth
(
Object rel) {
//Donne l'epaisseur d'une connexion
return
-
1
; //valeur par defaut
}
@Override
public
IFigure getTooltip
(
Object entity) {
//Affiche un tooltip sur la connexion
if
(
entity instanceof
Route) {
if
(((
Route)entity).isOuverte
(
)) {
return
new
Label
(
"Route ouverte"
);
}
else
{
return
new
Label
(
"Route fermee"
);
}
}
else
{
return
null
;
}
}
}
Chaque méthode donne directement accès à la connexion en cours de construction, ce qui permet très facilement de définir le style des éléments. Nous pouvons observer le résultat sur notre graphe :
Il est impossible d'utiliser cette méthode lorsque le graphe est construit à partir des nœuds. En effet, nous n'avons pas accès aux connexions lors de la construction du graphe. Pour cela, Zest propose l'interface IEntityConnectionStyleProvider. Reprenons 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.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
public
class
EntityLabelProvider extends
LabelProvider implements
IEntityConnectionStyleProvider {
// (...)
/* ********************************************************
* Methodes de l'interface IEntityConnectionStyleProvider
* ******************************************************** */
@Override
public
int
getConnectionStyle
(
Object src, Object dest) {
return
ZestStyles.CONNECTIONS_SOLID; //valeur par defaut
//Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche
// source -> destination
}
@Override
public
Color getColor
(
Object src, Object dest) {
//Donne la couleur de la connexion
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_BLACK);
}
@Override
public
Color getHighlightColor
(
Object src, Object dest) {
//Donne la couleur de la connexion si selectionnee
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_MAGENTA);
}
@Override
public
int
getLineWidth
(
Object src, Object dest) {
//Donne l'epaisseur de la connexion
return
-
1
;
}
@Override
public
IFigure getTooltip
(
Object entity) {
// Donne un tooltip pour le noeud selectionne
return
null
;
}
}
Chaque méthode permet d'accéder à la connexion définie par sa source et sa destination.
V-B. Mettre en forme les nœuds▲
Nous souhaitons maintenant afficher en rouge les villes qui ne sont plus desservies par aucune route. Reprenez la classe LinkLabelProvider.java et implémentez l'interface IEntityStyleProvider :
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.
>
public
class
LinkLabelProvider extends
LabelProvider implements
IConnectionStyleProvider, IEntityStyleProvider {
// (...)
/* **********************************************
* Methodes de l'interface IEntityStyleProvider
* ********************************************** */
@Override
public
Color getNodeHighlightColor
(
Object entity) {
//Donne la couleur du noeud si selectionne
return
null
;
}
@Override
public
Color getBorderColor
(
Object entity) {
//Donne la couleur de la bordure des noeud
return
null
; //valeur par defaut
}
@Override
public
Color getBorderHighlightColor
(
Object entity) {
//Donne la couleur de la bordure du noeud selectionne
return
null
; //Valeur par defaut
}
@Override
public
int
getBorderWidth
(
Object entity) {
// Donne l'epaisseur de la bordure du noeud
return
-
1
;
}
@Override
public
Color getBackgroundColour
(
Object entity) {
//Donne la couleur de fond par defaut des noeuds
if
(
entity instanceof
Ville) {
Ville v =
(
Ville) entity;
//On regarde si les routes qui desservent la ville sont toutes
// fermees ou non
boolean
estReliee =
false
;
for
(
Route r : Model.INSTANCE.getRoutes
(
)) {
if
(
r.getSource
(
).equals
(
v) ||
r.getDestination
(
).equals
(
v)) {
// La route consideree dessert la ville.
// Si elle est ouverte, la ville est accessible par au
// moins une route
if
(
r.isOuverte
(
)) {
estReliee =
true
;
}
}
}
//Si la ville est reliee par au moins une route, la couleur
//affichee est celle par defaut, sinon le noeud est colore en rouge
if
(
estReliee) {
return
null
;
}
else
{
return
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_RED);
}
}
else
{
return
null
;
}
}
@Override
public
Color getForegroundColour
(
Object entity) {
//Donne la couleur d'avant-plan des noeuds (le texte par ex.)
return
null
; //valeur par defaut
}
@Override
public
boolean
fisheyeNode
(
Object entity) {
//Cree un zoom sur l'entite quand elle est selectionnee
return
true
;
}
}
Une nouvelle fois, chaque méthode nous permet d'accéder très facilement à l'entité en cours d'affichage. Remarquez la méthode "fisheyeNode" qui permet d'effectuer un zoom sur le nœud lorsqu'il est survolé par la souris. Observons le résultat :
VI. Filtrer certains éléments▲
Zest nous permet aussi de mettre en place des filtres sur les graphes, de la même manière que sur les viewers JFace. Créez le filtre suivant, qui nous permettra de cacher les villes qui ne sont desservies par aucune route :
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.
package
com.abernard.zest.viewer;
import
org.eclipse.jface.viewers.Viewer;
import
org.eclipse.jface.viewers.ViewerFilter;
import
com.abernard.zest.model.Model;
import
com.abernard.zest.model.Route;
import
com.abernard.zest.model.Ville;
/**
* Ce filtre permet de cacher les villes desservies par aucune route. Si
* la ville n'est pas reliee par des routes, elle n'est pas affichee.
*
@author
A. Bernard
*/
public
class
EntityFilter extends
ViewerFilter {
@Override
public
boolean
select
(
Viewer viewer, Object parentElement, Object element)
{
// Determine si la ville doit etre affichee, ou non
if
(
element instanceof
Ville) {
Ville v =
(
Ville) element;
//On regarde si les routes qui desservent la ville sont toutes
// fermees ou non
boolean
estReliee =
false
;
for
(
Route r : Model.INSTANCE.getRoutes
(
)) {
if
(
r.getSource
(
).equals
(
v) ||
r.getDestination
(
).equals
(
v)) {
// La route consideree dessert la ville.
// Si elle est ouverte, la ville est accessible par au
// moins une route
if
(
r.isOuverte
(
)) {
estReliee =
true
;
}
}
}
//Si la ville est reliee par au moins une route, la ville est
// affichee, sinon elle est filtree
return
estReliee;
}
else
{
return
true
;
}
}
}
Ajoutez ce filtre au graphe dans la vue LinkGraphView.java :
2.
3.
4.
5.
6.
7.
8.
9.
10.
@Override
public
void
createPartControl
(
Composite parent) {
// (...)
//Definition du filtre
viewer.addFilter
(
new
EntityFilter
(
));
// (...)
}
Observez maintenant le résultat lorsque l'on ferme la route qui relie Metz à Paris :
VII. Conclusion▲
Nous avons vu dans cet article comment modifier l'affichage du
graphe et améliorer son ergonomie. Au terme de ces deux articles,
nous pouvons constater que Zest est un outil de visualisation de
graphes puissant tout en étant pratique et facile à utiliser,
et qui propose assez d'options pour représenter des graphes clairs
et précis, adaptés au modèle de chaque application.
Cette boîte à outils est notamment utilisée
pour l'outil de visualisation de dépendances de PDE
(Plugin Developppement Environment), actuellement dans
l'Incubator d'Eclipse : PDE Incubator
Dependency VisualizationPDE Incubator Dependency Visualization.
VIII. Liens utiles▲
Pour aller plus loin, voici quelques liens utiles :
IX. Remerciements▲
Je tiens à remercier pour cet article les membres de la communauté Java pour leurs remarques et leurs conseils. Un grand merci aussi à jacques_jean pour sa relecture attentive.