Looking for Computer Science  & Information Technology online courses ?
Check my new web site: https://www.yesik.it !

SWT est une bibliothèque pour créer des interfaces utilisateurs graphiques (GUI) issue du projet Eclipse. Cet article présente SWT et détaille pas à pas le fonctionnement d'une application basée sur cette bibliothèque.

Motivation

Eclipse est un environnement de développement intégré (IDE) initialement destiné au développement Java – et lui-même écrit en Java. Lorsque Eclipse est "sorti", la première chose qu'ont remarqué les utilisateurs a été la réactivité de l'interface utilisateur. En effet, "à l'époque", il était communément accepté que le prix à payer pour avoir une interface graphique portable était de sacrifier la vitesse de réaction de l'application.

La raison pour laquelle Eclipse réalise cette quadrature du cercle est qu'il n'utilise pas les bibliothèques graphiques Java standard (à savoir AWT et Swing). Mais une bibliothèque spécifiquement développée par IBM et la fondation Eclipse: SWT. Alors, les ingénieurs d'IBM seraient donc plus compétents que ceux de Sun? En fait, la raison de la vitesse de réaction d'une interface utilisateur basée sur SWT vient surtout d'un choix important d'architecture logicielle: Avec Swing, les ingénieurs de Sun ont mis au point une bibliothèque graphique parfaitement indépendante de la plate-forme d'exécution. En effet, cette bibliothèque est elle-même écrite en Java. Le point faible de cette approche, est que c'est donc Java qui doit prendre en charge toute la gestion de l'interface graphique. Sans pouvoir se reposer sur les services offerts par l'environnement d'exécution hôte (Windows, Gnome, OSF/Motif, Mac OSX, ...).

A l'inverse, les auteurs de SWT ont pris le parti inverse qui consiste à dire que les systèmes d'exploitation actuels offrent des composants logiciels capables de gérer de façon performante et efficace les éléments de l'IHM. Alors pourquoi s'en priver? En fait, en simplifiant outrageusement, SWT n'est qu'une couche d'interface qui permet à une application Java d'utiliser les composants natifs du système hôte.

Cette approche, pour aussi performante soit-elle, a tout de même deux inconvénients:

  1. Tout d'abord, puisque SWT dépend des services fournis par le système hôte, SWT est fortement lié à celui-ci. A tel point qu'il est nécessaire d'installer une version spécifique de SWT adaptée à la plate-forme d'exécution. Ce qui est en contradiction avec la philosophie de Java Write once, run everywhere. En outre, cela veut aussi dire que pour exécuter une application utilisant SWT, il faut être sur une plate-forme sur laquelle SWT a été porté. Heureusement, SWT tourne sous les principaux environnements graphiques PC, Mac et Unix. Ainsi que sur certains systèmes embarqués comme WindowsMobile. Par contre, si vous développez pour une plate-forme un tant soit peu exotique AWT/Swing serait peut-être un meilleur choix.
  2. Le second inconvénient est plus subtil. En effet, vous le savez certainement, un des points forts de Java est la gestion automatique de la mémoire libre. Et en particulier du recyclage des objets devenus inaccessibles. Or il s'agit bien là d'un service offert par la machine virtuel Java (JVM). Par conséquent, les objets de l'environnement hôte utilisés par SWT échappent au ramasse miette de Java. Autrement dit, c'est au programmeur qu'incombe la responsabilité de désigner les ressources SWT qui ne lui seront plus utiles afin qu'elles puissent être immédiatement libérées. Heureusement, dans la pratique, cela consiste en un simple appel à la méthode dispose en temps utile. Le tout étant de ne pas l'oublier...

Côté positif, outre des performances très satisfaisantes sur tous les matériels pour lesquels SWT a été porté, les applications utilisant SWT possèdent le même look and feel que les applications natives. Ce qui peut être un avantage important du point de vue de l'utilisateur.

Comme vous le comprenez SWT a ses points forts et ses points faibles. Et SWT n'est pas un remplacement pour AWT ou Swing. C'est juste une autre bibliothèque d'interface graphique. Basée sur d'autres principes que les bibliothèques standard Java. Par conséquent, utiliser l'une ou l'autre de ces bibliothèques doit être un choix conscient et réfléchi.

Dans la suite de cet article, nous allons voir comment mettre en oeuvre SWT.

Installer SWT

L'origine de SWT est le projet Eclipse. Néanmoins, il n'est pas nécessaire d'avoir installé Eclipse pour utiliser une application développée avec SWT.

Pour le prouver, nous allons ici installer SWT à la main et utiliser les outils Java en ligne de commande pour créer une application graphique.

  1. La première chose à faire est de télécharger à partir de http://www.eclipse.org/swt l'archive de SWT qui correspond à votre plate-forme. Dans mon cas, il s'agissait de swt-3.4-gtk-linux-x86.zip.
  2. Dans /usr/local/lib, créez le répertoire swt-version (où version correspond à la version de SWT que vous avez téléchargée. Dans mon cas, le répertoire à créer était /usr/local/lib/swt-3.4-gtk-linux-x86).
  3. Décompressez l'archive de SWT dans le répertoire que vous avez créé à l'étape précédente.
  4. Pour faciliter la migration d'une version à l'autre, nous allons aussi créer un lien symbolique vers le répertoire d'installation. Ce lien s'appellera juste swt.

Au final, vous devriez avoir dans /usr/local/lib:

sh$ ls -ld /usr/local/lib/swt*
lrwxrwxrwx 1 root staff   22 2009-01-23 19:50 /usr/local/lib/swt -> swt-3.4-gtk-linux-x86/
drwxr-sr-x 3 root staff 4096 2009-01-23 19:50 /usr/local/lib/swt-3.4-gtk-linux-x86

Le contenu exact du répertoire /usr/local/swt-version dépend de la version de SWT que vous avez téléchargée. Au minimum, il devrait contenir l'archive Java swt.jar. Dans mon cas, le contenu complet est le suivant:

sh$ ls /usr/local/lib/swt-3.4-gtk-linux-x86
about_files  about.html  src.zip  swt-debug.jar  swt.jar

Hello SWT

org.eclipse.swt.widgets.Display

Pour commencer, nous allons créer le minimum nécessaire pour une application basée sur SWT. Ainsi, toute application SWT doit créer une instance de la classe org.eclipse.swt.widgets.Display. C'est cette instance qui servira à SWT d'interface avec le système natif. Comme tous les objets SWT qui interagissent directement avec le système hôte, il faudra penser à appeler la méthode dispose lorsqu'il ne sera plus nécessaire à l'application. Dans le cas d'une instance de la classe Display, ce sera à la fin du programme:

import org.eclipse.swt.widgets.Display;
 
public class HelloSWT {
        public static void main(String[] args) {
                Display display = new Display();
 
                /* Tout le code utilisant SWT ici */
 
                display.dispose();
        }
}

Comme l'indique le commentaire, le code utilisant SWT doit donc prendre place entre la création de l'instance de la classe Display et la libération des ressources associées à cet objet (display.dispose()).

Afin de vérifier que SWT est bien installé, essayons tout de même de compiler et exécuter ce programme. Rien de spécial à noter, si ce n'est qu'il ne faut pas oublier d'ajouter /path/to/swt.jar dans le classpath (aussi bien à la compilation qu'à l'exécution!):

sh$ javac -cp .:/usr/local/lib/swt/swt.jar HelloSWT.java
sh$ java -cp .:/usr/local/lib/swt/swt.jar HelloSWT

Effectivement quand vous lancez le programme précédent, rien de visible ne se produit. Mais au moins, vous ne devriez avoir aucun message d'erreur.

Une fenêtre

Pour avoir un résultat visible, le minimum à ajouter est donc de créer une fenêtre. Dans SWT, c'est une instance de la classe org.eclipse.swt.widgets.Shell. Mais créer une fenêtre ne suffit pas: il faut aussi l'ouvrir. Sinon elle n'est pas visible.

Ajoutez donc les lignes suivantes à votre programme:

Shell	shell	= new Shell(display);
shell.open();

Pas de dispose?

Maintenant que j'ai bien insisté sur le fait qu'il faut appeler la méthode dispose pour chaque objet SWT utilisant des ressources système, je vous explique comment créer une fenêtre ... et ne fait pas appel à la méthode dispose!

La raison en est que, par défaut, la fenêtre est automatiquement disposée lors de sa fermeture (par un clic sur la case de fermeture par exemple).

Piège:

Si la compilation échoue avec un message du style:

HelloSWT.java:9: cannot find symbol
symbol  : class Shell
location: class HelloSWT
                Shell   shell   = new Shell(display);

C'est que vous avez oublié d'ajouter l'import org.eclipse.swt.widgets.Shell.

En effet, rappelez-vous qu'en Java, pour utiliser le nom réduit d'une classe, il faut penser à l'importer en début de programme!

Après cette modification, si vous exécutez l'application vous constaterez qu'une fenêtre s'ouvre ... et se referme immédiatement. De façon évidente, il nous manque encore un petit quelque-chose. Et ce quelque-chose, c'est la boucle événementielle.

Attendre les évènements

Dans un programme doté d'une interface utilisateur graphique, la boucle événementielle est en quelque sorte le coeur du programme. C'est une boucle qui tourne en attendant un événement (comme par exemple une action de l'utilisateur). Une fois un événement reçu, celui-ci est traité par l'application. Et bien sûr, la boucle ne devrait se terminer que quand l'utilisateur a décidé de quitter l'application.

Dans notre programme, la boucle événementielle:

Bref, voici le code de notre boucle événementielle. Bien entendu, la boucle prend place une fois la fenêtre créée et affichée:

while(!shell.isDisposed()) {
	if (!display.readAndDispatch())
		display.sleep();
}

Si vous lancez le programme, vous constaterez que la fenêtre s'ouvre toujours, mais maintenant elle reste affichée tant que l'utilisateur ne clique pas sur sa case de fermeture. Dès la fenêtre fermée, le programme se termine.

Un widget

Bien, une fenêtre vide n'a rien de très excitant. Ajoutons donc un petit message. Tous les éléments d'interface utilisateur qui apparaîtront dans une fenêtre sont matérialisés sous la forme de widgets. Ce sont des objets dont il existe des versions spécialisées pour chaque élément qui peut apparaître dans une fenêtre (boutons, textes, menus, etc...).

Ici nous allons créer un texte éditable sous la forme d'un widget de la classe org.eclipse.swt.widgets.Text.

Parmi les autres widgets possibles, on peut citer:

Créer un widget dans SWT se fait globalement en deux étapes:

  1. Tout d'abord on instancie la classe avec les options désirées. Les options varient d'un widget à l'autre. Elles permettent d'en modifier l'apparence ou le comportement. Ici nous nous en servirons pour pour centrer notre texte;
  2. Ensuite on donne au widget les caractéristiques désirées. Ici nous donneront le texte à afficher (setText) et fixerons la taille du widget à sa taille idéale (méthode pack).

Le code complet du programme (imports compris) est donné ci-dessous:

import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.SWT;
 
public class HelloSWT {
        public static void main(String[] args) {
                Display display = new Display();
 
                Shell   shell   = new Shell(display);
 
                Text    text    = new Text(shell, SWT.CENTER);
                text.setText("Hello");
                text.pack();
 
                shell.pack();
                shell.open();
 
 
                while(!shell.isDisposed()) {
                    if (!display.readAndDispatch())
                        display.sleep();
                }
 
 
                display.dispose();
        }
}

Si vous lancez le programme vous verrez notre texte "Hello" apparaître ... dans une toute petite fenêtre. La seule raison pour laquelle la fenêtre a été à ce point réduite est l'appel à shell.pack() qui redimentionne la fenêtre à sa taille idéale, c'est à dire le minimum nécessaire pour contenir tous ses widgets.

Remarque:

Si vous vous amusez à créer un second texte:

Text    text2    = new Text(shell, SWT.CENTER);
text2.setText("Hello");
text2.pack();

Vous aurez la surprise de constater que vous ne voyez dans la fenêtre qu'un seul des deux widgets. Mais il s'agit là d'une fausse impression: les deux sont affichés, mais à la même position. L'un cache donc l'autre.

En effet, pack redimentionne le widget, mais ne change pas sa position – qui est par défaut (0;0) (soit le coin haut-gauche de la fenêtre). Bref, il faut changer la position de notre widget:

Text    text2    = new Text(shell, SWT.CENTER);
text2.setText("Hello");
text2.setLocation(100, 100);
text2.pack();

Et là, miracle: les deux widgets deviennent bel et bien visibles.

Jouons un peu

Histoire de faire quelque-chose d'un peu plus utile (enfin, juste un peu) avec SWT nous allons réaliser une sorte de jeu qui pose une question à l'utilisateur puis valide ou non sa réponse.

L'interface utilisateur contiendra 4 contrôles:

  1. un champ de texte statique qui servira à afficher la question;
  2. une zone de texte éditable pour permettre à l'utilisateur de saisir sa réponse;
  3. un bouton de validation;
  4. enfin un champ de texte servira à afficher le message de succès ou d'échec de l'utilisateur.

Structure de l'application

Afin de structurer un minimum le programme, nous allons aussi éviter de tout mettre dans le main. Sans même faire intervenir SWT, on peut globalement définir notre programme grâce à la classe suivante:

public class QuestionApp {
    /** Initialise l'application et construit l'interface utilisateur */
    public QuestionApp() {
        question    = "Combien font 3+3?";
        answer      = "6";
 
        /* TODO: complete this constructor! */
    }
 
    /** Valide la réponse de l'utilisateur */
    public boolean isValidAnswer(String answer) {
        /* TODO: write this method! */
 
        return false;
    }
 
    /** Boucle événementielle */
    private void processEvents() {
        /* TODO: write this method! */
    }
 
    /** Fait le ménage avant de quitter */
    private void cleanUp() {
        /* TODO: write this method! */
    }
 
    public void run() {
        processEvents();
 
        cleanUp();
    }
 
    private String      question;
    private String      answer;
 
    /** Programme principal */
    public static void main(String[] args) {
        QuestionApp app = new QuestionApp();
        app.run();
    }
}

Si vous voulez, vous pouvez compiler ce programme et l'exécuter. Mais pour l'instant, il ne fait rien: comme vous le voyez, il y a encore pas mal de méthodes à écrire.

display et shell

La boucle événementielle et la méthode chargée de faire le ménage à la fin de l'application sont des classiques de SWT:

/* ... */
    /** Boucle évènementielle */
    private void processEvents() {
        while(!shell.isDisposed()) {
                if (!display.readAndDispatch())
                        display.sleep();
        }
    }
 
    /** Fait le ménage avant de quitter */
    private void cleanUp() {
        display.dispose();
    }
/* ... */

Par contre, en écrivant ces lignes, on peut se rendre compte que la variable display est partagée entre plusieurs méthodes. Et qu'il en sera certainement de même pour la variable shell. Bref ce sont deux candidats idéaux pour devenir des variables d'instance.

/* ... */
    private Display     display;
    private Shell       shell;
/* ... */

Créer l'interface utilisateur

Maintenant le gros morceau: construire la fenêtre principale dans le constructeur. En réalité le code est plus long que complexe. Il crée la fenêtre, puis les différents widgets:

/* ... */
    public QuestionApp() {
        question    = "Combien font 3+3?";
        answer      = "6";
 
        display = new Display();
        shell   = new Shell(display);
 
        Label   questionWidget = new Label(shell, SWT.LEFT);
        questionWidget.setText(question);
        questionWidget.pack();
        questionWidget.setLocation(0, 0);
 
        Text    answerWidget = new Text(shell, SWT.LEFT);
        answerWidget.setText("?????????????");
        answerWidget.pack();
        answerWidget.setLocation(0, 30);
 
        Button  submitButton = new Button(shell, SWT.PUSH);
        submitButton.setText("Valider");
        submitButton.pack();
        submitButton.setLocation(0, 60);
 
        Label   infoWidget = new Label(shell, SWT.CENTER);
        infoWidget.setText("Tapez votre réponse puis validez");
        infoWidget.pack();
        infoWidget.setLocation(0, 90);
 
 
        shell.pack();
        shell.open();
    }
/* ... */

Pour peu que vous ajoutiez les imports nécessaires – et que vous précisiez le chemin vers swt.jar dans le classpath – vous devriez pouvoir compiler et exécuter l'application.

Gestion des événements

Il ne nous manque que la gestion du clic sur le bouton afin de valider la réponse de l'utilisateur.

Cela se fait en ajoutant un gestionnaire d'événements au widget sur lequel peut agir l'utilisateur. Ici, l'événement à gérer est la sélection du bouton "Valider".

/* ... */
        submitButton.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                if (answerWidget.getText().equals(answer))
                    infoWidget.setText("Bonne réponse!");
                else
                    infoWidget.setText("Mauvaise réponse...");
            }
        });
/* ... */

Note:

Pensez aussi à ajouter les imports pour SelectionAdapter et SelectionEvent:

import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;

Si vous n'êtes pas habitués aux inner classes de Java, la syntaxe ci-dessus peut sembler curieuse. Surtout la partie: new SelectionAdapter() { ... }.

En fait, il s'agit de la création d'une instance de la classe org.eclipse.swt.events.SelectionAdapter (comme avec un new normal). Mais en plus, ici, la méthode widgetSelected est surchargée uniquement pour cet objet. En d'autres termes, l'objet créé se comporte exactement comme toute instance de SelectionAdapter sauf en ce qui concerne la méthode widgetSelected qui est remplacée par celle que nous avons codée.

Quand on sait que cette dernière méthode est appelée lorsque le widget est sélectionné (pour un bouton, lorsque l'utilisateur clique dessus), vous comprenez que c'est là qu'il faut mettre le code à exécuter quand l'utilisateur clique sur le bouton.

Ici, la logique est simple: le texte du widget infoWidget change en fonction de ce qui a été saisi par l'utilisateur.

Si vous compilez ce programme... vous obtiendrez un certain nombre d'erreurs de la forme:

QuestionApp.java:42: local variable answerWidget is accessed from within inner class; needs to be declared final
                if (answerWidget.getText().equals(answer))

Et oui: c'est une limitation des inner classes Java: elles ne peuvent accéder qu'aux variables locales du bloc englobant ayant été déclarées final. Ici, cela n'a aucune incidence particulière sur notre programme. Par conséquent la modification du code source est aisée. Au final, le constructeur de notre application est le suivant:

/* ... */
    public QuestionApp() {
        /* initialisation des données de l'application */
        question    = "Combien font 3+3?";
        answer      = "6";
 
        /* création de l'interface utilisateur graphique (GUI) */
        display = new Display();
        shell   = new Shell(display);
 
        Label   questionWidget = new Label(shell, SWT.LEFT);
        questionWidget.setText(question);
        questionWidget.pack();
        questionWidget.setLocation(0, 0);
 
        // note: answerWidget déclarée ''final'' pour pouvoir être
        // utilisée dans le gestionnaire d'événements
        final Text      answerWidget = new Text(shell, SWT.LEFT);
        answerWidget.setText("?????????????");
        answerWidget.pack();
        answerWidget.setLocation(0, 30);
 
        Button  submitButton = new Button(shell, SWT.PUSH);
        submitButton.setText("Valider");
        submitButton.pack();
        submitButton.setLocation(0, 60);
 
        // note: infoWidget déclarée ''final'' pour pouvoir être
        // utilisée dans le gestionnaire d'événements
        final Label infoWidget = new Label(shell, SWT.CENTER);
        infoWidget.setText("Tapez votre réponse puis validez");
        infoWidget.pack();
        infoWidget.setLocation(0, 90);
 
        /* ajout du gestionnaire d'événements */
        submitButton.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                if (answerWidget.getText().equals(answer))
                    infoWidget.setText("Bonne réponse!");
                else
                    infoWidget.setText("Mauvaise réponse...");
            }
        });
 
        /* redimensionnement et affichage de la fenêtre */ 
        shell.pack();
        shell.open();
    }

A vous de jouer

Tracé de courbe

Le widget org.eclipse.swt.widgets.Canvas est un peu spécial en ce sens qu'il est conçu pour permettre à l'utilisateur de dessiner ses propres éléments d'interface.

Le dessin se fait en réponse à l'événement org.eclipse.swt.events.PaintEvent et la mise en oeuvre est la suivante:

/* ... */
		Canvas	canvas	= new Canvas(shell, SWT.NONE);
		canvas.setSize(200, 200);
		canvas.addPaintListener(new PaintListener() {
			@Override
			public void paintControl(PaintEvent e) {
				GC		gc = e.gc;
 
				// TODO: utiliser des méthodes de <tt>gc</tt> pour dessiner
			}
		});
/* ... */

La classe org.eclipse.swt.graphics.GC fournit un ensemble de primitive de dessin. Parmi celles-ci on peut citer drawLine, drawString ou encore drawRectangle. A titre d'illustration, celles-ci sont utilisées dans l'exemple ci-dessous:

/* ... */
	@Override
	public void paintControl(PaintEvent e) {
		GC gc = e.gc;
 
		gc.drawRectangle(10, 10, 180, 180);
		gc.drawLine(0, 0, 200, 200);
		gc.drawLine(0, 200, 200, 0);
		gc.drawString("You are here", 100, 100);
	}
/* ... */

Muni de ces informations écrivez un programme qui trace la fonction sinus (java.lang.Math.sin) entre -π et π. Vous tracerez également les axes et les graduations adaptées.

Pie chart

Considérez le tableau associatif suivant (représentant la distribution des espèces de vertébrés en 2004 – source wikipedia:Biodiversity):

Map<String, Integer> vertebres = new LinkedHashMap<String, Integer>();
vertebres.put("poissons", 29300);
vertebres.put("amphibiens", 5743);
vertebres.put("reptiles", 8240);
vertebres.put("oiseaux", 10234);
vertebres.put("mammifères", 5416);

Ecrivez un programme utilisant SWT pour afficher un graphique en camembert (pie chart) pour synthétiser ces données.

Ressources