8. La POO graphique▲
Très utilisées dans les systèmes d'exploitation et dans les applications, les interfaces graphiques sont programmables en Python.
Parmi les différentes bibliothèques graphiques utilisables dans Python (GTK+, wxPython, Qt…), la bibliothèque tkinter, issue du langage tcl/Tk est installée de base dans toutes les distributions Python. tkinter facilite la construction d'interfaces graphiques simples.
Après avoir importé la bibliothèque, la démarche consiste à créer, configurer et positionner les éléments graphiques (widgets) utilisés, à coder les fonctions/méthodes associées aux widgets, puis d'entrer dans une boucle chargée de récupérer et traiter les différents événements pouvant se produire au niveau de l'interface graphique : interactions de l'utilisateur, besoins de mises à jour graphiques, etc.
8-1. Programmes pilotés par des événements▲
En programmation graphique objet, on remplace le déroulement séquentiel du script par une boucle d'événements (Fig. 8.1)
8-2. La bibliothèque tkinter▲
8-2-1. Présentation▲
C'est une bibliothèque assez simple qui provient de l'extension graphique, Tk, du langage Tcl(27). Cette extension a largement essaimé hors de Tcl/Tk et on peut l'utiliser en Perl, Python, Ruby, etc. Dans le cas de Python, l'extension a été renommée tkinter.
Parallèlement à Tk, des extensions ont été développées dont certaines sont utilisées en Python. Par exemple le module standard Tix met une quarantaine de widgets à la disposition du développeur.
De son côté, le langage Tcl/Tk a largement évolué. La version 8.5 actuelle offre une bibliothèque appelée Ttk qui permet d'habiller les widgets avec différents thèmes ou styles. Ce module est également disponible à partir de Python 3.1.1.
8-2-1-a. Un exemple tkinter simple (Fig. 8.2)▲
import
tkinter
# création d'un widget affichant un simple message textuel
widget =
tkinter.Label
(
None
, text=
'Bonjour monde graphique !'
)
widget.pack
(
) # positionnement du label
widget.mainloop
(
) # lancement de la boucle d'événements
8-2-2. Les widgets de tkinter▲
On appelle widget (mot valise, contraction de window et gadget) les composants graphiques de base d'une bibliothèque.
Liste des principaux widgets de tkinter :
- Tk fenêtre de plus haut niveau ;
- Frame contenant pour organiser d'autres widgets ;
- Label zone de message ;
- Button bouton d'action avec texte ou image ;
- Message zone d'affichage multiligne ;
- Entry zone de saisie ;
- Checkbutton bouton à deux états ;
- Radiobutton bouton à deux états exclusifs par groupe de boutons ;
- Scale glissière à plusieurs positions ;
- PhotoImage sert à placer des images (GIF et PPM/PGM) sur des widgets ;
- BitmapImage sert à placer des bitmaps (X11 bitmap data) sur des widgets ;
- Menu menu déroulant associé à un Menubutton ;
- Menubutton bouton ouvrant un menu d'options ;
- Scrollbar ascenseur ;
- Listbox liste à sélection pour des textes ;
- Text édition de texte simple ou multiligne ;
- Canvas zone de dessins graphiques ou de photos ;
- OptionMenu liste déroulante ;
- ScrolledText widget Text avec ascenseur ;
- PanedWindow interface à onglets ;
- LabelFrame contenant pour organiser d'autres widgets, avec un cadre et un titre ;
- Spinbox un widget de sélection multiple.
8-2-3. Le positionnement des widgets▲
tkinter possède trois gestionnaires de positionnement :
- le packer : dimensionne et place chaque widget dans un widget conteneur selon l'espace requis par chacun d'eux ;
- le gridder : dimensionne et positionne chaque widget dans les cellules d'un tableau d'un widget conteneur ;
- le placer : dimensionne et place chaque widget dans un widget conteneur selon l'espace explicitement demandé. C'est un placement absolu (usage peu fréquent).
8-3. Trois exemples▲
8-3-1. Une calculette▲
Cette application(28) implémente une calculette simple, mais complète (Fig. 8.3).
from
tkinter import
*
from
math import
*
def
evaluer
(
event) :
chaine.configure
(
text =
'=> '
+
str(
eval(
entree.get
(
))))
# Programme principal ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fenetre =
Tk
(
)
entree =
Entry
(
fenetre)
entree.bind
(
'<Return>'
, evaluer)
chaine =
Label
(
fenetre)
entree.pack
(
)
chaine.pack
(
)
fenetre.mainloop
(
)
La fonction evaluer() est exécutée chaque fois que l'utilisateur appuie sur la touche après avoir entré une expression mathématique. Cette fonction utilise la méthode configure() du widget chaine pour modifier son attribut text. Pour cela, evaluer() exploite les caractéristiques de la fonction intégrée eval() qui analyse et évalue son argument, une chaîne d'expression mathématique. Par exemple :
>>>
eval(
'(25 + 8) / 3'
)
11.0
Le programme principal se compose de l'instanciation d'une fenêtre Tk() contenant un widget chaine de type Label() et un widget entree de type Entry(). Nous associons l'événement <Return> au widget entree de façon qu'un appui de la touche déclenche la fonction evaluer(). Enfin, après avoir positionné les deux widget à l'aide de la méthode pack(), on active la boucle d'événement mainloop().
8-3-2. tkPhone, un exemple sans menu▲
On se propose de créer un script de gestion d'un carnet téléphonique. L'aspect de l'application est illustré Fig. 8.4.
8-3-2-a. Notion de callback▲
Nous avons vu que la programmation d'interface graphique passe par une boucle principale chargée de traiter les différents événements qui se produisent.
Cette boucle est généralement gérée directement par la bibliothèque d'interface graphique utilisée, il faut donc pouvoir spécifier à cette bibliothèque quelles fonctions doivent être appelées dans quels cas. Ces fonctions sont nommées des callbacks — ou rappels —, car elles sont appelées directement par la bibliothèque d'interface graphique lorsque des événements spécifiques se produisent.
8-3-2-b. Conception graphique▲
La conception graphique va nous aider à choisir les bons widgets.
En premier lieu, il est prudent de commencer par une conception manuelle ! En effet rien ne vaut un papier, un crayon et une gomme pour se faire une idée de l'aspect que l'on veut obtenir.
Dans notre cas on peut concevoir trois zones :
- Une zone supérieure, dédiée à l'affichage ;
- Une zone médiane est une liste alphabétique ordonnée ;
- Une zone inférieure est formée de boutons de gestion de la liste ci-dessus.
Chacune de ces zones est codée par une instance de Frame(), positionnée l'une sous l'autre grâce au packer, et toutes trois incluses dans une instance de Tk() (cf. conception Fig. 8.4).
8-3-2-c. Le code de l'interface graphique▲
Méthodologie : on se propose de séparer le codage de l'interface graphique de celui des callbacks. Pour cela on propose d'utiliser l'héritage entre une classe parente chargée de gérer l'aspect graphique et une classe enfant chargée de gérer l'aspect fonctionnel de l'application contenu dans les callbacks. Comme nous l'avons vu précédemment (cf. §7.6Notion de conception orientée objet), c'est un cas de polymorphisme de dérivation.
Voici donc dans un premier temps le code de l'interface graphique. L'initialisateur crée l'attribut phoneList, une liste qu'il remplit avec le contenu du fichier contenant les données (si le fichier n'existe pas, il est créé), crée la fenêtre de base root et appelle la méthode makeWidgets().
Cette méthode suit la conception graphique exposée ci-dessus et remplit chacun des trois frames.
Les callbacks sont vides (instruction pass minimale).
Comme tout bon module, un autotest permet de vérifier le bon fonctionnement (ici le bon aspect) de l'interface :
# -*- coding : utf-8 -*-
# Bob Cordeau
# tkPhone_IHM.py
import
tkinter as
tk
from
os.path import
isfile
# class
class
Allo_IHM :
"""IHM de l'application 'répertoire téléphonique'."""
def
__init__
(
self, fic) :
"""Initialisateur/lanceur de la fenêtre de base"""
self.phoneList =
[]
self.fic =
fic
if
isfile
(
self.fic) :
with
open(
self.fic) as
f :
for
line in
f :
self.phoneList.append
(
line[:-
1
].split
(
'*'
))
else
:
with
open(
self.fic, "w"
, encoding=
"utf8"
) :
pass
self.phoneList.sort
(
)
self.root =
tk.Tk
(
)
self.root.title
(
"Allo !"
)
self.root.config
(
relief=
tk.RAISED, bd=
3
)
self.makeWidgets
(
)
self.nameEnt.focus
(
)
self.root.mainloop
(
)
def
makeWidgets
(
self) :
"""Configure et positionne les widgets"""
# frame "saisie" (en haut avec bouton d'effacement)
frameH =
tk.Frame
(
self.root, relief=
tk.GROOVE, bd=
2
)
frameH.pack
(
)
tk.Label
(
frameH, text=
"Nom :"
).grid
(
row=
0
, column=
0
, sticky=
tk.W)
self.nameEnt =
tk.Entry
(
frameH)
self.nameEnt.grid
(
row=
0
, column=
1
, sticky=
tk.W, padx=
5
, pady=
10
)
tk.Label
(
frameH, text=
"Tel :"
).grid
(
row=
1
, column=
0
, sticky=
tk.W)
self.phoneEnt =
tk.Entry
(
frameH)
self.phoneEnt.grid
(
row=
1
, column=
1
, sticky=
tk.W, padx=
5
, pady=
2
)
b =
tk.Button
(
frameH, text=
"Effacer"
, command=
self.clear)
b.grid
(
row=
2
, column=
0
, columnspan=
2
, pady=
3
)
# frame "liste" (au milieu)
frameM =
tk.Frame
(
self.root)
frameM.pack
(
)
self.scroll =
tk.Scrollbar
(
frameM)
self.select =
tk.Listbox
(
frameM, yscrollcommand=
self.scroll.set, height=
6
)
self.scroll.config
(
command=
self.select.yview)
self.scroll.pack
(
side=
tk.RIGHT, fill=
tk.Y, pady=
5
)
self.select.pack
(
side=
tk.LEFT, fill=
tk.BOTH, expand=
1
, pady=
5
)
## remplissage de la Listbox
for
i in
self.phoneList :
self.select.insert
(
tk.END, i[0
])
self.select.bind
(
"<Double-Button-1>"
, lambda
event : self.afficher
(
event))
# frame "boutons" (en bas)
frameB =
tk.Frame
(
self.root, relief=
tk.GROOVE, bd=
3
)
frameB.pack
(
pady=
3
)
b1 =
tk.Button
(
frameB, text=
"Ajouter"
, command=
self.ajouter)
b2 =
tk.Button
(
frameB, text=
"Supprimer"
, command=
self.supprimer)
b3 =
tk.Button
(
frameB, text=
"Afficher"
, command=
self.afficher)
b1.pack
(
side=
tk.LEFT, pady=
2
)
b2.pack
(
side=
tk.LEFT, pady=
2
)
b3.pack
(
side=
tk.LEFT, pady=
2
)
def
ajouter
(
self) :
pass
def
supprimer
(
self) :
pass
def
afficher
(
self, event=
None
) :
pass
def
clear
(
self) :
pass
# autotest -------------------------------------------------------------------
if
__name__
==
'__main__'
:
# instancie l'IHM, callbacks inactifs
app =
Allo_IHM
(
"./phones.txt"
)
8-3-2-d. Le code de l'application▲
On va maintenant utiliser le module de la façon suivante :
- on importe la classe Allo_IHM depuis le module précédent ;
- on crée une classe Allo qui en dérive ;
- son initialisateur appelle l'initialisateur de la classe de base pour hériter de toutes ses caractéristiques ;
- il reste à surcharger les callbacks.
Enfin, le script instancie l'application :
# -*- coding : utf-8 -*-
# Bob Cordeau
# tkPhone.py
# import ----------------------------------------------------------------------
from
tkPhone_IHM import
Allo_IHM
# définition de classe --------------------------------------------------------
class
Allo
(
Allo_IHM) :
"""Répertoire téléphonique."""
def
__init__
(
self, fic=
'phones.txt'
) :
"""Constructeur de l'IHM."""
super(
).__init__
(
fic)
def
ajouter
(
self) :
# maj de la liste
ajout =
[""
,""
]
ajout[0
] =
self.nameEnt.get
(
)
ajout[1
] =
self.phoneEnt.get
(
)
if
(
ajout[0
] ==
""
) or
(
ajout[1
] ==
""
) :
return
self.phoneList.append
(
ajout)
self.phoneList.sort
(
)
# maj de la listebox
self.select.delete
(
0
, 'end'
)
for
i in
self.phoneList :
self.select.insert
(
'end'
, i[0
])
self.clear
(
)
self.nameEnt.focus
(
)
# maj du fichier
f =
open(
self.fic, "a"
)
f.write
(
"
%s
*
%s\n
"
%
(
ajout[0
], ajout[1
]))
f.close
(
)
def
supprimer
(
self) :
self.clear
(
)
# maj de la liste
retrait =
[""
,""
]
retrait[0
], retrait[1
] =
self.phoneList[int(
self.select.curselection
(
)[0
])]
self.phoneList.remove
(
retrait)
# maj de la listebox
self.select.delete
(
0
, 'end'
)
for
i in
self.phoneList :
self.select.insert
(
'end'
, i[0
])
# maj du fichier
f =
open(
self.fic, "w"
)
for
i in
self.phoneList :
f.write
(
"
%s
*
%s\n
"
%
(
i[0
], i[1
]))
f.close
(
)
def
afficher
(
self, event=
None
) :
self.clear
(
)
name, phone =
self.phoneList[int(
self.select.curselection
(
)[0
])]
self.nameEnt.insert
(
0
, name)
self.phoneEnt.insert
(
0
, phone)
def
clear
(
self) :
self.nameEnt.delete
(
0
, 'end'
)
self.phoneEnt.delete
(
0
, 'end'
)
# programme principal ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
app =
Allo
(
) # instancie l'application
8-3-3. IDLE, un exemple avec menu▲
Généralement les distributions Python comportent l'application IDLE, l'interpréteur/éditeur écrit en Python par Guido van Rossum(29). Cette application se présente sous l'aspect d'une interface graphique complète (Fig. 8.5), avec menu.
C'est un source Python dont le code est librement disponible(30) et constitue à lui seul un cours complet à tkinter(31).