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

Dans cet article de la série Interface graphique avec SWT sous Jython, nous allons nous attarder sur un cas d'usage typique des langages de script pour la plate-forme Java où Jython servira de glue entre des composants logiciels Java.

Ainsi, comme dans les autres articles de cette série, nous utiliserons une bibliothèque Java, SWT, pour créer une interface graphique. Mais en plus, maintenant, le code chargé du traitement effectif des données sera lui aussi écrit en Java. Python ne servant qu'à relier ces différents composants entre eux.

Un convertisseur de température

Le convertisseur de température: L'interface graphique est réalisée avec SWT, le convertisseur est un bean Java – et Python sert de glue entre tous ces composants.

A nouveau, le programme qui nous servira de support ne sera que modérément ambitieux. En effet, il s'agit ici de convertir des températures de degrés Fahrenheit en degrés Celcius.

Voici donc le code complet de ce bean chargé de la conversion:

public class TempConverter {
    public TempConverter() {
	celcius = 0;
    }
 
    public double getCelcius() {
	return celcius;
    }
 
    public void setCelcius(double celcius) {
	this.celcius = celcius;
    }
 
    public double getFahrenheit() {
	return celcius*1.8+32;
    }
 
    public void setFahrenheit(double fahrenheit) {
	celcius = (fahrenheit-32)/1.8;
    }
 
    private double celcius;
}

Une fois le code saisi, vous devrez compiler cette classe avant qu'elle ne puisse être utilisable par Jython:

sh$ javac TempConverter.java

Note:

Notez comment TempConverter utilise les conventions Java beans pour exposer les deux propriétés celcius et fahrenheit. Ce qui rend ces deux propriétés utilisables par Jython – même si l'une d'entre elle est en réalité une propriété virtuelle dont la valeur est calculée et non pas stockée dans une donnée membre:

>>> import TempConverter
>>> tc = TempConverter()
>>> tc.celcius          # Equivalent à tc.getCelcius()
0.0
>>> tc.fahrenheit       # Equivalent à tc.getFahrenheit()
32.0
>>> tc.celcius = 37.7   # Equivalent à tc.setCelcius(37.7)
>>> tc.fahrenheit      
99.86000000000001
>>> tc.fahrenheit = 0   # Equivalent à tc.setFahrenheit(0)
>>> tc.celcius
-17.77777777777778

L'interface graphique

Quand à l'interface graphique SWT celle-ci sera composée d'un certain nombre de widgets (deux Label et deux Text). Python nous permet une notation relativement compacte pour créer ces widgets:

widgets = {} # Dictionnaire des widgets
for n,i in enumerate(["celcius", "fahrenheit"]):
    Label(self.shell, SWT.RIGHT, 
		    text = i,
		    location = (5, 5+30*n),
		    size = (100, 20))
 
    widgets[i] = Text(self.shell, SWT.LEFT,
			    location = (110, 5+30*n),
			    size = (100, 20))

Note:

Les méthodes setLocation et setSize acceptant un objet en argument, Jython autorise à initialiser les propriétés virtuelles correspondantes à partir d'un tupple (en simplifiant, un groupe de valeurs). Et Jython se charge d'instancier automatiquement l'objet passé en argument. Ainsi, les trois fragments de code suivant sont équivalents:

widgets[i] = Text(self.shell, SWT.LEFT,
		    location = (110, 5+30*n),
		    size = (100, 20))
widgets[i] = Text(self.shell, SWT.LEFT)
widgets[i].location = (110, 5+30*n)
widgets[i].size = (100, 20)
widgets[i] = Text(self.shell, SWT.LEFT)
widgets[i].setLocation(Point(110, 5+30*n))
widgets[i].setSize(Point(100, 20))

Si la dernière version est plus proche du code que l'on écrirait en Java, en Jython, on privilégiera plutôt la première. C'est à dire, à nouveau la version à la fois la plus courte et la plus lisible...

Histoire de structurer un minimum notre programme, nous allons aussi encapsuler la création de ces widgets dans une classe:

class MyGUI:
    """Classe assurant la gestion de l'interface graphique.
    """
    def __init__(self):
	"""Création des differents widgets.
	"""
	self.converter = TempConverter()
	self.modifying = False
 
	self.display = Display()
	self.shell = Shell(self.display, text = "Temp converter")
 
	self.widgets = {} 
	for n,i in enumerate(["celcius", "fahrenheit"]):
	    Label(self.shell, SWT.RIGHT, 
			    text = i,
			    location = (5, 5+30*n),
			    size = (100, 20))
 
	    self.widgets[i] = Text(self.shell, SWT.LEFT,
			    location = (110, 5+30*n),
			    size = (100, 20))
 
	self.shell.pack()
	self.shell.open()
 
    def run(self):
	"""Boucle évènementielle
	"""
	while not self.shell.isDisposed():
	    if not self.display.readAndDispatch():
		self.display.sleep()
 
	self.display.dispose()
 
if __name__ == "__main__":
    """Programme principal
    """
    myGUI = MyGUI()
    myGUI.run()

A ce stade, le programme instancie un bean de la classe TempConverter et crée l'interface graphique SWT. Mais pour l'instant les deux ne sont pas connectés ensemble. Autrement dit, vous pouvez taper ce que vous voulez dans les champs, aucune conversion ne prend place. Et c'est à cela que nous allons remédier maintenant.

Gestion des événements

Pour relier l'interface graphique au bean TempConverter, il va falloir ajouter un gestionnaire d'événements capable de réagir lorsque l'utilisateur saisit une température.

Vous savez que Jython facilite la définition de ce genre de gestionnaire. Ainsi, il suffit de modifier la création des widgets Text:

	    self.widgets[i] = Text(self.shell, SWT.LEFT,
			    location = (110, 5+30*n),
			    size = (100, 20),
			    modifyText = self.doUpdateText)

Ah, oui, bien sûr il faut penser à définir la méthode doUpdateText!

class MyGUI
    #...
    def doUpdateText(self, event):
	"""Gestionnaire d'événements appelé après modification d'un
	des champs de texte.
	"""
	if self.modifying != True: # Protège d'un appel récursif
	    try:
		self.modifying = True
 
		# Pour simplifier les notations lors de l'accès aux widgets et au bean de conversion
		widgets = self.widgets
		converter = self.converter
 
		fv = float(event.widget.text or 0) # Le texte ou 0 si le texte est vide
		widgets["celcius"].foreground = self.display.getSystemColor(SWT.COLOR_BLACK)
		widgets["fahrenheit"].foreground = self.display.getSystemColor(SWT.COLOR_BLACK)
		if event.widget is widgets["celcius"]:
		    converter.celcius = fv
		    widgets["fahrenheit"].text = "%0.2f" % converter.fahrenheit
		else:
		    converter.fahrenheit = fv
		    widgets["celcius"].text = "%0.2f" % converter.celcius
	    except ValueError:
		widgets["celcius"].foreground = self.display.getSystemColor(SWT.COLOR_RED)
		widgets["fahrenheit"].foreground = self.display.getSystemColor(SWT.COLOR_RED)
	    finally:
		self.modifying = False

Cette méthode est un peu impressionnante au premier abord, mais en l'examinant de plus près, vous remarquerez qu'elle est plus longue que complexe. Je ne vais pas détailler chaque ligne de code, mais, dans le principe, elle récupère le texte tapé dans le widget à l'origine de l'événement. Puis, fait la conversion celcius/fahrenheit dans un sens ou l'autre en fonction du champ dans lequel l'utilisateur a tapé son texte.

Le point qui mérite d'être noté est la manière dont SWT et notre bean sont connectés: il s'agit essentiellement de copier des propriétés d'un objet à l'autre. Enfin, en apparence, puisque maintenant vous comprenez qu'en coulisse cela sera traduit par des appels de méthodes getPropriété et setPropriété – méthodes qui contiennent éventuellement du code actif.

Piège:

En particulier, j'attire votre attention sur les lignes de la forme:

widgets["..."].text = ... 

Ce code est traduit par un appel à la méthode setText du widget. Ce qui met en marche la machinerie de SWT pour la vérification du texte contenu dans un widget. Or nous avons nous même défini un gestionnaire d'événéments qui réagit en cas de modification du texte. Et qui plus est, c'est justement dans ce gestionnaire que nous faisons la modification en cause... Bref, cela doit évoquer dans votre esprit un appel récursif:

  1. La modification du texte
  2. entraîne l'appel à doUpdateText
  3. qui entraîne la modification du texte
  4. qui entraîne l'appel à doUpdateText
  5. qui entraîne la modification du texte
  6. qui entraîne l'appel à doUpdateText
  7. ...

Tout cela pour expliquer que le rôle de la variable modifying est d'éviter cette récursion infinie: si on est déjà en train de modifier le texte, on ignore toute autre modification.

Pour terminer

Au final, le code complet est celui que vous trouverez ci-dessous:

# vim: set 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 Text
from org.eclipse.swt.widgets import Label
 
import TempConverter
 
class MyGUI:
    """Classe assurant la gestion de l'interface graphique.
    """
    def __init__(self):
	"""Création des differents widgets.
	"""
	self.converter = TempConverter()
	self.modifying = False
 
	self.display = Display()
	self.shell = Shell(self.display, text = "Temp converter")
 
	self.widgets = {} 
	for n,i in enumerate(["celcius", "fahrenheit"]):
	    Label(self.shell, SWT.RIGHT, 
			    text = i,
			    location = (5, 5+30*n),
			    size = (100, 20))
 
	    self.widgets[i] = Text(self.shell, SWT.LEFT,
			    location = (110, 5+30*n),
			    size = (100, 20),
			    modifyText = self.doUpdateText)
 
	self.widgets["celcius"].text = "0.00"	
 
	self.shell.pack()
	self.shell.open()
 
    def doUpdateText(self, event):
	"""Gestionnaire d'événement appelé après modification d"un
	des champs de texte.
	"""
	if self.modifying != True: # Protège d'un appel récursif
	    try:
		self.modifying = True
 
		# Pour simplifier les notations lors de l'accès aux widgets et au bean de conversion
		widgets = self.widgets
		converter = self.converter
 
		fv = float(event.widget.text or 0) # Le texte ou 0 si le texte est vide
		widgets["celcius"].foreground = self.display.getSystemColor(SWT.COLOR_BLACK)
		widgets["fahrenheit"].foreground = self.display.getSystemColor(SWT.COLOR_BLACK)
		if event.widget is widgets["celcius"]:
		    converter.celcius = fv
		    widgets["fahrenheit"].text = "%0.2f" % converter.fahrenheit
		else:
		    converter.fahrenheit = fv
		    widgets["celcius"].text = "%0.2f" % converter.celcius
	    except ValueError:
		widgets["celcius"].foreground = self.display.getSystemColor(SWT.COLOR_RED)
		widgets["fahrenheit"].foreground = self.display.getSystemColor(SWT.COLOR_RED)
	    finally:
		self.modifying = False
 
    def run(self):
	"""Boucle évènementielle
	"""
	while not self.shell.isDisposed():
	    if not self.display.readAndDispatch():
		self.display.sleep()
 
	self.display.dispose()
 
if __name__ == "__main__":
    """Programme principal
    """
    myGUI = MyGUI()
    myGUI.run()

Peut-être que le code peut vous sembler long – mais si c'est le cas, demandez-vous ce que pourrait être le code Java équivalent. A votre avis, lequel des deux serait le plus long? Et surtout le plus lisible? Or c'est sans doute là la force d'utiliser des langages de script avec la plate-forme Java: faciliter l'écriture, la lecture et la modification du code. Et, quand il s'agit d'intégrer plusieurs composants logiciels, la simplicité et la souplesse sont souvent des atout majeurs. Ce qui fait de Python un langage de choix...

Enfin, si vous avez suivi les autres articles de cette série, vous devriez avoir en main toutes les cartes pour créer vos propres interfaces graphiques SWT avec Jython. Que ce soit en codant la logique en Python, ou encore un utilisant du code Java pour la partie traitement. Donc, bon amusement!