Jusqu'à présent, nous avons utilisé Python exclusivement « en mode texte ». Nous avons procédé ainsi parce qu'il nous fallait absolument d'abord dégager un certain nombre de concepts élémentaires ainsi que la structure de base du langage, avant d'envisager des expériences impliquant des objets informatiques plus élaborés (fenêtres, images, sons, etc.). Nous pouvons à présent nous permettre une petite incursion dans le vaste domaine des interfaces graphiques, mais ce ne sera qu'un premier amuse-gueule : il nous reste en effet encore bien des choses fondamentales à apprendre, et pour nombre d'entre elles l'approche textuelle reste la plus abordable.
Interfaces graphiques (GUI).
Si vous ne le saviez pas encore, apprenez dès à présent que le domaine des interfaces graphiques (ou "GUI : Graphical User Interface") est extrêmement complexe. Chaque système d'exploitation peut en effet proposer plusieurs « bibliothèques » de fonctions graphiques de base, auxquelles viennent fréquemment s'ajouter de nombreux compléments, plus ou moins spécifiques de langages de programmation particuliers. Tous ces composants sont généralement présentés comme des "classes d'objets", dont il vous faudra étudier les "attributs" et les "méthodes".
Avec Python, la bibliothèque graphique la plus utilisée jusqu'à présent est la bibliothèque "Tkinter", qui est une adaptation de la bibliothèque "Tk" développée à l'origine pour le langage "Tcl". Plusieurs autres bibliothèques graphiques fort intéressantes ont été proposées pour Python : "wxPython", "pyQT", "pyGTK", etc. Il existe également des possibilités d'utiliser les bibliothèques de "widgets Java" et les "MFC" de "Windows".
Dans le cadre de ces notes, nous nous limiterons cependant à "Tkinter", dont il existe fort heureusement des versions similaires (et gratuites) pour les plates-formes Linux, Windows et Mac.
Premiers pas avec Tkinter.
Pour la suite des explications, nous supposerons bien évidemment que le module "Tkinter" a déjà été installé sur votre système. Pour pouvoir en utiliser les fonctionnalités dans un script Python, il faut que l'une des premières lignes de ce script contienne l'instruction d'importation :
from Tkinter import *
Attention, suivant les versions de Python (a priori à partir de la version 3) l'appel du module Tkinter se fait avec un t minuscule :
from tkinter import *
Comme toujours sous Python, il n'est même pas nécessaire d'écrire un script. Vous pouvez faire un grand nombre d'expériences directement à la ligne de commande, en ayant simplement lancé Python en mode interactif. Dans l'exemple qui suit, nous allons créer une fenêtre très simple, et y ajouter deux "widgets" typiques : un bout de texte (ou "label") et un bouton (ou "button").
»> from Tkinter import *
»> fen1 = Tk()
»> tex1 = Label(fen1, text='Bonjour tout le monde !', fg='red')
»> tex1.pack()
»> bou1 = Button(fen1, text='Quitter', command = fen1.destroy)
»> bou1.pack()
»> fen1.mainloop()
Note : Suivant la version de Python utilisée, vous verrez déjà apparaître la fenêtre d'application immédiatement après avoir entré la deuxième commande de cet exemple, ou bien seulement après la septième.
Examinons à présent plus en détail chacune des lignes de commandes exécutées : 
Programmes pilotés par des événements.
Vous venez d'expérimenter votre premier programme utilisant une interface graphique. Ce type de programme est structuré d'une manière différente des scripts « textuels » étudiés auparavant.
Tous les programmes d'ordinateur comportent grosso-modo trois phases principales : "une phase d'initialisation", laquelle contient les instructions qui préparent le travail à effectuer (appel des modules externes nécessaires, ouverture de fichiers, connexion à un serveur de bases de données ou à l'internet, etc.), "une phase centrale" où l'on trouve la véritable fonctionnalité du programme (c'est-à-dire tout ce qu'il est censé faire : afficher des données à l'écran, effectuer des calculs, modifier le contenu d'un fichier, imprimer, etc.), et enfin "une phase de terminaison" qui sert à clôturer « proprement » les opérations (c'est-à-dire fermer les fichiers restés ouverts, couper les connexions externes, etc.)
Dans un programme « en mode texte », ces trois phases sont simplement organisées suivant un schéma linéaire comme dans l'illustration ci-contre. En conséquence, ces programmes se caractérisent par une interactivité très limitée avec l'utilisateur. Celui-ci ne dispose pratiquement d'aucune liberté : il lui est demandé de temps à autre d'entrer des données au clavier, mais toujours dans un ordre prédéterminé correspondant à la séquence d'instructions du programme.
Dans le cas d'un programme qui utilise une interface graphique, par contre, l'organisation interne est différente. On dit d'un tel programme qu'il est "piloté par les événements". Après sa phase d'initialisation, un programme de ce type se met en quelque sorte « en attente », et passe la main à un autre logiciel, lequel est plus ou moins intimement intégré au système d'exploitation de l'ordinateur et « tourne » en permanence.
Ce "réceptionnaire d'événements" scrute sans cesse tous les périphériques (clavier, souris, horloge, modem, etc.) et réagit immédiatement lorsqu'un événement y est détecté.
Un tel événement peut être une action quelconque de l'utilisateur : déplacement de la souris, appui sur une touche, etc., mais aussi un événement externe ou un automatisme (top d'horloge, par ex.)
Lorsqu'il détecte un événement, le réceptionnaire envoie un message spécifique au programme, lequel doit être conçu pour réagir en conséquence.
La phase d'initialisation d'un programme utilisant une interface graphique comporte un ensemble d'instructions qui mettent en place les divers composants interactifs de cette interface (fenêtres, boutons, cases à cocher, etc.). D'autres instructions définissent les messages d'événements qui devront être pris en charge : on peut en effet décider que le programme ne réagira qu'à certains événements en ignorant tous les autres.
Alors que dans un programme « textuel », la phase centrale est constituée d'une suite d'instructions qui décrivent à l'avance l'ordre dans lequel la machine devra exécuter ses différentes tâches (même s'il est prévu des cheminements différents en réponse à certaines conditions rencontrées en cours de route), on ne trouve dans la phase centrale d'un programme avec interface graphique qu'un ensemble de fonctions indépendantes. Chacune de ces fonctions est appelée spécifiquement lorsqu'un événement particulier est détecté par le système d'exploitation : elle effectue alors le travail que l'on attend du programme en réponse à cet événement, et rien d'autre.
Il est important de bien comprendre ici que pendant tout ce temps, le réceptionnaire continue à « tourner » et à guetter l'apparition d'autres événements éventuels.
S'il arrive d'autres événements, il peut donc se faire qu'une seconde fonction (ou une 3e, une 4e, ...) soit activée et commence à effectuer son travail « en parallèle » avec la première qui n'a pas encore terminé le sien.
En particulier, la même fonction peut être appelée plusieurs fois en réponse à l'occurrence de quelques événements identiques, la même tâche étant alors effectuée en plusieurs exemplaires concurrents.
Les systèmes d'exploitation et les langages modernes permettent en effet ce parallélisme que l'on appelle aussi "multitâche".
Au chapitre précédent de ces notes, nous vous avons déjà fait remarquer que la structure du texte d'un programme n'indique pas directement l'ordre dans lequel les instructions seront finalement exécutées. Cette remarque s'applique encore bien davantage dans le cas d'un programme avec interface graphique, puisque l'ordre dans lequel les fonctions sont appelées n'est plus inscrit nulle part dans le programme. Ce sont les événements qui pilotent !
Tout ceci doit vous paraître un peu compliqué. Nous allons l'illustrer dans quelques exemples.
Exemple graphique : tracé de lignes dans un canevas.
Le script décrit ci-dessous crée une fenêtre comportant trois boutons et un "canevas". Suivant la terminologie de "Tkinter", un canevas est une surface rectangulaire délimitée, dans laquelle on peut installer ensuite divers dessins et images à l'aide de méthodes spécifiques.
Lorsque l'on actionne le bouton « Tracer une ligne », une nouvelle ligne colorée apparaît sur le canevas, avec à chaque fois une inclinaison différente de la précédente.
Si l'on actionne le bouton « Autre couleur », une nouvelle couleur est tirée au hasard dans une série limitée. Cette couleur est celle qui s'appliquera aux tracés suivants.
Le bouton « Quitter » sert bien évidemment à terminer l'application en refermant la fenêtre.
from Tkinter import *
from random import randrange
def drawline():
 "Tracé d'une ligne dans le canevas can1"
 global x1, y1, x2, y2, coul
 can1.create_line(x1,y1,x2,y2,width=2,fill=coul)
 # modification des coordonnées pour la ligne suivante :
 y2, y1 = y2+10, y1-10
def changecolor():
 "Changement aléatoire de la couleur du tracé"
 global coul
 pal=['purple','cyan','maroon','green','red','blue','orange','yellow']
 c = randrange(8) # => génère un nombre aléatoire de 0 à 7
 coul = pal[c]
x1, y1, x2, y2 = 10, 190, 190, 10 # coordonnées de la ligne
coul = 'dark green' # couleur de la ligne
fen1 = Tk()
can1 = Canvas(fen1,bg='dark grey',height=200,width=200)
can1.pack(side=LEFT)
bou1 = Button(fen1,text='Quitter',command=fen1.quit)
bou1.pack(side=BOTTOM)
bou2 = Button(fen1,text='Tracer une ligne',command=drawline)
bou2.pack()
bou3 = Button(fen1,text='Autre couleur',command=changecolor)
bou3.pack()
fen1.mainloop() # démarrage du réceptionnaire d'événements
fen1.destroy() # destruction (fermeture) de la fenêtre
Conformément à ce que nous avons expliqué dans le texte des pages précédentes, la fonctionnalité de ce programme est essentiellement assurée par les deux fonctions codice_1 et codice_2, qui seront activées par des événements, ceux-ci étant eux-mêmes définis dans la phase d'initialisation.
Dans cette phase d'initialisation, on commence par importer l'intégralité du module "Tkinter" ainsi qu'une fonction du module "random" qui permet de tirer des nombres au hasard. On crée ensuite les différents "widgets" par instanciation à partir des classes codice_3, codice_4 et codice_5. Remarquons au passage que le mot canevas s'écrit différemment en français et en anglais !
L'initialisation se termine avec l'instruction codice_6 qui démarre le réceptionnaire d'événements. Les instructions qui suivent ne seront exécutées qu'à la sortie de cette boucle, sortie elle-même déclenchée par la méthode codice_7 (voir ci-après).
L'option codice_8 utilisée dans l'instruction d'instanciation des boutons permet de désigner la fonction qui devra être appelée lorsqu'un événement « clic gauche de la souris sur le "widget" » se produira. Il s'agit en fait d'un raccourci pour cet événement particulier, qui vous est proposé par "Tkinter" pour votre facilité parce que cet événement est celui que l'on associe naturellement à un "widget" de type bouton.
Les fonctions de ce script peuvent modifier les valeurs de certaines variables qui ont été définies au niveau principal du programme. Cela est rendu possible grâce à l'instruction global utilisée dans la définition de ces fonctions. Nous nous permettrons de procéder ainsi pendant quelque temps encore (ne serait-ce que pour vous habituer à distinguer les comportements des variables locales et globales), mais cette pratique n'est pas tout à fait recommandable, surtout lorsqu'il s'agit d'écrire de grands programmes.
Dans notre fonction codice_2, une couleur est choisie au hasard dans une liste. Nous utilisons pour ce faire la fonction codice_10 importée du module "random". Appelée avec un argument N, cette fonction renvoie un nombre entier, tiré au hasard entre zéro et N-1.
La commande liée au bouton « Quitter » appelle la méthode codice_11 de la fenêtre codice_12. Cette méthode sert à fermer (quitter) le réceptionnaire d'événements ("mainloop") associé à cette fenêtre. Lorsque cette méthode est activée, l'exécution du programme se poursuit avec les instructions qui suivent l'appel de "mainloop". Dans notre exemple, cela consiste donc à effacer ("destroy") la fenêtre.
Exemple graphique : deux dessins alternés.
Cet autre exemple vous montrera comment vous pouvez exploiter les connaissances que vous avez acquises précédemment concernant les boucles, les listes et les fonctions, afin de réaliser de nombreux dessins avec seulement quelques lignes de code. Il s'agit d'une petite application qui affiche l'un ou l'autre des deux dessins reproduits ci-contre, en fonction du bouton choisi.
from Tkinter import *
def cercle(x, y, r, coul ='black'):
 "tracé d'un cercle de centre (x,y) et de rayon r"
 can.create_oval(x-r, y-r, x+r, y+r, outline=coul)
def figure_1():
 "dessiner une cible"
 # Effacer d'abord tout dessin préexistant :
 can.delete(ALL)
 # tracer les deux lignes (vert. et horiz.) :
 can.create_line(100, 0, 100, 200, fill ='blue')
 can.create_line(0, 100, 200, 100, fill ='blue')
 # tracer plusieurs cercles concentriques :
 rayon = 15
 while rayon < 100:
 cercle(100, 100, rayon)
 rayon += 15 
def figure_2():
 "dessiner un visage simplifié"
 # Effacer d'abord tout dessin préexistant :
 can.delete(ALL)
 # Les caractéristiques de chaque cercle sont
 # placées dans une liste de listes :
 cc =100, 100, 80, 'red'], # visage
 [70, 70, 15, 'blue'], # yeux
 [130, 70, 15, 'blue'], 
 [70, 70, 5, 'black'], 
 [130, 70, 5, 'black'],
 [44, 115, 20, 'red'], # joues
 [156, 115, 20, 'red'],
 [100, 95, 15, 'purple'], # nez
 [100, 145, 30, 'purple' # bouche
 # on trace tous les cercles à l'aide d'une boucle :
 i =0
 while i < len(cc): # parcours de la liste
 el = cc[i] # chaque élément est lui-même une liste
 cercle(el[0], el[1], el[2], el[3])
 i += 1
fen = Tk()
can = Canvas(fen, width =200, height =200, bg ='ivory')
can.pack(side =TOP, padx =5, pady =5)
b1 = Button(fen, text ='dessin 1', command =figure_1)
b1.pack(side =LEFT, padx =3, pady =3)
b2 = Button(fen, text ='dessin 2', command =figure_2)
b2.pack(side =RIGHT, padx =3, pady =3)
fen.mainloop()
Commençons par analyser le programme principal, à la fin du script :
Nous y créons une fenêtre, par instanciation d'un objet de la classe codice_3 dans la variable codice_29.
Ensuite, nous installons 3 widgets dans cette fenêtre : un canevas et deux boutons. Le canevas est instancié dans la variable codice_30, et les deux boutons dans les variables codice_31 et codice_32. Comme dans le script précédent, les widgets sont mis en place dans la fenêtre à l'aide de leur méthode codice_33, mais cette fois nous utilisons celle-ci avec des options :
Les boutons commandent l'affichage des deux dessins, en invoquant les fonctions codice_41 et codice_42. Considérant que nous aurions à tracer un certain nombre de cercles dans ces dessins, nous avons estimé qu'il serait bien utile de définir d'abord une fonction codice_43 spécialisée. En effet : Vous savez probablement déjà que le canevas "Tkinter" est doté d'une méthode codice_44 qui permet de dessiner des ellipses quelconques (et donc aussi des cercles), mais cette méthode doit être invoquée avec quatre arguments qui seront les coordonnées des coins supérieur gauche et inférieur droit d'un rectangle fictif, dans lequel l'ellipse viendra alors s'inscrire. Cela n'est pas très pratique dans le cas particulier du cercle : il nous semblera plus naturel de commander ce tracé en fournissant les coordonnées de son centre ainsi que son rayon. C'est ce que nous obtiendrons avec notre fonction codice_43, laquelle invoque la méthode codice_44 en effectuant la conversion des coordonnées. Remarquez que cette fonction attend un argument facultatif en ce qui concerne la couleur du cercle à tracer (noir par défaut).
L'efficacité de cette approche apparaît clairement dans la fonction codice_41, ou nous trouvons une simple boucle de répétition pour dessiner toute la série de cercles (de même centre et de rayon croissant). Notez au passage l'utilisation de l'opérateur += qui permet d'incrémenter une variable (dans notre exemple, la variable r voit sa valeur augmenter de 15 unités à chaque itération).
Le second dessin est un peu plus complexe, parce qu'il est composé de cercles de tailles variées centrés sur des points différents. Nous pouvons tout de même tracer tous ces cercles à l'aide d'une seule boucle de répétition, si nous mettons à profit nos connaissances concernant les listes.
En effet. Ce qui différencie les cercles que nous voulons tracer tient en quatre caractéristiques : coordonnées x et y du centre, rayon et couleur. Pour chaque cercle, nous pouvons placer ces quatre caractéristiques dans une petite liste, et rassembler toutes les petites listes ainsi obtenues dans une autre liste plus grande. Nous disposerons ainsi d'une "liste de listes", qu'il suffira ensuite de parcourir à l'aide d'une boucle pour effectuer les tracés correspondants.
Exemple graphique : calculatrice minimaliste.
Bien que très court, le petit script ci-dessous implémente une calculatrice complète, avec laquelle vous pourrez même effectuer des calculs comportant des parenthèses et des fonctions scientifiques. N'y voyez rien d'extraordinaire. Toute cette fonctionnalité n'est qu'une conséquence du fait que vous utilisez un interpréteur plutôt qu'un compilateur pour exécuter vos programmes.
Comme vous le savez, le compilateur n'intervient qu'une seule fois, pour traduire l'ensemble de votre code source en un programme exécutable. Son rôle est donc terminé "avant" même l'exécution du programme. L'interpréteur, quant à lui, est toujours actif "pendant" l'exécution du programme, et donc tout à fait disponible pour traduire un nouveau code source quelconque, comme par exemple une expression mathématique entrée au clavier par l'utilisateur.
Les langages interprétés disposent donc toujours de fonctions permettant d'évaluer une chaîne de caractères comme une suite d'instructions du langage lui-même. Il devient alors possible de construire en peu de lignes des structures de programmes très dynamiques. Dans l'exemple ci-dessous, nous utilisons la fonction intégrée codice_48 pour analyser l'expression mathématique entrée par l'utilisateur dans le champ prévu à cet effet, et nous n'avons plus ensuite qu'à afficher le résultat.
from Tkinter import *
from math import *
def evaluer(event):
 chaine.configure(text = "Résultat = " + str(eval(entree.get())))
fenetre = Tk()
entree = Entry(fenetre)
entree.bind("<Return>", evaluer)
chaine = Label(fenetre)
entree.pack()
chaine.pack()
fenetre.mainloop()
Au début du script, nous commençons par importer les modules "Tkinter" et "math", ce dernier étant nécessaire afin que la dite calculatrice puisse disposer de toutes les fonctions mathématiques et scientifiques usuelles : sinus, cosinus, racine carrée, etc.
Ensuite nous définissons une fonction codice_49, qui sera en fait la commande exécutée par le programme lorsque l'utilisateur actionnera la touche "Return" (ou "Enter") après avoir entré une expression mathématique quelconque dans le champ d'entrée décrit plus loin.
Cette fonction utilise la méthode codice_50 du widget codice_51, pour modifier son attribut text. L'attribut en question reçoit donc ici une nouvelle valeur, déterminée par ce que nous avons écrit à la droite du signe égale : il s'agit en l'occurrence d'une chaîne de caractères construite dynamiquement, à l'aide de deux fonctions intégrées dans Python : codice_48 et codice_53, et d'une méthode associée à un widget "Tkinter" : la méthode codice_54.
codice_48 fait appel à l'interpréteur pour évaluer une expression Python qui lui est transmise dans une chaîne de caractères. Le résultat de l'évaluation est fourni en retour. Exemple :
chaine = "(25 + 8)/3" # chaîne contenant une expression mathématique
res = eval(chaine) # évaluation de l'expression contenue dans la chaîne 
print res +5 # => le contenu de la variable res est numérique
codice_53 transforme une expression numérique en chaîne de caractères. Nous devons faire appel à cette fonction parce que la précédente renvoie une valeur numérique, que nous convertissons à nouveau en chaîne de caractères pour pouvoir l'incorporer au message « Résultat = ».
codice_54 est une méthode associée aux widgets de la classe Entry. Dans notre petit programme exemple, nous utilisons un widget de ce type pour permettre à l'utilisateur d'entrer une expression numérique quelconque à l'aide de son clavier. La méthode codice_54 permet en quelque sorte « d'extraire » du widget « entree » la chaîne de caractères qui lui a été fournie par l'utilisateur.
Le corps du programme principal contient la phase d'initialisation, qui se termine par la mise en route de l'observateur d'événements ("mainloop"). On y trouve l'instanciation d'une fenêtre codice_3, contenant un widget « chaine » instancié à partir de la classe codice_60, et un widget « entree » instancié à partir de la classe codice_61.
Attention, à présent : afin que ce dernier widget puisse vraiment faire son travail, c'est-à-dire transmettre au programme l'expression que l'utilisateur y aura encodée, "nous lui associons un événement" à l'aide de la méthode codice_62 :
 entree.bind("<Return>",evaluer)
Cette instruction signifie : « Lier l'événement “pression sur la touche Return” à l'objet <entree>, le gestionnaire de cet événement étant la fonction <evaluer> ».
L'événement à prendre en charge est décrit dans une chaîne de caractères spécifique (dans notre exemple, il s'agit de la chaîne codice_63). Il existe un grand nombre de ces événements (mouvements et clics de la souris, enfoncement des touches du clavier, positionnement et redimensionnement des fenêtres, passage au premier plan, etc.). Vous trouverez la liste des chaînes spécifiques de tous ces événements dans les ouvrages de référence traitant de "Tkinter".
Profitons de l'occasion pour observer encore une fois la syntaxe des instructions destinées à mettre en œuvre une méthode associée à un objet :
objet.méthode(arguments)
On écrit d'abord le nom de l'objet sur lequel on désire intervenir, puis le point (qui fait office d'opérateur), puis le nom de la méthode à mettre en œuvre ; entre les parenthèses associées à cette méthode, on indique enfin les arguments qu'on souhaite lui transmettre.
Exemple graphique : détection et positionnement d'un clic de souris.
Dans la définition de la fonction « evaluer » de l'exemple précédent, vous aurez remarqué que nous avons fourni un argument event (entre les parenthèses).
Cet argument est obligatoire. Lorsque vous définissez une fonction gestionnaire d'événement qui est associée à un widget quelconque à l'aide de sa méthode bind(), vous devez toujours l'utiliser comme premier argument. Il s'agit d'un objet Python standard, créé automatiquement, qui permet de transmettre au gestionnaire d'événement un certain nombre d'attributs de cet événement :
Nous n'allons pas entrer dans trop de détails. Si vous voulez bien encoder et expérimenter le petit script ci-dessous, vous aurez vite compris le principe.
from Tkinter import *
def pointeur(event):
 chaine.configure(text = "Clic détecté en X =" + str(event.x) +\
 ", Y =" + str(event.y))
fen = Tk()
cadre = Frame(fen, width =200, height =150, bg="light yellow")
cadre.bind("<Button-1>", pointeur)
cadre.pack()
chaine = Label(fen)
chaine.pack()
fen.mainloop()
Le script fait apparaître une fenêtre contenant un codice_64 ("frame") rectangulaire de couleur jaune pâle, dans lequel l'utilisateur est invité à effectuer des clics de souris.
La méthode codice_62 du widget cadre associe l'événement "<clic à l'aide du premier bouton de la souris>" au gestionnaire d'événement « pointeur ».
Ce gestionnaire d'événement peut utiliser les attributs x et y de l'objet event généré automatiquement par Python, pour construire la chaîne de caractères qui affichera la position de la souris au moment du clic.
def avance(event):
 can1.coords(oval,event.x-r,event.y-r,event.x+r,event.y+r)
x,y,r=10,10,10
def oval(x,y,r,coul):
 can1.create_oval(x-r,y-r,x+r,y+r,outline=coul)
fen1=Tk()
fen1.title("Exercice d'animation avec Tkinter")
can1=Canvas(fen1,bg='dark grey',height=300,width=300)
oval=can1.create_oval(x,y,x+30,y+30,width=2,fill='red')
can1.bind("<Button-1>",avance)
can1.pack()
can1.pack(side=LEFT)
Button(fen1,text='Quitter',command=fen1.quit).pack(side=BOTTOM)
fen1.mainloop()
fen1.destroy()
Les classes de widgets Tkinter.
Il existe 15 classes de base pour les widgets Tkinter :
Ces classes de widgets intègrent chacune un grand nombre de méthodes et on peut aussi leur associer (lier) des événements. En outre, tous ces widgets peuvent être positionnés dans les fenêtres à l'aide de trois méthodes différentes : la méthode codice_66, la méthode codice_33 et la méthode codice_68.
L'utilité de ces méthodes apparaît clairement lorsque l'on s'efforce de réaliser des programmes "portables" (c'est-à-dire susceptibles de fonctionner indifféremment sur des systèmes d'exploitation aussi différents que Unix, MacOS ou Windows), et dont les fenêtres soient redimensionnables.
Utilisation de la méthode grid() pour contrôler la disposition des widgets.
Jusqu'à présent, nous avons toujours disposé les widgets dans leur fenêtre, à l'aide de la méthode codice_33. Cette méthode présentait l'avantage d'être extraordinairement simple, mais elle ne nous donnait pas beaucoup de liberté pour disposer les widgets à notre guise. Comment faire, par exemple, pour obtenir la fenêtre ci-contre ?
Nous pourrions effectuer un certain nombre de tentatives en fournissant à la méthode codice_33 des arguments de type « side = », comme nous l'avons déjà fait précédemment, mais cela ne nous mène pas très loin. Essayons par exemple :
from Tkinter import *
fen1 = Tk()
txt1 = Label(fen1, text = 'Premier champ :')
txt2 = Label(fen1, text = 'Second :')
entr1 = Entry(fen1)
entr2 = Entry(fen1)
txt1.pack(side =LEFT)
txt2.pack(side =LEFT)
entr1.pack(side =RIGHT)
entr2.pack(side =RIGHT)
fen1.mainloop()
... mais le résultat n'est pas vraiment celui que nous recherchions !!! :
Pour mieux comprendre comment fonctionne la méthode codice_33, vous pouvez encore essayer différentes combinaisons d'options, telles que side codice_72, side codice_73, pour chacun de ces quatre widgets. Mais vous n'arriverez certainement pas à obtenir ce qui vous a été demandé. Vous pourriez peut-être y parvenir en définissant deux widgets codice_74 supplémentaires, et en y incorporant ensuite séparément les widgets codice_60 et codice_61. Cela devient fort compliqué.
Il est temps que nous apprenions à utiliser une autre approche du problème. Veuillez donc analyser le script ci-dessous : il contient en effet (presque) la solution :
from Tkinter import *
fen1 = Tk()
txt1 = Label(fen1, text = 'Premier champ :')
txt2 = Label(fen1, text = 'Second :')
entr1 = Entry(fen1) 
entr2 = Entry(fen1)
txt1.grid(row =0)
txt2.grid(row =1)
entr1.grid(row =0, column =1)
entr2.grid(row =1, column =1)
fen1.mainloop()
Dans ce script, nous avons donc remplacé la méthode codice_33 par la méthode codice_66. Comme vous pouvez le constater, l'utilisation de la méthode codice_66 est très simple. Cette méthode considère la fenêtre comme un tableau (ou une grille). Il suffit alors de lui indiquer dans quelle ligne ("row") et dans quelle colonne ("column") de ce tableau on souhaite placer les widgets. On peut numéroter les lignes et les colonnes comme on veut, en partant de zéro, ou de un, ou encore d'un nombre quelconque : "Tkinter" ignorera les lignes et colonnes vides. Notez cependant que si vous ne fournissez aucun numéro pour une ligne ou une colonne, la valeur par défaut sera zéro.
"Tkinter" détermine automatiquement le nombre de lignes et de colonnes nécessaire. Mais ce n'est pas tout : si vous examinez en détail la petite fenêtre produite par le script ci-dessus, vous constaterez que nous n'avons pas encore tout à fait atteint le but poursuivi. Les deux chaînes apparaissant dans la partie gauche de la fenêtre sont centrées, alors que nous souhaitions les aligner l'une et l'autre par la droite. Pour obtenir ce résultat, il nous suffit d'ajouter un argument dans l'appel de la méthode codice_66 utilisée pour ces widgets. L'option codice_81 peut prendre l'une des quatre valeurs codice_82, codice_83, codice_84, codice_85 (les quatre points cardinaux en anglais). En fonction de cette valeur, on obtiendra un alignement des widgets par le haut, par le bas, par la gauche ou par la droite. Remplacez donc les deux premières instructions codice_66 du script par :
txt1.grid(row =0, sticky =E)
txt2.grid(row =1, sticky =E)
… et vous atteindrez enfin exactement le but recherché.
Analysons à présent la fenêtre suivante :.
Cette fenêtre comporte 3 colonnes : une première avec les 3 chaînes de caractères, une seconde avec les 3 champs d'entrée, et une troisième avec l'image. Les deux premières colonnes comportent chacune 3 lignes, mais l'image située dans la dernière colonne s'étale en quelque sorte sur les trois.
Le code correspondant est le suivant :
from Tkinter import *
fen1 = Tk()
txt1 = Label(fen1, text ='Premier champ :')
txt2 = Label(fen1, text ='Second :')
txt3 = Label(fen1, text ='Troisième :')
entr1 = Entry(fen1)
entr2 = Entry(fen1)
entr3 = Entry(fen1)
can1 = Canvas(fen1, width =160, height =160, bg ='white')
photo = PhotoImage(file ='Martin_P.gif')
item = can1.create_image(80, 80, image =photo)
txt1.grid(row =1, sticky =E)
txt2.grid(row =2, sticky =E)
txt3.grid(row =3, sticky =E)
entr1.grid(row =1, column =2)
entr2.grid(row =2, column =2)
entr3.grid(row =3, column =2)
can1.grid(row =1, column =3, rowspan =3, padx =10, pady =5)
fen1.mainloop()
Pour pouvoir faire fonctionner ce script, il vous faudra probablement remplacer le nom du fichier image ("Martin_P.gif") par le nom d'une image de votre choix. Attention : la bibliothèque "Tkinter" standard n'accepte qu'un petit nombre de formats pour cette image. Choisissez de préférence le format GIF.
Composition d'instructions pour écrire un code plus compact.
Du fait que Python est un langage de programmation de haut niveau, il est souvent possible (et souhaitable) de retravailler un script afin de le rendre plus compact.
Vous pouvez par exemple assez fréquemment utiliser la composition d'instructions pour appliquer la méthode de mise en page des widgets (grid(), pack() ou place()) au moment même où vous créez ces widgets. Le code correspondant devient alors un peu plus simple, et parfois plus lisible. Vous pouvez par exemple remplacer les deux lignes :
txt1 = Label(fen1, text ='Premier champ :')
txt1.grid(row =1, sticky =E)
du script précédent par une seule, telle que :
Label(fen1, text ='Premier champ :').grid(row =1, sticky =E)
Dans cette nouvelle écriture, vous pouvez constater que nous faisons l'économie de la variable intermédiaire codice_87. Nous avions utilisé cette variable pour bien dégager les étapes successives de notre démarche, mais elle n'est pas toujours indispensable. Le simple fait d'invoquer la classe codice_60 provoque en effet l'instanciation d'un objet de cette classe, même si l'on ne mémorise pas la référence de cet objet dans une variable ("Tkinter" la conserve de toute façon dans sa représentation interne de la fenêtre). Si l'on procède ainsi, la référence est perdue pour le restant du script, mais elle peut tout de même être transmise à une méthode de mise en page telle que codice_66 au moment même de l'instanciation, en une seule instruction composée. Voyons cela un peu plus en détail :
Jusqu'à présent, nous avons créé des objets divers (par instanciation à partir d'une classe quelconque), en les affectant à chaque fois à des variables. Par exemple, lorsque nous avons écrit :
txt1 = Label(fen1, text ='Premier champ :')
Nous avons créé une instance de la classe codice_60, que nous avons assignée à la variable codice_87.
La variable codice_87 peut alors être utilisée pour faire référence à cette instance, partout ailleurs dans le script, mais dans les faits nous ne l'utilisons qu'une seule fois pour lui appliquer la méthode codice_66, le widget dont il est question n'étant rien d'autre qu'une simple étiquette descriptive. Or, créer ainsi une nouvelle variable pour n'y faire référence ensuite qu'une seule fois (et directement après sa création) n'est pas une pratique très recommandable, puisqu'elle consiste à réserver inutilement un certain espace mémoire.
Lorsque ce genre de situation se présente, il est plus judicieux d'utiliser la composition d'instructions. Par exemple, on préférera le plus souvent remplacer les deux instructions :
somme = 45 + 72
print somme
par une seule instruction composée, telle que :
print 45 + 72
On fait ainsi l'économie d'une variable.
De la même manière, lorsque l'on met en place des widgets auxquels on ne souhaite plus revenir par après, comme c'est souvent le cas pour les widgets de la classe codice_60, on peut en général appliquer la méthode de mise en page (codice_66, codice_33 ou codice_68) directement au moment de la création du widget, en une seule instruction composée.
Cela s'applique seulement aux widgets qui ne sont plus référencés après qu'on les ait créés. "Tous les autres doivent impérativement être assignés à des variables", afin que l'on puisse encore interagir avec eux ailleurs dans le script.
Et dans ce cas, il faut obligatoirement utiliser deux instructions distinctes, l'une pour instancier le widget et l'autre pour lui appliquer ensuite la méthode de mise en page. Vous ne pouvez pas, par exemple, construire une instruction composée telle que :
entree = Entry(fen1).pack() # faute de programmation !!!
En apparence, cette instruction devrait instancier un nouveau widget et l'assigner à la variable entree, la mise en page s'effectuant dans la même opération à l'aide de la méthode codice_33.
Dans la réalité, cette instruction produit bel et bien un nouveau widget de la classe codice_61, et la méthode codice_33 effectue bel et bien sa mise en page dans la fenêtre, mais la valeur qui est mémorisée dans la variable entree est la valeur de retour de la méthode codice_33 : "ce n'est pas la référence du widget". Et vous ne pouvez rien faire de cette valeur de retour : il s'agit d'un objet vide : codice_102.
Pour obtenir une vraie référence du widget, vous devez utiliser deux instructions :
entree = Entry(fen1) # instanciation du widget
entree.pack() # application de la mise en page
Note : Lorsque vous utilisez la méthode codice_66, vous pouvez simplifier encore un peu votre code, en omettant l'indication de nombreux numéros de lignes et de colonnes. À partir du moment où c'est la la méthode codice_66 qui est utilisée pour positionner les widgets, "Tkinter" considère en effet qu'il existe forcément des lignes et des colonnes11. Si un numéro de ligne ou de colonne n'est pas indiqué, le widget correspondant est placé dans la première case vide disponible.
Le script ci-dessous intègre les simplifications que nous venons d'expliquer :
from Tkinter import *
fen1 = Tk()
Label(fen1, text = 'Premier champ :').grid(sticky =E)
Label(fen1, text = 'Second :').grid(sticky =E)
Label(fen1, text = 'Troisième :').grid(sticky =E)
entr1 = Entry(fen1)
entr2 = Entry(fen1) # ces widgets devront certainement
entr3 = Entry(fen1) # être référencés plus loin :
entr1.grid(row =0, column =1) # il faut donc les assigner chacun
entr2.grid(row =1, column =1) # à une variable distincte
entr3.grid(row =2, column =1)
chek1 = Checkbutton(fen1, text ='Case à cocher, pour voir')
chek1.grid(columnspan =2)
can1 = Canvas(fen1, width =160, height =160, bg ='white')
photo = PhotoImage(file ='Martin_P.gif')
can1.create_image(80,80, image =photo)
can1.grid(row =0, column =2, rowspan =4, padx =10, pady =5)
fen1.mainloop()
Modification des propriétés d'un objet - Animation.
À ce stade de votre apprentissage, vous souhaitez certainement pouvoir faire apparaître un petit dessin quelconque dans un canevas, et puis le déplacer à volonté, par exemple à l'aide de boutons.
Veuillez donc écrire, tester, puis analyser le script ci-dessous :
from Tkinter import *
def avance(gd, hb):
 global x1, y1
 x1, y1 = x1 +gd, y1 +hb
 can1.coords(oval1, x1, y1, x1+30, y1+30)
def depl_gauche():
 avance(-10, 0)
def depl_droite():
 avance(10, 0)
def depl_haut():
 avance(0, -10)
def depl_bas():
 avance(0, 10) 
x1, y1 = 10, 10 # coordonnées initiales
fen1 = Tk()
fen1.title("Exercice d'animation avec Tkinter")
can1 = Canvas(fen1,bg='dark grey',height=300,width=300)
oval1 = can1.create_oval(x1,y1,x1+30,y1+30,width=2,fill='red')
can1.pack(side=LEFT)
Button(fen1,text='Quitter',command=fen1.quit).pack(side=BOTTOM)
Button(fen1,text='Gauche',command=depl_gauche).pack()
Button(fen1,text='Droite',command=depl_droite).pack()
Button(fen1,text='Haut',command=depl_haut).pack()
Button(fen1,text='Bas',command=depl_bas).pack()
fen1.mainloop()
Le corps de ce programme reprend de nombreuses éléments connus : nous y créons une fenêtre fen1, dans laquelle nous installons un canevas contenant lui-même un cercle coloré, plus cinq boutons de contrôle. Veuillez remarquer au passage que nous n'instancions pas les widgets boutons dans des variables (c'est inutile, puisque nous n'y faisons plus référence par après) : nous devons donc appliquer la méthode codice_33 directement au moment de la création de ces objets.
La vraie nouveauté de ce programme réside dans la fonction codice_106 définie au début du script. Chaque fois qu'elle sera appelée, cette fonction redéfinira les coordonnées de l'objet « cercle coloré » que nous avons installé dans le canevas, ce qui provoquera l'animation de cet objet.
Cette manière de procéder est tout à fait caractéristique de la programmation « orientée objet » :
On commence par créer des objets, et puis "on agit sur ces objets en modifiant leurs propriétés", par l'intermédiaire de méthodes.
En programmation procédurale « à l'ancienne » (c'est-à-dire sans utilisation d'objets), on anime des figures en les effaçant à un endroit pour les redessiner ensuite un petit peu plus loin. En programmation « orientée objet », par contre, ces tâches sont prises en charge automatiquement par les classes dont les objets dérivent, et il ne faut donc pas perdre son temps à les reprogrammer.
Animation automatique - Récursivité.
Pour conclure cette première prise de contact avec l'interface graphique "Tkinter", voici un dernier exemple d'animation, qui fonctionne cette fois de manière autonome dès qu'on l'a mise en marche.
from Tkinter import *
def move():
 "déplacement de la balle"
 global x1, y1, dx, dy, flag
 x1, y1 = x1 +dx, y1 + dy
 if x1 >210:
 x1, dx, dy = 210, 0, 15
 if y1 >210:
 y1, dx, dy = 210, -15, 0
 if x1 <10:
 x1, dx, dy = 10, 0, -15
 if y1 <10:
 y1, dx, dy = 10, 15, 0
 can1.coords(oval1,x1,y1,x1+30,y1+30)
 if flag >0: 
 fen1.after(50,move) # => boucler après 50 millisecondes
def stop_it():
 "arret de l'animation"
 global flag 
 flag =0
def start_it():
 "démarrage de l'animation"
 global flag
 if flag ==0: # pour ne lancer qu'une seule boucle 
 flag =1
 move()
x1, y1 = 10, 10 # coordonnées initiales
dx, dy = 15, 0 # 'pas' du déplacement
flag =0 # commutateur
fen1 = Tk()
fen1.title("Exercice d'animation avec Tkinter")
can1 = Canvas(fen1,bg='dark grey',height=250, width=250)
can1.pack(side=LEFT, padx =5, pady =5)
oval1 = can1.create_oval(x1, y1, x1+30, y1+30, width=2, fill='red')
bou1 = Button(fen1,text='Quitter', width =8, command=fen1.quit)
bou1.pack(side=BOTTOM)
bou2 = Button(fen1, text='Démarrer', width =8, command=start_it)
bou2.pack()
bou3 = Button(fen1, text='Arrêter', width =8, command=stop_it)
bou3.pack()
fen1.mainloop()
La seule nouveauté mise en œuvre dans ce script se trouve tout à la fin de la définition de la fonction codice_107 : vous y noterez l'utilisation de la méthode codice_108. Cette méthode peut s'appliquer à un widget quelconque. Elle déclenche l'appel d'une fonction "après qu'un certain laps de temps se soit écoulé". Ainsi par exemple, codice_109 déclenche pour le widget window un appel de la fonction qqc() après une pause de 200 millisecondes.
Dans notre script, la fonction qui est appelée par la méthode codice_108 est la fonction codice_107 elle-même. Nous utilisons donc ici pour la première fois une technique de programmation très puissante, que l'on appelle "récursivité". Pour faire simple, nous dirons que "la récursivité est ce qui se passe lorsqu'une fonction s'appelle elle-même". On obtient bien évidemment ainsi un bouclage, qui peut se perpétuer indéfiniment si l'on ne prévoit pas aussi un moyen pour l'interrompre.
Voyons comment cela fonctionne dans notre exemple :
La fonction codice_107 est invoquée une première fois lorsque l'on clique sur le bouton « Démarrer ». Elle effectue son travail (c'est-à-dire positionner la balle), puis elle s'invoque elle-même après une petite pause. Elle repart donc pour un second tour, puis s'invoque elle-même à nouveau, et ainsi de suite indéfiniment…
C'est du moins ce qui se passerait si nous n'avions pas pris la précaution de placer quelque part dans la boucle une instruction de sortie. En l'occurrence, il s'agit d'un simple test conditionnel : à chaque itération de la boucle, nous examinons le contenu de la variable codice_113 à l'aide d'une instruction codice_114. Si le contenu de la variable codice_113 est zéro, alors le bouclage ne s'effectue plus et l'animation s'arrête. codice_113 étant une variable globale, nous pouvons aisément changer sa valeur à l'aide d'autres fonctions, celles que nous avons associées aux boutons « Démarrer » et « Arrêter ».
Nous obtenons ainsi un mécanisme simple pour lancer ou arrêter notre animation :
Un premier clic sur le bouton « Démarrer » assigne une valeur non-nulle à la variable codice_113, puis provoque immédiatement un premier appel de la fonction codice_107. Celle-ci s'exécute et continue ensuite à s'appeler elle-même toutes les 50 millisecondes, tant que codice_113 ne revient pas à zéro. Si l'on continue à cliquer sur le bouton « Démarrer », la fonction codice_107 ne peut plus être appelée tant que la valeur de codice_113 vaut 1. On évite ainsi le démarrage de plusieurs boucles concurrentes.
Le bouton « Arrêter » remet codice_113 à zéro, et la boucle s'interrompt.