Utiliser un TableViewer avec SWT / JFace

Au début, je voulais simplement donner une astuce pour la largeur des colonnes en SWT.
J’aurais pu aller droit au but, mais j’avais envie de quelque chose de plus général. Aussi, j’ai étendu la portée de cet article.

Introduction (ou rappels)

Commençons donc par un rappel.
En Java, on connaît trois principales librairies graphiques, de base en tout cas : AWT, la première historiquement et proche des widgets natifs des OS ; SWING, la librairie purement Java ; et SWT, qui utilise les widgets natifs lorsqu’ils sont disponibles, et des widgets Java quand ils ne le sont pas.

SWT est principalement utilisée dans Eclipse, mais tourne également en-dehors, dans des applications stand-alone.
Malheureusement, SWT seul ne facilite pas la mise en oeuvre de patrons d’architecture tels que MVC. C’est pour cela qu’il y a une librairie au-dessus de SWT, qui s’appelle JFace. JFace fournit des wrappers pour certains widgets de SWT, wrappers qui facilitent la mise en place d’un MVC.

Par exemple, SWT définit la classe Table, qui permet d’afficher un tableau.
Et JFace fournit un wrapper qui s’appelle TableViewer, et qui permet d’associer à un objet Table, une entrée (input : quelle est la source des données à afficher dans la table), un fournisseur de contenu (ContentProvider : quels éléments de la source on va afficher dans le tableau) et un fournisseur d’affichage (LabelProvider : comment va-t-on afficher les éléments à faire apparaître ?). Il est également possible d’associer d’autres éléments à un Viewer, comme par exemple un objet pour trier (Sorter) les lignes, les colonnes, etc.

Un exemple

Supposons que l’on ait une une collection de Personnes.
Une personne a un nom, une adresse et une date de naissance.

public class Person {

	private String name, address;
	private GregorianCalendar birthDate;

	/**
	 * Constructor.
	 * @param name
	 * @param address
	 * @param birthDate
	 */
	public Person( String name, String address, GregorianCalendar birthDate ) {
		this.name = name;
		this.address = address;
		this.birthDate = birthDate;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * @param name the name to set
	 */
	public void setName( String name ) {
		this.name = name;
	}

	/**
	 * @return the address
	 */
	public String getAddress() {
		return this.address;
	}

	/**
	 * @param address the address to set
	 */
	public void setAddress( String address ) {
		this.address = address;
	}

	/**
	 * @return the birthDate
	 */
	public GregorianCalendar getBirthDate() {
		return this.birthDate;
	}

	/**
	 * @param birthDate the birthDate to set
	 */
	public void setBirthDate( GregorianCalendar birthDate ) {
		this.birthDate = birthDate;
	}
}

Et on souhaite maintenant afficher une liste de personnes dans un tableau.
Voilà comment faire avec un TableViewer.

public void displayPersons() {

	Shell shell = new Shell();
	TableViewer viewer = new TableViewer( shell );

	// The input will be an array or a collection.
	// We will need every element of this input.
	viewer.setContentProvider( new ArrayContentProvider());

	// For very element to display, we will display its default string
	// (i.e. Object#toString()).
	viewer.setLabelProvider( new LabelProvider());

	// Eventually, let's define an input
	Person[] persons = new Person[ 50 ];
	for( int i=0; i<=50; i++ ) {
		persons[ i ] = new Person(
				"Personne " + i,
				"Addresse " + i,
				new GregorianCalendar( 1950 + i, i % 12, 2 * i %  30 ));
	}

	viewer.setInput( persons );
}

Si jamais vous avez d’autres personnes à modifier, il vous suffit de créer ou de passer une nouvelle collection en entrée et de rafraîchir le viewer.

viewer.setInput( persons );
viewer.refresh();

Si vous voulez la même chose avec SWT, il vous faudra beaucoup plus de lignes de code pour y arriver.

Ajouter des colonnes dans votre TableViewer

Nous allons compliquer un peu notre exemple.
Actuellement, nos objets personnes vont être vilains comme tout, puisque l’on n’a pas redéfini la méthode toString().
Nous allons cette fois-ci ajouter 3 colonnes, chaque colonne affichant un des attributs d’une personne.

Voici le code pour faire cela.

public void displayPersons() {

	Shell shell = new Shell();
	TableViewer viewer = new TableViewer( shell );

	// Define the columns
	String[] COLUMNS = new String[] { "Nom", "Adresse", "Date de naissance" };
	for( String element : COLUMNS ) {
		TableColumn col = new TableColumn( viewer.getTable(), SWT.CENTER );
		col.setText( element);
	}

	// The input will be an array or a collection.
	// We will need every element of this input.
	viewer.setContentProvider( new ArrayContentProvider());

	// For very element to display, we will select what to display
	// in function of the column index
	viewer.setLabelProvider( new ITableLabelProvider() {
		SimpleDateFormat formatter = new SimpleDateFormat( "dd/mm/yy" );

		@Override
		public void removeListener( ILabelProviderListener arg0 ) {
			// nothing
		}

		@Override
		public boolean isLabelProperty( Object arg0, String arg1 ) {
			return false;
		}

		@Override
		public void dispose() {
			// nothing
		}

		@Override
		public void addListener( ILabelProviderListener arg0 ) {
			// nothing
		}

		@Override
		public String getColumnText( Object element, int colmnIndex ) {

			String result = null;
			switch( colmnIndex ) {
			case 0:
				result = ((Person) element).getName();
				break;

			case 1:
				result = ((Person) element).getAddress();
				break;

			case 2:
				result = this.formatter.format(((Person) element).getBirthDate());
				break;
			}

			return result;
		}

		@Override
		public Image getColumnImage( Object element, int colmnIndex ) {
			return null;
		}
	});

	// Eventually, let's define an input
	Person[] persons = new Person[ 50 ];
	for( int i=0; i<=50; i++ ) {
		persons[ i ] = new Person(
				"Personne " + i,
				"Addresse " + i,
				new GregorianCalendar( 1950 + i, i % 12, 2 * i %  30 ));
	}

	viewer.setInput( persons );
	viewer.refresh();
}

La grande nouveauté, c’est l’ajout de colonnes et l’utilisation d’un label provider qui supporte plusieurs colonnes.

Définir la largeur des colonnes dans un TableViewer

A l’origine, je ne voulais faire un post (ou rappel) que sur cette partie.

Avec le code précédent, chaque colonne ne va occuper que l’espace nécessaire à son contenu.
Si le contenu prend 50 px, alors la colonne fera 50 px plus une petite marge intérieure.

Comment faire pour modifier ce comportement ?
Une solution est de figer la largeur des colonnes en dur. C’est à dire en gros…

TableColumn col = new TableColumn( viewer.getTable(), SWT.CENTER );
col.setText( element );
col.setWidth( 80 );

On a vu mieux comme solution.
Une autre alternative, plus souple, c’est d’utiliser une Layout particulière.
Comme en SWING et AWT, les layout SWT définissent comment des widgets enfants vont se placer dans un widget parent. Généralement, ça n’a pas trop de sens pour les tables, vu qu’elles ont une layout par défaut. Mais SWT a rajouté une layout particulière (TableLayout) pour les tables, en plus de celle par défaut. Grâce à cette layout, on va pouvoir configurer la largeur de nos tables : soit en donnant une largeur exacte (en pixels), soit en pondérant la largeur de la colonne par rapport à la largeur du tableau.

Et ceci se configure pour chaque colonne, indépendamment des autres.
On peut donc mélanger les 2 approches : pour certains colonnes, on donnera une pondération avec une largeur minimale (TableWeightData), et pour d’autres, on fixera la largeur en pixels (TablePixelData).

Dans notre exemple, nous allons définir les règles suivantes :
+ La 1ère colonne doit faire au moins 100 pixels de large et occuper 40% de la largeur de la table.
+ La 2ème colonne doit faire au moins 150 pixels de large et occuper 40% de la largeur de la table.
+ La 3ème colonne doit faire exactement 100 pixels de large.

Voilà l’implémentation de ces règles.

public void displayPersons() {

	Shell shell = new Shell();
	TableViewer viewer = new TableViewer( shell );

	// Define the columns
	String[] COLUMNS = new String[] { "Nom", "Adresse", "Date de naissance" };
	for( String element : COLUMNS ) {
		TableColumn col = new TableColumn( viewer.getTable(), SWT.CENTER );
		col.setText( element );
	}

	// Define the layout of the table
	TableLayout tlayout = new TableLayout();
	tlayout.addColumnData( new ColumnWeightData( 40, 100, true ));
	tlayout.addColumnData( new ColumnWeightData( 40, 150, true ));
	tlayout.addColumnData( new ColumnPixelData( 100, true ));
	viewer.getTable().setLayout( tlayout );

	// The input will be an array or a collection.
	// We will need every element of this input.
	viewer.setContentProvider( new ArrayContentProvider());

	// For very element to display, we will select what to display
	// in function of the column index
	viewer.setLabelProvider( new ITableLabelProvider() {
		SimpleDateFormat formatter = new SimpleDateFormat( "dd/mm/yy" );

		@Override
		public void removeListener( ILabelProviderListener arg0 ) {
			// nothing
		}

		@Override
		public boolean isLabelProperty( Object arg0, String arg1 ) {
			return false;
		}

		@Override
		public void dispose() {
			// nothing
		}

		@Override
		public void addListener( ILabelProviderListener arg0 ) {
			// nothing
		}

		@Override
		public String getColumnText( Object element, int colmnIndex ) {

			String result = null;
			switch( colmnIndex ) {
			case 0:
				result = ((Person) element).getName();
				break;

			case 1:
				result = ((Person) element).getAddress();
				break;

			case 2:
				result = this.formatter.format(((Person) element).getBirthDate());
				break;
			}

			return result;
		}

		@Override
		public Image getColumnImage( Object element, int colmnIndex ) {
			return null;
		}
	});

	// Eventually, let's define an input
	Person[] persons = new Person[ 50 ];
	for( int i=0; i<=50; i++ ) {
		persons[ i ] = new Person(
				"Personne " + i,
				"Addresse " + i,
				new GregorianCalendar( 1950 + i, i % 12, 2 * i %  30 ));
	}

	viewer.setInput( persons );
	viewer.refresh();
}

Combiner les 2 approches (pondérée et largeur de base en pixel) est très pratique.
Les ColumnPixelData se montrent très utiles pour les colonnes qui n’ont par exemple qu’une image comme contenu. C’est ce que l’on choisira pour des énumérations (homme / femme, statut, degré de validité…) où une image sera plus explicite que du texte. Les largeurs pondérées sont elles plus utiles pour des colonnes avec du texte.

Extensions

Un tableau a évidemment des colonnes.
Mais il existe aussi d’autres objets graphiques dans SWT et JFace qui peuvent avoir des colonnes.

Notamment, il est possible qu’un arbre (classe Tree de SWT) puisse avoir des colonnes, ce qui permet de replier ou déplier des lignes.
L’exemple donné avec un TableViewer peut donc être porté sur un TreeViewer. Il faudra simplement faire quelques adaptations : utiliser des TreeColumns d’une part, et utiliser un ITreeContentProvider pour définir la structure arborescente d’autre part.

Le reste sera similaire.

Publicités

About this entry