Tkinter

tkinter

Module standard de Python

objectif: composer des interfaces graphiques - IHM

Documentation

Exemple 1 - Salutation

Lancer l’interpréteur interactif - console - python (version 3 ou plus)

Vous obtenez quelquechose comme

Python 3.2.3 (default, Feb 27 2014, 21:31:18)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>

fenêtre principale

>>> from tkinter import *
>>> fen = Tk()
>>> # configuration du titre
>>> fen.title('première fenêtre')
''
_images/ill1-1.png

Widgets - composants graphiques

Création d’un widget w

w = Widget(parent, opt1=val1, opt2=val2, ...)

Label - étiquette

Label

Afficher du texte

>>> salut = Label(fen, text="Bonjour tout l'monde!")

Argh! Rien ne se produit!

positionnement

Tout widget w doit être positionné dans son conteneur

w.pack(opt1=v1, opt2=v2, ...)

Testons

>>> salut.pack()
_images/ill1-2.png

Button - bouton

Button

>>> b = Button(fen, text='Action')
>>> b.pack(pady=5)
_images/ill1-3.png

obtenir des informations

Obtenir la liste des options du widget w

w.keys() # ou Widget.keys()

Obtenir la liste des méthodes (ou attributs) de w

dir(w) # ou dir(Widget)

Testons

>>> salut.keys() # ou Label.keys()
['activebackground', 'activeforeground', ...]
>>> dir(salut) # ou dir(Label)
['_Misc__winfo_getint', ...]
>>> help(Label.pack) # «q» pour sortir de l'aide en ligne

Récupérer/modifier une option

  1. Récupérer la valeur d’une option o d’un widget w
w['o'] # ou w.cget('o') -> str
  1. Modifier une option
w['o'] = valeur
# ou w.config(o1=v1, o2=v2, ...)
# ou w.configure(o1=v1, o2=v2, ...)

Testons

>>> salut['bg'] = 'black'
>>> salut['fg'] = 'green'
>>> salut['width'] = 30
>>> salut['height'] = 3
>>> salut['anchor']
'center'
>>> salut['anchor'] = 'sw' # s ? w ?
>>> salut['anchor'] = 'center'

agir avec le bouton

  1. Définir une fonction sans argument qui réalise l’action,
  1. Associer cette fonction à l’option command du bouton.
def action():
    # ...

# ...
b['command'] = action # b -> bouton

Exercice

Le bouton bascule la couleur du texte: vert <-> rouge

Solution

_images/ill1-4.png
>>> def action():
...     et = salut
...     et['fg'] = 'red' if et['fg'] == 'green' else 'green'
...
>>> b['command'] = action

Complément sur «pack»

Quatre widgets Frame - f1, f2, f3, f4 - «packer» dans cet ordre

_images/ill1-5.png

Code complet

Frame(fen, bg='blue', height=50).pack(fill='both')
Frame(fen, bg='red', width=50).pack(side='left', fill='both')
Frame(fen, bg='green', height=50).pack(side='bottom', fill='both')
Frame(fen, bg='white', width=50).pack(side='right', fill='both')

Exemple 2 : gestionnaire de tâches

_images/ill2-1.png

Récupérer l’icone (clic droit -> enregistrer ...)

Construction de la GUI

>>> fen = Tk()
>>> fen.title('«todo» liste')
>>> et1 = Label(fen, text='Nouvelle tâche')

Utilisons une image - PhotoImage - pour la seconde étiquette.

>>> icone = PhotoImage(file='todolist.gif')
>>> # l'image doit être dans le répertoire courant de la console
>>> # pour le connaître: import os puis os.getcwd()
>>> et2 = Label(fen, image=icone)

Il nous faut encore un champ de saisi, Entry et une liste, Listbox .

>>> saisi = Entry(fen) # pour une boîte de saisie
>>> liste = Listbox(fen, height=3) # et une simple liste
>>> b = Button(fen, text='Vider')

Positionnement

Cette fois-ci, nous utiliserons le gestionnaire de grille, grid

w.grid(row=ligne, column=col, ...)
# ligne, col: entier à partir de 0

Pour nous, une grille à 3 ligne et 2 colonnes avec fusion des deux colonnes de la dernière ligne.

>>> et1.grid(row=0, column=0, pady=3)
>>> saisi.grid(row=0, column=1)
>>> et2.grid(row=1, column=0, pady=3)
>>> liste.grid(row=1, column=1)
>>> b.grid(row=2, column=0, columnspan=2, pady=3)

Gestion des événements

Event

Événement: l’utilisateur agit sur l’interface

Pour réagir à un événement

  1. Définir un gestionnaire d’événement - event handler
def gestEvt(evt):
    # evt: objet de type Event qui sert à obtenir
    # des informations sur les circonstances de l'appel
    # du gestionnaire ...

    # code qui réalise l'action
  1. Lier le widget et l’événement surveillé au gestionnaire
w.bind('<description_evt>', gestEvt)

Ajouter des tâches

<Return> : insère le contenu de saisi à la fin de liste

>>> def inserer(evt): # définition du gestionnaire
...     tache = saisi.get()
...     liste.insert('end', tache)
...     saisi.delete(0, 'end')
...
>>> saisi.bind('<Return>', inserer) # liaison

Vider la liste

Fonction lambda: fonction anonyme composée d’une unique instruction possédant une valeur (expression)

lambda x,y,...: expression
# x,y,... sont les paramètres de la fonction
# valeur de retour: la valeur de l'expression

Exemple

>>> L = lambda x, y: (x ** 2 + y ** 2) ** 0.5
>>> # RAPPEL x ** n signifie « x puissance n »
>>> L(3,4)
5

Application

>>> b['command'] = lambda : liste.delete(0, 'end')

Exercice 1

En utilisant une fonction «lambda», permettre à l’utilisateur de supprimer l’élément courant de la liste en appuyant sur la touche Suppr.

  1. Supprimer un élément de la liste
liste.delete(i) # où i est l'index de l'élément à supprimer
  1. Récupérer l’index i de l’élément courant
i = liste.curselection()
  1. Nom de l’événement «appui sur la touche Suppr»
'<Delete>'

SOLUTION

liste.bind('<Delete', lambda evt: liste.delete(liste.curselection()))

Exercice 2

Permettre à l’utilisateur de modifier l’ordre des éléments dans la liste par «cliquer-glisser-relâcher»

Éléments de la solution

index = -1 # pour mémoriser l'index de l'item à déplacer

def selclic(evt):
   global index # nécessaire pour modifier la variable globale index
   # ???

def selrel(evt):
   # ???

# l'utilisateur appuie sur le bouton gauche de la souris
liste.bind('<Button-1>', selclic, '+')
# lorsqu'il relâche ce bouton
liste.bind('<ButtonRelease-1>', selrel)

Aide pour l'exercice

  1. Position verticale courante de la souris
evt.y
  1. Index de l’item de liste le plus proche de la position y
indexElt = liste.nearest(y)
  1. Récupérer l’élément d’index i
elt = liste.get(i)
  1. Insérer elt à la position i dans la liste
liste.insert(i, elt)

Faire cela dans un fichier

Sortir de l’interpréteur - exit(), ouvrir Geany, créer le fichier exemple2.py et y coller le code suivant

from tkinter import *

def ajout(evt):
   text = saisi.get()
   liste.insert('end', text)
   saisi.delete(0, 'end')

index = -1

def selclic(evt):
   global index
   pass # à coder

def selrel(evt):
   pass # à coder

# Construction de la fenêtre principale ...
fen = Tk()
fen.title('«todo» liste')

# ... et des différents widgets qui la composent ...
et1 = Label(fen, text='Nouvelle tâche:')
saisi = Entry(fen)
icon = PhotoImage(file='todolist.gif')
# ATTENTION: todolist.gif doit être dans le même répertoire que ce programme !
et2 = Label(fen, image=icon)
# EN cas de pb: Remplacer image=icon par text='à faire' ET commenter la ligne icon = ....
liste = Listbox(fen, height=3)
b = Button(fen, text="Vider", command=lambda: liste.delete(0,'end'))

# ... qu'on positionne dans une grille.
et1.grid(row=0,column=0, pady=3)
saisi.grid(row=0,column=1)
et2.grid(row=1,column=0, pady=3)
liste.grid(row=1,column=1)
b.grid(row=2, column=0, columnspan=2, pady=3)

# On définit le comportement de la GUI
saisi.bind('<Return>', ajout)
liste.bind('<Delete>', lambda evt: liste.delete(liste.curselection()))
liste.bind('<Button-1>', selclic, '+') # '+' ? voir NOTE tout à la fin
liste.bind('<ButtonRelease-1>', selrel)

# On lance la boucle principale (cela est automatique dans la console)
# Cette boucle est responsable de la gestion des événements
fen.mainloop()

# NOTE: tkinter gère déjà l'événement «clic gauche» dans la liste
#  afin de positionner la sélection courante.
#  Pour ne pas supprimer son gestionnaire, nous devons ajouter le
#  nôtre : liste.bind(..., ..., '+')

Solution

  1. Mémoriser l’index de l’item le plus proche de la souris lors du clic
def selclic(evt):
   global index # pour pouvoir modifier index qui est «globale»
   index = liste.nearest(evt.y)
  1. L’échanger avec celui qui est le plus proche de la souris au relâchement du clic
def selrel(evt):
   item1 = liste.get(index)
   index2 = liste.nearest(evt.y)
   if index2 == index: # rien à faire !
        return
   item2 = liste.get(index2)
   liste.delete(index) # on supprime le premier item ...
   liste.insert(index, item2) # qu'on remplace par le second
   liste.delete(index2) # et vice versa
   liste.insert(index2, item1)

Exemple 3 - Dessiner, animer

Widget Canvas: zone rectangulaire dans laquelle on peut dessiner

>>> from tkinter import *
>>> fen = Tk()
>>> fen.title('Un canevas pour dessiner')
>>> can = Canvas(fen, width=500, height=500)
>>> can.pack(padx=5, pady=5)
>>> can['bg'] = 'white'

«Items» graphiques

Pour créer un item graphique dans un canevas can,

id = can.create_<item>(points, option=valeur, ...)
# où <item> est à remplacer par rectangle, line, polygon, oval ...
# la valeur de retour est l'identifiant numérique de l'item créé

le supprimer,

can.delete(id)

le déplacer,

can.move(id, dx, dy)

récupérer la liste de ses coordonnées,

pts = can.coords(id) # pts de la forme [x1, y1, x2, y2, ...]

modifier/récupérer une de ses options.

can.itemconfig(id, opt=val, ...)
val = can.itemcget(id, 'option')

les méthodes du canevas

Expérimentation ...

>>> pt1 = 200, 200
>>> pt2 = 300, 300
>>> can.create_rectangle(pt1, pt2, fill='black')
1
>>> idc = 1
>>> can.move(idc, 0, -100)
>>> pts = can.coords(idc)
>>> pts
>>> can.itemconfig(idc, width=10, outline='blue')
>>> annul = can.bind('<Button-3>',
... lambda e: print('({},{})'.format(e.x, e.y)))
>>> # clic droit sur le canevas
>>> can.unbind('<Button-3>', annul)

... Expérimentation

>>> ido = can.create_oval(pts, fill='white')

Pour déplacer une figure formée de plusieurs items, on les marque (tag) avec une chaîne

>>> can.itemconfig(idc, tags='fig')
>>> can.itemconfig(ido, tags='fig')
>>> can.move('fig', 0, 100)

Transformations disponibles: translation, homothétie (scale)

>>> x1, y1, x2, y2 = can.coords('fig')
>>> can.scale(ido, (x1+x2)/2, (y1+y2)/2, 0.5, 0.5)
>>> # .scale(idOuTag, x_centre, y_centre, facteur_x, facteur_y)

Animation avec after ...

Pour réaliser une action après un certain délai

fen.after(delai_en_millisecondes, fonction_a_appeler_alors)
>>> fen.after(5000, lambda : print('action'))
>>> # noter le retour immédiat de la méthode: pas de blocage

Animer = agir périodiquement

def animation():
     fen.after(500, animation)
     # code de l'action à effectuer
     print('action')

# lancer l'animation
animation()

... Animer avec after

>>> ok, dx, dy = True, 5, 0
>>> def animation():
...   if ok: fen.after(500, animation)
...   can.move('fig', dx, dy)
...
>>> animation()
>>> # puis jouer avec dx et dy pour contrôler le mvt
>>> ok = False # met fin à l'animation

pilotage au clavier

Commençons par définir les actions élémentaires

>>> gauche = lambda : can.move('fig', -5, 0)
>>> droite = lambda : can.move('fig', 5, 0)
>>> bas = lambda : can.move('fig', 0, 5)
>>> haut = lambda : can.move('fig', 0, -5)

Puis associons chaque symbole de touche (keysym) avec l’action correspondante

>>> touches = {'Left': gauche, 'Right': droite, 'Down': bas, 'Up': haut}

Définissons le gestionnaire qui s’occupera d’invoquer l’action en fonction de la touche enfoncée

>>> def depl(evt):
...    t = evt.keysym
...    if t in touches:
...       action = touches[t]
...       action()
...
>>> fen.bind('<Key>', depl)

Mouvement fluide

Le mouvement est plutôt «saccadé» ; voyons d’où vient le problème.

Copier-coller le code suivant dans un fichier test_touches.py

import tkinter as tk

def appui(evt):
    print('APPUI  = touche: {e.keysym}, temps: {e.time}'.format(e=evt))

def relache(evt):
    print('RELACHE= touche: {e.keysym}, temps: {e.time}'.format(e=evt))


fen = tk.Tk()

fen.bind('<KeyPress>', appui) # équivalent à '<Key>'
fen.bind('<KeyRelease>', relache)

fen.mainloop()

répétition automatique

On obtient par exemple

RELACHE= touche: f, temps: 872580888
APPUI  = touche: a, temps: 872593933
RELACHE= touche: a, temps: 872594433
APPUI  = touche: a, temps: 872594433
RELACHE= touche: a, temps: 872594471
APPUI  = touche: a, temps: 872594471
RELACHE= touche: a, temps: 872594504
APPUI - 1/2 sec - RELACHE/APPUI - ~ 40 ms - RELACHE/APPUI ...

résolution du «problème» ...

  1. Mémoriser l’état des touches à surveiller
touches = {
    'nom_touche1': {
        'etat': False, # True si cette touche est enfoncée
        'action': action1 # ref vers fonction
    },
    'nom_touche2': {
        'etat': False,
        'action': action2
    },
    # ...
}

... résolution ...

  1. Lier les événements <Key> et <KeyPress> à des gestionnaires chargés de mettre à jour l’état des touches,
def appui(evt):
    t = evt.keysym
    if t in touches: touches[t]['etat'] = True

def relache(evt):
    t = evt.keysym
    if t in touches: touches[t]['etat'] = False

fen.bind('<Key>', appui)
fen.bind('<KeyRelease>', relache)

... du «problème»

  1. Controler périodiquement l’état des touches et agir en conséquence
def controleur():
     fen.after(40, controleur)
     for t in touches:
        v = touches[t]
        if v['etat']:
            action = v['action']
            action()

Exercice

Mettre en oeuvre cette solution pour déplacer ‘fig’ de manière fluide

Partir de l’ébauche suivante:

import tkinter as tk

# Déclarations générales

gauche = lambda : can.move('fig', -5, 0)
droite = lambda : can.move('fig', 5, 0)
bas = lambda : can.move('fig', 0, 5)
haut = lambda : can.move('fig', 0, -5)

# ...

# GUI
fen = tk.Tk()
fen.title("mouvement fluide")
can = tk.Canvas(fen, width=500, height=500, bg="white")
can.pack(padx=5, pady=5)

# dessin de la figure
can.create_rectangle(200, 200, 300, 300,
                     fill='black', width=10, outline='blue',
                     tags='fig')
can.create_oval(225, 225, 275, 275,
                fill='white', tags='fig')

# liaisons d'événements et controleur ...


# boucle principale
fen.mainloop()

solution complète

   import tkinter as tk

   # Déclarations générales

   gauche = lambda: can.move('fig', -5, 0)
   droite = lambda: can.move('fig', 5, 0)
   bas = lambda: can.move('fig', 0, 5)
   haut = lambda: can.move('fig', 0, -5)

   touches = {
       'Left': {
           'etat': False,
           'action': gauche
       },
       'Right': {
           'etat': False,
           'action': droite
       },
       'Down': {
           'etat': False,
           'action': bas
       },
       'Up': {
           'etat': False,
           'action': haut
       },
   }

   def appui(evt):
       t = evt.keysym
       if t in touches: touches[t]['etat'] = True

   def relache(evt):
       t = evt.keysym
       if t in touches: touches[t]['etat'] = False

   def controleur():
       fen.after(40, controleur)
       for t in touches:
           t = touches[t]
           if t['etat']:
               t['action']()

   # GUI
   fen = tk.Tk()
   fen.title("mouvement fluide")
   can = tk.Canvas(fen, width=500, height=500, bg="white")
   can.pack(padx=5, pady=5)

   # dessin de la figure
   can.create_rectangle(200, 200, 300, 300,
                        fill='black', width=10, outline='blue',
                        tags='fig')
   can.create_oval(225, 225, 275, 275,
                   fill='white', tags='fig')

   # liaisons d'événements et controleur ...
   fen.bind('<Key>', appui)
   fen.bind('<KeyRelease>', relache)
   controleur()

   # boucle principale
   fen.mainloop()