Développement avancé avec Eclipse Zest

Cet article présente différents compléments sur la boîte à outils de visualisation de graphes sous Eclipse Zest. Il complète l'article d'introduction précédemment publié Introduction à ZestIntroduction à Zest. On se propose ici d'enrichir le graphe que nous avions créé avec divers effets de mise en forme proposés par l'API de Zest. Cet article se base sur Eclipse 3.7.

Les sources de l'exemple sont disponibles à l'adresse suivante : FTPftp-sources ou HTTPhttp-sources.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : 1 commentaire Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.
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.

@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.

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.
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.

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;
    }
}
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.
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.

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

OpenRoadAction.java
CacherSélectionnez
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.

/**
 * 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 :

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.
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.

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 :

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.
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.

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.

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

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.
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.

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 :

LinkGraphView.java
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.

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

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.