Un peu de vocabulaire
Widget : acronyme de Windows, Icons, Dialog box, Graphics Extensions, Track ball. Aussi remplacé par WInDows gadGET.
GTK : the GIMP Toolkit était à la base utilisé par les développeurs du logiciel the GIMP. Elle a été séparée du logiciel depuis 1997 et il existe maintenant 2 versions GTK 1.0 et GTK 2.0, cette dernière étant la plus utilisée et aboutie.
GLib : bibliothèque proposant beaucoup de fonctions pour tout type de choses.
Pango : bibliothèque d'affichage et de rendu des textes.
GObject : bibliothèque qui propose une base d'implémentation des objets pour faire de la POO. Eh oui, le C n'est pas spécialement prévu pour faire de l'orienté objet. C'est donc GObject qui va faciliter un peu les choses.
GDK : bibliothèque graphique mais de bas niveau. GTK ne propose que l'utilisation de widgets.
JNI : acronyme de Java Native Interface. C'est un framework qui permet d'appeler des fonctions natives avec des bibliothèques basées sur des langages tels que C, C++ et assembleur.
1. Faisons les présentations
1.1 Quoi, comment, pourquoi ?
En Java, lorsque l'on pense interface graphique, on pense immédiatement à AWT et Swing. Ces deux bibliothèques ont fait leurs preuves mais s'intègrent relativement mal au bureau GNOME (de manière esthétique). De plus, les bibliothèques utilisées par GNOME sont réputées stables et solides pour construire des applications. C'est cette réflexion qui m'a fait découvrir la bibliothèque qui allait révolutionner ma façon de créer une interface graphique en Java. « Opening GTK and GNOME to Java Programmers », voilà le slogan. On traduira de manière rapide par « Ouvrir GTK et GNOME aux Développeurs Java ». Ça y est, on sait désormais à quoi va servir java-gnome. Les développeurs de cette API partent d'un principe simple. Un développeur Java saura mieux écrire un programme en Java, alors pourquoi ne pas lui proposer de développer son application avec des bibliothèques qu'il apprécie, le tout en Java. Java-gnome propose alors de pouvoir exploiter en Java les bibliothèques utilisées par GNOME, en particulier GTK, sans plonger les doigts dans cette affreuse JNI.
1.2 La mécanique interne
L'API java-gnome est née en 1998. Depuis, beaucoup de changements se sont déroulés. La version 2.x, maintenant obsolète, n'a jamais vraiment été utilisable et c'est pour mettre fin à cette période peu prometteuse que la version 4.x de java-gnome a fait son apparition. Les différences sont énormes. Auparavant, tout était écrit à la main, y compris la partie JNI, ce qui était, il faut l'avouer, courageux de la part des développeurs. À présent, une grande partie du code est générée automatiquement, mais pas tout ! Qu'est-ce que ça change ? Le développement de l'API est plus rapide et plus facile, mais il y a un minimum de contrôle par des humains. Mais pourquoi ne pas tout générer automatiquement ? La question est pertinente. Afin que les fonctionnalités soient disponibles aux développeurs, il est nécessaire qu'un développeur ou contributeur de java-gnome écrive la partie publique de l'API. L'inconvénient est que toutes les bibliothèques ne sont pas encore utilisables. En revanche, la qualité en est grandement améliorée. L'API publique est faite par des humains pour des humains. La personne écrivant l'API publique ne fera alors qu'implémenter les méthodes, les constructeurs d'une ou plusieurs classes et en faire la documentation.
Figure 1 : Architecture de java-gnome
Mais qu'est-ce que ça donne concrètement ? De manière réaliste, java-gnome permet déjà de faire beaucoup de choses, même s'il lui manque aussi pas mal de fonctionnalités sympathiques (le drag'n'drop, par exemple). Bon, parlons un peu code, pour rentrer doucement dans le sujet qui nous intéresse. Vous savez peut-être comment utiliser GTK en C, alors voyons comment une méthode en C est « traduite » en langage Java avec java-gnome. Pour modifier le label d'un bouton, en C, on utilise la méthode suivante :
GtkButton
void gtk_button_set_label(const gchar* label)
Ce qui signifie, en français, j'appelle la méthode gtk_button_set_label() de la classe GtkButton. Voilà ce que donne son équivalent en Java :
org.gnome.gtk
Button
void setLabel(String label)
On traduit alors, j'appelle la méthode setLabel() de classe Button située dans le package org.gnome.gtk. Ça y est, vous avez compris, c'est impossible de se perdre, vous voyez. Maintenant que l'on a la théorie, passons à la pratique.
2. Parlons sérieux, parlons code
2.1 La compilation de l'engin
La première chose à faire, comme avec toutes les bibliothèques et logiciels, c'est l'installation, et éventuellement la compilation si aucun paquet (à jour) n'est proposé pour votre distribution. Sur un système basé sur Debian, on installera le paquet libjava-gnome-java. À l'heure actuelle, Debian met à disposition la version 4.0.14 (dernière en date) dans Sid. En revanche, si vous avez besoin de compiler le code source de l'API, il vous faudra vous munir de Bazaar pour récupérer le code source (ou une archive du code [1]). Avec Debian, on fera alors :
# aptitude install bzr junit libgtk2.0-dev libatk1.0-dev libglade2-dev libgtksourceview2.0-dev libnotify-dev libenchant-dev libgtkspell-dev libunique-dev
Ensuite, on peut récupérer le code source de la branche principale. Il faut savoir qu'il est toujours possible de compiler et d'utiliser le code de la branche « mainline ». À chaque nouvelle version, c'est la dernière version de cette branche qui donnera vie (sans changement) à la nouvelle release.
$ bzr checkout bzr://research.operationaldynamics.com/bzr/java-gnome/mainline/ java-gnome
La compilation se fait de manière classique via un makefile. Cependant, nous n'avons pas besoin d'un outil comme automake. En effet, le fichier configure est écrit en Perl et s'occupera de trouver les dépendances manquantes, le compilateur Java et la JVM. Voici comment on compilerait et installerait java-gnome toujours sur un système Debian.
$ cd java-gnome
$ ./configure prefix=/usr libdir=/usr/lib/jni jardir=/usr/share/java
$ make
# make install
Maintenant, pour chaque utilisation, il n'y a plus qu'à inclure le fichier gtk.jar situé dans /usr/share/java/ dans le classpath.
2.2 L'exemple simple et expliqué
Pour commencer, on va y aller doucement. Quoi de plus simple qu'une fenêtre contenant un bouton qui ne fait rien ? Allons-y. Créons la classe ComeOnBabyLightMyFire et mettons-y le code suivant :
import org.gnome.gtk.Button;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Window;
public class ComeOnBabyLightMyFire {
public static void main(String[] args) {
final Window w;
final Button b;
Gtk.init(args);
w = new Window();
b = new Button("Don't press me!");
w.add(b);
w.showAll();
Gtk.main();
}
}
Voilà comment se déroule le code. On initialise la bibliothèque GTK. On crée une fenêtre (invisible pour l'instant), on crée un bouton (invisible lui aussi), on met le bouton dans la fenêtre, on rend le tout visible et enfin, on lance la boucle principale de GTK. Rien de vraiment compliqué jusque-là. Pourquoi ne pas afficher les widgets au fur et à mesure ? C'est pour une raison assez simple. Si on ajoute un widget puis si on le rend visible tout de suite après, l'utilisateur va voir apparaître les widgets un à un. Alors qu'en rendant le tout visible à la fin, l'interface se dévoilera déjà entièrement construite.
2.3 Utilisation des signaux
Notre exemple précédent fonctionne, mais ne fait pas grand chose à part afficher une fenêtre et un bouton. De plus, il faut obligatoirement utiliser CTRL + C pour l'arrêter. On va donc apporter deux modifications pour le rendre un peu plus intéressant. Pour utiliser le bouton et fermer la fenêtre proprement, nous allons utiliser des signaux. Et c'est ici que l'on voit la facilité d'utilisation de java-gnome à mon sens. En C, il faudrait faire une fonction de callback, ici, pas besoin de ça. Pour faire réagir notre bouton, nous allons simplement utiliser le signal Button.Clicked qui, comme son nom l'indique, sera émis lorsque l'on va cliquer sur le bouton. On ajoute le code suivant avant l'appel à la méthode main() de GTK.
b.connect(new Button.Clicked() {
public void onClicked(Button source) {
System.out.println("You clicked me!");
}
});
Maintenant que notre bouton réagit au clic, nous allons faire terminer le programme lorsque l'utilisateur va fermer la fenêtre. Pour cela, on utilise le signal Window.DeleteEvent (il sera émis lors du clic sur la croix de fermeture de la fenêtre). On ajoute donc avant le main() de GTK le code suivant. J'oubliais de préciser que toute l'interface et le code réalisé avec java-gnome doit être fait avant d'appeler la méthode Gtk.main() !
w.connect(new Window.DeleteEvent() {
public boolean onDeleteEvent(Widget source, Event event) {
Gtk.mainQuit();
return false;
}
});
Eh oui, comme vous l'aurez deviné, la méhode Gtk.mainQuit() permet de sortir de la boucle principale lancée via Gtk.main().
2.4 Et une « vraie » application ?
Bien évidemment, on peut en faire une. Dans notre cas, on va faire un générateur de mot de passe. Pour que ça ne soit pas trop lourd en code, nous allons étudier le code de l'interface graphique seule et pas le reste. Tout d'abord, notre classe qui va nous permettre de créer l'interface va hériter de la classe Window (eh oui, on peut faire ça).
public class GtkInterface extends Window {
...
}
Dans cette classe, un constructeur sans argument permettra d'ajouter tous les widgets dans la fenêtre et de connecter tous les signaux nécessaires (je passe la déclaration des widgets en dehors du constructeur). Tout d'abord, on appelle le constructeur de la classe mère, puis on déclare tous les conteneurs qui nous seront utiles. Il est extrêmement important de ne pas oublier le super(), sinon l'appel à la fonction en C pour créer la fenêtre ne se fera pas. Par conséquent, le programme va simplement crasher.
super();
final VBox container;
final HBox columns;
final VButtonBox column1;
final VButtonBox column2;
final HButtonBox buttons;
final SizeGroup group;
On voit que l'on va utiliser une VBox (pour disposer des widgets verticalement), une HBox (idem mais horizontalement), des VButtonBoxs (idem que la VBox mais pour les boutons), une HButtonBox (idem mais horizontalement), enfin, le SizeGroup permet de mettre plusieurs widgets à la même taille. On définit alors un titre pour la fenêtre et on ajoute tous les conteneurs à l'intérieur.
setTitle("Générateur de mot de passe");
container = new VBox(false, 6);
add(container);
columns = new HBox(false, 0);
container.packStart(columns, false, false, 0);
column1 = new VButtonBox();
columns.packStart(column1, false, false, 0);
column2 = new VButtonBox();
columns.packStart(column2, false, false, 0);
group = new SizeGroup(SizeGroupMode.BOTH);
group.add(column1);
group.add(column2);
On ajoute maintenant des CheckButtons pour sélectionner les types de caractères à utiliser dans le mot de passe.
uppers = new CheckButton("_Majuscules");
lowers = new CheckButton("Mi_nuscules");
digits = new CheckButton("_Chiffres");
specials = new CheckButton("_Spéciaux");
column1.packStart(uppers, false, false, 0);
column2.packStart(lowers, false, false, 0);
column1.packStart(digits, false, false, 0);
column2.packStart(specials, false, false, 0);
On met ensuite une HScale (échelle horizontale) pour définir la taille du mot de passe. Le mot de passe fera au minimum 6 caractères et au maximum 16, et le pas de l'échelle sera de 1.
length = new HScale(6, 16, 1);
container.packStart(length, false, false, 0);
Une Entry sera utilisée pour afficher le mot de passe. Voici comment l'ajouter.
password = new Entry();
container.packStart(password, false, false, 0);
On termine l'ajout des widgets avec les boutons ; un pour générer le mot de passe, un pour le copier dans le presse-papiers.
generate = new Button("Générer");
generate.setImage(new Image(Stock.DIALOG_AUTHENTICATION, IconSize.BUTTON));
buttons.packStart(generate, false, false, 0);
copy = new Button(Stock.COPY);
buttons.packStart(copy, false, false, 0);
On voit que les boutons utilisent des constantes définies dans la classe Stock. Elles permettent d'avoir une image ainsi qu'un texte associés au bouton directement. Pour finir, on connecte les signaux et rend le tout visible.
connectButtonsSignals();
connect(new Window.DeleteEvent() {
public boolean onDeleteEvent(Widget source, Event event) {
Gtk.mainQuit();
return false;
}
});
showAll();
La méthode connectButtonsSignals() va connecter les signaux Button.Clicked aux deux boutons.
private void connectButtonsSignals() {
generate.connect(new Button.Clicked() {
public void onClicked(Button source) {
boolean[] checks = { digits.getActive(), uppers.getActive(),
lowers.getActive(), specials.getActive() };
int size = (int) length.getValue();
if (!checks[0] && !checks[1] && !checks[2] && !checks[3]) {
return;
}
String value = getNewPassword(size, checks);
password.setText(value);
}
});
copy.connect(new Button.Clicked() {
public void onClicked(Button source) {
Clipboard.getDefault().setText(password.getText());
}
});
}
Voilà, le code est complet. Pour utiliser l'interface, il ne suffira que de quelques lignes dans le main() de notre programme.
Gtk.init(args)
GtkInterface window = new GtkInterface();
window.showAll();
Gtk.main();
Oui, on utilise la méthode showAll(). Rappelez-vous, GtkInterface hérite de Window. Normalement, voilà ce que ça donne.
Figure 2 : Notre application
2.5 Ne partez pas, il y en a encore
Pfiou, on s'en est sorti. On a fait un premier vrai programme avec java-gnome. Mais là, on a simplement utilisé quelques composants de GTK, il y en a encore plein d'autres. On n'a pas non plus utilisé toutes les bibliothèques disponibles ! On pourrait utiliser Cairo, Enchant, Glade, Pango, Libnotify, Libunique, Sourceview. Quoi, vous voulez un exemple de chaque ? J'aimerais bien, mais je doute que l'on m'accorde 50 pages de plus sur le magazine. Vous voyez les possibilités, il y en a déjà beaucoup, revenez dans quelques mois, il y en aura encore plus. Allez, juste pour vous mettre l'eau à la bouche, voilà ce que l'on peut faire avec Cairo, par exemple.
Figure 3 : Un petit donut pour le goûter ?
Vous avez pu le constater, on peut faire pas mal de choses, mais malheureusement pas tout (pour le moment). C'est à la fois un avantage et un inconvénient. Comme je l'ai dit plus haut, cela vient du fait que la partie publique de l'API est écrite manuellement. Donc pour qu'un développeur ou contributeur écrive cette dernière, il faut qu'il s'y intéresse et qu'elle lui soit utile. Voilà pourquoi, il manque encore des fonctionnalités. L'avantage, car oui il y en a un, c'est que lorsque l'on utilise java-gnome, cela se fait de manière relativement facile pour tout développeur Java. L'API est compréhensible et cohérente. Le désavantage, lui, est simple à cerner, c'est le manque de fonctionnalités. De plus, on peut remarquer que beaucoup de bibliothèques (pour GNOME dans notre cas) possèdent leurs implémentations dans des langages tels que Python, Vala ou encore C++. Des implémentations qui sont très complètes d'ailleurs. Ce n'est pas le cas en Java, car faire le lien entre les appels de fonctions en C et le code Java est globalement plus embêtant à faire (vive la JNI) par rapport à un langage tel que Vala, qui ne demande que la rédaction d'un fichier Vala API.
3. Pour conclure
3.1 Petit résumé
Tout au long de cet article, j'ai essayé (de manière non ennuyeuse) de vous montrer la puissance de l'API java-gnome. J'ai fait un rapide sondage autour de moi, pas forcément représentatif, afin de savoir ce que les gens pensaient du développement avec les bibliothèques GNOME. Je leur ai demandé avec quels langages nous pouvons développer un programme pour le bureau GNOME. Les réponses ont été C, Python, C#, C++. J'ai donc constaté que peu de personnes doivent savoir qu'on peut aussi le faire en Vala ou encore en Java (et bien d'autres langages). Java-gnome fait partie des « bindings » officiels de GNOME, c'est-à-dire qu'il est reconnu par la communauté, preuve de sa qualité.
3.2 C'est utilisé ?
La réponse est « oui », même si ce n'est pas aussi répandu que pygtk, par exemple. On peut citer Slashtime [2] ou encore GNOME Split [3] (il y en a d'autres). Ces deux applications utilisent java-gnome et s'en portent même très bien. Alors si vous aussi vous aimez Java et GNOME, rejoignez les utilisateurs de l'API sur IRC, canal #java-gnome sur irc.gimp.net ou tout simplement via les mailing lists [4]. Les développeurs et utilisateurs seront ravis de pouvoir partager des informations avec vous.
3.3 Remerciements
Je tiens à remercier Andrew Cowie, mainteneur de java-gnome, pour son excellent travail et ses initiatives. C'est entre autres grâce à lui que l'API est de si grande qualité. Il m'a également beaucoup aidé afin que je puisse contribuer au développement et je me dois de lui dire merci au moins pour ça. Bien entendu, je remercie également tous les développeurs et contributeurs sans qui java-gnome ne serait pas ce qu'elle est à l'heure actuelle. Enfin, si vous nous rejoignez, vous me croiserez probablement sur IRC ;).
Références
[1] http://ftp.gnome.org/pub/gnome/sources/java-gnome/4.0/
[2] http://research.operationaldynamics.com/projects/slashtime/
[3] https://launchpad.net/gnome-split
[4] http://java-gnome.sourceforge.net/4.0/lists/