SwingBuilder et l'API de Twitter, 1ère partie

Article d'origine: 

Construire des IHM avec Swing n'a jamais été aussi facile

Résumé: 

Dans ce volet de Groovy par la pratique, Scott Davis s'attaque à un sujet qui plonge dans la terreur la plupart des développeurs Java habitués au côté serveur: Swing. Comme vous le verrez, le SwingBuilder de Groovy rend moins pénible l'utilisation de ce framework d'IHM puissant mais complexe.

Récemment, j'ai interviewé (voir les Ressources) Ted Neward, l'auteur de la série d'articles The busy Java developer's guide to Scala pour IBM developerWorks. Nous avons discuté d'une librairie pour Twitter qu'il développe dans sa série, et qui est intéressante, appelée Scitter (Scala + Twitter). Scitter met en exergue les possibilités de Scala dans le domaine des services Web et de l'analyse XML, mais Ted a admis qu'il ne développerait probablement jamais de frontal pour cette API. Ceci m'a bien sûr fait imaginer ce à quoi une IHM pour Twitter écrite en Groovy pourrait ressembler. Est-ce que Gwitter (Groovy + Twitter) sonnerait bien pour nommer ce projet ?

Je ne m'attaquerai pas à l'intégration Scala + Groovy dans cet article,bien qu'il y ait beaucoup de synergies potentielles entre les deux langages. Je m'intéresserai plutôt à une portion de l'écosystème Java souvent sous-estimée par les développeurs Java: Swing. Mais avant cela, voyons comment le XmlSlurper de Groovy traite aisément le flux Atom de Twitter.

L'API de recherche de Twitter

En parcourant la documentation en ligne de l'API de recherche de Twitter (voir les Ressources), on s'aperçoit qu'il est possible de faire une simple requête HTTP GET pour effectuer une recherche Twitter. La requête est passée par le biais d'un paramètre nommé q dans la chaine de requête, et les résultats peuvent être retournés soit sous forme Atom (un format XML de syndication) ou JSON (JavaScript Object Notation). Par exemple pour obtenir un flux Atom contenant toutes les occurences de thirstyhead, il faut faire la requête HTTP GET suivante: http://search.twitter.com/search.atom?q=thirstyhead.

Comme le montre le Listing 1, les résultats sont retournés comme une série d'éléments <entry> imbriqués dans un élément <feed>:

Listing 1. Les résultats d'une recherche Twitter sous forme Atom

<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
<entry>
    <title>thirstyhead: New series from Andrew Glover: Java Development 2.0 
           http://bit.ly/bJX5i</title>
    <content type="html">thirstyhead: New series from Andrew Glover: Java 
                         Development 2.0 http://bit.ly/bJX5i</content>
    <id>tag:twitter.com,2007:
        http://twitter.com/thirstyhead/statuses/3419507135</id>
    <published>2009-08-20T02:54:54+00:00</published>
    <updated>2009-08-20T02:54:54+00:00</updated>
    <link type="text/html" rel="alternate" 
          href="http://twitter.com/thirstyhead/statuses/3419507135"/>
    <link type="image/jpeg" rel="image" 
          href="http://s3.amazonaws.com/twitter_production/profile_images/
          73550313/flame_normal.jpg"/>
    <author>
      <name>ThirstyHead.com</name>
      <uri>http://www.thirstyhead.com</uri>
    </author>
  </entry>

  <entry>...</entry>
  <entry>...</entry>  
  <!-- snip -->
</feed>

Dans "Générer, analyser et ingurgiter du XML", vous avez vu combien il est facile d'expédier l'analyse du XML en utilisant XmlSlurper. Maintenant que vous savez à quoi le résultat de la requête va ressembler, créez un fichier nommé searchCli.groovy, comme dans le Listing 2:

Listing 2. Un script Groovy pour analyser les résultats Atom

if(args){
 def username = args[0]
 def addr = "http://search.twitter.com/search.atom?q=${username}"
 def feed = new XmlSlurper().parse(addr)
 feed.entry.each{
   println it.author.name
   println it.published
   println it.title
   println "-"*20
 }  
}else{
 println "USAGE: groovy searchCli <query>"
}

Entrez groovy searchCli thirstyhead en ligne de commande (ndT: cette recherche ne retourne plus aucun résultat maintenant, essayez autre chose !), et prenez plaisir à votre maitrise en 13 lignes des résultats Atom, comme le montre le Listing 3:

Listing 3. Exécution du script searchCli.groovy

$ groovy searchCli thirstyhead

thirstyhead (ThirstyHead.com)
2009-08-20T02:54:54Z
New series from Andrew Glover: 
Java Development 2.0 http://bit.ly/bJX5i
--------------------
kung_foo (kung_foo)
2009-08-18T12:33:32Z
ThirstyHead interviews Venkat Subramaniam: 
http://blip.tv/file/2484840 "Groovy and Scala are good friends..." 
(via @mittie). very good.

//snip

Création des classes initiales de Gwitter

Même si les scripts Groovy vont très bien pour les petits utilitaires informels et les expérimentations de preuves de concept, écrire des classes Groovy n'est pas beaucoup plus difficile. Et en guise de récompense, vous pouvez compiler les classes Groovy et les appeler dans du code Java.

Par exemple, créez Tweet.groovy à l'exemple du Listing 4:

Listing 4. Tweet.groovy

class Tweet{
  String content
  String published
  String author
  
  String toString(){
    return "${author}: ${content}"
  }
}

Comme vous le savez déjà, ce POGO (Plain Old Groovy Object) est un substitut du POJO (Plain Old Java Object) significativement plus verbeux.

Maintenant, convertissez le script de recherche du Listing 2 en la classe Search.groovy, comme le montre le Listing 5:

Listing 5. Search.groovy

class Search{
  static final String addr = "http://search.twitter.com/search.atom?q="
  
  static Object[] byKeyword(String query){
    def results = []
    def feed = new XmlSlurper().parse(addr + query)
    feed.entry.each{entry->
      def tweet = new Tweet()
      tweet.author = entry.author.name
      tweet.published = entry.published
      tweet.content = entry.title
      results << tweet
    }
    return results as Object[]    
  }
}

En temps normal, je me contenterais du résulat sous forme de java.util.ArrayList. Mais la javax.swing.JList que vous utiliserez plus loin dans l'article a besoin d'un Object[], considérez donc cela comme une toute petite avance sur la suite.

Notez que j'ai ôté la méthode main() de Search.groovy. Comment interagir avec cette classe maintenant ? Eh bien, avec un test unitaire, bien sûr ! Créez SearchTest.groovy, comme le montre le Listing 6:

Listing 6. SearchTest.groovy

class SearchTest extends GroovyTestCase{
  void testSearchByKeyword(){
    def results = Search.byKeyword("thirstyhead")
    results.each{
      assertTrue it.content.toLowerCase().contains("thirstyhead") ||
                 it.author.toLowerCase().contains("thirstyhead")
    }    
  }
}

Si vous tapez groovy SearchTest en ligne de commande et que vous voyez  (voir le Listing 7), vous avez converti avec succès votre simple script de recherche en un ensemble de classes réutilisables:

Listing 7. Résultat d'une exécution réussie du test

$ groovy SearchTest
.
Time: 4.64

OK (1 test)

Maintenant que la structure sous-jacente est en place, la prochaine étape est de commencer à lui faire un joli visage.

Introduction à SwingBuilder

Swing est une boite à outils IHM incroyablement puissante. Malheureusement parfois la puissance est cachée par la complexité. Si vous découvrez Swing, vous pourriez avoir la sensation d'apprendre à piloter un Boeing 747 quand tout ce dont vous avez vraiment besoin est un Cessna monomoteur - ou un deltaplane.

Le SwingBuilder de Groovy ne diminue pas la complexité intrinsèque de tâches telles que choisir le bon LayoutManager ou gérer correctement les problèmes de processus. C'est la complexité syntaxique qui est réduite. Comme vous le verrez dans un instant, Groovy offre des constructeurs avec arguments nommés et en nombre variable qui conviennent parfaitement pour les différents JComponents qu'il est nécessaire d'instancier puis de configurer par une série de setters. (Pour plus de détail sur SwingBuilder, voir les Ressources).

Mais l'utilisation des fermetures de Groovy est tout aussi appréciable. Mon problème depuis toujours avec Swing est qu'une arborescence naturelle semble disparaitre parmi les détails d'implémentation. Avec Java, vous finissez avec un ensemble décousu de composants sans aucune véritable intuition de qui appartient à qui. Vous pouvez déclarer une JFrame, un JPanel, et un JLabel dans n'importe quel ordre. Dans le code, on dirait qu'ils sont d'égale importance, alors qu'en fait la JFrame contient le JPanel, qui à son tour contient le JLabel. Regardez le Listing 8 pour avoir un exemple:

Listing 8. HelloJavaSwing.java

import javax.swing.*;

public class HelloJavaSwing {
  public static void main(String[] args) {
    JPanel panel = new JPanel();
    JLabel label = new JLabel("Hello Java Swing");

    JFrame frame = new JFrame("Hello Java Swing");
    panel.add(label);
    frame.add(panel);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(200,300);
    frame.setVisible(true);
  }
}

La compilation (javac HelloJavaSwing.java) et l'exécution (java HelloJava) de ce code devraient conduire à l'application illustrée dans la Figure 1:

Figure 1. HelloJavaSwing

Hello Java Swing

Le Listing 9 montre la même application écrite en Groovy. Comme vous le voyez, l'utilisation judicieuse que SwingBuilder fait des fermetures vous permet de voir clairement la chaine de composition dans du code déclaratif.

Listing 9. HelloGroovySwing.groovy

import groovy.swing.SwingBuilder
import javax.swing.*

def swingBuilder = new SwingBuilder()
swingBuilder.frame(title:"Hello Groovy Swing", 
                   defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                   size:[200,300],
                   show:true) {
  panel(){
    label("Hello Groovy Swing")    
  }
}

Entrez groovy HelloGroovySwing pour voir l'application de la Figure 2:

Figure 2. HelloGroovySwing

Hello Groovy Swing

Remarquez dans le Listing 9 que les J devant les noms de composants n'existent plus - un peu comme les get et set superflus sont ôtés des noms de méthodes. De plus , notez les arguments nommés du constructeur de frame. En coulisse, Groovy appelle le constructeur sans argument par défaut puis les setters - sans aucune différence avec l'exemple Java précédent. Mais le fait que tout soit rassemblé dans le constructeur fait que les choses sont bien nettes et ordonnées, et le retrait du préfixe set ainsi que des parenthèses finales réduit considérablement le bruit visuel.

Si vous ne connaissiez pas encore Swing, tout çà semble encore un peu compliqué. Mais si vous avez ne serait-ce qu'un peu d'expérience de Swing, cela ressemble certainement à du Swing réduit à son essence même: propre, clair, et efficace.

Comme vous l'avez fait dans la section précédente, prenez ce que vous avez appris avec ce script et convertissez-le en une classe. Créez un fichier nommé Gwitter.groovy, comme dans le Listing 10. Ce sont là les modestes débuts de l'interface utilisateur de votre client Groovy + Twitter.

Listing 10. Squelette de l'interface utilisateur de Gwitter

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
    }    
  }  
} 

Tapez groovy Gwitter pour vérifier qu'une fenêtre vide s'affiche. Si tout se passe comme prévu, la prochaine étape est d'ajouter un menu simple à l'application.

Ajouter une barre de menu

La création de menus avec Swing donne un autre exemple de composants qui sont naturellement hiérarchisés. Vous créez une JMenuBar qui contient un ou plusieurs JMenus, qui à leur tour contiennent un ou plusieurs JMenuItems.

Pour créer un menu File avec un item Exit, ajoutez le code du Listing 11 à Gwitter.groovy:

Listing 11. Ajout d'un menu File à Gwitter

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: { dispose() })
        }
      }  
    }    
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
    }    
  }  
}

Notez la hiérarche d'imbrications de la fermeture customMenuBar. Je l'ai placé séparément pour des raisons de lisibilité, mais j'aurais aussi bien pu la définir sur place, au milieu de la définition de la fenêtre. Une fois que la fermeture est définie, je l'appelle à l'intérieur de la fermeture frame. Entrez groovy Gwitter une fois de plus pour vérifier que le menu File apparait, comme le montre la Figure 3. Sélectionnez File > Exit pour sortir de l'application.

Figure 3. Le menu File de Gwitter

Le menu File de Gwitter

Regardez de nouveau le Listing 11. Notez que le gestionnaire actionPerformed est défini dans une fermeture et non dans une classe anonyme. Est-ce que ça ne rend pas le code propre et facile à lire par comparaison à l'alternative Java ?

Désormais il est temps d'ajouter quelques éléments de formulaire pour permettre la recherche.

Ajout du panneau de recherche

Les développeurs Swing expérimentés sont adeptes de l'assemblage de JPanels distincts pour former une application. Ces conteneurs permettent de grouper facilement des composants dont les fonctions sont apparentées.

Par exemple, Gwitter a besoin d'un JTextField pour permettre aux utilisateurs de saisir un critère de recherche, et d'un JButton pour soumettre la requête. Ca semble une bonne idée de regrouper ces deux objets dans une fermeture searchPanel, comme dans le Listing 12:

Listing 12. Ajout de searchPanel

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  def searchField
  
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
        }
      }  
    }    

    def searchPanel = {
      swingBuilder.panel(constraints: BorderLayout.NORTH){
        searchField = textField(columns:15)
        button(text:"Search", actionPerformed:{ /* TODO */ } )
      }
    }
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
      searchPanel()
    }    
  }  
}

Une fois que vous commencez à travailler avec les panneaux, le problème du choix d'un LayoutManager approprié ne tarde pas à venir. Par défaut, un JPanel utilise un FlowLayout. Cela veut dire que le textField et le button seront placés horizontalement, l'un après l'autre.

Le contentPane d'une JFrame est légèrement différent - il utilise un BorderLayout par défaut. Quand vous ajoutez le searchPanel à la fenêtre, vous devez spécifier dans quelle zone vous voudriez le voir apparaitre: NORTH, SOUTH, EAST, WEST, ou CENTER. (Pour les nuls en géographie, il est aussi possible d'utiliser PAGE_START, PAGE_END, LINE_START, LINE_END, et CENTER). Pour en savoir plus sur les différents LayoutManagers disponibles dans Swing, voir les Ressources.

Notez que la variable searchField est déclarée au niveau de la classe. De cette manière les autres composants comme le bouton peuvent y accéder. Le reste des composants est anonyme. Un coup d'oeil rapide à la liste des attributs de classe donne une indication de l'importance relative des composants.

Vous avez probablement remarqué que l'écouteur actionPerformed du bouton ne fait rien... pour l'instant. Il ne saurait pas quelle tâche effectuer à cet instant précis. Avant de pouvoir l'implémenter, il faut ajouter une autre panneau à l'application: celui qui permettra d'afficher le résultat de la recherche.

Ajout du panneau d'affichage des résultats

Comme le montre le Listing 13, vous définissez un resultsPanel dans une fermeture imbriquée comme vous l'avez fait avec searchPanel. Seulement cette fois, vous ajoutez un conteneur à l'intérieur d'un panneau: un JScrollPane. Ce composant permet à des barres de défilement horizontales et verticales d'apparaitre et de disparaitre en fonction du besoin. Les résultats de la méthode Search.byKeyword() sont affichés dans une JList nommée resultsList. (Les méthodes JList.setListData() prennent en paramètre un Object[] - rappelez-vous que c'est justement ce que vous avez fait en sorte que la méthode Search.byKeyword() retourne.)

Listing 13. Ajout de resultsPanel

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

class Gwitter{   
  def searchField
  def resultsList
  
  static void main(String[] args){
    def gwitter = new Gwitter()
    gwitter.show()
  }
    
  void show(){
    def swingBuilder = new SwingBuilder()  
    
    def customMenuBar = {
      swingBuilder.menuBar{
        menu(text: "File", mnemonic: 'F') {
          menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
        }
      }  
    }    

    def searchPanel = {
      swingBuilder.panel(constraints: BorderLayout.NORTH){
        searchField = textField(columns:15)
        button(text:"Search", actionPerformed:{ 
          resultsList.listData = Search.byKeyword(searchField.text) } )
      }
    }
    
    def resultsPanel = {
      swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
        resultsList = list()
      }
    }    
    
    swingBuilder.frame(title:"Gwitter", 
                       defaultCloseOperation:JFrame.EXIT_ON_CLOSE, 
                       size:[400,500],
                       show:true) {
      customMenuBar()                         
      searchPanel()
      resultsPanel()
    }    
  }  
}

Notez que la variable resultsList, comme searchField, est définie au niveau de la classe. Les deux variables sont utilisées dans le gestionnaire actionPerformed du bouton se trouvant dans le searchPanel.

Avec l'ajout du resultsPanel, Gwitter est désormais fonctionnel. Entrez en ligne de commande et vérifiez que ça fonctionne comme prévu. Chercher devrait renvoyer des résultats similaires à ceux de la Figure 4:

Figure 4. Première mouture des résultats de recherche

Première mouture des résultats de recherche

Vous pourriez arrêter là et crier victoire, mais j'aimerais attirer votre attention sur deux problèmes avant que vous n'effectuiez votre tour d'honneur. Le premier concerne la gestion des processus qui peut potentiellement virer au cauchemar, telle qu'elle est gérée par l' du bouton de recherche. Le second est la rusticité générale de l'application. Ces deux points sont résolus dans les deux prochaines sections.

Le processus de distribution des événements

Ce qui est cruellement ironique avec Swing, c'est qu'il attend des concepteurs d'IHM qu'ils soient à l'aise avec les problèmes de gestion de multiples processus plutôt destinés à des ingénieurs en développement, ou bien des ingénieurs en développement qu'ils comprennent les subtilités de la conception d'IHM et les problèmes d'utilisabilté.

Il est impossible de couvrir un sujet aussi complexe que les problèmes de gestion des processus dans une application Swing en quelques paragraphes. Il nous suffira de dire que les applications Swing sont fondamentalement mono-processus à la base. Tout se passe dans le processus de distribution des événements (EDT: Event Dispatch Thread). Lorsque les utilisateurs grognent a propos des temps de réponse de leur application Swing, c'est généralement parce qu'un développeur débutant a effectué une requête longue et coûteuse sur une base de données, ou bien a appelé des services Web, dans l'EDT - le même processus qui gère le rafraichissement de l'écran, les clics dans les menus, etc. Je vous ai fait faire involontairement ce genre de choses dans le gestionnaire actionPerformed du bouton de recherche. (Vous voyez comme il est facile de faire cette erreur courante).

Heureusement, la classe javax.swing.SwingUtilities propose quelques méthodes utilitaires bien nommées - invokeAndWait() et invokeLater() - qui aident à résoudre ces problèmes de gestion des processus. Les deux méthodes permettent d'agir de manière synchrone ou asynchrone sur l'EDT. (Voir les Ressources pour en savoir davantage sur la classe SwingUtilities). SwingBuilder facilite l'appel de ces deux méthodes, et vous donne une troisième option: la possibilité de créer sans effort un nouveau processus pour exécuter des actions potentiellement coûteuses.

Pour effectuer un appel synchrone sur l'EDT (SwingUtilities.invokeAndWait()), vous pouvez envelopper l'appel dans la fermeture edt{}. Pour effectuer un appel asynchrone sur l'EDT (SwingUtilities.invokeLater()), enveloppez l'appel dans une fermeture doLater{}. Mais je vais vous faire pratiquer la troisième possibilité: monter un nouveau processus pour gérer l'appel de méthode Search.byKeyword(). Pour ce faire, enveloppez le code dans une fermeture doOutside{}, à l'exemple du Listing 14:

Listing 14. Utilisation de la fermeture doOutside

def searchPanel = {
  swingBuilder.panel(constraints: BorderLayout.NORTH){
    searchField = textField(columns:15)
    button(text:"Search", actionPerformed:{ 
      doOutside{ 
        resultsList.listData = Search.byKeyword(searchField.text)
      }
    } )
  }
}

Pour une application aussi simple que Gwitter, il est probable que vous ne ressentiriez jamais vraiment comme il est pénible d'appeler le service Web dans l'EDT. Mais à la seconde où vous montreriez votre code à des experts Swing, ils vous regarderaient avec le mépris que l'on réserve d'habitude à ceux qui conduisent lentement sur la voie de gauche ou qui se garent sur les emplacements réservés aux personnes handicapées sur les parkings des magasins. Comme SwingBuilder rend faciles les bonnes pratiques concernant la gestion des processus, il n'y a pas de raison de s'en priver.

Maintenant que vous avez résolu ce problème de processus, la ligne suivante sur notre liste de choses à faire est de faire en sorte que cette application fasse moins mal aux yeux.

Ajouter des zébrures à la liste

Je n'ai pas peur d'admettre que Gwitter est assez laid pour l'instant. Je vais faire deux choses très simples pour améliorer l'apparence avec un peu de HTML. Par bonheur, les JLabels sont capables d'interpréter du HTML rudimentaire. Modifiez la méthode toString() de Tweet.groovy comme indiqué dans le Listing 15. La méthode toString() est appelée par JList pour afficher les résultats.

Listing 15. Renvoyer du HTML dans la méthode toString()

class Tweet{
  String content
  String published
  String author
  
  String toString(){
    //return "${author}: ${content}"

    return """<html>
         <body>
           <p><b><i>${author}:</i></b></p>
           <p>${content}</p>
         </body>
       </html>"""
  }
}

La modification suivante est un peu plus subtile. Une astuce courante dans les IHM est de zébrer les listes ou les tables longues. En alternant les couleurs sur les lignes paires et impaires, la liste est plus facile à parcourir. J'ai cherché JList stripes dans un moteur de recherche et j'ai suivi le conseil du premier article que j'ai trouvé. L'auteur recommende de créer un DefaultListCellRenderer personnalisé. J'ai hoché la tête avec un air de profonde sagesse en lisant chaque paragraphe, et ensuite j'ai volé sans aucune honte son exemple de code (voir les Ressources pour l'article complet).

Comme la syntaxe Groovy est un sur-ensemble de la syntaxe Java, j'ai pu copier et coller le code Java dans un fichier Groovy sans le modifier. Si j'avais disposé d'un système de construction complet pour compiler à la fois du code Groovy et du code Java, j'aurais même pu le laisser dans un fichier Java. Mais en lui donnant l'extension .groovy, je peux exécuter tout le code de Gwitter sans le compiler. Encore une fois, je tire parti de l'intégration sans faille entre Java et Groovy. Vous pouvez utiliser n'importe quelle solution Java dans une application Groovy sans changement.

Créez un fichier nommé StripeRenderer.groovy et mettez-y le code du Listing 16:

Listing 16. Création d'un CellRenderer pour les bandes

import java.awt.*;
import javax.swing.*;

class StripeRenderer extends DefaultListCellRenderer {
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {
        JLabel label = (JLabel) super.getListCellRendererComponent(list, value,
                index, isSelected, cellHasFocus);
       
        if(index%2 == 0) {
            label.setBackground(new Color(230,230,255));
        }
        
        label.setVerticalAlignment(SwingConstants.TOP);
        return label;
    }
}

Maintenant que la classe StripeRenderer est en place, la dernière chose à faire est de permettre à la JList d'en bénéficier. Modifiez resultsPanel comme le montre le Listing 17:

Listing 17. Ajout du CellRenderer personnalisé à la pour les bandes

def resultsPanel = {
  swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
    //resultsList = list()
    resultsList = 
       list(fixedCellWidth: 380, fixedCellHeight: 75, cellRenderer:new StripeRenderer())
  }
}

Entrez groovy Gwitter en ligne de commande une dernière fois. Chercher thirstyhead devrait vous renvoyer des résultats similaires à ceux de la Figure 5:

Figure 5. Résultats avec les zébrures

Résultats avec les zébrures

Je pourrais passer beaucoup plus de temps à polir l'interface de Gwitter, mais j'espère que vous êtes impressionnés par ce que 50 lignes de code Swing (sans compter les classes de support, bien sûr) ont accompli.

Conclusion

Comme vous l'avez appris dans cet article, Groovy ne réduit pas la complexité intrinsèque de Swing, mais il diminue spectaculairement sa complexité syntaxique. Cela veut libère pour combattre d'autres batailles, plus importantes.

Si j'ai piqué votre curiosité à propos de Groovy et Swing avec cet article, vous devriez vous intéresser au projet Griffon (voir les Ressources). Il offre le même outillage et utilise le même principe de convention avant configuration que Grails, mais il est bâti sur les fondements que sont SwingBuilder et Groovy plutôt que sur Spring MVC et Hibernate. Le projet en est encore à ses débuts - au moment de l'écriture de cet article, la version la plus récente est la 0.2 (ndT: au 01/05/2010 c'est la 0.3) - mais il a été assez robuste pour gagner la Scripting Bowl pour Groovy à JavaOne 2009. Et un des projets d'exemple qu'il fournit est Greet: un client Twitter complet, implémenté en Groovy.

La prochaine fois, vous ajouterez de nouvelles fonctions à Gwitter. Vous apprendrez comment gérer les bases de l'authentification HTTP, tirerez profit d'un cousin de XmlSlurper appelé ConfigSlurper, utiliserez Gwitter pour requêter et analyser les fils Twitter de vos amis et ajouterez des onglets. Dans la troisième partie, vous ajouterez la touche finale à Gwitter en lui ajoutant la capacité d'émettre des POST HTTP de manière à envoyer vos propres tweets. En attendant, j'espère que vous trouverez beaucoup d'utilisations pratiques de Groovy.