Les événements

\(\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}\)

Les événements

Capturer des événements

Comme il a été vu lors de la présentation de la programmation événementielle, une interface Tkinter est à l’écoute de certains événements liés à la souris ou au clavier.

Un événement du clavier sera le fait d’appuyer ou de relâcher une touche du clavier ; un événement de la souris consiste en le fait de cliquer, de déplacer (en cliquant ou pas) la souris, de tourner la molette ou de relâcher un bouton.

Le point essentiel est qu’il est possible de lier un des événements précédents à une fenêtre ou un widget. Voici un exemple d’une simple fenêtre Tkinter qui réagit par un message dans la console si elle perçoit un clic de souris ou la pression sur une touche du clavier. Le message indique, selon les cas, la position de la souris dans la fenêtre ou alors la touche qui a été pressée :

../../../_images/capture_evenements.gif

Le code correspondant :

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

def f(event):
    t=event.keysym
    print("Touche pressée :", t)

def g(event):
    x=event.x
    y=event.y
    print("Position :", x, y)

root = Tk()

root.bind("<Key>", f)
root.bind("<Motion>",g)
root.mainloop()
  • Ligne 12 : on crée une fenêtre Tkinter ; elle est à l’écoute des événements.
  • Ligne 14 : la chaîne "<Key>" représente l’événement de pression quelconque sur une touche du clavier. Cette ligne de code lie (bind en anglais) la fonction f définie lignes 3-5 et ces événements. Chaque fois qu’un tel événement se produit, la fonction f est exécutée.
  • Lignes 3-5 : Lorqu’on presse une touche du clavier, la fonction f reçoit l’événement correspondant. Cet événement, appelé ici event (mais qu’on pourrait appeler par tout autre nom) est un objet dont un des attributs keysym est le nom de la touche sur laquelle on a appuyé. La fonction f affiche le nom de cette touche.
  • Ligne 15 : la chaîne "<Motion>" représente l’événement de déplacement de la souris dans la fenêtre. Chaque point de la fenêtre root a une position exprimée en pixels. La position du coin supérieur gauche est (0,0). Cette ligne de code lie la fonction g définie lignes 7-10 et ces déplacements. Chaque fois que la souris bouge, la fonction g est exécutée.
  • Ligne 7-10 : lorque la souris bouge, la fonction g reçoit l’événement correspondant (ligne 7). Cet événement est un objet dont les attributs x et y représentent la position courante de la souris (lignes 8-9). La fonction g affiche (ligne 10) alors cette position.
  • Des fonctions telles que f et g sont dites des fonctions de rappel (en anglais, callback function).

Il existe une liste d’événements du clavier et de la souris reconnus par Tkinter, désignés par une chaîne de caractères littérale, comme "<Key>" ou "<Motion>". D’une manière générale, les événements standard sont accessibles par une syntaxe de la forme "<event>"event est une chaîne de caractères comme Key ou Motion ou encore space, cette dernière désignant l’appui sur la touche ESPACE.

Pour une information plus complète sur les événements sous Tkinter, cf. la documentation de Fredrik Lundh.

Événement du clavier

Ci-dessous, on aperçoit une fenêtre Tkinter sans contenu. Mais lorsqu’on appuie sur une touche du clavier, la console affiche un message indiquant sur quelle touche on a pressé.

../../../_images/evenement_clavier.gif

Le code correspondant est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

def touche(event):
    t=event.keysym
    print("Touche %s pressée" %t)

root = Tk()

root.bind('<Key>', touche)

root.mainloop()
12
13
14
Touche space pressée
Touche p pressée
Touche KP_4 pressée
  • Ligne 7 : une fenêtre est créée
  • Ligne 9 : les événements du clavier perçus par l’application sont liés (cf. touch) à la fonction touche.
  • Lignes 3-5 : lorsqu’on appuie sur n’importe quelle touche, la fonction touche s’exécute. Elle reçoit l’événement du clavier correspondant. Cet événement (nommé ici event) contient un attribut keysym qui est le nom de la touche sur laquelle on a appuyé. Un message est lisible dans la console indiquant la touchée qui a atét choisie.

L’événement de clavier que l’on cherche généralement à détecter est l’appui sur une touche (en anglais key press) ; parfois, on cherche aussi à détecter le relâchement d’une touche (key release).

L’événement associé à la pression d’une touche quelconque du clavier est nommé Key cf; ligne 9.

Codes de quelques touches

Un code d’événement est une chaîne littérale de la forme "<event>"event est une chaîne de caractères telle que Enter. Ci-dessous, le nom des événements représentant "<event>" et associés à l’appui sur certaines touches :

  • Flèches : Left, Right, Up, Down
  • ESPACE : space
  • Touche ENTRÉE : Return ou, sur le pavé numérique, KP_Enter
  • Touche ECHAP : Escape

L’appui sur une touche de caractère, par exemple z, se nomme KeyPress-z mais on peut l’abréger tout simplement en z. Pour la majuscule Z, on peut écrire Z. Pour le relâchement de la touche z, on écrira KeyRelease-z.

Pour une combinaison de touches comme ALT+CTRL-a (appui simultané), l’événement sera nommé <Control-Shift-KeyPress-a> :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from tkinter import Tk, Canvas

root = Tk()

def f(event):
    print("OK")

root.bind('<Control-Shift-KeyPress-a>', f)

root.mainloop()

Par exemple, l’événement à déclarer si on appuie sur la touche ESPACE est <space>. On trouvera la liste complète dans la documentation : Nom des touches.

Les nombres du pavé numérique

L’appui sur une touche du pavé numérique génère un événement dont le début du nom est KP_ qui signifie KeyPad et qui est suivi d’une chaîne précisant la touche. Par exemple, l’appui sur la touche 3 du pavé numérique génère l’événement KP_3. Cette syntaxe permet en analysant le retour du nom event.keysym de savoir la valeur numérique corresponsante.

Voici un exemple d’application : si on appuie sur la touche valant le chiffre N du pavé numérique avec N valant 1, 2 ou 3 alors sont dessinés N carrés aléatoires sur un canevas :

../../../_images/focus_canevas_clavier_bis.gif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from tkinter import Tk, Canvas
from random import randrange

SIDE=200

root = Tk()
cnv = Canvas(root, width=SIDE, height=SIDE, bg='ivory')
cnv.pack(padx=10, pady=10)
cnv.focus_set()

def dessiner(event):
    n=int(event.keysym[3])
    for i in range(n):
        a=randrange(SIDE)
        b=randrange(SIDE)
        cnv. create_rectangle(a, b, a+20, b+20, fill="red")

cnv.bind('<KP_1>', dessiner)
cnv.bind('<KP_2>', dessiner)
cnv.bind('<KP_3>', dessiner)

root.mainloop()
  • Ligne 9 : on donne le focus au canevas, comme ça, l’application réagit à l’appui sur une touche.
  • Lignes 18-20 : seules les touches 1, 2 et 3 du pavé numérique vont appeler la fonction dessiner
  • ligne 12 : on filtre sur l’appui des touches 1, 2 et 3 du pavé numérique qui est le caractère d’indice 3 dans la chaîne représentant l’événemlent, comme la chaîne "KP_1".
  • Ligne 12 : on capture la chaîne représentant l’appui, elle commence par KP_ ; le caractère suivant, d’indice 3 donc, représente le chiffre sur lequel on a appuyé. Ce caractère est converti en vrai entier avec la fonction int.
  • Ligne 13 : on est en mesure, avec range(n) de dessiner le bon nombre de carrés.

Tracer un chemin réversible au clavier

Le programme suivant vise à illustrer les événements du clavier agissant sur un canevas. L’interface ci-dessous

../../../_images/chemin_clavier.gif

permet de tracer avec les flèches du clavier un chemin sur un canevas, le chemin pouvant être reparcouru en arrière (avec effacement du chemin de retour).

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
33
34
35
36
37
from tkinter import *

WIDTH=400
HEIGHT=200
COTE=40

root = Tk()
cnv = Canvas(root, width=WIDTH, height=HEIGHT, background="ivory")
cnv.pack()

DIR={'Left':(-1,0),'Right':(1,0),'Up':(0,-1),'Down':(0,1)}

def bouge(event):
    key=event.keysym
    dx, dy=DIR[key]
    a,b, segment=pile[-1]
    if len(pile)>=2 and a+dx==pile[-2][0] and b+dy==pile[-2][1] :
            pile.pop()
            print("back")
            cnv.delete(segment)
    else:
        segment=cnv.create_line(a*COTE+COTE//2, b*COTE+COTE//2,
                                a*COTE+COTE//2+dx*COTE,
                                b*COTE+COTE//2+dy*COTE,
                                fill='blue', width=4, capstyle=ROUND)
        pile.append([a+dx, b+dy, segment])

    cnv.move("perso", dx*COTE, dy*COTE)

perso=cnv.create_rectangle(0, 0,COTE, COTE, fill="blue",
                           outline='', tag='perso')
pile=[(0,0, perso)]

for key in ["<Left>", "<Right>", "<Up>", "<Down>"]:
    root.bind(key, bouge)

root.mainloop()
  • Ligne 34 : l’appui sur une des 4 flèches du clavier est détecté par le canevas.
  • Ligne 30 : un carré bleu indique la position courante sur le canevas (utile si on repasse sur un chemin déjà parcouru)
  • Ligne 35 : le canevas a le focus car la fenêtre entière (root) a le focus quand l’application est lancée.
  • Lignes 34-35 : tout appui sur une des touches du clavier exécute la fonction bouge.
  • Ligne 11 : chaque appui sur une des flèches du clavier est associé à un décalage suivant le repère du canevas. Par exemple, à un appui sur la flèche bas est associé la direction correspondante, ici le couple (0, -1). C’est juste une facilité pour coder la fonction bouge.
  • Ligne 32 : les différents mouvements de l’utilisateur sont enregistrés dans une pile. La pile permet (en dépilant) de revenir en arrière et d’effacer le trajet de retour.
  • Ligne 17-19 : la détection d’une marche arrière. le segment est effacé (ligne 19)
  • Lignes 22-26 : si pas de retour en arrière, le segment pour le nouveau déplacement est rendu visible et rajouté dans la pile.
  • Ligne 28 : la sortie du canevas n’est pas gérée par le programme.

Evénement du clavier : press vs release

On a parfois besoin de distinguer entre une pression sur une touche du clavier et un relâchement de cette touche. Cela correspond à deux événements différents, comme le montre le code ci-dessous (en mode texte, pas véritablement de fenêtre pertinente ici):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from tkinter import Tk, Canvas

root = Tk()

def press(event):
    print("press:", event.keysym)

def release(event):
    print("release:", event.keysym)

root.bind('<KeyPress>', press)
root.bind('<KeyRelease>', release)
root.mainloop()

qui peut afficher

1
2
3
4
5
6
7
release: Return
press: a
release: a
press: space
release: space
press: Return
release: Return

Un évènement press correspond à l’appui sur une touche. Quand on relâche la touche, on obtient un évènement release. Dans la sortie ci-dessus, la première ligne correspond au relâchement de la touche ENTRÉE qui a servi à lancer le programme en console.

Concernant la question des événements d’appui vs relâchement, on pourra lire le fil de discussion TkInter keypress, keyrelease events.

Evénement du clavier : majuscule vs minuscule

On peut distinguer (automatiquement) l’appui sur une touche et l’appui sur la même touche mais en majuscule (on appuie simultanément sur la touche MAJ), comme le montre le code ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=400, height=100, bg="ivory")
cnv.pack()
cnv.focus_set()
x=0

def texte(event):
    global x
    cnv.create_text(x, 40, text =event.keysym, font="Arial 50 bold")
    x+=30

cnv.bind('<Key-a>', texte)
cnv.bind('<Key-B>', texte)

root.mainloop()
  • Ligne 14 : détection de l’appui simple sur une touche.
  • Ligne 15 : détection de l’appui simultané sur une touche et la touche MAJ.

Relâchement d’une touche (sous Linux, auto-repeat)

Il est parfois important d’observer quand l’utilisateur relâche une touche. C’est assez délicat à détecter sous Linux : une possibilité est de désactiver l’auto-repeat (et penser à le réactiver en fin de script sinon il sera perdu pour toutes les applications, y compris en dehors de Python !), comme indiqué dans TkInter keypress, keyrelease events. Voici un exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from tkinter import Tk, Canvas
import os

os.system('xset r off')

root = Tk()
cnv = Canvas(root, width=400, height=100, bg="ivory")
cnv.pack()
cnv.focus_set()
x=0

def texte(event):
    global x
    cnv.create_text(x, 40, text =event.keysym, font="Arial 50 bold")
    x+=30

cnv.bind('<KeyRelease-a>', texte)

root.mainloop()
os.system('xset r on')
  • Ligne 17 : chaque fois que l’utilisateur relâche la touche A minuscule du clavier, la lettre a est écrite en minuscule sur le canevas.
  • Lignes 12-15 : on vérifie que si laisse la touche A appuyé, aucun événement n’est détecté.
  • Ligne 4 : si cette ligne est supprimée, on vérifie que le relâchement de touche n’est pas détecté.
  • Ligne 20 : on réactive la détection de répétition de touche.

Pression continue et simultanée sur deux touches

L’appui continu et simultané sur deux touches engendre des événements qui dépendent du système d’exploitation. Le code suivant

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

root = Tk()

def press(event):
    print("press:", event.keysym, time())

def release(event):
    print("release:", event.keysym, time())

for key in ["a", "z"]:
    root.bind('<KeyPress-%s>' %key, press)
    root.bind('<KeyRelease-%s>' %key, release)

root.mainloop()

indique si la touche a ou z a été pressée ou relâchée et à quel moment cela se produit. Plus précisément, lors de l’exécution, on va

  • appuyer continûment sur la touche a,
  • appuyer continûment sur la touche z, sans relâcher a
  • relâcher la touche z et laisser enfoncée un certain temps la touche a
  • relâcher enfin la touche a.

Sous Linux, on lira ceci (code en partie tronqué)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
press: a 1555710017.386128
release: a 1555710017.986468
press: a 1555710017.9867318
release: a 1555710018.026511
press: a 1555710018.0267715
release: a 1555710018.066585
press: a 1555710018.066844
...
release: a 1555710018.3864462
press: a 1555710018.3866968
release: a 1555710018.4267566
press: a 1555710018.4269776
press: z 1555710018.434321
release: z 1555710019.0345645
press: z 1555710019.034829
release: z 1555710019.0747504
press: z 1555710019.0750027
...
release: z 1555710019.5153375
press: z 1555710019.515587
release: z 1555710019.5553439
press: z 1555710019.5556386
release: z 1555710019.5705464
release: a 1555710020.0423253

On observe, comme pour le cas d’une unique touche maintenue enfoncée, qu’est générée, pour chacune des touches, une suite d’événements très rapprochés release puis ``press``(par exemple lignes 6-7 pour a et lignes 19-20 pour z). On obseve aussi que lorsque survient l’appui sur la touche z (ligne 13), l’appui maintenu sur a n’engendre plus aucun événement. Lorsque la pression sur z est relâchée (cf. ligne 23), même si la pression sur a est prolongée, aucun événement venant de a n’est généré et c’est seulement lorsque la touche a est relâché qu’un événement est émis (ligne 24).

Sous Windows, on lira plutôt ceci (code en partie tronqué)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
press: a 1555711099.043254
press: a 1555711099.5322323
press: a 1555711099.6011677
press: a 1555711099.6329112
...
press: a 1555711100.1418097
press: a 1555711100.171071
press: z 1555711100.195916
press: z 1555711100.666682
press: z 1555711100.7084837
...
press: z 1555711101.175861
press: z 1555711101.2056215
release: z 1555711101.2358758
release: a 1555711101.7203972

Cette fois, un appui continu engendre un événement press (lignes 1-13). Le 2e appui cache le premier (lignes 8-14). La fin se termine par deux événements release (lignes 14-15).

Voici deux approches pour gérer un appui simultané sur deux touches :

Déplacement amélioré avec deux touches (Windows)

Une première version de déplacement oblique d’un objet par appui simultané et continu sur deux touches a été fournie mais l’exécution souffre d’une latence assez sensible au lancement du mouvement. Voici une version qui corrige ce problème sous Windows.

../../../_images/mvt_deux_touches_bis.gif

Pour bien comprendre le code, il faut s’être penché sur la gestion des événements press et release lors d’un appui continu sur une ou deux touches. Sous Windows, c’est très simple, un événement press est généré pour tout appui continu et un événement release uniquement si l’utilisateur relâche la touche. Il suffit donc de capturer les appuis et ordonner le déplacement qui en résulte

L’idée est d’enregistrer en permanence dans un dictionnaire l’état (pressée ou pas) des 4 touches de déplacement. Cet état des touches est ensuite examiné toutes les 20 ms par une fonction qui décide de déplacer ou non l’objet.

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

WIDTH = 800
HEIGHT = 500

root = Tk()
cnv = Canvas(root, width=WIDTH, height=HEIGHT, bg="ivory")
cnv.pack()
cnv.focus_set()

SIDE = 30
UNIT = 2

rect = cnv.create_rectangle(
    (WIDTH / 2 - SIDE / 2, HEIGHT / 2 - SIDE / 2),
    (WIDTH / 2 + SIDE / 2, HEIGHT / 2 + SIDE / 2),
    fill="black",
)


def handler():
    unit = UNIT

    if sum(keys.values()) > 1:
        unit = UNIT/1.5

    for key in drn:
        if keys[key]:
            if key == "Up":
                cnv.move(rect, 0, -unit)
            elif key == "Right":
                cnv.move(rect, unit, 0)
            elif key == "Left":
                cnv.move(rect, -unit, 0)
            elif key == "Down":
                cnv.move(rect, 0, unit)

    root.after(20, handler)


def press(event):
    keys[event.keysym] = True


def release(event):
    keys[event.keysym] = False


drn = ["Up", "Right", "Left", "Down"]
keys = dict.fromkeys(drn, False)

for key in drn:
    cnv.bind("<KeyPress-%s>" % key, press)
    cnv.bind("<KeyRelease-%s>" % key, release)

handler()

root.mainloop()
  • Lignes 49-50 : l’état de chacune des 4 flèches du clavier est enregistré dans le dictionnaire keys. Pour chacune des flèches, on enregistre si elle est activée ou pas.
  • Lignes 41 et 45 : les états sont enregistrés par les fonctions press et release qui sont appelées automatiquement par Tkinter après liaison (lignes 52-54)
  • Ligne 38 : toutes les 20 ms, la fonction handler est appelée. Elle examine l’état de chacune des 4 touches (ligne 27).
  • Lignes 28-36 : si la touche est active (elle est pressée), le mouvement correspondant est exécuté.
  • Lignes 24-25 : si deux touches sont pressées simultanément, la vitesse de déplacement est rectifiée pour qu’elle corresponde à peu près au déplacement horizontal ou vertical.

Déplacement amélioré avec deux touches (Linux)

Une première version de déplacement oblique d’un objet par appui simultané et continu sur deux touches a été fournie mais l’exécution souffre d’une latence assez sensible au lancement du mouvement. Voici une version qui corrige ce problème sous Linux.

../../../_images/mvt_deux_touches.gif

Pour bien comprendre le code, il faut s’être penché sur la gestion des événements press et release lors d’un appui continu sur une ou deux touches. Sous Linux (et macOS aussi) la difficulté provient du fait qu’un appui continu engendre automatiquement un événement release qu’il faut arriver à discerner d’un relâchement provoqué par l’utilisateur. Par ailleurs, il faut arriver à contourner le problème de latence au démarrage en provoquant le déplacement dès qu’une pression est enregistrée.

L’idée est d’enregistrer en permanence dans une liste l’état des 4 touches de déplacement : la touche a-t-elle été pressée ? à quel moment reçoit-elle un événement press ? à quel moment reçoit-elle un événement release ? Cet état des touches est ensuite examiné toutes les 20 ms par une fonction qui décide de déplacer ou non l’objet.

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

WIDTH = 800
HEIGHT = 500

root = Tk()
cnv = Canvas(root, width=WIDTH, height=HEIGHT, bg="ivory")
cnv.pack()
cnv.focus_set()

SIDE = 30
UNIT = 2

rect = cnv.create_rectangle(
    (WIDTH / 2 - SIDE / 2, HEIGHT / 2 - SIDE / 2),
    (WIDTH / 2 + SIDE / 2, HEIGHT / 2 + SIDE / 2),
    fill="black",
)


def handler():
    for i in range(4):

        if keys[i][0] and keys[i][2] is not None:
            if keys[i][1] is None or keys[i][1] - keys[i][
                    2] > 3 or keys[i][1] - keys[i][2] < 0:
                keys[i][0] = False

        elif not keys[i][
                0] and keys[i][1] is not None and keys[i][2] is None:
            keys[i][0] = True

        if keys[i][0]:
            if i == 0:
                cnv.move(rect, 0, -UNIT)
            if i == 1:
                cnv.move(rect, UNIT, 0)
            if i == 2:
                cnv.move(rect, -UNIT, 0)
            if i == 3:
                cnv.move(rect, 0, UNIT)
        keys[i][1] = None
        keys[i][2] = None

    root.after(20, handler)


def press(event):
    key = event.keysym
    if key == "Up":
        keys[0][1] = time.time()
    elif key == "Right":
        keys[1][1] = time.time()
    elif key == "Left":
        keys[2][1] = time.time()
    elif key == "Down":
        keys[3][1] = time.time()


def release(event):
    key = event.keysym
    if key == "Up":
        keys[0][2] = time.time()
    elif key == "Right":
        keys[1][2] = time.time()
    elif key == "Left":
        keys[2][2] = time.time()
    elif key == "Down":
        keys[3][2] = time.time()


for key in ["Up", "Right", "Left", "Down"]:
    cnv.bind("<KeyPress-%s>" % key, press)
    cnv.bind("<KeyRelease-%s>" % key, release)

keys = [
    [False, None, None],  # up
    [False, None, None],  # right
    [False, None, None],  # left
    [False, None, None],  # down
]
handler()

root.mainloop()
  • Ligne 2 : La fonction time du module time permet de savoir à la microseconde quand un événement a lieu (cf. ligne 49 par exemple)

  • lignes 73 : l’état de chacune des 4 flèches du clavier est enregistré dans la liste keys. Pour chacune des flèches, on enregistre :

    • si elle a été pressée par l’utilisateur
    • l’instant où un événement press est capturé
    • l’instant où un événement release est capturé.
  • lignes 49 et 61 : les états sont enregistrés par les fonctions press et release qui sont appelées automatiquement par Tkinter après liaison (lignes 73-75)

  • Ligne 46 : toutes les 20 ms, la fonction handler est appelée. Elle examine l’état de chacune des 4 touches (cf. ligne 23).

  • Si la touche a été pressée initialement (ligne 25), elle détermine si cette touche est relâchée (ligne 28) par l’utilisateur. Une succession trop proche (moins de 3 ms, cf. ligne 27) entre un événement release et un événement press n’est pas considérée comme un relâchement de l’utilisateur.

  • Si la touche n’a pas été pressée (ligne 30), la fonction détermine si la touche est pressée par l’utilisateur (ligne 32). En particulier, si dans l’interalle de 20 ms, une pression est enregistrée sans événement release, on considère qu’une pression continue sur la touche est exercée.

  • lignes 34-42 : une fois l’appui sur les touches connu, les mouvement qui s’en déduisent sont exécutés.

Pour contrôler la vitesse de déplacement, on peut jouer sur la valeur de UNIT (ligne 13); en particulier, on pourrait modifier le code pour que les déplacements obliques et verticaux/horizontaux se fassent à la même vitesse (actuellement, ils sont plus rapides).

Événements de la souris

Un widget peut capturer des actions de la souris. Par exemple, dans l’interface ci-dessous :

../../../_images/inout.gif

si l’utilisateur clique à l’extérieur du rectangle, la console affiche OUTside, sinon elle affiche INside.

Voici le code correspondant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from tkinter import Tk, Canvas

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

cnv.create_rectangle(50, 50, 150, 150, fill='gray')

def je_clique(event):
    if 50< event.x <150 and 50< event.y <150:
        print("INside")
    else:
        print("OUTside")

cnv.bind("<Button-1>",je_clique)

root.mainloop()
  • Ligne 7 : création d’un rectangle gris.
  • Ligne 15 : tout appui sur le bouton gauche de la souris (événement <Button-1>) entraîne l’exécution de la la fonction je_clique.
  • Lignes 9-13 : la fonction je_clique, quand elle est automatiquement appelée suite à un clic gauche dans le canevas, reçoit un argument représenté par le paramètre event traduisant le clic de souris. Cet événement contient en attributs les coordonnées (event.x, event.y) dans le canevas du clic de souris. Un simple calcul permet de savoir si le clic est dans le rectangle ou à l’extérieur.

Récapitulatif des événement de la souris

Voici un résumé des principaux événements liés à la souris :

Nom de l’évenement de souris Action
Button-1 Clic gauche
Button-3 Clic bouton droit
Button-2 Clic bouton central
Button-4 Molette vers le haut
Button-5 Molette vers le bas
ButtonRelease Relâchement d’un bouton
Motion Déplacement du curseur de la souris
MouseWheel Roulette (pas sous Linux)
Button Un des boutons

L’événement du clic de souris

La programmation d’interfaces graphiques est de la programmation événementielle : une boucle infinie (la mainloop) attend des événements (c’est comme ça qu’on dit) tel qu’un appui sur une touche du clavier, un clic de souris ; éventuellement, le programme réagit à ces événements.

Pour surveiller un clic de souris sur le canevas, il suffit

  • d’écrire une fonction (disons clic) qui sera appelée (automatiquement) chaque fois qu’un clic aura lieu sur le canevas ;
  • de lier l’événement clic de souris à l’appel de la fonction \(\mathtt{clic}\).

On dit que la fonction clic est une fonction de rappel (callback function en anglais). La liaison est appelée binding en anglais, cf. le code ci-dessous.

Le programme présenté ci-dessous :

../../../_images/position_clic1.gif

montre la réaction à un événement de clic de souris sur un canevas affichant les coordonnées du clic et dessinant un disque là où on a cliqué. Le code correspondant est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tkinter import *

root = Tk()
cnv = Canvas(root, width=300, height=300, bg="ivory")
cnv.pack()

def clic(event):
    x, y = event.x, event.y
    print(x, y)
    cnv.create_oval(x-3, y-3, x+3, y+3,
                    fill='red', outline='')

cnv.bind("<Button-1>", clic)
root.mainloop()
15
16
17
18
19
20
151 244
121 225
101 155
92 110
86 89
61 63
  • Ligne 9 : la fonction de rappel clic; chaque fois qu’on clique sur le canevas, cette fonction est appelée et reçoit en argument, sans que le programmeur ait la main dessus, un objet appelé ici event qui représente (et donne accès) à toutes les propriétés du clic qui a été effectié.

  • Ligne 12 : la liaison de l’événement clic gauche de souris, qui se code en Tkinter par "<Button-1>" et de la fonction clic : chaque fois qu’un événement clic de souris est intercepté sur le canevas, la fonction clic est appelée. -

  • La fonction clic effectue ici deux actions :

    • elle affiche dans la console les coordonnées sur le canevas du pixel qui a été cliqué. Dans l’exemple (lignes 15-19), on voit que l’utilisateur a cliqué 6 fois sur le canevas. Remarquez que les valeurs lues sont, bien sûr, entre 0 et 300 ;
    • elle dessine quelque chose au point du clic (ici, un petit disque rouge, c’est juste pour que l’image soit compréhensible).

L’événement "<Button>" (sans numéro) s’applique à tout événement relatif à un bouton de souris, le clic gauche comme le clic droit ou l’action sur la molette. Si on veut capturer exactement un clic gauche de souris, il faut utiliser l’événement "<Button-1>".

Événement du déplacement de la souris

Le code ci-dessous donne un exemple de capture de tout mouvement de la souris (en javascript, c’est « onmouseover ») :

../../../_images/in_out.gif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from tkinter import Tk, Canvas

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

cnv.create_rectangle(50, 50, 150, 150, fill='gray')

def mouvement(event):
    if 50< event.x <150 and 50< event.y <150:
        print("INside")
    else:
        print("OUTside")

cnv.bind("<Motion>",mouvement)

root.mainloop()
  • Ligne 15 : tout mouvement de la souris sur le canevas entraîne l’exécution de la fonction mouvement.
  • Lignes 9-13 : la position du curseur (event.x, event.y) est capturée. En fonction de sa valeur, un message est affiché dans la console pour indiquer si le curseur est à l’intérieur ou à l’extérieur du rectangle gris.

On peut aussi capturer tout mouvement de la souris mais avec le bouton gauche enfoncé :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from tkinter import Tk, Canvas

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

cnv.create_rectangle(50, 50, 150, 150, fill='gray')

def glisser(event):
    if 50< event.x <150 and 50< event.y <150:
        print("INside")
    else:
        print("OUTside")

cnv.bind("<B1-Motion>",glisser)

root.mainloop()
  • Ligne 15 : l’événement « <B1-Motion> » capture le bouton 1 enfoncé et le mouvement de la souris.

Déplacer un objet à la souris

L’interface ci-dessous

../../../_images/deplacer_souris.gif

montre un carré rouge dans un canevas que la souris peut déplacer par glisser-déposer (le bouton gauche de la souris reste appuyé).

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
from tkinter import Tk, Canvas

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

rect=cnv.create_rectangle(30, 30, 130, 130, fill="red", outline='')

old=[None, None]

def clic(event):
    old[0]=event.x
    old[1]=event.y

def glisser(event):
    cnv.move(rect, event.x-old[0], event.y-old[1])
    old[0]=event.x
    old[1]=event.y


cnv.bind("<B1-Motion>",glisser)
cnv.bind("<Button-1>",clic)

root.mainloop()
  • Lignes 9-22 : le principe est qu’une variable old (ligne 9) enregistre la dernière position de la souris, ce qui permet de savoir, à l’instant courant, de combien (ligne 16) on doit déplacer l’objet. Cette position est initialisée au clic qui précède le glisser-déposer (ligne 12-13).
  • Ligne 21 : chaque fois que la souris bouge dans le canevas, la fonction glisser est appelée.
  • Ligne 22 : chaque fois que le bouton gauche est enfoncé, la fonction clic est appelée.
  • Ligne 11-13 : permet de mémoriser la position (event.x, event.y) du clic qui précède le déplacement du carré
  • Ligne 15-18 : ( event.x-old[0], event.y-old[1]) représente le vecteur de déplacement entre le moment courant et le dernier enregistrement de position.
  • Lignes 17-18 : on met à jour la position de la souris.

Si on veut faire un déplacement uniquement dans une direction et en bloquant le déplacement :

 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
from tkinter import Tk, Canvas

RIGHT=300

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

rect=cnv.create_rectangle(30, 30, 130, 130, fill="red", outline='')

old=[None, None]

def clic(event):
    old[0]=event.x
    old[1]=event.y

def glisser(event):
    a,b,c,d=cnv.coords(rect)
    if c< RIGHT or event.x<old[0]:
        cnv.move(rect, event.x-old[0], 0)
    old[0]=event.x
    old[1]=event.y

cnv.bind("<B1-Motion>",glisser)
cnv.bind("<Button-1>",clic)

root.mainloop()
  • Lignes 3, 6 et 19 : RIGHT permet de bloquer le déplacement vers la droite
  • Lignes 18 : c donne l’abscisse courante du bord droit du rectangle.
  • Ligne 20 : à cause du dernier paramètre qui vaut 0, il n’y a pas de déplacement du rectangle suivant la composante verticale.

Supprimer des images à la souris

Soit l’interface suivante :

../../../_images/supprimer_images.gif

où un canevas contient des images placées aléatoirement du logo Python et que l’on peut supprimer à la souris en se plaçant assez proche de l’image.

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

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

logo = PhotoImage(file="python64.gif")

for i in range(20):
    x, y= randrange(SIDE),randrange(SIDE)
    cnv.create_image(x, y, image=logo)

def clic(event):
    x=event.x
    y=event.y
    t=cnv.find_closest(x, y)
    if t:
        cnv.delete(t[0])

cnv.bind("<Button>",clic)

root.mainloop()
  • Ligne 22 : tout clic venant d’un des boutons de la souris appelle la fonction clic.
  • Ligne 15-20 : la fonction clic enregistre la position (x, y) du clic (lignes 16-17) et appelle la méthode find_closest du canevas(ligne 18) et recherche l’item t le plus proche de la position (x, y). L’appel récupère l’item en question (ou None si le canevas est vide) et le supprime (ligne 20) grâce à son id qui est le premier élément du tuple (ligne 20) représentant l’item.

Position de la souris

Si un événement event est lié à la souris, par exemple un clic de souris :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from tkinter import *

def clic(event):
    x=event.x
    y=event.y
    print("Position :", x, y)

root = Tk()
root.bind("<Button-1>",clic)
root.mainloop()

il est possible de capturer la position de la souris lors de l’exécution de l’événement via les prises d’attributs event.x et event.y (lignes 4 et 5 dans l’exemple).

Bien que la docstring de Tkinter ne le précise pas, il semble que ces positions soient de type entier et non pas flottant, comme semble le montrer cet extrait du code-source de Tkinter :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def _substitute(self, *args):

        # ....

        getint = self.tk.getint
        def getint_event(s):
            """Tk changed behavior in 8.4.2, returning "??" rather more often."""
            try:
                return getint(s)
            except (ValueError, TclError):
                return s

        # ....

        e.x = getint_event(x)
        e.y = getint_event(y)

Désassocier un événement

Quand une partie dans un jeu est terminée (par exemple, le joueur a perdu), on souhaite désactiver la capture de certains événements du clavier ou de la souris et qui avaient été initialement associés à certains widget. On utilise pour cela la méthode unbind et elle peut s’appliquer à n’importe quel type d’événement.

Voici un exemple :

../../../_images/unbind.gif

si le joueur clique sur la zone verte, il reçoit un message et la partie peut alors continuer, sinon, un message indique Perdu ! et la zone de clic est désactivée.

Le code est ci-dessous :

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

root = Tk()
cnv=Canvas(width=400, height=400, bg="lightgreen")
cnv.pack(side="left")
lbl=Label(text='', width=10, font="arial 20 bold")
lbl.pack(side='right')

MESSAGE=["Bien\n joué !", "Nice!", "Super !", "Bravo !", "Champion !", "OK !"]

def f(event):
    x, y=event.x, event.y
    if 100<x<300 and 100<y<300:
        lbl['text']="Perdu !"
        root.unbind("<Button-1>")
    else:
        lbl['text']=MESSAGE[randrange(len(MESSAGE))]


A=(100,100)
B=(300, 300)
cnv.create_rectangle(A, B, fill='pink', outline='')

root.bind("<Button-1>", f)

root.mainloop()
  • Ligne 25 : la fonction f est associée à un clic gauche dans la fenêtre.
  • Ligne 16 : si le joueur clique dans la zone en rose (cf. ligne 14 et ligne 23), on lui annonce qu’il a perdu (ligne 15) et le clic gauche dans la fenêtre ne réagira plus (à cause de unbind).

Si la méthode unbind est appelée sur un événement sans nom de fonction particlulière (comme c’est le cas dans l’exemple ci-dessus), toutes les fonctions du widget associées à l’événement sont désactivées. Mais il serait possible de faire une désactivation sélective.

Modifier un widget par survol de la souris

Un événement du type <Enter> ne doit pas être confondu avec un événement d’appui sur la touche Enter du clavier (qui est plutôt l’événement <Return>). L’événement <Enter> est activé lorsque la souris entre dans la zone délimitée par un widget. Il existe aussi l’événement inverse <Leave> qui est activé lorsque la souris quitte la zone. Voici un exemple :

../../../_images/enter_leave.gif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from tkinter import *

root = Tk()
side=350
pad=30
cnv = Canvas(root, width=side, height=side)
cnv.pack(padx=100, pady=100)

def go_in(event):
    cnv['bg']="pink"

def go_out(event):
    cnv['bg']="royal blue"

cnv.bind("<Enter>", go_in)
cnv.bind("<Leave>", go_out)

root.mainloop()

Quand la souris entre dans le canevas cnv, la couleur de fond passe en rose. Quand la souris quitte le canevas,la couleur de fond passe en bleu.