Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Dans cet article de la série Interface graphique avec SWT sous Jython, nous allons voir comment mettre en oeuvre une interface graphique SWT pour un programme Python autonome.

Ici, il s'agira non seulement d'utiliser Python pour créer l'interface utilisateur, mais aussi pour répondre aux événements générés par celle-ci.

Le programme qui nous servira de support ici est modérément ambitieux. Puisqu'il s'agit simplement de compter le nombre de clics sur un bouton... Néanmoins, ce sera suffisant pour introduire les concepts clés de la gestion des événements.

L'interface utilisateur

Notre interface sera composé de deux widgets: le bouton, et un label qui affichera le nombre de clics. Sans plus attendre, voici le code complet de ce programme. En effet, celui-ci est très proche de ce que vous avez pu voir dans la partie 1:

#!/usr/bin/env jython
# vim : fileencoding=utf-8 :
from org.eclipse.swt import SWT
from org.eclipse.swt.widgets import Display
from org.eclipse.swt.widgets import Shell
from org.eclipse.swt.widgets import Label
from org.eclipse.swt.widgets import Button
from org.eclipse.swt.layout import FillLayout
 
def MyButtonAction(event):
    """L'action à effectuer quand on clique sur le bouton
    """
    global my_counter
    try:
	my_counter += 1
    except NameError:
	my_counter = 1
 
    label.text = "%d" % my_counter
 
#
# Création de l'interface graphique
#
display = Display()
 
shell = Shell(display, text = "Button GUI")
shell.setLayout(FillLayout(SWT.VERTICAL))
# ^^^^^^^^^^ Piège: ci-dessus, shell.layout = ... ne fonctionnerait pas!
 
button = Button(shell, SWT.CENTER,
		text = "Click me!",
		widgetSelected = MyButtonAction) # ``cable'' l'action au bouton
label = Label(shell, SWT.CENTER,
		text = "0")
 
shell.pack()
shell.open()
 
#
# Boucle événementielle
#
while not shell.isDisposed():
    if not display.readAndDispatch():
	display.sleep()
 
display.dispose()

Vous pouvez soit taper ce code dans l'interpréteur interactif de Jython, soit – ce que je vous conseille car le code commence à être long – le taper dans un fichier, et exécuter ce programme à l'aide de la commande:

sh$ jython NomDuFichier.py

setLayout

Attention à la ligne:

shell.setLayout(FillLayout(SWT.VERTICAL))

Si vous avez suivi l'article précédent, vous devez vous dire que j'aurais pu écrire plus simplement:

shell.layout = FillLayout(SWT.VERTICAL)

Hélas! Cela conduit à l'erreur d'exécution suivante:

Traceback (most recent call last):
  File "./MiniGUI.py", line 27, in <module>
    shell.layout = FillLayout(SWT.VERTICAL)
TypeError: readonly attribute

Pourquoi? La propriété serait-elle en lecture seule? Pas exactement. Le problème vient de ce que la classe Shell définit aussi une méthode appelée layout. Or Jython croit par erreur que l'on tente de redéfinir la méthode layout, ce qui n'est pas possible. C'est pourquoi, face à cette confusion, j'ai du me rabattre sur l'appel explicite à setLayout.

Bon, d'accord, ce n'est ni du Python de haute voltige, ni du SWT de haut niveau. Mais au moins, même sans grande expérience préalable, le code est facile à suivre. Malgré tout, deux points méritent d'être détaillés:

  1. La manière dont les propriétés des widgets sont initialisées;
  2. et la manière dont on associe une action à un widget.

Et c'est ce que nous allons expliquer maintenant...

Initialisation des propriétés

Notez la syntaxe suivante dans le code d'initialisation des widgets:

label = Label(shell, SWT.CENTER,
		text = "0")

Le point important est le dernier argument. En Python, c'est la syntaxe pour initialiser une propriété lors de la création d'un objet. En fait elle est équivalente à ceci:

label = Label(shell, SWT.CENTER)
label.text = "0"

Et en Java?

Et en Java, le code serait:

Label label = new Label(shell, SWT.CENTER);
 label.setText("0");

Actions

Notez aussi la manière dont les actions sont associées à un widget:

button = Button(shell, SWT.CENTER,
		text="Click me!",
		widgetSelected = MyButtonAction)

Ici, c'est via la propriété widgetSelected du bouton que l'on connecte le bouton à une fonction. Ceci dit, vous pouvez chercher autant que vous le voulez cette propriété dans la documentation de SWT, vous ne la trouverez pas! En effet, cette propriété a été synthétisée par Jython.

En fait, Jython a même synthétisé une telle propriété pour chaque méthode de chaque gestionnaire d'événement supporté par le widget.

Pour que ce soit plus clair, comme le widget Button supporte le gestionnaire d'événement SelectionListener, et que cette interface possède les méthodes widgetSelected et widgetDefaultSelected, Jython a ajouté au widget deux propriétés virtuelles de même nom. Pour pouvoir lui associer l'action à effectuer en réponse à chacun de ces événements.

Autrement dit encore, écrire:

button.widgetSelected = MyButtonAction

Est en réalité un raccourci pour:

class MySelectionListener(SelectionListener):
    def widgetSelected(self, event):
        MyButtonAction(event)
    def widgetDefaultSelected(self, event):
        pass
 
button.addSelectionListener(MySelectionListener())

D'accord, il s'avère parfois indispensable de créer une classe mettant en oeuvre l'interface Listener. En particulier si pour une raison ou une autre, le même gestionnaire doit répondre à plusieurs événements. Néanmoins, la plupart du temps, la notation raccourcie permise par Jython est suffisante – et tellement plus lisible!

Associer plusieurs action

S'il est facile à comprendre, le code de notre programme initial est sujet à critiques. En particulier par l'usage d'une variable globale pour la gestion du compteur:

def MyButtonAction(event):
    """L'action à effectuer quand on clique sur le bouton
    """
    global my_counter    # /
    try:                 # |
        my_counter += 1  # | Pas du meilleur style, tout ça...
    except NameError:    # |
        my_counter = 1   # \
 
    label.text = "%d" % my_counter

Ne soyons pas plus royaliste que le roi: Sur un programme court et simple comme ici, ça n'est pas un réel problème. Par contre, ça n'est ni élégant, ni adapté dès que l'on voudra avoir une logique plus complexe.

Ainsi, dans la version suivante, j'ai encapsulé la gestion des événements dans la classe Python Counter. Et j'ai aussi ajouté un second bouton – pour décrémenter ce compteur.

#!/usr/bin/env jython
# vim : fileencoding=utf-8 :
from org.eclipse.swt import SWT
from org.eclipse.swt.widgets import Display
from org.eclipse.swt.widgets import Shell
from org.eclipse.swt.widgets import Label
from org.eclipse.swt.widgets import Button
from org.eclipse.swt.layout import FillLayout
 
class Counter:
    """Encapsule le compteur et les traitements associés dans une classe.
    """
    def __init__(self, label):
        self.value = 0
        self.label = label
 
    def doPlus(self, event):
        self.value += 1
        self.label.text = "%d" % self.value
 
    def doMinus(self, event):
        self.value -= 1
        self.label.text = "%d" % self.value
 
#
# Création de l'interface graphique
#
display = Display()
 
shell = Shell(display, text = "Button GUI")
shell.setLayout(FillLayout(SWT.VERTICAL))
# ^^^^^^^^^^ Piège: ci-dessus, shell.layout = ... ne fonctionnerait pas!
 
label = Label(shell, SWT.CENTER,
		text = "0")
counter = Counter(label)
 
plus = Button(shell, SWT.CENTER,
		text = "Plus",
		widgetSelected = counter.doPlus) # ``cable'' l'action au bouton
minus = Button(shell, SWT.CENTER,
		text = "Minus",
		widgetSelected = counter.doMinus) # ``cable'' l'action au bouton
 
shell.pack()
shell.open()
 
#
# Boucle événementielle
#
while not shell.isDisposed():
    if not display.readAndDispatch():
	display.sleep()
 
display.dispose()

J'attire votre attention dans le code ci-dessus sur la manière dont est maintenant passée la fonction chargée de répondre à un clic sur un bouton (en anglais, on parle de callback – c'est à dire de fonction appelée en retour):

plus = Button(shell, SWT.CENTER,
		text = "Plus",
		widgetSelected = counter.doPlus) # ``cable l'action au bouton
minus = Button(shell, SWT.CENTER,
		text = "Minus",
		widgetSelected = counter.doMinus) # ``cable l'action au bouton

Ici, puisque nous passons non plus une fonction mais une méthode, Python nous permet la notation counter.doPlus qui permet non seulement d'indiquer la méthode à exécuter (doPlus), mais aussi l'objet sur lequel doit porter cette méthode (le receveur counter). Ainsi, mes deux boutons permettent d'incrémenter et décrémenter le même compteur.

Remarque:

Pour vous convaincre que la callback est bien associée à un objet Compteur vous pouvez vous amuser à rajouter un second compteur au programme:

# ...
 
#
# Un second compteur
label = Label(shell, SWT.CENTER,
                text = "0")
counter = Counter(label)
 
plus = Button(shell, SWT.CENTER,
                text = "Plus",
                widgetSelected = counter.doPlus) # ``cable'' l'action au bouton
minus = Button(shell, SWT.CENTER,
                text = "Minus",
                widgetSelected = counter.doMinus) # ``cable'' l'action au bouton
 
# ...

Oui, c'est le même code. Non, ça n'est pas le même compteur. Allez-y, vérifiez: la première paire de boutons modifiera le premier compteur. La seconde, le second compteur.

Et la suite?

En quelques lignes, nous avons réussi un programme capable non seulement de présenter une interface graphique à l'utilisateur, mais aussi de réagir à ses actions.

Par ailleurs, nous en avons aussi profité pour voir à quel point Jython simplifie l'utilisation des bibliothèques Java. Et améliore par là même l'expressivité du code et sa lisibilité. Pour la suite, nous allons maintenant nous atteler à une variante de ce qui est présenté ici: cette fois ci, la partie intelligente du programme – c'est à dire celle qui effectue des traitements en fonction des actions de l'utilisateur – sera écrite en Java, et non plus en Python. C'est ce que nous verrons dans la troisième partie de cet article.