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 :

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 :

LinkGraphView.java
CacherSélectionnez
  1.  
  2. @Override 
  3. public void createPartControl(Composite parent) { 
  4.  
  5.     //(...) 
  6.  
  7.     //Creation de la liste des layouts disponibles 
  8.     layouts = new HashMap<String, LayoutAlgorithm>(); 
  9.     layouts.put("Grid Layout", new 
  10.       GridLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  11.     layouts.put("Horizontal Shift", new 
  12.       HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  13.     layouts.put("Horizontal Layout", new 
  14.       HorizontalLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  15.     layouts.put("Horizontal Tree Layout", new 
  16.       HorizontalTreeLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  17.     layouts.put("Radial Layout", new 
  18.       RadialLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  19.     layouts.put("Spring Layout", new 
  20.       SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  21.     layouts.put("Tree Layout", new 
  22.       TreeLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  23.     layouts.put("Vertical Layout", new 
  24.       SpringLayoutAlgorithm(LayoutStyles.NO_LAYOUT_NODE_RESIZING)); 
  25.     layouts.put("Composite Layout", new CompositeLayoutAlgorithm( new 
  26.       LayoutAlgorithm[] {new SpringLayoutAlgorithm(),  
  27. 	new HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING)})); 
  28.  
  29.     //Creation du menu de selection des layouts 
  30.     IMenuManager menuManager = this.getViewSite().getActionBars() 
  31. 	.getMenuManager(); 
  32.     MenuManager layoutsMenu = new MenuManager("Layouts"); 
  33.     menuManager.add(layoutsMenu); // le menu de selection apparait dans le menu 
  34.       localise 
  35.  
  36.     Action a; 
  37.     for (final String s : layouts.keySet()) { 
  38.     //pour chaque layout on cree un element de menu permettant de le 
  39.       selectionner 
  40.     a = new Action(s, IAction.AS_RADIO_BUTTON) { 
  41. 	@Override 
  42. 	public void run() { 
  43. 	viewer.setLayoutAlgorithm(layouts.get(s), true); 
  44. 	} 
  45.     }; 
  46.     layoutsMenu.add(a); //l'item est ajoute au menu des layouts 
  47.     //par defaut, on selectionne le Spring Layout 
  48.     if (s.equals("Spring Layout")) { 
  49. 	a.run(); 
  50. 	a.setChecked(true); 
  51.     } 
  52.     } 
  53.  
  54.     // (...) 
  55.  
  56. } 

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.

Tree Layout
Tree Layout
GridLayout
GridLayout

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 :

LinkGraphView.java
CacherSélectionnez
  1.  
  2. public class LinkGraphView extends ViewPart implements IZoomableWorkbenchPart { 
  3.  
  4.     // (...) 
  5.  
  6.     @Override 
  7.     public void createPartControl(Composite parent) { 
  8.  
  9.      // (...) 
  10.  
  11.     //Creation d'un menu pour zoomer sur le graphe 
  12.     //ce menu sera affiche dans le menu localise de la vue 
  13.     IMenuManager menuManager = this.getViewSite().getActionBars() 
  14. 	.getMenuManager(); 
  15.  
  16.     MenuManager zoomMenu = new MenuManager("Zoom"); 
  17.     ZoomContributionViewItem zoomItem = new ZoomContributionViewItem(this); 
  18.     zoomMenu.add(zoomItem); 
  19.  
  20.     menuManager.add(zoomMenu); 
  21.  
  22.     // (...) 
  23.  
  24.     } 
  25.  
  26.     @Override 
  27.     public AbstractZoomableViewer getZoomableViewer() { 
  28.     return viewer; 
  29.     } 
  30. } 
Zoom
Zoom

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" :

LinkGraphView.java
CacherSélectionnez
  1.  
  2. @Override 
  3. public void createPartControl(Composite parent) { 
  4.  
  5.     // (...) 
  6.  
  7.     //Creation du menu contextuel 
  8.     final MenuManager mgr = new MenuManager(); 
  9.     viewer.getControl().setMenu(mgr.createContextMenu(viewer.getControl())); 
  10.     mgr.setRemoveAllWhenShown(true); //le menu est recree a chaque affichage 
  11.     mgr.addMenuListener(new IMenuListener() { 
  12.     @Override 
  13.     public void menuAboutToShow(IMenuManager manager) { 
  14. 	//le contenu du menu depend de la selection courante 
  15. 	IStructuredSelection selection = (IStructuredSelection) 
  16. 	  viewer.getSelection(); 
  17. 	if (!selection.isEmpty() && selection.getFirstElement() != null) { 
  18. 	Object first = selection.getFirstElement(); 
  19. 	if (first instanceof Route) { 
  20. 	    manager.add(new OpenRoadAction((Route)first)); 
  21. 	} 
  22. 	} 
  23.     } 
  24.     });  
  25.  
  26.     // (...) 
  27.  
  28. } 

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 :

OpenRoadAction.java
CacherSélectionnez
  1.  
  2. /** 
  3.  * Cette action permet a l'utilisateur d'ouvrir/fermer les routes  
  4.  * directement depuis le graphe. 
  5.  * @author A. Bernard 
  6.  */ 
  7. public class OpenRoadAction extends Action { 
  8.  
  9.     /** 
  10.      * la route geree par l'action 
  11.      */ 
  12.     private Route route; 
  13.  
  14.     /**  
  15.      * Constructeur. 
  16.      * Definit le texte de l'action affiche. 
  17.      * @param r la route selectionnee 
  18.      */ 
  19.     public OpenRoadAction(Route r) { 
  20.     super(); 
  21.     this.route = r; 
  22.     } 
  23.  
  24.     @Override 
  25.     public String getText() { 
  26.     if (route.isOuverte()) { 
  27. 	return "Fermer la route"; 
  28.     } else { 
  29. 	return "Ouvrir la route"; 
  30.     } 
  31.     } 
  32.  
  33.     @Override 
  34.     public void run() { 
  35.     if (route.isOuverte()) { 
  36. 	route.setOuverte(false); 
  37.     } else { 
  38. 	route.setOuverte(true); 
  39.     } 
  40.     //ne pas oublier de rafraichir le graphe 
  41.     viewer.refresh(); 
  42.     } 
  43. } 

On peut alors observer le résultat lors d'un clic droit sur le graphe :

Menu sur route ouverte
Menu sur route ouverte
Menu sur route fermée
Menu sur route fermée

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 :

LinkLabelProvider.java
CacherSélectionnez
  1.  
  2. public class LinkLabelProvider extends LabelProvider  implements 
  3.   IConnectionStyleProvider { 
  4.  
  5.     // (...) 
  6.  
  7.     /* ************************************************** 
  8.      *	  Methodes de l'interface IConnectionStyleProvider 
  9.      * ************************************************** */ 
  10.     @Override 
  11.     public int getConnectionStyle(Object rel) { 
  12.     return ZestStyles.CONNECTIONS_SOLID; //valeur par defaut 
  13.     //Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche  
  14.     // source -> destination 
  15.     } 
  16.  
  17.     @Override 
  18.     public Color getColor(Object rel) { 
  19.     //Donne la couleur de base de la connexion 
  20.     if (rel instanceof Route) { 
  21. 	Route r = (Route) rel; 
  22. 	if (r.isOuverte()) { 
  23. 	return Display.getDefault().getSystemColor(SWT.COLOR_GREEN); 
  24. 	} else { 
  25. 	return Display.getDefault().getSystemColor(SWT.COLOR_RED); 
  26. 	} 
  27.     } else { 
  28. 	return null; //valeur par defaut 
  29.     } 
  30.     } 
  31.  
  32.     @Override 
  33.     public Color getHighlightColor(Object rel) { 
  34.     //Donne la couleur de la connexion lorsque cette derniere est 
  35.       selectionnee 
  36.     if (rel instanceof Route) { 
  37. 	Route r = (Route) rel; 
  38. 	if (r.isOuverte()) { 
  39. 	return Display.getDefault().getSystemColor(SWT.COLOR_GREEN); 
  40. 	} else { 
  41. 	return Display.getDefault().getSystemColor(SWT.COLOR_RED); 
  42. 	} 
  43.     } else { 
  44. 	return null; //valeur par defaut 
  45.     } 
  46.     } 
  47.  
  48.     @Override 
  49.     public int getLineWidth(Object rel) { 
  50.     //Donne l'epaisseur d'une connexion 
  51.     return -1; //valeur par defaut 
  52.     } 
  53.  
  54.     @Override 
  55.     public IFigure getTooltip(Object entity) { 
  56.     //Affiche un tooltip sur la connexion 
  57.     if (entity instanceof Route) { 
  58. 	if (((Route)entity).isOuverte()) { 
  59. 	return new Label("Route ouverte"); 
  60. 	} else { 
  61. 	return new Label("Route fermee"); 
  62. 	} 
  63.     } else { 
  64. 	return null; 
  65.     } 
  66.     } 
  67. } 

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 :

Image non disponible
Mise en forme des connexions

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 :

EntityLabelProvider.java
CacherSélectionnez
  1.  
  2. public class EntityLabelProvider extends LabelProvider implements 
  3.   IEntityConnectionStyleProvider { 
  4.  
  5.     // (...) 
  6.  
  7.     /* ******************************************************** 
  8.      *	  Methodes de l'interface IEntityConnectionStyleProvider 
  9.      * ******************************************************** */ 
  10.  
  11.     @Override 
  12.     public int getConnectionStyle(Object src, Object dest) { 
  13.     return ZestStyles.CONNECTIONS_SOLID; //valeur par defaut 
  14.     //Utiliser ZestStyles.CONNECTION_DIRECTED pour afficher une fleche  
  15.     // source -> destination 
  16.     } 
  17.  
  18.     @Override 
  19.     public Color getColor(Object src, Object dest) { 
  20.     //Donne la couleur de la connexion 
  21.     return Display.getDefault().getSystemColor(SWT.COLOR_BLACK); 
  22.     } 
  23.  
  24.     @Override 
  25.     public Color getHighlightColor(Object src, Object dest) { 
  26.     //Donne la couleur de la connexion si selectionnee 
  27.     return Display.getDefault().getSystemColor(SWT.COLOR_MAGENTA); 
  28.     } 
  29.  
  30.     @Override 
  31.     public int getLineWidth(Object src, Object dest) { 
  32.     //Donne l'epaisseur de la connexion 
  33.     return -1; 
  34.     } 
  35.  
  36.     @Override 
  37.     public IFigure getTooltip(Object entity) { 
  38.     // Donne un tooltip pour le noeud selectionne 
  39.     return null; 
  40.     } 
  41.  
  42. } 

Chaque méthode permet d'accéder à la connexion définie par sa source et sa destination.

Mise en forme des connexions
Mise en forme des connexions

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 :

LinkLabelProvider.java
CacherSélectionnez
  1. > 
  2. public class LinkLabelProvider extends LabelProvider  implements 
  3.   IConnectionStyleProvider, IEntityStyleProvider { 
  4.  
  5.     // (...) 
  6.  
  7.     /* ********************************************** 
  8.      *	  Methodes de l'interface IEntityStyleProvider 
  9.      * ********************************************** */ 
  10.  
  11.     @Override 
  12.     public Color getNodeHighlightColor(Object entity) { 
  13.     //Donne la couleur du noeud si selectionne 
  14.     return null; 
  15.     } 
  16.  
  17.     @Override 
  18.     public Color getBorderColor(Object entity) { 
  19.     //Donne la couleur de la bordure des noeud 
  20.     return null; //valeur par defaut 
  21.     } 
  22.  
  23.     @Override 
  24.     public Color getBorderHighlightColor(Object entity) { 
  25.     //Donne la couleur de la bordure du noeud selectionne 
  26.     return null; //Valeur par defaut 
  27.     } 
  28.  
  29.     @Override 
  30.     public int getBorderWidth(Object entity) { 
  31.     // Donne l'epaisseur de la bordure du noeud 
  32.     return -1; 
  33.     } 
  34.  
  35.     @Override 
  36.     public Color getBackgroundColour(Object entity) { 
  37.     //Donne la couleur de fond par defaut des noeuds 
  38.     if (entity instanceof Ville) { 
  39. 	Ville v = (Ville) entity; 
  40. 	//On regarde si les routes qui desservent la ville sont toutes  
  41. 	// fermees ou non 
  42. 	boolean estReliee = false; 
  43. 	for (Route r : Model.INSTANCE.getRoutes()) { 
  44. 	if (r.getSource().equals(v) || r.getDestination().equals(v)) { 
  45. 	    // La route consideree dessert la ville. 
  46. 	    // Si elle est ouverte, la ville est accessible par au  
  47. 	    // moins une route 
  48. 	    if (r.isOuverte()) { 
  49. 	    estReliee = true; 
  50. 	    } 
  51. 	} 
  52. 	} 
  53. 	//Si la ville est reliee par au moins une route, la couleur  
  54. 	//affichee est celle par defaut, sinon le noeud est colore en rouge 
  55. 	if (estReliee) { 
  56. 	return null; 
  57. 	} else { 
  58. 	return Display.getDefault().getSystemColor(SWT.COLOR_RED); 
  59. 	} 
  60.     } else { 
  61. 	return null; 
  62.     } 
  63.     } 
  64.  
  65.     @Override 
  66.     public Color getForegroundColour(Object entity) { 
  67.     //Donne la couleur d'avant-plan des noeuds (le texte par ex.) 
  68.     return null; //valeur par defaut 
  69.     } 
  70.  
  71.     @Override 
  72.     public boolean fisheyeNode(Object entity) { 
  73.     //Cree un zoom sur l'entite quand elle est selectionnee 
  74.     return true; 
  75.     } 
  76.  
  77. } 

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 :

Mise en forme des entités
Mise en forme des entités

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 :

EntityFilter.java
CacherSélectionnez
  1.  
  2. package com.abernard.zest.viewer; 
  3.  
  4. import org.eclipse.jface.viewers.Viewer; 
  5. import org.eclipse.jface.viewers.ViewerFilter; 
  6.  
  7. import com.abernard.zest.model.Model; 
  8. import com.abernard.zest.model.Route; 
  9. import com.abernard.zest.model.Ville; 
  10.  
  11. /** 
  12.  * Ce filtre permet de cacher les villes desservies par aucune route. Si  
  13.  * la ville n'est pas reliee par des routes, elle n'est pas affichee. 
  14.  * @author A. Bernard 
  15.  */ 
  16. public class EntityFilter extends ViewerFilter { 
  17.  
  18.     @Override 
  19.     public boolean select(Viewer viewer, Object parentElement, Object element) 
  20.       { 
  21.     // Determine si la ville doit etre affichee, ou non 
  22.     if (element instanceof Ville) { 
  23. 	Ville v = (Ville) element; 
  24. 	//On regarde si les routes qui desservent la ville sont toutes  
  25. 	// fermees ou non 
  26. 	boolean estReliee = false; 
  27. 	for (Route r : Model.INSTANCE.getRoutes()) { 
  28. 	if (r.getSource().equals(v) || r.getDestination().equals(v)) { 
  29. 	    // La route consideree dessert la ville. 
  30. 	    // Si elle est ouverte, la ville est accessible par au  
  31. 	    // moins une route 
  32. 	    if (r.isOuverte()) { 
  33. 	    estReliee = true; 
  34. 	    } 
  35. 	} 
  36. 	} 
  37. 	//Si la ville est reliee par au moins une route, la ville est  
  38. 	// affichee, sinon elle est filtree 
  39. 	return estReliee; 
  40.     } else { 
  41. 	return true; 
  42.     } 
  43.     } 
  44.  
  45. } 

Ajoutez ce filtre au graphe dans la vue LinkGraphView.java :

LinkGraphView.java
CacherSélectionnez
  1.  
  2. @Override 
  3. public void createPartControl(Composite parent) { 
  4.  
  5.     // (...) 
  6.  
  7.     //Definition du filtre 
  8.     viewer.addFilter(new EntityFilter()); 
  9.  
  10.     // (...) 
  11. } 

Observez maintenant le résultat lorsque l'on ferme la route qui relie Metz à Paris :

Filtre sur le graphe
Filtre sur le graphe

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

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.