16. Et pour quelques widgets de plus ...▲
Pour vous aider à ébaucher vos propres projets personnels, nous vous présentons ici quelques nouveaux widgets ainsi que des utilisations avancées de ceux que vous connaissez déjà (sans prétendre toutefois édifier ainsi une documentation de référence sur tkinter : vous trouverez plutôt celle-ci dans les ouvrages ou les sites web spécialisés). Mais attention : au-delà de leur visée documentaire, les pages qui suivent sont également destinées à vous apprendre par l'exemple comment s'articule une application construite à l'aide de classes et d'objets. Vous y découvrirez d'ailleurs au passage quelques techniques Python qui n'ont pas encore été abordées auparavant, telles par exemple les expressions lambda ou le paramétrage implicite des fonctions.
16-A. Les boutons radio▲
Les widgets « boutons radio » permettent de proposer à l'utilisateur un ensemble de choix mutuellement exclusifs. On les appelle ainsi par analogie avec les boutons de sélection que l'on trouvait jadis sur les postes de radio. Ces boutons étaient conçus de telle manière qu'un seul à la fois pouvait être enfoncé : tous les autres ressortaient automatiquement.
from
tkinter import
*
class
RadioDemo
(
Frame):
"""Démo : utilisation de widgets 'boutons radio'"""
def
__init__
(
self, boss =
None
):
"""Création d'un champ d'entrée avec 4 boutons radio"""
Frame.__init__
(
self)
self.pack
(
)
# Champ d'entrée contenant un petit texte :
self.texte =
Entry
(
self, width =
30
, font =
"Arial 14"
)
self.texte.insert
(
END, "La programmation, c'est génial"
)
self.texte.pack
(
padx =
8
, pady =
8
)
# Nom français et nom technique des quatre styles de police :
stylePoliceFr =
["Normal"
, "Gras"
, "Italique"
, "Gras/Italique"
]
stylePoliceTk =
["normal"
, "bold"
, "italic"
, "bold italic"
]
# Le style actuel est mémorisé dans un 'objet-variable' tkinter ;
self.choixPolice =
StringVar
(
)
self.choixPolice.set(
stylePoliceTk[0
])
# Création des quatre 'boutons radio' :
for
n in
range(
4
):
bout =
Radiobutton
(
self,
text =
stylePoliceFr[n],
variable =
self.choixPolice,
value =
stylePoliceTk[n],
command =
self.changePolice)
bout.pack
(
side =
LEFT, padx =
5
)
def
changePolice
(
self):
"""Remplacement du style de la police actuelle"""
police =
"Arial 15 "
+
self.choixPolice.get
(
)
self.texte.configure
(
font =
police)
if
__name__
==
'__main__'
:
RadioDemo
(
).mainloop
(
)
16-A-1. Commentaires▲
- Ligne 3 : Cette fois encore, nous préférons construire notre petite application comme une classe dérivée de la classe Frame(), ce qui nous permettrait éventuellement de l'intégrer sans difficulté dans une application plus importante.
- Ligne 8 : En général, on applique les méthodes de positionnement des widgets (pack(), grid(), ou place()) après instanciation de ceux-ci, ce qui permet de choisir librement leur disposition à l'intérieur des fenêtres maîtresses. Comme nous le montrons ici, il est cependant tout à fait possible de déjà prévoir ce positionnement dans le constructeur du widget.
- Ligne 11 : Les widgets de la classe Entry disposent de plusieurs méthodes pour accéder à la chaîne de caractères affichée. La méthode get() permet de récupérer la chaîne entière. La méthode insert() permet d'insérer de nouveaux caractères à un emplacement quelconque (c'est-à-dire au début, à la fin, ou même à l'intérieur d'une chaîne préexistante éventuelle). Cette méthode s'utilise donc avec deux arguments, le premier indiquant l'emplacement de l'insertion (utilisez 0 pour insérer au début, END pour insérer à la fin, ou encore un indice numérique quelconque pour désigner un caractère dans la chaîne). La méthode delete() permet d'effacer tout ou partie de la chaîne. Elle s'utilise avec les mêmes arguments que la précédente (cf. projet « Code des couleurs », page ).
- Lignes 14-15 : Plutôt que de les instancier dans des instructions séparées, nous préférons créer nos quatre boutons à l'aide d'une boucle. Les options spécifiques à chacun d'eux sont d'abord préparées dans les deux listes stylePoliceFr et stylePoliceTk : la première contient les petits textes qui devront s'afficher en regard de chaque bouton, et la seconde les valeurs qui devront leur être associées.
- Lignes 17-18 : Comme expliqué à la page précédente, les quatre boutons forment un groupe autour d'une variable commune. Cette variable prendra la valeur associée au bouton radio que l'utilisateur décidera de choisir. Nous ne pouvons cependant pas utiliser une variable ordinaire pour remplir ce rôle, parce que les attributs internes des objets tkinter ne sont accessibles qu'au travers de méthodes spécifiques. Une fois de plus, nous utilisons donc ici un objet-variable tkinter, de type chaîne de caractères, que nous instancions à partir de la classe StringVar(), et auquel nous donnons une valeur par défaut à la ligne 18.
- Lignes 20 à 26 : Instanciation des quatre boutons radio. Chacun d'entre eux se voit attribuer une étiquette et une valeur différentes, mais tous sont associés à la même variable tkinter commune (self.choixPolice). Tous invoquent également la même méthode self.changePolice(), chaque fois que l'utilisateur effectue un clic de souris sur l'un ou l'autre.
- Lignes 28 à 31 : Le changement de police s'obtient par re-configuration de l'option font du widget Entry. Cette option attend un tuple
contenant le nom de la police, sa taille, et éventuellement son style. Si le nom de la police ne contient pas d'espaces, le tuple peut aussi être
remplacé par une chaîne de caractères. Exemples :
('Arial', 12, 'italic')
('Helvetica', 10)
('Times New Roman', 12, 'bold italic')
"Verdana 14 bold"
"President 18 italic" (Voyez également les exemples de la page ).
16-B. Utilisation de cadres pour la composition d'une fenêtre▲
Vous avez déjà abondamment utilisé la classe de widgets Frame() (« cadre », en français), notamment pour créer de nouveaux widgets complexes par dérivation.
Le petit script ci-dessous vous montre l'utilité de cette même classe pour regrouper des ensembles de widgets et les disposer d'une manière déterminée dans une fenêtre. Il vous démontre également l'utilisation de certaines options décoratives (bordures, relief, etc.).
Pour composer la fenêtre ci-contre, nous avons utilisé deux cadres f1 et f2, de manière à réaliser deux groupes de widgets bien distincts, l'un à gauche et l'autre à droite. Nous avons coloré ces deux cadres pour bien les mettre en évidence, mais ce n'est évidemment pas indispensable.
Le cadre f1 contient lui-même 6 autres cadres, qui contiennent chacun un widget de la classe Label(). Le cadre f2 contient un widget Canvas() et un widget Button(). Les couleurs et garnitures sont de simples options.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
from
tkinter import
*
fen =
Tk
(
)
fen.title
(
"Fenêtre composée à l'aide de frames"
)
fen.geometry
(
"300x300"
)
f1 =
Frame
(
fen, bg =
'#80c0c0'
)
f1.pack
(
side =
LEFT, padx =
5
)
fint =
[0
]*
6
for
(
n, col, rel, txt) in
[(
0
, 'grey50'
, RAISED, 'Relief sortant'
),
(
1
, 'grey60'
, SUNKEN, 'Relief rentrant'
),
(
2
, 'grey70'
, FLAT, 'Pas de relief'
),
(
3
, 'grey80'
, RIDGE, 'Crête'
),
(
4
, 'grey90'
, GROOVE, 'Sillon'
),
(
5
, 'grey100'
, SOLID, 'Bordure'
)]:
fint[n] =
Frame
(
f1, bd =
2
, relief =
rel)
e =
Label
(
fint[n], text =
txt, width =
15
, bg =
col)
e.pack
(
side =
LEFT, padx =
5
, pady =
5
)
fint[n].pack
(
side =
TOP, padx =
10
, pady =
5
)
f2 =
Frame
(
fen, bg =
'#d0d0b0'
, bd =
2
, relief =
GROOVE)
f2.pack
(
side =
RIGHT, padx =
5
)
can =
Canvas
(
f2, width =
80
, height =
80
, bg =
'white'
, bd =
2
, relief =
SOLID)
can.pack
(
padx =
15
, pady =
15
)
bou =
Button
(
f2, text=
'Bouton'
)
bou.pack
(
)
fen.mainloop
(
)
16-B-1. Commentaires▲
- Lignes 3 à 5 : Afin de simplifier au maximum la démonstration, nous ne programmons pas cet exemple comme une nouvelle classe. Remarquez à la ligne 5 l'utilité de la méthode geometry() pour fixer les dimensions de la fenêtre principale.
- Ligne 7 : Instanciation du cadre de gauche. La couleur de fond (une variété de bleu cyan) est déterminée par l'argument bg
(background). Cette chaîne de caractères contient en notation hexadécimale la description des trois composantes rouge, verte et bleue de la
teinte que l'on souhaite obtenir : après le caractère # signalant que ce qui suit est une valeur numérique hexadécimale, on trouve
trois groupes de deux symboles alphanumériques. Chacun de ces groupes représente un nombre compris entre 1 et
255. Ainsi, 80 correspond à 128, et c0 correspond à 192 en notation décimale. Dans notre exemple, les composantes rouge, verte et bleue de la teinte
à représenter valent donc respectivement 128, 192 et 192.
En application de cette technique descriptive, le noir serait obtenu avec #000000, le blanc avec #ffffff, le rouge pur avec #ff0000, un bleu sombre avec #000050, etc. - Ligne 8 : Puisque nous lui appliquons la méthode pack(), le cadre sera automatiquement dimensionné par son contenu. L'option side =LEFT le positionnera à gauche dans sa fenêtre maîtresse. L'option padx =5 ménagera un espace de 5 pixels à sa gauche et à sa droite (nous pouvons traduire « padx » par « espacement horizontal »).
- Ligne 10 : Dans le cadre f1 que nous venons de préparer, nous avons l'intention de regrouper 6 autres cadres similaires contenant chacun une étiquette. Le code correspondant sera plus simple et plus efficace si nous instancions ces widgets dans une liste plutôt que dans des variables indépendantes. Nous préparons donc cette liste avec 6 éléments que nous remplacerons plus loin.
- Lignes 11 à 16 : Pour construire nos 6 cadres similaires, nous allons parcourir une liste de 6 tuples contenant les caractéristiques
particulières de chaque cadre. Chacun de ces tuples est constitué de 4 éléments : un indice, une constante tkinter définissant un type de
relief, et deux chaînes de caractères décrivant respectivement la couleur et le texte de l'étiquette.
La boucle for effectue 6 itérations pour parcourir les 6 éléments de la liste. À chaque itération, le contenu d'un des tuples est affecté aux variables n, col, rel et txt (et ensuite les instructions des lignes 17 à 20 sont exécutées).
Le parcours d'une liste de tuples à l'aide d'une boucle for constitue une construction particulièrement compacte, qui permet de réaliser de nombreuses affectations avec un très petit nombre d'instructions.
- Ligne 17 : Les 6 cadres sont instanciés comme des éléments de la liste fint. Chacun d'entre eux est agrémenté d'une bordure décorative de 2 pixels de large, avec un certain effet de relief.
- Lignes 18-20 : Les étiquettes ont toutes la même taille, mais leurs textes et leurs couleurs de fond diffèrent. Du fait de l'utilisation de la méthode pack(), c'est la dimension des étiquettes qui détermine la taille des petits cadres. Ceux-ci à leur tour déterminent la taille du cadre qui les regroupe (le cadre f1). Les options padx et pady permettent de réserver un petit espace autour de chaque étiquette, et un autre autour de chaque petit cadre. L'option side =TOP positionne les 6 petits cadres les uns en dessous des autres dans le cadre conteneur f1.
- Lignes 22-23 : Préparation du cadre f2 (cadre de droite). Sa couleur sera une variété de jaune, et nous l'entourerons d'une bordure décorative ayant l'aspect d'un sillon.
- Lignes 25 à 28 : Le cadre f2 contiendra un canevas et un bouton. Notez encore une fois l'utilisation des options padx et pady pour ménager des espaces autour des widgets (considérez par exemple le cas du bouton, pour lequel cette option n'a pas été utilisée : de ce fait, il entre en contact avec la bordure du cadre qui l'entoure). Comme nous l'avons fait pour les cadres, nous avons placé une bordure autour du canevas. Sachez que d'autres widgets acceptent également ce genre de décoration : boutons, champs d'entrée, etc.
16-C. Comment déplacer des dessins à l'aide de la souris▲
Le widget canevas est l'un des points forts de la bibliothèque graphique tkinter. Il intègre en effet un grand nombre de dispositifs très efficaces pour manipuler des dessins. Le script ci-après est destiné à vous montrer quelques techniques de base. Si vous voulez en savoir plus, notamment en ce qui concerne la manipulation de dessins composés de plusieurs parties, veuillez consulter l'un ou l'autre ouvrage de référence traitant de tkinter.
Au démarrage de notre petite application, une série de dessins sont tracés au hasard dans un canevas (il s'agit en l'occurrence de simples ellipses colorées). Vous pouvez déplacer n'importe lequel de ces dessins en le « saisissant » à l'aide de votre souris.
Lorsqu'un dessin est déplacé, il passe à l'avant-plan par rapport aux autres, et sa bordure apparaît plus épaisse pendant toute la durée de sa manipulation.
Pour bien comprendre la technique utilisée, vous devez vous rappeler qu'un logiciel utilisant une interface graphique est un logiciel « piloté par les événements » (revoyez au besoin les explications de la page ). Dans cette application, nous allons mettre en place un mécanisme qui réagit aux événements : « enfoncement du bouton gauche de la souris », « déplacement de la souris, le bouton gauche restant enfoncé », « relâchement du bouton gauche ».
Ces événements sont générés par le système d'exploitation et pris en charge par l'interface tkinter. Notre travail de programmation consistera donc simplement à les associer à des gestionnaires différents (fonctions ou méthodes).
Pour développer cette petite application en suivant la « philosophie objet », nous préférerons créer une nouvelle classe Bac_a_sable, dérivée du canevas de base, et y insérer la fonctionnalité souhaitée, plutôt que de programmer cette fonctionnalité au niveau du corps principal du programme, en agissant sur un canevas ordinaire. Ainsi nous produisons du code réutilisable.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
from
tkinter import
*
from
random import
randrange
class
Bac_a_sable
(
Canvas):
"Canevas modifié pour prendre en compte quelques actions de la souris"
def
__init__
(
self, boss, width=
80
, height=
80
, bg=
"white"
):
# invocation du constructeur de la classe parente :
Canvas.__init__
(
self, boss, width=
width, height=
height, bg=
bg)
# association-liaison d'événements <souris> au présent widget :
self.bind
(
"<Button-1>"
, self.mouseDown)
self.bind
(
"<Button1-Motion>"
, self.mouseMove)
self.bind
(
"<Button1-ButtonRelease>"
, self.mouseUp)
def
mouseDown
(
self, event):
"Opération à effectuer quand le bouton gauche de la souris est enfoncé"
self.currObject =
None
# event.x et event.y contiennent les coordonnées du clic effectué :
self.x1, self.y1 =
event.x, event.y
# <find_closest> renvoie la référence du dessin le plus proche :
self.selObject =
self.find_closest
(
self.x1, self.y1)
# modification de l'épaisseur du contour du dessin :
self.itemconfig
(
self.selObject, width =
3
)
# <lift> fait passer le dessin à l'avant-plan :
self.lift
(
self.selObject)
def
mouseMove
(
self, event):
"Op. à effectuer quand la souris se déplace, bouton gauche enfoncé"
x2, y2 =
event.x, event.y
dx, dy =
x2 -
self.x1, y2 -
self.y1
if
self.selObject:
self.move
(
self.selObject, dx, dy)
self.x1, self.y1 =
x2, y2
def
mouseUp
(
self, event):
"Op. à effectuer quand le bouton gauche de la souris est relâché"
if
self.selObject:
self.itemconfig
(
self.selObject, width =
1
)
self.selObject =
None
if
__name__
==
'__main__'
: # ---- Programme de test ----
couleurs =(
'red'
,'orange'
,'yellow'
,'green'
,'cyan'
,'blue'
,'violet'
,'purple'
)
fen =
Tk
(
)
# mise en place du canevas - dessin de 15 ellipses colorés :
bac =
Bac_a_sable
(
fen, width =
400
, height =
300
, bg =
'ivory'
)
bac.pack
(
padx =
5
, pady =
3
)
# bouton de sortie :
b_fin =
Button
(
fen, text =
'Terminer'
, bg =
'royal blue'
, fg =
'white'
,
font =(
'Helvetica'
, 10
, 'bold'
), command =
fen.quit)
b_fin.pack
(
pady =
2
)
# tracé de 15 ellipses avec couleur et coordonnées aléatoires :
for
i in
range(
15
):
coul =
couleurs[randrange
(
8
)]
x1, y1 =
randrange
(
300
), randrange
(
200
)
x2, y2 =
x1 +
randrange
(
10
, 150
), y1 +
randrange
(
10
, 150
)
bac.create_oval
(
x1, y1, x2, y2, fill =
coul)
fen.mainloop
(
)
16-C-1. Commentaires▲
Le script contient essentiellement la définition d'une classe graphique dérivée de Canvas().
Cette nouvelle classe étant susceptible d'être réutilisée dans d'autres projets, nous plaçons l'ensemble du programme de test de cette classe dans la structure désormais classique : if __name__ ="__main__": Ainsi le script peut être utilisé tel quel en tant que module à importer, pour d'autres applications à votre gré..
Le constructeur de notre nouveau widget Bac_a_sable() attend la référence du widget maître (boss) comme premier paramètre, suivant la convention habituelle. Il fait appel au constructeur de la classe parente, puis met en place des mécanismes locaux.
En l'occurrence, il s'agit d'associer les trois identificateurs d'événements <Button-1>, <Button1-Motion> et <Button1-ButtonRelease> aux noms des trois méthodes choisies comme gestionnaires de cesévénements(80).
Lorsque l'utilisateur enfonce le bouton gauche de sa souris, la méthode mouseDown() est donc activée, et le système d'exploitation lui transmet en argument un objet event, dont les attributs x et y contiennent les coordonnées du curseur souris dans le canevas, déterminées au moment du clic.
Nous mémorisons directement ces coordonnées dans les variables d'instance self.x1 et self.x2, car nous en aurons besoin par ailleurs. Ensuite, nous utilisons la méthode find_closest() du widget canevas, qui nous renvoie la référence du dessin le plus proche. Cette méthode bien pratique renvoie toujours une référence, même si le clic de souris n'a pas été effectué à l'intérieur du dessin.
Le reste est facile à comprendre : la référence du dessin sélectionné est mémorisée dans une variable d'instance, et nous pouvons faire appel à d'autres méthodes du canevas de base pour modifier ses caractéristiques. En l'occurrence, nous utilisons les méthodes itemconfig() et lift() pour épaissir son contour et le faire passer à l'avant-plan.
Le « transport » du dessin est assuré par la méthode mouseMove(), invoquée à chaque fois que la souris se déplace alors que son bouton gauche est resté enfoncé. L'objet event contient cette fois encore les coordonnées du curseur souris, au terme de ce déplacement. Nous nous en servons pour calculer les différences entre ces nouvelles coordonnées et les précédentes, afin de pouvoir les transmettre à la méthode move() du widget canevas, qui effectuera le transport proprement dit.
Nous ne pouvons cependant faire appel à cette méthode que s'il existe effectivement un objet sélectionné (c'est le rôle de la variable d'instance selObject), et il nous faut veiller également à mémoriser les nouvelles coordonnées acquises.
La méthode mouseUp() termine le travail. Lorsque le dessin transporté est arrivé à destination, il reste à annuler la sélection et rendre au contour son épaisseur initiale. Ceci ne peut être envisagé que s'il existe effectivement une sélection, bien entendu.
Dans le corps du programme de test, nous instancions 15 dessins sans nous préoccuper de conserver leurs références dans des variables. Nous pouvons procéder ainsi parce que tkinter conserve lui-même une référence interne pour chacun de ces objets (cf. page ; si vous travaillez avec d'autres bibliothèques graphiques, vous devrez probablement prévoir une mémorisation de ces références).
Les dessins sont de simples ellipses colorées. Leur couleur est choisie au hasard dans une liste de 8 possibilités, l'indice de la couleur choisie étant déterminé par la fonction randrange() importée du module random.
16-D. Widgets complémentaires, widgets composites▲
Si vous explorez la volumineuse documentation que l'on peut trouver sur l'internet concernant tkinter, vous vous rendrez compte qu'il en existe différentes extensions, sous la forme de bibliothèques annexes. Ces extensions vous proposent des classes de widgets supplémentaires qui peuvent se révéler très précieuses pour le développement rapide d'applications complexes. Nous ne pouvons évidemment pas nous permettre de présenter tous ces ces widgets dans le cadre restreint de ce cours d'initiation. Si vous êtes intéressés, veuillez consulter les sites web traitant des bibliothèques Tix et Ttk (entre entres). La bibliothèque Tix propose plus de 40 widgets complémentaires. La bibliothèque Ttk est plutôt destinée à « habiller » les widgets avec différents thèmes (styles de boutons, de fenêtres, etc.). Certaines de ces bibliothèques sont écrites entièrement en Python, comme par exemple Pmw (Python Mega Widgets).
Vous pouvez cependant faire une multitude de choses sans chercher d'autres ressources que la bibliothèque standard tkinter. Vous pouvez en effet assez aisément construire vous-même de nouvelles classes de widgets composites adaptées à vos besoins. Cela peut vous demander un certain travail au départ, mais en procédant ainsi, vous contrôlez très précisément ce que contiennent vos applications, et vous garantissez la portabilité de celles-ci sur tous les systèmes qui acceptent Python, puisque tkinter fait partie de la distribution standard du langage. Lorsque vous utilisez des bibliothèques tierces, en effet, vous devez toujours vérifier la disponibilité et la compatibilité de celles-ci pour les machines cibles de vos programmes, et prévoir leur installation si nécessaire.
Les pages qui suivent vous expliquent les principes généraux à mettre en œuvre pour réaliser vous-même des classes de widgets composites, avec quelques exemples parmi les plus utiles.
16-D-1. Combo box simplifié▲
La petite application ci-après vous montre comment construire une nouvelle classe de widget de type Combo box. On appelle ainsi un widget qui associe un champ d'entrée à une boîte de liste : l'utilisateur de ce widget peut entrer dans le système, soit un des éléments de la liste proposée (en cliquant sur son nom), soit un élément non répertorié (en entrant un nouveau nom dans le champ d'entrée). Nous avons un peu simplifié le problème en laissant la liste visible en permanence, mais il est parfaitement possible de perfectionner ce widget pour qu'il prenne la forme classique d'un champ d'entrée assorti d'un petit bouton provoquant l'apparition de la liste, celle-ci étant cachée au départ. (Voir exercice 14.1, page ).
Tel que nous l'imaginons, notre widget combo va donc regrouper en une seule entité trois widgets de base tkinter : un champ d'entrée, une boîte de liste (listbox) et un « ascenseur » (barre de défilement vertical ou scrollbar).
La boîte de liste et son ascenseur seront étroitement associés, puisque l'ascenseur permet de faire défiler la liste dans sa boîte. Il faudra d'ailleurs s'assurer que l'ascenseur ait toujours la même hauteur que la boîte, quelle que soit la taille choisie pour celle-ci.
Pour tester celui-ci, nous l'incluons dans une petite application très simple : lorsque l'utilisateur choisit une couleur dans la liste (il peut aussi entrer un nom de couleur directement dans le champ d'entrée), cette couleur devient automatiquement la couleur de fond pour la fenêtre maîtresse.
Dans cette fenêtre maîtresse, nous avons ajouté un libellé et un bouton, afin de vous montrer comment vous pouvez accéder à la sélection opérée précédemment dans le ComboBox lui-même (le bouton provoque l'affichage du nom de la dernière couleur choisie).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
from
tkinter import
*
class
ComboBox
(
Frame):
"Widget composite associant un champ d'entrée avec une boîte de liste"
def
__init__
(
self, boss, item=
''
, items=
[], command =
''
, width =
10
,
listSize =
5
):
Frame.__init__
(
self, boss) # constructeur de la classe parente
# (<boss> est la réf. du widget 'maître')
self.items =
items # items à placer dans la boîte de liste
self.command =
command # fonction à invoquer après clic ou <enter>
self.item =
item # item entré ou sélectionné
# Champ d'entrée :
self.entree =
Entry
(
self, width =
width) # largeur en caractères
self.entree.insert
(
END, item)
self.entree.bind
(
"<Return>"
, self.sortieE)
self.entree.pack
(
side =
TOP)
# Boîte de liste, munie d'un 'ascenseur' (scroll bar) :
cadreLB =
Frame
(
self) # cadre pour l'ensemble des 2
self.bListe =
Listbox
(
cadreLB, height =
listSize, width =
width-
1
)
scrol =
Scrollbar
(
cadreLB, command =
self.bListe.yview)
self.bListe.config
(
yscrollcommand =
scrol.set)
self.bListe.bind
(
"<ButtonRelease-1>"
, self.sortieL)
self.bListe.pack
(
side =
LEFT)
scrol.pack
(
expand =
YES, fill =
Y)
cadreLB.pack
(
)
# Remplissage de la boîte de liste avec les items fournis :
for
it in
items:
self.bListe.insert
(
END, it)
def
sortieL
(
self, event =
None
):
# Extraire de la liste l'item qui a été sélectionné :
index =
self.bListe.curselection
(
) # renvoie un tuple d'index
ind0 =
int(
index[0
]) # on ne garde que le premier
self.item =
self.items[ind0]
# Actualiser le champ d'entrée avec l'item choisi :
self.entree.delete
(
0
, END)
self.entree.insert
(
END, self.item)
# Exécuter la commande indiquée, avec l'item choisi comme argument :
self.command
(
self.item)
def
sortieE
(
self, event =
None
):
# Exécuter la commande indiquée, avec l'argument-item encodé tel quel :
self.command
(
self.entree.get
(
))
def
get
(
self):
# Renvoyer le dernier item sélectionné dans la boîte de liste
return
self.item
if
__name__
==
"__main__"
: # --- Programme de test ---
def
changeCoul
(
col):
fen.configure
(
background =
col)
def
changeLabel
(
):
lab.configure
(
text =
combo.get
(
))
couleurs =
(
'navy'
, 'royal blue'
, 'steelblue1'
, 'cadet blue'
,
'lawn green'
, 'forest green'
, 'yellow'
, 'dark red'
,
'grey80'
,'grey60'
, 'grey40'
, 'grey20'
, 'pink'
)
fen =
Tk
(
)
combo =
ComboBox
(
fen, item =
"néant"
, items =
couleurs, command =
changeCoul,
width =
15
, listSize =
6
)
combo.grid
(
row =
1
, columnspan =
2
, padx =
10
, pady =
10
)
bou =
Button
(
fen, text =
"Test"
, command =
changeLabel)
bou.grid
(
row =
2
, column =
0
, padx =
8
, pady =
8
)
lab =
Label
(
fen, text =
"Bonjour"
, bg =
"ivory"
, width =
15
)
lab.grid
(
row =
2
, column =
1
, padx =
8
)
fen.mainloop
(
)
16-D-1-A. Commentaires▲
- Lignes 5-8 : Le constructeur de notre widget attend la référence du widget maître (boss) comme premier paramètre, suivant la convention habituelle. Les autres paramètres permettent notamment de prévoir un texte par défaut dans le champ d'entrée (item), de fournir la liste des éléments à insérer dans la boîte (items), et de désigner la fonction à invoquer lorsque l'utilisateur effectuera un clic dans la liste, ou enfoncera la touche <enter> de son clavier (command). Nous avons conservé des noms anglais pour ces paramètres, afin que notre widget puisse être utilisé avec les mêmes conventions que les widgets de base dont il dérive.
- Lignes 15, 39, 40 : Les méthodes du widget Entry ont déjà été décrites précédemment (cf. page ). Rappelons simplement que la méthode insert() permet d'insérer du texte dans le champ, sans faire disparaître un texte préexistant éventuel. Le premier argument permet de préciser à quel endroit du texte préexistant l'insertion doit avoir lieu. Ce peut être un entier, ou bien une valeur symbolique (en important l'ensemble du module tkinter à la ligne 1, on a importé une série de variables globales, dont END, qui contiennent ces valeurs symboliques, et END désigne bien entendu la fin du texte préexistant).
- Lignes 16 et 24 : deux événements seront associés à des méthodes locales : le fait de relâcher le bouton droit de la souris alors que son pointeur se trouve dans la boîte de liste (événement <ButtonRelease-1>) et le fait d'enfoncer la touche <enter> (événement <Return>).
- Ligne 21 : création de la boîte de liste (classe de base Listbox). Sa largeur s'exprime en nombre de caractères de la police courante. On en retranche un ou deux, afin de compenser approximativement la place qu'occupera l'ascenseur ci-après (l'ensemble des deux devant avoir à peu près la même largeur que le champ d'entrée).
- Ligne 22 : création de la barre de défilement verticale (classe de base Scrollbar). La commande qu'on lui associe : command =self.bListe.yview indique la méthode du widget Listbox qui sera invoquée pour provoquer le défilement de la liste dans la boîte, lorsque l'on actionnera cet ascenseur.
- Ligne 23 : symétriquement, on doit re-configurer la boîte de liste pour lui indiquer quelle méthode du widget Scrollbar elle devra invoquer afin que la position de l'ascenseur soit le reflet correct de la position relative de l'item couramment sélectionné dans la liste. Il n'était pas possible d'indiquer déjà cette commande dans la ligne d'instruction créant la boîte de liste, à la ligne 21, car à ce moment-là le widget Scrollbar n'existait pas encore. Y faire référence était donc exclu(81).
- Ligne 33 : cette méthode est invoquée chaque fois que l'utilisateur sélectionne un élément dans la liste. Elle fait appel à la méthode curselection() du widget Listbox de base, laquelle lui renvoie un tuple d'indices, car il a été prévu par les développeurs de tkinter que l'utilisateur puisse avoir sélectionné plusieurs items dans la liste (à l'aide de la touche <ctrl>). Nous supposerons cependant ici qu'un seul a été pointé, et récupérons donc seulement le premier élément de ce tuple. À la ligne 47, nous pouvons alors extraire l'item correspondant de la liste et l'utiliser, à la fois pour mettre à jour le champ d'entrée (lignes 39-40), ainsi que comme argument pour exécuter la commande (ligne 42) dont la référence avait été fournie lors de l'instanciation du widget (dans le cas de notre petite application, ce sera donc la fonction changeCoul()).
- Lignes 44-46 : la même commande est invoquée lorsque l'utilisateur actionne la touche <enter> après avoir encodé une chaîne de caractères dans le champ d'entrée. Le paramètre event, non utilisé ici, permettrait de récupérer le ou les événements associés.
- Lignes 48-49 : nous avons aussi inclus une méthode get(), suivant la convention suivie par d'autres widgets, afin de permettre la récupération libre du dernier item sélectionné.
16-D-2. Le widget Text assorti d'un ascenseur▲
En procédant de la même manière que dans l'exemple précédent, vous pouvez associer les widgets standard tkinter de multiples façons. Ainsi nous vous présentons ci-après un widget composite qui pourrait vous servir à ébaucher un système de traitement de textes rudimentaire. Son principal composant est le widget Text standard, lequel peut afficher des textes formatés, c'est-à-dire des textes intégrant divers attributs de style, comme par exemple le gras, l'italique, l'exposant ..., ainsi que des polices de caractères différentes, de la couleur, et même des images. Nous l'avons simplement associé à une barre de défilement verticale pour vous montrer une fois de plus les interactions que vous pouvez créer entre ces composants.
Par exemple, dans l'application décrite ci-après, le fait de cliquer sur le nom « Jean de la Fontaine », à l'aide du bouton droit de la souris, provoque le défilement automatique du texte (scrolling), jusqu'à ce qu'une rubrique décrivant cet auteur devienne visible dans le widget (voir le script correspondant page suivante). D'autres fonctionnalités sont présentes, telles la possibilité de sélectionner à l'aide de la souris n'importe quelle portion du texte affiché pour lui faire subir un traitement quelconque, mais nous ne présenterons ici que les plus fondamentales. Veuillez donc consulter les ouvrages ou sites web spécialisés pour en savoir davantage.
16-D-2-A. Gestion du texte affiché▲
Vous pouvez accéder à n'importe quelle portion du texte pris en charge par un widget Text grâce à deux concepts complémentaires, les index et les balises :
- Chaque caractère du texte affiché est référencé par un index, lequel doit être une chaîne de caractères contenant deux valeurs numériques reliées par un point (ex : "5.2"). Ces deux valeurs indiquent respectivement le numéro de ligne et le numéro de colonne où se situe le caractère.
- N'importe quelle portion du texte peut être associée à une ou plusieurs balise(s), dont vous choisissez librement le nom et les propriétés. Celles-ci vous permettent de définir la police, les couleurs d'avant- et d'arrière-plan, les événements associés, etc.
Pour la bonne compréhension du script ci-dessous, veuillez considérer que le texte de la fable traitée doit être accessible, dans un fichier nommé CorbRenard.txt, encodé en latin-1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
from
tkinter import
*
class
ScrolledText
(
Frame):
"""Widget composite, associant un widget Text et une barre de défilement"""
def
__init__
(
self, boss, baseFont =
"Times"
, width =
50
, height =
25
):
Frame.__init__
(
self, boss, bd =
2
, relief =
SUNKEN)
self.text =
Text
(
self, font =
baseFont, bg =
'ivory'
, bd =
1
,
width =
width, height =
height)
scroll =
Scrollbar
(
self, bd =
1
, command =
self.text.yview)
self.text.configure
(
yscrollcommand =
scroll.set)
self.text.pack
(
side =
LEFT, expand =
YES, fill =
BOTH, padx =
2
, pady =
2
)
scroll.pack
(
side =
RIGHT, expand =
NO, fill =
Y, padx =
2
, pady =
2
)
def
importFichier
(
self, fichier, encodage =
"Utf8"
):
"insertion d'un texte dans le widget, à partir d'un fichier"
of =
open(
fichier, "r"
, encoding =
encodage)
lignes =
of.readlines
(
)
of.close
(
)
for
li in
lignes:
self.text.insert
(
END, li)
def
chercheCible
(
event=
None
):
"défilement du texte jusqu'à la balise <cible>, grâce à la méthode see()"
index =
st.text.tag_nextrange
(
'cible'
, '0.0'
, END)
st.text.see
(
index[0
])
### Programme principal : fenêtre avec un libellé et un 'ScrolledText' ###
fen =
Tk
(
)
lib =
Label
(
fen, text =
"Widget composite : Text + Scrollbar"
,
font =
"Times 14 bold italic"
, fg =
"navy"
)
lib.pack
(
padx =
10
, pady =
4
)
st =
ScrolledText
(
fen, baseFont=
"Helvetica 12 normal"
, width =
40
, height =
10
)
st.pack
(
expand =
YES, fill =
BOTH, padx =
8
, pady =
8
)
# Définition de balises, liaison d'un événement <clic du bouton droit> :
st.text.tag_configure
(
"titre"
, foreground =
"brown"
,
font =
"Helvetica 11 bold italic"
)
st.text.tag_configure
(
"lien"
, foreground =
"blue"
,
font =
"Helvetica 11 bold"
)
st.text.tag_configure
(
"cible"
, foreground =
"forest green"
,
font =
"Times 11 bold"
)
st.text.tag_bind
(
"lien"
, "<Button-3>"
, chercheCible)
titre =
"""Le Corbeau et le Renard
par Jean de la Fontaine, auteur français
\n
"""
auteur =
"""
Jean de la Fontaine
écrivain français (1621-1695)
célèbre pour ses Contes en vers,
et surtout ses Fables, publiées
de 1668 à 1694."""
# Remplissage du widget Text (2 techniques) :
st.importFichier
(
"CorbRenard.txt"
, encodage =
"Latin1"
)
st.text.insert
(
"0.0"
, titre, "titre"
)
st.text.insert
(
END, auteur, "cible"
)
# Insertion d'une image :
photo =
PhotoImage
(
file=
"penguin.gif"
)
st.text.image_create
(
"6.14"
, image =
photo)
# Ajout d'une balise supplémentaire :
st.text.tag_add
(
"lien"
, "2.4"
, "2.23"
)
fen.mainloop
(
)
16-D-2-B. Commentaires▲
- Lignes 3 à 6 : le widget composite que nous définissons dans cette classe sera une fois de plus obtenu par dérivation de la classe Frame(). Son constructeur prévoit quelques paramètres d'instanciation à titre d'exemple (police utilisée, largeur et hauteur), avec des valeurs par défaut. Ces paramètres seront simplement transmis au widget Text « interne » (aux lignes 7 et 8). Vous pourriez bien évidemment en ajouter beaucoup d'autres, pour déterminer par exemple l'apparence du curseur, la couleur du fond ou des caractères, la manière dont les lignes trop longues doivent être coupées ou non, etc. Vous pourriez aussi de la même façon transmettre divers paramètres à la barre de défilement.
- Lignes 11 et 12 : l'option expand de la méthode pack() n'accepte que les valeurs YES ou NO. Elle détermine si le widget doit s'étirer ou non lorsque la fenêtre est éventuellement redimensionnée. L'option complémentaire fill peut prendre les 3 valeurs : X, Y ou BOTH. Elle indique si l'étirement peut s'effectuer horizontalement (axe X), verticalement (axe Y), ou dans les deux directions (BOTH). Lorsque vous développez une application, Il est important de prévoir ces redimensionnements éventuels, surtout si l'application est destinée à être utilisée dans des environnements variés (Windows, Linux, MacOS, …).
- Lignes 22 à 25 : Cette fonction est un gestionnaire d'événement, qui est appelé lorsque l'utilisateur clique avec le
bouton droit sur le nom de l'auteur (l'événement en question est associé à la balise correspondante, à la ligne 42).
À la ligne 24, on utilise la méthode tag_nextrange() du widget Text pour trouver les index de la portion de texte associée à la balise « cible ». La recherche de ces index est limitée au domaine défini par les 2e et 3e arguments (dans notre exemple, on recherche du début à la fin du texte entier). La méthode tag_nextrange() renvoie un tuple de deux index (ceux des premier et dernier caractères de la portion de texte associée à la balise « cible »). À la ligne 25, nous nous servons d'un seul de ces index (le premier) pour activer la méthode see(). Celle-ci provoque un défilement automatique du texte (scrolling), de telle manière que le caractère correspondant à l'index transmis devienne visible dans le widget (avec en général un certain nombre des caractères qui suivent). - Lignes 27 à 33 : Construction classique d'une fenêtre contenant deux widgets.
- Lignes 35 à 42 : Ces lignes définissent les trois balises titre, lien et cible ainsi que le formatage du texte qui leur sera associé. La ligne 42 précise en outre que le texte associé à la balise lien sera « cliquable » (le bouton n°3 de la souris est le bouton droit), avec indication du gestionnaire d'événement correspondant.
- Ligne 55 : Vous pouvez incorporer n'importe quelle fonctionnalité dans la définition d'une classe, comme nous l'avons fait ici en prévoyant une méthode d'importation de fichier texte dans le widget lui-même, avec le paramètre ad hoc pour un décodage éventuel. Avec cette méthode, le texte importé s'insère à la fin du texte déjà présent dans le widget, mais vous pourriez aisément l'améliorer en lui ajoutant un paramètre pour préciser l'endroit exact où doit se faire l'insertion.
- Lignes 56-57 : Ces instructions insèrent des fragments de texte (respectivement au début et à la fin du texte préexistant), en associant une balise à chacun d'eux.
- Ligne 62 : L'association des balises au texte est dynamique. À tout moment, vous pouvez activer une nouvelle association (comme nous le faisons ici en rattachant la balise « lien » à une portion de texte préexistante). Note : pour « détacher » une balise, utilisez la méthode tag_delete().
16-D-3. Le widget Canvas assorti d'un ascenseur▲
Nous avons déjà beaucoup exploité le widget Canvas, dont les possibilités sont extrêmement étendues. Nous avons déjà vu aussi comment nous pouvons encore enrichir cette classe par dérivation. C'est ce que nous allons faire une fois de plus dans l'exemple ci-après, avec la définition d'une nouvelle classe ScrolledCanvas, dans laquelle nous associerons au canevas standard deux barres de défilement (une verticale et une horizontale).
Afin de rendre l'exercice plus attrayant, nous nous servirons de notre nouvelle classe de widget pour réaliser un petit jeu d'adresse, dans lequel l'utilisateur devra réussir à cliquer sur un bouton qui s'esquive sans cesse. (Note : Si vous éprouvez vraiment des difficultés pour l'attraper, commencez d'abord par dilater la fenêtre !).
Le widget Canvas est très polyvalent : il vous permet de combiner à volonté des dessins, des images bitmap, des fragments de texte, et même d'autres widgets, dans un espace parfaitement extensible. Si vous souhaitez développer l'un ou l'autre jeu graphique, c'est évidemment le widget qu'il vous faut apprendre à maîtriser en priorité.
Comprenez bien cependant que les indications que nous vous fournissons à ce sujet dans les présentes notes sont forcément très incomplètes. Leur objectif est seulement de vous aider à comprendre quelques concepts de base, afin que vous puissiez ensuite consulter les ouvrages de référence spécialisés dans de bonnes conditions.
Notre petite application se présente comme une nouvelle classe FenPrinc(), obtenue par dérivation à partir de la classe de base Tk(). Elle contient deux widgets : un simple libellé, et notre nouveau widget composite ScrolledCanvas. Celui-ci est une « vue » sur un espace de dessin beaucoup plus grand, dans lequel nous pourrons « voyager » grâce aux barres de défilement.
Afin que l'espace disponible soit bien repérable, nous commençons par y planter un décor simple, constitué de 80 ellipses de couleur dont l'emplacement et les dimensions sont tirés au hasard. Nous y ajoutons également un petit clin d'œil sous la forme d'une image bitmap, destinée avant tout à vous rappeler comment vous pouvez gérer ce type de ressource.
Pour terminer, nous y installons aussi un véritable widget fonctionnel : en l'occurrence un simple bouton, mais la technique mise en œuvre pourrait s'appliquer à n'importe quel autre type de widget, y compris un gros widget composite comme ceux que nous avons développés précédemment. Cette grande souplesse dans le développement d'applications complexes est l'un des principaux bénéfices apportés par le mode de programmation « orientée objet ».
Le bouton s'anime dès qu'on l'a enfoncé une première fois, et l'animation s'arrête si on arrive à nouveau à cliquer dessus. Dans votre analyse du script ci-après, soyez attentifs aux méthodes utilisées pour modifier les propriétés d'un objet existant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
from
tkinter import
*
from
random import
randrange
class
ScrolledCanvas
(
Frame):
"""Canevas extensible avec barres de défilement"""
def
__init__
(
self, boss, width =
100
, height =
100
, bg=
"white"
, bd=
2
,
scrollregion =(
0
, 0
, 300
, 300
), relief=
SUNKEN):
Frame.__init__
(
self, boss, bd =
bd, relief=
relief)
self.can =
Canvas
(
self, width=
width-
20
, height=
height-
20
, bg=
bg,
scrollregion =
scrollregion, bd =
1
)
self.can.grid
(
row =
0
, column =
0
)
bdv =
Scrollbar
(
self, orient =
VERTICAL, command =
self.can.yview, bd =
1
)
bdh =
Scrollbar
(
self, orient =
HORIZONTAL, command =
self.can.xview, bd =
1
)
self.can.configure
(
xscrollcommand =
bdh.set, yscrollcommand =
bdv.set)
bdv.grid
(
row =
0
, column =
1
, sticky =
NS) # sticky =>
bdh.grid
(
row =
1
, column =
0
, sticky =
EW) # étirer la barre
# Lier l'événement <redimensionnement> à un gestionnaire approprié :
self.bind
(
"<Configure>"
, self.redim)
self.started =
False
def
redim
(
self, event):
"opérations à effectuer à chaque redimensionnement du widget"
if
not
self.started:
self.started =
True
# Ne pas redimensionner dès la création
return
# du widget (sinon => bouclage)
# À partir des nouvelles dimensions du cadre, redimensionner le canevas
# (la diff. de 20 pixels sert à compenser l'épaisseur des scrollbars) :
larg, haut =
self.winfo_width
(
)-
20
, self.winfo_height
(
)-
20
self.can.config
(
width =
larg, height =
haut)
class
FenPrinc
(
Tk):
def
__init__
(
self):
Tk.__init__
(
self)
self.libelle =
Label
(
text =
"Scroll game"
, font=
"Helvetica 14 bold"
)
self.libelle.pack
(
pady =
3
)
terrainJeu =
ScrolledCanvas
(
self, width =
500
, height =
300
, relief=
SOLID,
scrollregion =(-
600
,-
600
,600
,600
), bd =
3
)
terrainJeu.pack
(
expand =
YES, fill =
BOTH, padx =
6
, pady =
6
)
self.can =
terrainJeu.can
# Décor : tracé d'une série d'ellipses aléatoires :
coul =(
'sienna'
,'maroon'
,'brown'
,'pink'
,'tan'
,'wheat'
,'gold'
,'orange'
,
'plum'
,'red'
,'khaki'
,'indian red'
,'thistle'
,'firebrick'
,
'salmon'
,'coral'
,'yellow'
,'cyan'
,'blue'
,'green'
)
for
r in
range(
80
):
x1, y1 =
randrange
(-
600
,450
), randrange
(-
600
,450
)
x2, y2 =
x1 +
randrange
(
40
,300
), y1 +
randrange
(
40
,300
)
couleur =
coul[randrange
(
20
)]
self.can.create_oval
(
x1, y1, x2, y2, fill=
couleur, outline=
'black'
)
# Ajout d'une petite image GIF :
self.img =
PhotoImage
(
file =
'linux2.gif'
)
self.can.create_image
(
50
, 20
, image =
self.img)
# Bouton à attraper :
self.x, self.y =
50
, 100
self.bou =
Button
(
self.can, text =
"Start"
, command =
self.start)
self.fb =
self.can.create_window
(
self.x, self.y, window =
self.bou)
def
anim
(
self):
if
self.run ==
0
:
return
self.x +=
randrange
(-
60
, 61
)
self.y +=
randrange
(-
60
, 61
)
self.can.coords
(
self.fb, self.x, self.y)
self.libelle.config
(
text =
'Cherchez en
%s
%s
'
%
(
self.x, self.y))
self.after
(
250
, self.anim)
def
stop
(
self):
self.run =
0
self.bou.configure
(
text =
"Start"
, command =
self.start)
def
start
(
self):
self.bou.configure
(
text =
"Attrapez-moi !"
, command =
self.stop)
self.run =
1
self.anim
(
)
if
__name__
==
"__main__"
: # --- Programme de test ---
FenPrinc
(
).mainloop
(
)
16-D-3-A. Commentaires▲
- Lignes 6 à 10 : comme beaucoup d'autres, notre nouveau widget est dérivé de Frame(). Son constructeur accepte un certain nombre de paramètres. Remarquez bien que ces paramètres sont transmis pour partie au cadre (paramètres bd, relief), et pour partie au canevas (paramètres width, height, bg, scrollregion). Vous pouvez bien évidemment faire d'autres choix selon vos besoins. L'option scrollregion du widget Canvas sert à définir l'espace de dessin dans lequel la « vue » du canevas pourra se déplacer.
- Lignes 11 à 16 : Nous utiliserons cette fois la méthode grid() pour mettre en place le canevas et ses barres de
défilement (cette méthode vous a été présentée page ). La méthode pack() ne conviendrait guère pour mettre en place
correctement les deux barres de défilement, car elle imposerait l'utilisation de plusieurs cadres (frames) imbriqués (essayez, à titre
d'exercice !). Les interactions à mettre en place entre les barres de défilement et le widget qu'elles contrôlent (lignes 12, 13, 14) ont
déjà été décrites en détail pour les deux widgets composites précédents. L'option orient des barres de défilement n'avait pas été utilisée jusqu'ici, parce que sa valeur par défaut (VERTICAL) convenait aux cas traités.
Aux lignes 15 et 16, les options sticky =NS et sticky =EW provoquent l'étirement des barres de défilement jusqu'à occuper toute la hauteur (NS = direction Nord-Sud) ou toute la largeur (EW = direction Est-Ouest)de la cellule dans la grille. Il n'y aura cependant pas de redimensionnement automatique comme c'est le cas avec la méthode pack() (les options expand et fill ne sont d'ailleurs pas disponibles : voir ci-après). - Ligne 18 : Du fait que la méthode grid() n'inclut pas le redimensionnement automatique, nous devons guetter l'événement qui est généré par le système lorsque l'utilisateur redimensionne le widget, et l'associer à une méthode appropriée pour effectuer nous-mêmes le redimensionnement des composants du widget.
- Lignes 19 à 29 : La méthode de redimensionnement consistera simplement à redimensionner le canevas (les barres de
défilement s'adapteront toutes seules, du fait de l'option sticky qui leur est appliquée). Notez au passage que les dimensions actualisées
d'un widget peuvent être trouvées dans ses attributs winfo_width() et winfo_height().
La variable d'instance self.started est un simple interrupteur, qui permet d'éviter que le redimensionnement soit déjà appelé prématurément, lors de l'instanciation du widget (ce qui produit un bouclage curieux : essayez sans !). - Lignes 31 à 55 : Cette classe définit notre petite application de jeu. Son constructeur instancie notre nouveau widget dans la variable terrainJeu (ligne 36). Remarquez que le type de bordure et son épaisseur s'appliqueront au cadre du widget composite, alors que les autres arguments choisis s'appliqueront au canevas. Avec l'option scrollregion, nous définissons un espace de jeu nettement plus étendu que la surface du canevas lui-même, ce qui obligera le joueur à déplacer (ou redimensionner) celui-ci.
- Lignes 54-55 : C'est la méthode create_window() du widget Canvas qui permet d'y insérer n'importe quel autre widget (y compris un widget composite). Le widget à insérer doit cependant avoir été défini lui-même au préalable comme un esclave du canevas ou de sa fenêtre maîtresse. La méthode create_window() attend trois arguments : les coordonnées X et Y du point où l'on souhaite insérer le widget, et la référence de ce widget.
- Lignes 57 à 64 : Cette méthode est utilisée pour l'animation du bouton. Après avoir repositionné le bouton au hasard à une certaine distance de sa position précédente, elle se ré-appelle elle-même après une pause de 250 millisecondes. Ce bouclage s'effectue sans cesse, aussi longtemps que la variable self.run contient une valeur non-nulle.
- Lignes 66 à 73 : Ces deux gestionnaires d'événement sont associés au bouton en alternance. Ils servent évidemment à démarrer et à arrêter l'animation.
16-E. Application à fenêtres multiples - paramétrage implicite▲
La classe Toplevel() de tkinter permet de créer des fenêtres « satellites » de votre application principale. Ces fenêtres sont autonomes, mais elles se refermeront automatiquement lorsque vous fermerez la fenêtre principale. Cette restriction mise à part, elles se traitent de la manière habituelle : vous pouvez y placer n'importe quelle combinaison de widgets, à volonté.
La petite application ci-après vous montre quelques-unes de leurs possibilités. Elle est constituée d'une fenêtre principale très ordinaire, contenant simplement trois boutons. Ces boutons sont créés à partir d'une classe dérivée de la classe Button() de base, ceci afin de vous montrer encore une fois combien il est facile d'adapter les classes d'objets existantes à vos besoins. Vous pourrez noter au passage quelques options « décoratives » intéressantes.
Le bouton « Top1 » fait apparaître une première fenêtre satellite contenant un canevas avec une image. Nous avons doté cette fenêtre de propriétés particulières : elle ne possède ni bandeau-titre, ni bordure, et il est impossible de la redimensionner à l'aide de la souris. De plus, cette fenêtre est modale : on qualifie ainsi une fenêtre qui reste toujours au premier plan, devant toutes les autres fenêtres d'application éventuellement présentes à l'écran.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
from
tkinter import
*
class
FunnyButton
(
Button):
"Bouton de fantaisie : vert virant au rouge quand on l'actionne"
def
__init__
(
self, boss, **
Arguments):
Button.__init__
(
self, boss, bg =
"dark green"
, fg =
"white"
, bd =
5
,
activebackground =
"red"
, activeforeground =
"yellow"
,
font =(
'Helvetica'
, 12
, 'bold'
), **
Arguments)
class
SpinBox
(
Frame):
"widget composite comportant un Label et 2 boutons 'up' & 'down'"
def
__init__
(
self, boss, largC=
5
, largB =
2
, vList=
[0
], liInd=
0
, orient =
Y):
Frame.__init__
(
self, boss)
self.vList =
vList # liste des valeurs à présenter
self.liInd =
liInd # index de la valeur à montrer par défaut
if
orient ==
Y:
s, augm, dimi =
TOP, "^"
, "v"
# Orientation 'verticale'
else
:
s, augm, dimi =
RIGHT, ">"
, "<"
# Orientation 'horizontale'
Button
(
self, text =
augm, width =
largB, command =
self.up).pack
(
side =
s)
self.champ =
Label
(
self, bg =
'white'
, width =
largC,
text =
str(
vList[liInd]), relief =
SUNKEN)
self.champ.pack
(
pady =
3
, side =
s)
Button
(
self, text=
dimi, width=
largB, command =
self.down).pack
(
side =
s)
def
up
(
self):
if
self.liInd <
len(
self.vList) -
1
:
self.liInd +=
1
else
:
self.bell
(
) # émission d'un bip
self.champ.configure
(
text =
str(
self.vList[self.liInd]))
def
down
(
self):
if
self.liInd >
0
:
self.liInd -=
1
else
:
self.bell
(
) # émission d'un bip
self.champ.configure
(
text =
str(
self.vList[self.liInd]))
def
get
(
self):
return
self.vList[self.liInd]
class
FenDessin
(
Toplevel):
"Fenêtre satellite (modale) contenant un simple canevas"
def
__init__
(
self, **
Arguments):
Toplevel.__init__
(
self, **
Arguments)
self.geometry
(
"250x200+100+240"
)
self.overrideredirect
(
1
) # => fenêtre sans bordure ni bandeau
self.transient
(
self.master) # => fenêtre 'modale'
self.can =
Canvas
(
self, bg=
"ivory"
, width =
200
, height =
150
)
self.img =
PhotoImage
(
file =
"papillon2.gif"
)
self.can.create_image
(
90
, 80
, image =
self.img)
self.can.pack
(
padx =
20
, pady =
20
)
class
FenControle
(
Toplevel):
"Fenêtre satellite contenant des contrôles de redimensionnement"
def
__init__
(
self, boss, **
Arguments):
Toplevel.__init__
(
self, boss, **
Arguments)
self.geometry
(
"250x200+400+230"
)
self.resizable
(
width =
0
, height =
0
) # => empêche le redimensionnement
p =(
10
, 30
, 60
, 90
, 120
, 150
, 180
, 210
, 240
, 270
, 300
)
self.spX =
SpinBox
(
self, largC=
5
,largB =
1
,vList =
p,liInd=
5
,orient =
X)
self.spX.pack
(
pady =
5
)
self.spY =
SpinBox
(
self, largC=
5
,largB =
1
,vList =
p,liInd=
5
,orient =
Y)
self.spY.pack
(
pady =
5
)
FunnyButton
(
self, text =
"Dimensionner le canevas"
,
command =
boss.redimF1).pack
(
pady =
5
)
class
Demo
(
Frame):
"Démo. de quelques caractéristiques du widget Toplevel"
def
__init__
(
self):
Frame.__init__
(
self)
self.master.geometry
(
"400x300+200+200"
)
self.master.config
(
bg =
"cadet blue"
)
FunnyButton
(
self, text =
"Top 1"
, command =
self.top1).pack
(
side =
LEFT)
FunnyButton
(
self, text =
"Top 2"
, command =
self.top2).pack
(
side =
LEFT)
FunnyButton
(
self, text =
"Quitter"
, command =
self.quit).pack
(
)
self.pack
(
side =
BOTTOM, padx =
10
, pady =
10
)
def
top1
(
self):
self.fen1 =
FenDessin
(
bg =
"grey"
)
def
top2
(
self):
self.fen2 =
FenControle
(
self, bg =
"khaki"
)
def
redimF1
(
self):
dimX, dimY =
self.fen2.spX.get
(
), self.fen2.spY.get
(
)
self.fen1.can.config
(
width =
dimX, height =
dimY)
if
__name__
==
"__main__"
: # --- Programme de test ---
Demo
(
).mainloop
(
)
16-E-1. Commentaires▲
- Lignes 3 à 8 : Si vous souhaitez pouvoir disposer du même style de boutons à différents endroits de votre projet,
n'hésitez pas à créer une classe dérivée, comme nous l'avons fait ici. Cela vous évitera d'avoir à reprogrammer partout les
mêmes options spécifiques.
Notez bien les deux astérisques qui préfixent le nom du dernier paramètre du constructeur : **Arguments. Elles signifient que la variable correspondante sera en fait un dictionnaire, capable de réceptionner automatiquement n'importe quel ensemble d'arguments « avec étiquettes ». Ces arguments pourront alors être transmis tels quels au constructeur de la classe parente (à la ligne 8). Cela nous évite d'avoir à reproduire dans notre liste de paramètres toutes les options de paramétrage du bouton de base, qui sont fort nombreuses. Ainsi vous pourrez instancier ces boutons de fantaisie avec n'importe quelle combinaison d'options, du moment que celles-ci soient disponibles pour les boutons de base. On appelle ce qui précède un paramétrage implicite. Vous pouvez utiliser cette forme de paramétrage avec n'importe quelle fonction ou méthode. - Lignes 10 à 24 : Le constructeur de notre widget SpinBox ne nécessite guère de commentaires. En fonction de l'orientation souhaitée, la méthode pack() disposera les boutons et le libellé de haut en bas ou de gauche à droite (arguments TOP ou RIGHT pour son option side).
- Lignes 26 à 38 : Ces deux méthodes ne font rien d'autre que de modifier la valeur affichée dans le libellé. Notez au passage que la classe Frame() dispose d'une méthode bell() pour provoquer l'émission d'un « bip » sonore.
- Lignes 43 à 53 : La première fenêtre satellite est définie ici. Remarquez à nouveau l'utilisation du paramétrage implicite du constructeur, à l'aide de la variable **Arguments. C'est lui qui nous permet d'instancier cette fenêtre (à la ligne 81) en spécifiant une couleur de fond. (Nous pourrions aussi demander une bordure, etc.). Les méthodes invoquées aux lignes 47 à 49 définissent quelques propriétés particulières (elles qui sont applicables à n'importe quelle fenêtre). La méthode geometry() permet de fixer à la fois les dimensions de la fenêtre et son emplacement à l'écran (l'indication +100+240 signifie que son coin supérieur gauche doit être décalé de 100 pixels vers la droite, et de 240 pixels vers le bas, par rapport au coin supérieur gauche de l'écran).
- Lignes 45 et 57 : Veuillez remarquer la petite différence entre les listes de paramètres de ces lignes. Dans le constructeur de FenDessin, nous avons omis le paramètre boss, qui est bien présent dans le constructeur de FenControle. Vous savez que ce paramètre sert à transmettre la référence du widget « maître » à son « esclave ». Il est très généralement nécessaire (à la ligne 67, par exemple, nous nous en servons pour référencer une méthode de l'application principale), mais il n'est pas absolument indispensable : dans FenDessin nous n'en avons aucune utilité. Vous retrouverez bien évidemment la différence correspondante dans les instructions d'instanciation de ces fenêtres, aux lignes 82 et 84.
- Lignes 55 à 67 : À l'exception de la différence mentionnée ci-dessus, la construction du widget FenControle est très similaire à celle de FenDessin. Remarquez à la ligne 60 la méthode permettant d'empêcher le redimensionnement d'une fenêtre (dans le cas d' une fenêtre sans bordure ni bandeau-titre, comme FenDessin, cette méthode est inutile).
- Lignes 73-74 (et 49) : Toutes les classes dérivées des widgets tkinter sont dotées automatiquement d'un attribut master, qui contient la référence de la classe parente. C'est ce qui nous permet ici d'accéder aux dimensions et à la couleur de fond de la fenêtre maîtresse.
- Lignes 86 à 88 : Cette méthode récupère les valeurs numériques affichées dans la fenêtre de contrôle (méthode get() de ce widget), pour redimensionner le canevas de la fenêtre de dessin. Cet exemple simple vous montre une fois de plus comment peuvent s'établir les communications entre divers composants de votre programme.
16-F. Barres d'outils - expressions lambda▲
De nombreux programmes comportent une ou plusieurs « barres d'outils » (toolbar) constituées de petits boutons sur lesquels sont représentés des pictogrammes (icônes). Cette façon de faire permet de proposer à l'utilisateur un grand nombre de commandes spécialisées, sans que celles-ci n'occupent une place excessive à l'écran (un petit dessin vaut mieux qu'un long discours, dit-on).
L'application décrite ci-après comporte une barre d'outils et un canevas. Lorsque l'utilisateur clique sur l'un des 8 premiers boutons de la barre, le pictogramme qu'il porte est recopié dans le canevas, à un emplacement choisi au hasard. Lorsqu'il clique sur le dernier bouton, le contenu du canevas est effacé.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
from
tkinter import
*
from
random import
randrange
class
ToolBar
(
Frame):
"Barre d'outils (petits boutons avec icônes)"
def
__init__
(
self, boss, images =
[], command =
None
, **
Arguments):
Frame.__init__
(
self, boss, bd =
1
, **
Arguments)
# <images> = liste des noms d'icônes à placer sur les boutons
self.command =
command # commande à exécuter lors du clic
nBou =
len(
images) # Nombre de boutons à construire
# Les icônes des boutons doivent être placées dans des variables
# persistantes. Une liste fera l'affaire :
self.photoI =
[None
]*
nBou
for
b in
range(
nBou):
# Création de l'icône (objet PhotoImage Tkinter) :
self.photoI[b] =
PhotoImage
(
file =
images[b] +
'.gif'
)
# Création du bouton. On fait appel à une fonction lambda
# pour pouvoir transmettre un argument à la méthode <action> :
bou =
Button
(
self, image =
self.photoI[b], bd =
2
, relief =
GROOVE,
command =
lambda
arg =
b: self.action
(
arg))
bou.pack
(
side =
LEFT)
def
action
(
self, index):
# Exécuter <command> avec l'indice du bouton comme argument :
self.command
(
index)
class
Application
(
Frame):
def
__init__
(
self):
Frame.__init__
(
self)
# noms des fichiers contenant les icones (format GIF):
icones =(
'floppy_2'
, 'coleo'
, 'papi2'
, 'pion_1'
, 'pion_2'
, 'pion_3'
,
'pion_4'
, 'help_4'
, 'clear'
)
# Création de la barre d'outils :
self.barOut =
ToolBar
(
self, images =
icones, command =
self.transfert)
self.barOut.pack
(
expand =
YES, fill =
X)
# Création du canevas destiné à recevoir les images :
self.ca =
Canvas
(
self, width =
400
, height =
200
, bg =
'orange'
)
self.ca.pack
(
)
self.pack
(
)
def
transfert
(
self, b):
if
b ==
8
:
self.ca.delete
(
ALL) # Effacer tout dans le canevas
else
:
# Recopier l'icône du bouton b (extraite de la barre) => canevas :
x, y =
randrange
(
25
,375
), randrange
(
25
,175
)
self.ca.create_image
(
x, y, image =
self.barOut.photoI[b])
Application
(
).mainloop
(
)
16-F-1. Métaprogrammation - expressions lambda▲
Vous savez qu'en règle générale, on associe à chaque bouton une commande, laquelle est la référence d'une méthode ou d'une fonction particulière qui se charge d'effectuer le travail lorsque le bouton est activé. Or, dans l'application présente, tous les boutons doivent faire à peu près la même chose (recopier un dessin dans le canevas), la seule différence entre eux étant le dessin concerné.
Pour simplifier notre code, nous voudrions donc pouvoir associer l'option command de tous nos boutons avec une seule et même méthode (ce sera la méthode action() ), mais en lui transmettant à chaque fois la référence du bouton particulier utilisé, de manière à ce que l'action accomplie puisse être différente pour chacun d'eux.
Une difficulté se présente, cependant, parce que l'option command du widget Button accepte seulement une valeur ou une expression, et non une instruction. Il s'agit en fait de lui indiquer la référence d'une fonction, mais certainement pas d'invoquer la fonction à cet endroit avec des arguments (l'invocation ne pourra avoir lieu en effet que lorsque l'utilisateur cliquera sur le bouton : c'est alors le réceptionnaire d'événements de tkinter qui devra la provoquer). C'est la raison pour laquelle on indique seulement le nom de la fonction, sans parenthèses.
On peut résoudre cette difficulté de deux manières :
- Du fait de son caractère dynamique, Python accepte qu'un programme puisse se modifier lui-même, par exemple en définissant de
nouvelles fonctions au cours de son exécution (c'est le concept de métaprogrammation).
Il est donc possible de définir à la volée une fonction avec des paramètres, en indiquant pour chacun de ceux-ci une valeur par défaut, et de fournir ensuite la référence de cette fonction à l'option command. Puisque la fonction est définie en cours d'exécution, ces valeurs par défaut peuvent être les contenus de variables. Lorsque l'événement « clic sur le bouton » provoquera l'appel de la fonction, celle-ci utilisera donc les valeurs par défaut de ses paramètres, comme s'il s'agissait d'arguments. Le résultat de l'opération équivaut par conséquent à un transfert d'arguments classique.
Pour illustrer cette technique, remplacez les lignes 17 à 20 du script par les suivantes :
# Création du bouton. On définit à la volée une fonction avec un
# paramètre dont la valeur par défaut est l'argument à transmettre.
# Cette fonction appelle la méthode qui nécessite un argument :
def agir(arg = b):
self.action(arg)
# La commande associée au bouton appelle la fonction ci-dessus :
bou = Button(self, image = self.photoI[b], relief = GROOVE,
command = agir)
- Voilà pour le principe. Mais tout ce qui précède peut être simplifié, en faisant appel à une expression lambda. Ce mot réservé Python désigne une expression qui renvoie un objetfonction, similaire à ceux que vous créez avec l'instruction def, mais avec la différence que lambda étant une expression et non une instruction, on peut l'utiliser comme interface afin d'invoquer une fonction (avec passage d'arguments) là où ce n'est normalement pas possible. Notez au passage qu'une telle fonction est anonyme (elle ne possède pas de nom).
Par exemple, l'expression :
lambda ar1=b, ar2=c : bidule(ar1,ar2)
renvoie la référence d'une fonction anonyme créée à la volée, qui pourra elle-même invoquer la fonction bidule() en lui transmettant les arguments b et c , ceux-ci étant utilisés comme valeurs par défaut dans la définition des paramètres ar1 et ar2 de la fonction.
Cette technique utilise finalement le même principe que la précédente, mais elle présente l'avantage d'être plus concise, raison pour laquelle nous l'avons utilisée dans notre script. En revanche, elle est un peu plus difficile à comprendre :
command = lambda arg =b: self.action(arg)
Dans cette portion d'instruction, la commande associée au bouton se réfère à une fonction anonyme dont le paramètre arg
possède une valeur par défaut : la valeur de l'argument b.
Invoquée sans argument par la commande, cette fonction anonyme peut tout de même utiliser son paramètre arg (avec la valeur par défaut)
pour faire appel à la méthode cible self.action(), et l'on obtient ainsi un véritable transfert d'argument vers cette méthode
.
Nous ne détaillerons pas davantage ici la question des expressions lambda, car elle déborde du cadre que nous nous sommes fixés pour cet ouvrage d'initiation. Si vous souhaitez en savoir plus, veuillez donc consulter l'un ou l'autre des ouvrages de référence cités dans la bibliographie.
16-F-2. Passage d'une fonction (ou d'une méthode) comme argument▲
Vous avez déjà rencontré de nombreux widgets comportant une telle option command, à laquelle il faut à chaque fois associer le nom d'une fonction (ou d'une méthode). En termes plus généraux, cela signifie donc qu'une fonction avec paramètres peut recevoir la référence d'une autre fonction comme argument, et l'utilité de la chose apparaît clairement ici.
Nous avons d'ailleurs programmé nous-même une fonctionnalité de ce genre dans notre nouvelle classe ToolBar(). Vous pouvez constater que nous avons inclus le nom command dans la liste de paramètres de son constructeur, à la ligne 6. Ce paramètre attend la référence d'une fonction ou d'une méthode comme argument. La dite référence est alors mémorisée dans une variable d'instance (à la ligne 9), de manière à être accessible depuis les autres méthodes de la classe. Celles-ci peuvent dès lors invoquer la fonction ou la méthode (au besoin en lui transmettant des arguments si nécessaire, suivant la technique expliquée à la rubrique précédente). C'est ce que fait donc notre méthode action(), à la ligne 25. En l'occurrence, la méthode ainsi transmise est la méthode transfert() de la classe Application (cf. ligne 34).
Nous sommes parvenus ainsi à développer une classe d'objets ToolBar parfaitement réutilisables dans d'autres contextes. Comme le montre notre petite application, il suffit en effet d'instancier ces objets en indiquant la référence d'une fonction quelconque en argument de l'option command. Cette fonction sera alors automatiquement appelée elle-même avec le numéro d'ordre du bouton cliqué par l'utilisateur.
Libre à vous d'imaginer à présent ce que la fonction devra effectuer !
Pour en terminer avec cet exemple, remarquons encore un petit détail : chacun de nos boutons apparaît entouré d'un sillon (option relief =GROOVE). Vous pouvez aisément obtenir d'autres aspects en choisissant judicieusement les options relief et bd (bordure) dans l'instruction d'instanciation de ces boutons. En particulier, vous pouvez choisir relief =FLAT et bd =0 pour obtenir des petits boutons « plats », sans aucun relief.
16-G. Fenêtres avec menus▲
Pour terminer notre petite visite guidée des widgets tkinter, nous allons décrire à présent la construction d'une fenêtre d'application dotée de différents types de menus « déroulants », chacun de ces menus pouvant être « détaché » de l'application principale pour devenir lui-même une petite fenêtre indépendante, comme dans l'illustration ci-contre.
Cet exercice un peu plus long nous servira également de révision, et nous le réaliserons par étapes, en appliquant une stratégie de programmation que l'on appelle développement incrémental.
Comme nous l'avons déjà expliqué précédemment(82), cette méthode consiste à commencer l'écriture d'un programme par une ébauche, qui ne comporte que quelques lignes seulement mais qui est déjà fonctionnelle. On teste alors cette ébauche soigneusement afin d'en éliminer les bugs éventuels. Lorsque l'ébauche fonctionne correctement, on y ajoute une fonctionnalité supplémentaire. On teste ce complément jusqu'à ce qu'il donne entière satisfaction, puis on en ajoute un autre, et ainsi de suite...
Cela ne signifie pas que vous pouvez commencer directement à programmer sans avoir au préalable effectué une analyse sérieuse du projet, dont au moins les grandes lignes devront être convenablement décrites dans un cahier des charges clairement rédigé.
Il reste également impératif de commenter convenablement le code produit, au fur et à mesure de son élaboration. S'efforcer de rédiger de bons commentaires est en effet nécessaire, non seulement pour que votre code soit facile à lire (et donc à maintenir plus tard, par d'autres ou par vous-même), mais aussi pour que vous soyez forcés d'exprimer ce que vous souhaitez vraiment que la machine fasse (Cf. erreurs sémantiques, page .)
16-G-1. Cahier des charges de l'exercice▲
Notre application comportera simplement une barre de menus et un canevas. Les différentes rubriques et options des menus ne serviront qu'à faire apparaître des fragments de texte dans le canevas ou à modifier des détails de décoration, mais ce seront avant tout des exemples variés, destinés à donner un aperçu des nombreuses possibilités offertes par ce type de widget, accessoire indispensable de toute application moderne d'une certaine importance.
Nous souhaitons également que le code produit dans cet exercice soit bien structuré. Pour ce faire, nous ferons usage de deux classes : une classe pour l'application principale, et une autre pour la barre de menus. Nous voulons procéder ainsi afin de bien mettre en évidence la construction d'une application type incorporant plusieurs classes d'objets interactifs.
16-G-2. Première ébauche du programme▲
Lorsque l'on construit l'ébauche d'un programme, il faut tâcher d'y faire apparaître le plus tôt possible la structure d'ensemble, avec les relations entre les principaux blocs qui constitueront l'application définitive. C'est ce que nous nous sommes efforcés de faire dans l'exemple ci-dessous :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
from
tkinter import
*
class
MenuBar
(
Frame):
"""Barre de menus déroulants"""
def
__init__
(
self, boss =
None
):
Frame.__init__
(
self, borderwidth =
2
)
##### Menu <Fichier> #####
fileMenu =
Menubutton
(
self, text =
'Fichier'
)
fileMenu.pack
(
side =
LEFT)
# Partie "déroulante" :
me1 =
Menu
(
fileMenu)
me1.add_command
(
label =
'Effacer'
, underline =
0
,
command =
boss.effacer)
me1.add_command
(
label =
'Terminer'
, underline =
0
,
command =
boss.quit)
# Intégration du menu :
fileMenu.configure
(
menu =
me1)
class
Application
(
Frame):
"""Application principale"""
def
__init__
(
self, boss =
None
):
Frame.__init__
(
self)
self.master.title
(
'Fenêtre avec menus'
)
mBar =
MenuBar
(
self)
mBar.pack
(
)
self.can =
Canvas
(
self, bg=
'light grey'
, height=
190
,
width=
250
, borderwidth =
2
)
self.can.pack
(
)
self.pack
(
)
def
effacer
(
self):
self.can.delete
(
ALL)
if
__name__
==
'__main__'
:
app =
Application
(
)
app.mainloop
(
)
Veuillez donc encoder ces lignes et en tester l'exécution. Vous devriez obtenir une fenêtre avec un canevas gris clair surmonté d'une barre de
menus. À ce stade, la barre de menus ne comporte encore que la seule rubrique « Fichier ».
Cliquez sur la rubrique « Fichier » pour faire apparaître le menu correspondant : l'option « Effacer » n'est pas encore fonctionnelle (elle servira plus loin à effacer le contenu du canevas), mais l'option « Terminer » devrait déjà vous permettre de fermer proprement l'application.
Comme tous les menus gérés par tkinter, le menu que vous avez créé peut être converti en menu « flottant » : il suffit de cliquer sur la ligne pointillée apparaissant en-tête de menu. Vous obtenez ainsi une petite fenêtre satellite, que vous pouvez alors positionner où bon vous semble sur le bureau.
16-G-2-A. Analyse du script▲
La structure de ce petit programme devrait vous apparaître familière : afin que les classes définies dans ce script puissent
éventuellement être (ré)utilisées dans d'autres projets par importation, comme nous l'avons déjà expliqué
précédemment(83), le corps principal du programme (lignes 35
à 37) comporte l'instruction désormais classique :
if __name__ == '__main__':
Les deux instructions qui suivent consistent seulement à instancier un objet app et à faire fonctionner sa méthode mainloop(). Comme vous le savez certainement, nous aurions pu également condenser ces deux instructions en une seule.
L'essentiel du programme se trouve cependant dans les définitions de classes qui précèdent.
La classe MenuBar() contient la description de la barre de menus. Dans l'état présent du script, elle se résume à une ébauche de constructeur.
- Ligne 5 : Le paramètre boss réceptionne la référence de la fenêtre maîtresse du widget au moment de son instanciation. Cette référence va nous permettre d'invoquer les méthodes associées à cette fenêtre maîtresse, aux lignes 14 et 16.
- Ligne 6 : Activation obligatoire du constructeur de la classe parente.
- Ligne 9 : Instanciation d'un widget de la classe Menubutton(), défini comme un « esclave » de self (c'est-à-dire l'objet composite « barre de menus » dont nous sommes occupés à définir la classe). Comme l'indique son nom, ce type de widget se comporte un peu comme un bouton : une action se produit lorsque l'on clique dessus.
- Ligne 12 : Afin que cette action consiste en l'apparition véritable d'un menu, il reste encore à définir celui-ci : ce sera encore un nouveau widget, de la classe Menu() cette fois, défini lui-même comme un « esclave » du widget Menubutton instancié à la ligne 9.
- Lignes 13 à 16 : On peut appliquer aux widgets de la classe Menu() un certain nombre de méthodes spécifiques, chacune d'elles
acceptant de nombreuses options. Nous utilisons ici la méthode add_command() pour installer dans le menu les deux items « Effacer »
et « Terminer ». Nous y intégrons tout de suite l'option underline, qui sert à définir un raccourci clavier : cette
option indique en effet lequel des caractères de l'item doit apparaître souligné à l'écran. L'utilisateur sait alors qu'il lui suffit de
frapper ce caractère au clavier pour que l'action correspondant à cet item soit activée (comme s'il avait cliqué dessus à l'aide de la
souris).
L'action à déclencher lorsque l'utilisateur sélectionne l'item est désignée par l'option command. Dans notre script, les commandes invoquées sont toutes les deux des méthodes de la fenêtre maîtresse, dont la référence aura été transmise au présent widget au moment de son instanciation par l'intermédiaire du paramètre boss. La méthode effacer(), que nous définissons nous-même plus loin, servira à vider le canevas. La méthode prédéfinie quit() provoque la sortie de la boucle mainloop() et donc l'arrêt du réceptionnaire d'événements associé à la fenêtre d'application. - Ligne 18 : Lorsque les items du menu ont été définis, il reste encore à reconfigurer le widget maître Menubutton de manière à ce que son option « menu » désigne effectivement le Menu que nous venons de construire. En effet, nous ne pouvions pas déjà préciser cette option lors de la définition initiale du widget Menubutton, puisqu'à ce stade le Menu n'existait pas encore. Nous ne pouvions pas non plus définir le widget Menu en premier lieu, puisque celui-ci doit être défini comme un « esclave » du widget Menubutton. Il faut donc bien procéder en trois étapes comme nous l'avons fait, en faisant appel à la méthode configure(). Cette méthode peut être appliquée à n'importe quel widget préexistant pour en modifier l'une ou l'autre option.
La classe Application() contient la description de la fenêtre principale du programme ainsi que les méthodes gestionnaires d'événements qui lui sont associées.
- Ligne 20 : Nous préférons faire dériver notre application de la classe Frame(), qui présente de nombreuses options, plutôt que de la classe primordiale Tk(). De cette manière, l'application toute entière est encapsulée dans un widget, lequel pourra éventuellement être intégré par la suite dans une application plus importante. Rappelons que, de toute manière, tkinter instanciera automatiquement une fenêtre maîtresse de type Tk() pour contenir cette Frame.
- Lignes 23-24 : Après l'indispensable activation du constructeur de la classe parente, nous utilisons l'attribut master que tkinter associe automatiquement à chaque widget, pour référencer la classe parente (dans le cas présent, l'objet correspondant est la fenêtre principale de l'application) et en redéfinir le bandeau-titre.
- Lignes 25 à 29 : Instanciation de deux widgets esclaves pour notre cadre (Frame) principal. La « barre de menus » est évidemment le widget défini dans l'autre classe.
- Ligne 30 : Comme n'importe quel autre widget, notrecadre principal doit être confié à une méthode de mis en place afin d'apparaître véritablement.
- Lignes 32-33 : La méthode servant à effacer le canevas est définie dans la classe présente (puisque l'objet canevas en fait
partie), mais elle est invoquée par l'option command d'un widget esclave défini dans une autre classe.
Comme nous l'avons expliqué plus haut, ce widget esclave reçoit la référence de son widget maître par l'intermédiaire du paramètre boss. Toutes ces références sont hiérarchisées à l'aide de la qualification des noms par points.
16-G-3. Ajout de la rubrique Musiciens▲
Continuez le développement de ce petit programme, en ajoutant les lignes suivantes dans le constructeur de la classe MenuBar() (après la ligne 18) :
##### Menu <Musiciens> #####
self.musi =
Menubutton
(
self, text =
'Musiciens'
)
self.musi.pack
(
side =
LEFT, padx =
'3'
)
# Partie "déroulante" du menu <Musiciens> :
me1 =
Menu
(
self.musi)
me1.add_command
(
label =
'17e siècle'
, underline =
1
,
foreground =
'red'
, background =
'yellow'
,
font =(
'Comic Sans MS'
, 11
),
command =
boss.showMusi17)
me1.add_command
(
label =
'18e siècle'
, underline =
1
,
foreground=
'royal blue'
, background =
'white'
,
font =(
'Comic Sans MS'
, 11
, 'bold'
),
command =
boss.showMusi18)
# Intégration du menu :
self.musi.configure
(
menu =
me1)
... ainsi que les définitions de méthodes suivantes à la classe Application() (après la ligne 33) :
def
showMusi17
(
self):
self.can.create_text
(
10
, 10
, anchor =
NW, text =
'H. Purcell'
,
font=(
'Times'
, 20
, 'bold'
), fill =
'yellow'
)
def
showMusi18
(
self):
self.can.create_text
(
245
, 40
, anchor =
NE, text =
"W. A. Mozart"
,
font =(
'Times'
, 20
, 'italic'
), fill =
'dark green'
)
Lorsque vous y aurez ajouté toutes ces lignes, sauvegardez le script et exécutez-le.
Votre barre de menus comporte à présent une rubrique supplémentaire : la rubrique « Musiciens ».
Le menu correspondant propose deux items qui sont affichés avec des couleurs et des polices personnalisées. Vous pourrez vous inspirer de ces techniques décoratives pour vos projets personnels. À utiliser avec modération !
Les commandes que nous avons associées à ces items sont évidemment simplifiées afin de ne pas alourdir l'exercice : elles provoquent l'affichage de petits textes sur le canevas.
16-G-3-A. Analyse du script▲
Les seules nouveautés introduites dans ces lignes concernent l'utilisation de polices de caractères bien déterminées (option font), ainsi que de couleurs pour l'avant-plan (option foreground) et le fond (option background) des textes affichés.
Veuillez noter encore une fois l'utilisation de l'option underline pour désigner les caractères correspondant à des raccourcis claviers (en n'oubliant pas que la numérotation des caractères d'une chaîne commence à partir de zéro), et surtout que l'option command de ces widgets accède aux méthodes de l'autre classe, par l'intermédiaire de la référence mémorisée dans l'attribut boss.
La méthode create_text() du canevas doit être utilisée avec deux arguments numériques, qui sont les coordonnées X et Y d'un point dans le canevas. Le texte transmis sera positionné par rapport à ce point, en fonction de la valeur choisie pour l'option anchor : celle-ci détermine comment le fragment de texte doit être « ancré » au point choisi dans le canevas, par son centre, par son coin supérieur gauche, etc., en fonction d'une syntaxe qui utilise l'analogie des points cardinaux géographiques (NW = angle supérieur gauche, SE = angle inférieur droit, CENTER = centre, etc.).
16-G-4. Ajout de la rubrique Peintres▲
Cette nouvelle rubrique est construite d'une manière assez semblable à la précédente, mais nous lui avons ajouté une fonctionnalité supplémentaire : des menus « en cascade ». Veuillez donc ajouter les lignes suivantes dans le constructeur de la classe MenuBar() :
##### Menu <Peintres> #####
self.pein =
Menubutton
(
self, text =
'Peintres'
)
self.pein.pack
(
side =
LEFT, padx=
'3'
)
# Partie "déroulante" :
me1 =
Menu
(
self.pein)
me1.add_command
(
label =
'classiques'
, state=
DISABLED)
me1.add_command
(
label =
'romantiques'
, underline =
0
,
command =
boss.showRomanti)
# Sous-menu pour les peintres impressionistes :
me2 =
Menu
(
me1)
me2.add_command
(
label =
'Claude Monet'
, underline =
7
,
command =
boss.tabMonet)
me2.add_command
(
label =
'Auguste Renoir'
, underline =
8
,
command =
boss.tabRenoir)
me2.add_command
(
label =
'Edgar Degas'
, underline =
6
,
command =
boss.tabDegas)
# Intégration du sous-menu :
me1.add_cascade
(
label =
'impressionistes'
, underline=
0
, menu =
me2)
# Intégration du menu :
self.pein.configure
(
menu =
me1)
... et les définitions suivantes dans la classe Application() :
def
showRomanti
(
self):
self.can.create_text
(
245
, 70
, anchor =
NE, text =
"E. Delacroix"
,
font =(
'Times'
, 20
, 'bold italic'
), fill =
'blue'
)
def
tabMonet
(
self):
self.can.create_text
(
10
, 100
, anchor =
NW, text =
'Nymphéas à Giverny'
,
font =(
'Technical'
, 20
), fill =
'red'
)
def
tabRenoir
(
self):
self.can.create_text
(
10
, 130
, anchor =
NW,
text =
'Le moulin de la galette'
,
font =(
'Dom Casual BT'
, 20
), fill =
'maroon'
)
def
tabDegas
(
self):
self.can.create_text
(
10
, 160
, anchor =
NW, text =
'Danseuses au repos'
,
font =(
'President'
, 20
), fill =
'purple'
)
16-G-4-A. Analyse du script▲
Vous pouvez réaliser aisément des menus en cascade, en enchaînant des sous-menus les uns aux autres jusqu'à un niveau quelconque (il vous est cependant déconseillé d'aller au-delà de 5 niveaux successifs : vos utilisateurs s'y perdraient).
Un sous-menu est défini comme un menu « esclave » du menu de niveau précédent (dans notre exemple, me2 est défini comme un menu « esclave » de me1). L'intégration est assurée ensuite à l'aide de la méthode add_cascade().
L'un des items est désactivé (option state = DISABLED). L'exemple suivant vous montrera comment vous pouvez activer ou désactiver à volonté des items, par programme.
16-G-5. Ajout de la rubrique Options▲
La définition de cette rubrique est un peu plus compliquée, parce que nous allons y intégrer l'utilisation de variables internes à tkinter.
Les fonctionnalités de ce menu sont cependant beaucoup plus élaborées : les options ajoutées permettent en effet d'activer ou de désactiver à volonté les rubriques « Musiciens » et « Peintres », et vous pouvez également modifier à volonté l'aspect de la barre de menus elle-même.
Veuillez donc ajouter les lignes suivantes dans le constructeur de la classe MenuBar() :
##### Menu <Options> #####
optMenu =
Menubutton
(
self, text =
'Options'
)
optMenu.pack
(
side =
LEFT, padx =
'3'
)
# Variables tkinter :
self.relief =
IntVar
(
)
self.actPein =
IntVar
(
)
self.actMusi =
IntVar
(
)
# Partie "déroulante" du menu :
self.mo =
Menu
(
optMenu)
self.mo.add_command
(
label =
'Activer :'
, foreground =
'blue'
)
self.mo.add_checkbutton
(
label =
'musiciens'
,
command =
self.choixActifs, variable =
self.actMusi)
self.mo.add_checkbutton
(
label =
'peintres'
,
command =
self.choixActifs, variable =
self.actPein)
self.mo.add_separator
(
)
self.mo.add_command
(
label =
'Relief :'
, foreground =
'blue'
)
for
(
v, lab) in
[(
0
,'aucun'
), (
1
,'sorti'
), (
2
,'rentré'
),
(
3
,'sillon'
), (
4
,'crête'
), (
5
,'bordure'
)]:
self.mo.add_radiobutton
(
label =
lab, variable =
self.relief,
value =
v, command =
self.reliefBarre)
# Intégration du menu :
optMenu.configure
(
menu =
self.mo)
... ainsi que les définitions de méthodes suivantes (toujours dans la classe MenuBar()) :
def
reliefBarre
(
self):
choix =
self.relief.get
(
)
self.configure
(
relief =
[FLAT,RAISED,SUNKEN,GROOVE,RIDGE,SOLID][choix])
def
choixActifs
(
self):
p =
self.actPein.get
(
)
m =
self.actMusi.get
(
)
self.pein.configure
(
state =
[DISABLED, NORMAL][p])
self.musi.configure
(
state =
[DISABLED, NORMAL][m])
16-G-6. Menu avec cases à cocher▲
Notre nouveau menu déroulant comporte deux parties. Afin de bien les mettre en évidence, nous avons inséré une ligne de séparation ainsi que deux « faux items » (« Activer : » et « Relief : ») qui servent simplement de titres. Nous faisons apparaître ceux-ci en couleur pour que l'utilisateur ne les confonde pas avec de véritables commandes.
Les items de la première partie sont dotées de « cases à cocher ». Lorsque l'utilisateur effectue un clic de souris sur l'un ou l'autre de ces items, les options correspondantes sont activées ou désactivées, et ces états « actif / inactif » sont affichés sous la forme d'une encoche. Les instructions qui servent à mettre en place ce type de rubrique sont assez explicites. Elles présentent en effet ces items comme des widgets de type chekbutton :
self.mo.add_checkbutton
(
label =
'musiciens'
, command =
choixActifs,
variable =
mbu.me1.music)
Il est important de comprendre ici que ce type de widget comporte nécessairement une variable interne, destinée à mémoriser l'état « actif / inactif » du widget. Comme nous l'avons déjà expliqué plus haut, cette variable ne peut pas être une variable Python ordinaire, parce que les classes de la bibliothèque tkinter sont écrites dans un autre langage. Et par conséquent, on ne pourra accéder à une telle variable interne qu'à travers un objet-interface, que nous appellerons variable tkinter pour simplifier(84).
C'est ainsi que dans notre exemple, nous utilisons la classe tkinterIntVar() pour créer des objets équivalents à des variables de type entier.
- Nous instancions donc un de ces objets-variables, que nous mémorisons comme attribut d'instance : self.actMusi
=IntVar().
Après cette affectation, l'objet référencé dans self.actMusi contient désormais l'équivalent d'une variable de type entier, dans un format spécifique à tkinter. - Il faut ensuite associer l'option variable de l'objet checkbutton à la variable tkinter ainsi définie :
self.mo.add_checkbutton(label ='musiciens', variable =self.actMusi). - Il est nécessaire de procéder ainsi en deux étapes, parce que tkinter ne peut pas directement assigner des valeurs aux variables Python.
Pour une raison similaire, il n'est pas possible à Python de lire directement le contenu d'une variable tkinter. Il faut utiliser pour cela les
méthodes spécifiques de cette classe d'objets : la méthode get() pour lire, et la méthode set() pour
écrire :
m = self.actMusi.get().
Dans cette instruction, nous affectons à m (variable ordinaire de Python) le contenu de la variable tkinter self.actMusi (laquelle est elle-même associée à un widget bien déterminé).
Tout ce qui précède peut vous paraître un peu compliqué. Considérez simplement qu'il s'agit de votre première rencontre avec les problèmes d'interfaçage entre deux langages de programmation différents, utilisés ensemble dans un projet composite.
16-G-7. Menu avec choix exclusifs▲
La deuxième partie du menu « Options » permet à l'utilisateur de choisir l'aspect que prendra la barre de menus, parmi six possibilités. Il va de soi que l'on ne peut activer qu'une seule de ces possibilités à la fois. Pour mettre en place ce genre de fonctionnalité, on fait classiquement appel appel à des widgets de type « boutons radio ». La caractéristique essentielle de ces widgets est que plusieurs d'entre eux doivent être associés à une seule et même variable tkinter. À chaque bouton radio correspond alors une valeur particulière, et c'est cette valeur qui est affectée à la variable lorsque l'utilisateur sélectionne le bouton.
Ainsi, l'instruction :
self.mo.add_radiobutton
(
label =
'sillon'
, variable =
self.relief,
value =
3
, command =
self.reliefBarre)
configure un item du menu « Options » de telle manière qu'il se comporte comme un bouton radio.
Lorsque l'utilisateur sélectionne cet item, la valeur 3 est affectée à la variable tkinterself.relief (celle-ci étant désignée à l'aide de l'option variable du widget), et un appel est lancé en direction de la méthode reliefBarre(). Celle-ci récupère alors la valeur mémorisée dans la variable tkinter pour effectuer son travail.
Dans le contexte particulier de ce menu, nous souhaitons proposer 6 possibilités différentes à l'utilisateur. Il nous faut donc six « boutons radio », pour lesquels nous pourrions encoder six instructions similaires à celle que nous avons reproduite ci-dessus, chacune d'elles ne différant des cinq autres que par ses options value et label. Dans une situation de ce genre, la bonne pratique de programmation consiste à placer les valeurs de ces options dans une liste, et à parcourir ensuite cette liste à l'aide d'une boucle for, afin d'instancier les widgets avec une instruction commune :
for
(
v, lab) in
[(
0
,'aucun'
), (
1
,'sorti'
), (
2
,'rentré'
),
(
3
,'sillon'
), (
4
,'crête'
), (
5
,'bordure'
)]:
self.mo.add_radiobutton
(
label =
lab, variable =
self.relief,
value =
v, command =
self.reliefBarre)
La liste utilisée est une liste de 6 tuples (valeur, libellé). À chacune des 6 itérations de la boucle, un nouvel item radiobutton est instancié, dont les options label et value sont extraites de la liste par l'intermédiaire des variables lab et v.
Dans vos projets personnels, il vous arrivera fréquemment de constater que vous pouvez ainsi remplacer des suites d'instructions similaires par une structure de programmation plus compacte (en général, la combinaison d'une liste et d'une boucle, comme dans l'exemple ci-dessus).
Vous découvrirez petit à petit encore d'autres techniques pour alléger votre code : nous en fournissons un exemple dans le paragraphe suivant. Tâchez cependant de garder à l'esprit cette règle essentielle : un bon programme doit avant tout rester très lisible et bien commenté.
16-G-8. Contrôle du flux d'exécution à l'aide d'une liste▲
Veuillez à présent considérer la définition de la méthode reliefBarre().
À la première ligne, la méthode get() nous permet de récupérer l'état d'une variable tkinter qui contient le numéro du choix opéré par l'utilisateur dans le sous-menu « Relief : ».
À la seconde ligne, nous utilisons le contenu de la variable choix pour extraire d'une liste de six éléments celui qui nous intéresse. Par exemple, si choix contient la valeur 2, c'est l'option SUNKEN qui sera utilisée pour reconfigurer le widget.
La variable choix est donc utilisée ici comme un index, servant à désigner un élément de la liste. En lieu et place de cette construction compacte, nous aurions pu programmer une série de tests conditionnels, comme :
if
choix ==
0
:
self.configure
(
relief =
FLAT)
elif
choix ==
1
:
self.configure
(
relief =
RAISED)
elif
choix ==
2
:
self.configure
(
relief =
SUNKEN)
...
etc.
D'un point de vue strictement fonctionnel, le résultat serait exactement le même. Vous admettrez cependant que la construction que nous avons choisie est d'autant plus efficace que le nombre de possibilités de choix est élevé. Imaginez par exemple que l'un de vos programmes personnels doive effectuer une sélection dans un très grand nombre d'éléments : avec une construction du type ci-dessus, vous seriez peut-être amené à encoder plusieurs pages de elif !
Nous utilisons encore la même technique dans la méthode choixActifs(). Ainsi l'instruction :
self.pein.configure
(
state =
[DISABLED, NORMAL][p])
utilise le contenu de la variable p comme index pour désigner lequel des deux états DISABLED, NORMAL doit être sélectionné pour reconfigurer le menu « Peintres ».
Lorsqu'elle est appelée, la méthode choixActifs() reconfigure donc les deux rubriques « Peintres » et « Musiciens » de la barre de menus, pour les faire apparaître « normales » ou « désactivées » en fonction de l'état des variables m et p, lesquelles sont elles-mêmes le reflet de variables tkinter.
Ces variables intermédiaires m et p ne servent en fait qu'à clarifier le script. Il serait en effet parfaitement possible de les éliminer, et de rendre le script encore plus compact, en utilisant la composition d'instructions. On pourrait par exemple remplacer les deux instructions :
m =
self.actMusi.get
(
)
self.musi.configure
(
state =
[DISABLED, NORMAL][m])
par une seule, telle que :
self.musi.configure
(
state =
[DISABLED, NORMAL][self.actMusi.get
(
)])
Notez cependant que ce que l'on gagne en compacité peut se payer d'une certaine perte de lisibilité.
16-G-9. Pré-sélection d'une rubrique▲
Pour terminer cet exercice, voyons encore comment vous pouvez déterminer à l'avance certaines sélections, ou bien les modifier par programme.
Veuillez donc ajouter l'instruction suivante dans le constructeur de la classe Application() (juste avant l'instruction self.pack(), par exemple) :
mBar.mo.invoke
(
2
)
Lorsque vous exécutez le script ainsi modifié, vous constatez qu'au départ la rubrique « Musiciens » de la barre de menus est active, alors que la rubrique « Peintres » ne l'est pas. Programmées comme elles le sont, ces deux rubriques devraient être actives toutes deux par défaut. Et c'est effectivement ce qui se passe si nous supprimons l'instruction mBar.mo.invoke(2).
Nous vous avons suggéré d'ajouter cette instruction au script pour vous montrer comment vous pouvez effectuer par programme la même opération que celle que l'on obtient normalement avec un clic de souris.
L'instruction ci-dessus invoque le widget mBar.mo en actionnant la commande associée au deuxième item de ce widget. En consultant le listing, vous pouvez vérifier que ce deuxième item est bien l'objet de type checkbutton qui active/désactive le menu « Peintres » (rappelons encore une fois que l'on numérote toujours à partir de zéro).
Au démarrage du programme, tout se passe donc comme si l'utilisateur effectuait tout de suite un premier clic sur la rubrique « Peintres » du menu « Options », ce qui a pour effet de désactiver le menu correspondant.
Exercice
.Perfectionnez le widget « combo box simplifié » décrit à la page , de manière à ce que la liste soit cachée au départ, et qu'un petit bouton à droite du champ d'entrée en provoque l'apparition. Vous devrez pour ce faire placer la liste et son ascenseur dans une fenêtre satellite sans bordure (Cf. widget Toplevel, page ), positionner celle-ci correctement (il vous faudra probablement consulter les sites web traitant de Tkinter pour trouver les informations nécessaires, mais cela fait partie de votre apprentissage !), et vous assurer que cette fenêtre disparaisse après que l'utilisateur ait sélectionné un item dans la liste.