Illustrations

\(\newcommand{\ds}{\displaystyle}\) \(\newcommand{\Frac}{\ds\frac}\) \(\renewcommand{\r}{\mathbb{ R}}\) \(\newcommand{\C}{\mathbb{ C}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\z}{\mathbb{ Z}}\) \(\newcommand{\Q}{\mathbb{ Q}}\) \(\newcommand{\N}{\mathbb{ N}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\ol}{\overline}\) \(\newcommand{\abs}[1]{\left| \,{#1} \right|}\) \(\newcommand{\pv}{\;;\;}\) \(\newcommand{\ens}[1]{\left\{ {#1} \right\}}\) \(\newcommand{\mens}[1]{\setminus\left\{ {#1} \right\}}\) \(\newcommand{\Par}[1]{\left({#1}\right)}\) \(\newcommand{\pe}[1]{\left\lfloor {#1} \right\rfloor}\)

Illustrations

Voir/cacher un mot de passe

Comme dans l’enregisteur de mots de passe de Google Chrome, on souhaite réaliser une mini-application :

../../../_images/mot_passe.gif

qui affiche ou cache un mot de passe dans un champ si on clique sur le bouton.

Pour les icones masquer et démasquer sur le bouton, il faut disposer des deux images view.png et hide.png :

../../../_images/view.png
../../../_images/hide.png

Voici le code correspondant :

 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
from tkinter import *
root = Tk()

lbl = Label(root, text="Mot de\npasse ", font='Times 15 bold')
lbl.pack(side='left', padx=20)

my_entry = Entry(
    root, font='Courier 20 bold', width=20, bg='lavender', show='●')

my_entry.pack(padx=20, pady=40, side="left")
my_entry.focus_set()


def update_entry():
    global hidden
    if hidden:
        my_entry['show'] = ''
        btn['image'] = hide
    else:
        my_entry['show'] = '●'
        btn['image'] = view
    hidden = not hidden


hidden = True

hide = PhotoImage(file='hide.png')
view = PhotoImage(file='view.png')

btn = Button(
    root,
    image=hide,
    width=90,
    font='Times 15 bold',
    command=update_entry)
btn.pack(side="left")
root.mainloop()

Il faut créer trois widgets :

  • ligne 7 : une entrée dans laquelle est écrit le mot de passe
  • ligne 30 : un bouton qui va démasquer/masquer le mot de passe
  • ligne 4 : un label pour indiquer qu’il faut entrer un mot de passe.

Lorsque le mot de passe est caché, chaque caractère est remplacé par un disque noir \(\bullet\). Pour que l’écriture du mot de passe ne montre que le caractère \(\bullet\), il faut activer l’option show de Entry en posant show=" \(\bullet\) ", cf. ligne 8. On peut écrire tel quel le caractère entre guillemets, par exemple en faisant un copier-coller. Ce caractère est connu sous le nom Unicode de Black Circle. En Python 3, on peut aussi produire ce caractère avec une séquence d’échappement unicode :

print("\u25CF")

L’option command du bouton (ligne 35) référence une fonction update_entry (ligne 14) qui va se charger :

  • de masquer/démasquer l’entrée (lignes 20 et 17)
  • de changer l’icône du bouton (lignes 18 et 21).

L’état de visibilité du mot de passe est enregistré dans une variable globale hidden (ligne 25) qui vaut initialement False afin que le mot de passe soit invisible. Comme cette variable est modifiée par la fonction update_entry (ligne 22), elle y est déclarée global (ligne 15).

Pour cacher chaque caractère de l’entrée, on a vu qu’il fallait que l’option show du bouton référence la chaîne qui va remplacer chaque caractère. Pour que les caractères soient normalement visible, il faut que cette option soit à la chaine vide "". Comme on le ferait pour toute option de widget, on peut changer l’option show avec une affectation de my_entry["show"] (lignes 17 et 20).

Pour changer l’image du bouton, c’est le même principe, on réaffecte btn["image"] (lignes 18 et 21). L’image doit pointer vers un objet de type PhotoImage que l’on aura créé au préalable (lignes 27 et 28).

Interface pour vérifier un mot de passe

Voici une interface où un utilisateur entre un mot de passe (caché par des astérisques) et le système lui répond si son login est valide ou pas :

../../../_images/verif_mdp.gif

Le code correspondant est :

 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
from tkinter import *

def estValide() :
    if user.get() == "moi" and mdp.get()=="3.14":
        label_login["text"]="Login correct"
    else:
        label_login["text"]="Login incorrect"

app = Tk()

user=StringVar()
mdp=StringVar()

label_user = Label(app,text="Identifiant")
label_mdp = Label(app,text="Mot de passe")
label_login = Label(app, font="Arial 20 bold")

btn = Button(app,text="Valider",command=estValide, width=20)
entry_user = Entry(app, textvariable=user)
entry_mdp = Entry(app, textvariable=mdp, show="*")


label_user.pack()
entry_user.pack()

label_mdp.pack()
entry_mdp.pack()

btn.pack(padx=100)
label_login.pack()

app.mainloop()
  • Ligne 11-12 : les textes des entrées sont enregistrées dans des variables de contrôle.
  • Lignes 3 et 18 : quand l’utilisateur clique sur le bouton, la fonction estValide est lancée et elle examine si le login est valide ; la réponse est alors affichée dans un label prévu à cet effet (ligne 16).

En pratique, un mot de passe n’est jamais stocké en clair. Cet exemple est basé en partie sur une question posée sur le forum Python d’OpenClassroom.

De quel bouton vient le clic ?

Imaginons une situation où plusieurs boutons peuvent modifier un widget, comme ci-dessous :

../../../_images/boutons_canevas.gif

où si on clique sur un des boutons, le numéro du bouton est dessiné sur un canevas. Comme expliqué dans la FAQ pour ISN de developpez.com, une situation typique serait le cas des boutons d’une calculatrice.

Pour réaliser cela, on se dit qu’on aimerait pouvoir ne créer qu” une seule fonction de rappel et qu’elle réagisse en fonction du bouton sur lequel on a cliqué. Mais, comme la fonction de rappel d’un bouton ne reçoit aucun argument, on ne peut pas savoir quel bouton a appelé. Par exemple, dans le code ci-dessous où le clic entraîne juste l’affichage d’un message :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from tkinter import *
from random import randrange

WIDTH=400
HEIGHT=200
NBUTTONS=5

root = Tk()
cnv = Canvas(root, width=WIDTH, height=HEIGHT, background="ivory")
cnv.grid(row=0,columnspan=NBUTTONS)

def clic():
    print("Clic")

for i in range(NBUTTONS):
    Button(root, text="Bouton\n%s" %i, command=clic).grid(row=1, column=i)

root.mainloop()

si on clique sur un des 5 boutons, la fonction clic est appelée et rien n’indique dans sa définition qui l’aurait appelée (d’ailleurs, n’importe quelle portion de code pourrait appeler cette fonction).

On va donc créer 5 fonctions de rappel, une par bouton. Pour cela, on peut utiliser une fonction qui génère, pour chaque bouton, des fonctions de rappel. Voici un code réalisant cela :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from tkinter import *


SIZE=200
NBUTTONS=5

root = Tk()
cnv = Canvas(root, width=SIZE, height=SIZE, background="ivory")
cnv.grid(row=0,columnspan=NBUTTONS)

def make_clic(i):
    def clic():
        cnv.delete(ALL)
        cnv.create_text(SIZE/2, SIZE/2, text=i, font="Arial 90 bold")
    return clic

for i in range(NBUTTONS):
    btni=Button(root, text=i, command=make_clic(i))
    btni.grid(row=1, column=i)

root.mainloop()
  • Lignes 17-19 : dans une boucle parcourue par un indice i, on génère le bouton portant la valeur i. Non seulement le texte dépend de i mais aussi la façon de réagir du bouton. En effet, chaque commande est le retour d’un appel de fonction qui renvoie une fonction mémorisant i.
  • Lignes 11-15 : cette fonction génère une fonction de rappel personnalisée pour chaque bouton. Cette fonction est appelée avec un numéro de bouton. Pour chaque numéro, elle crée une fonction (lignes 12-14) qui réagit au clic sur le bouton i. Cette fonction est créée dans le corps de la fonction et est renvoyée (ligne 15) après sa création. Cette fonction, nommée clic mémorise un contexte, en particulier le numéro i.
  • Ligne 11 : une telle fonction est appelée une clôture (closure).

Alternative avec paramètre par défaut

J’ai vu cette autre façon de faire dans un message de RedTenZ sur le forum Python d’OpenClassrooms. Voici l’adaptation de son code :

from tkinter import *

SIZE=200
NBUTTONS=5

root = Tk()
cnv = Canvas(root, width=SIZE, height=SIZE, background="ivory")
cnv.grid(row=0,columnspan=NBUTTONS)

for i in range(NBUTTONS):
    def clic(i=i):
        cnv.delete(ALL)
        cnv.create_text(SIZE/2, SIZE/2, text=i, font="Arial 90 bold")
    btni=Button(root, text=i, command=clic)
    btni.grid(row=1, column=i)

root.mainloop()

Toute l’astuce est dans le paramètre par défaut i dans la définiton de clic (l’auteur avait utilisé une fonction lambda qui risque d’être peu lisible dans le code ci-dessus). Cette méthode est signalée dans la documentation de Shipman au §54.7.

Alternative identifiant le widget

Une autre façon de faire est d’associer avec bind chaque bouton au clic de souris sur le bouton à une même fonction (ci-dessous clic). Cette fonction sera appelée lorsqu’on cliquera sur un des boutons en générant un événement, disons event. Et event permet d’identifier le widget associé par l’attribut event.widget. Il ne reste plus qu’à récupérer le texte du bouton via event.widget["text"] pour identifier la valeur. D’où le code :

from tkinter import *

SIZE=200
NBUTTONS=5

root = Tk()
cnv = Canvas(root, width=SIZE, height=SIZE, background="ivory")
cnv.grid(row=0,columnspan=NBUTTONS)

def clic(event):
    i=event.widget["text"]
    cnv.delete(ALL)
    cnv.create_text(SIZE/2, SIZE/2, text=i, font="Arial 90 bold")


for i in range(NBUTTONS):
    btni=Button(root, text=i)
    btni.grid(row=1, column=i)
    btni.bind("<Button>", clic)

root.mainloop()

Alternative utilisant partial

Si on veut utiliser la fonction de rappel suivante qui affiche un numéro sur le canevas :

def clic(nro):
    cnv.delete(ALL)
    cnv.create_text(SIZE/2, SIZE/2, text=nro, font="Arial 90 bold")

on a le problème que cette fonction prend déjà un paramètre alors que la fontion passée à command dans un bouton n’en prend pas. On peut toutefois transmettre l’argument en utilisant la fonction partial du module standard functools, ce qui donne le code suivant :

from tkinter import *
from functools import partial

SIZE=200
NBUTTONS=5

root = Tk()
cnv = Canvas(root, width=SIZE, height=SIZE, background="ivory")
cnv.grid(row=0,columnspan=NBUTTONS)

def clic(nro):
    cnv.delete(ALL)
    cnv.create_text(SIZE/2, SIZE/2, text=nro, font="Arial 90 bold")


for i in range(NBUTTONS):
    btni=Button(root, text=i, command=partial(clic,i))
    btni.grid(row=1, column=i)

root.mainloop()

J’ai vu cette méthode indiquée dans un message de LeCobriste128 sur le forum Python d’OpenClassrooms.

Plus généralement, on pourra consulter cette discussion sur Stack Overflow.

Construire sa propre barre de progression

Tkinter ne propose pas de barre de progression, pour cela il faut utiliser son extension (standard) Ttk. Néanmoins, la barre de Ttk n’est pas facilement personnalisable (il faut utiliser un style Ttk et cela n’empêche pas certaines limitations). On peut donc être tenté de créer sa propre barre de progression. Pour cela, on utilise un canevas et, périodiquement, on modifie la longueur d’un rectangle gris emboîté dans un rectangle blanc :

../../../_images/progressbar_tk.gif

Voici une base de code :

 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
from tkinter import *

DELAY = 100

root = Tk()

# Le canevas
cnv = Canvas(root, width=400, height=100, bg='ivory')
cnv.pack()

# Label
message = StringVar()
w = Label(root, textvariable=message, font='Arial 30 bold')
w.pack()

# Largeur de la barre
W = 300

# Durées
period_s = 10
period_ms = period_s * 1000
DELAY_BAR = int(round(period_ms / W))

# Les deux rectangles
A = (50, 50)
B = (50 + W , 30)
bg = cnv.create_rectangle(A, B, outline="gray", fill="white")
bar = cnv.create_rectangle(A, B, outline="gray", fill="gray", width=0)


def animate(L, bar):
    if L >= 0:
        cnv.delete(bar)
        newbar = cnv.create_rectangle(
            50, 50, 50 + L, 30, outline="gray", fill="gray", width=0)
        L -= 1
        cnv.after(DELAY_BAR, animate, L, newbar)


def chrono(s, message):
    if s >= 0:
        message.set(str(s))
        root.after(1000, chrono, s - 1, message)


animate(W, bar)
chrono(period_s, message)

root.mainloop()
  • Lignes 27-28 : Le rectangle gris fait office de barre de progression. Quand il évolue, le fond blanc de la barre se découvre.
  • Ligne 22 : on précalcule la durée de rafraîchissement du rectangle pour 1 pixel.
  • Lignes 31-37 : on fait évoluer la barre pixel par pixel (la longueur L). Pour cela, on remplace le rectangle gris par un rectangle ayant 1 pixel de moins de longueur.
  • Lignes 40-43 : on fait évoluer le compteur seconde par seconde jusqu’à écoulement de la période donnée initialement (period_s, ligne 20).

Dessiner un dégradé

Par défaut, Tkinter ne permet pas de créer des gradients. Toutefois, on peut réaliser une forme de gradient en écrivant soi-même le code.

Voici un exemple avec un gradient rouge :

../../../_images/gradient_rouge.gif
from tkinter import *

LENGTH = 800
TOP_RED = 120

root = Tk()
cnv = Canvas(root, width=400, height=400, bg="white")
cnv.pack()


def rgb_10to16(r, g, b):
    return ("#%.02x" % r) + ("%.02x" % g) + ("%.02x" % b)


def gradient(x):
    y = int((-255 + TOP_RED) / LENGTH * (int(x)) + 255)
    cnv["bg"] = rgb_10to16(255, y, y)


curseur = Scale(
    root,
    orient="horizontal",
    command=gradient,
    length=400,
    from_=0,
    to=LENGTH)
curseur.pack()

root.mainloop()

Voici un exemple avec des niveaux de gris

from tkinter import Tk, Canvas

SIDE=600

root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE*0.3, background="ivory")
cnv.pack()
top=50
H=100

a=10
N=100
W=0.95*SIDE//N

gris0=40
gris1=200
h=(gris1-gris0)//N


def toHex(v):
    z=hex(v)[2:]
    return '0'*(len(z)==1)+z


for i in range(N):
    color=toHex(gris1-i*h)
    cnv.create_rectangle(a, top, a+W, top+H, outline='',
                         fill="#%s%s%s" %(color, color, color))
    a+=W

root.mainloop()

ce qui produit :

../../../_images/gradient_gris.png

Rotation d’un item

Ce qui suit va montrer comment faire tourner un item géométrique du canevas autour d’un point d’un certain angle :

../../../_images/rotation_item.gif

On utilise pour cela la rotation vectorielle fournie par la classe Vec2D du module Turtle. Voici le code :

 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
from tkinter import *
from turtle import Vec2D

def rot(C, t, M):
    CM=Vec2D(*M)-Vec2D(*C)
    return Vec2D(*C)+CM.rotate(t)

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

# Le centre
C=(200, 200)

# Un triangle
P=(250, 200)
Q=(300, 100)
R=(380, 200)
tr=cnv.create_polygon(P, Q, R, width=8, fill="blue")

cnv.create_oval(200-2, 200-2, 200+2, 200+2, fill="black")

def rotate(t):
    PP=rot(C, float(t), P)
    QQ=rot(C, float(t), Q)
    RR=rot(C, float(t), R)
    cnv.coords(tr, *PP, *QQ, *RR)

curseur = Scale(root, orient = "horizontal", command=rotate, from_=0, to=360)
curseur.pack()

root.mainloop()
  • Lignes 4-6 : la fonction rot calcule les coordonnées du transformé de M par la rotation de centre C et d’angle t.
  • Lignes 13 : on crée un centre de rotation C et (ligne 21) on le marque avec un petit disque de 2 pixels de rayon.
  • Lignes 16-19 : on crée un motif triangulaire PQR qui va tourner autour du centre.
  • Ligne 29 : un curseur permettant de choisir l’angle de rotation (entre 0 et 360°) autour de C par rapport à la position initiale.
  • ligne 23 : chaque fois que le curseur est déplacé (command à la ligne 29), le triangle doit tourner depuis sa position au lancement de l’application d’un certain angle t. On calcule la nouvelle position PP, QQ et RR de chacun des sommets (ligne 24-26) et on déplace le triangle avec la méthode coords à sa nouvelle position (ligne 27).

Le code ci-dessus est adapté d’un message du forum Python d”OpenClassroom.