Le canevas

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

Le canevas

Le widget Canvas

Tkinter dispose d’un widget canevas : c’est une surface permettant de dessiner des formes géométriques (rectangles, disques, du texte, etc) et de les manipuler (personnalisation, déplacement, suppression, etc). Ce type de widget permet de créer des jeux de toutes sortes.

Le widget Canvas est très souple d’emploi grâce à son système de tags et d’id. Il est robuste car, selon cet expert de TkInter, il peut supporter la gestion de plusieurs dizaines de milliers d’items.

Dans le jargon Tkinter, les formes géométriques sont appelés des items. A la différences des widgets Tkinter tels qu’un bouton ou un label ou une fenêtre qui sont définis par des classes comme la classe Button, la classe Label, etc et qui contiennent d’autres widgets, les formes géométriques que l’on peut dessiner sur le canvas ne sont ni des widgets ni des instances de classes.

Voici en résumé les items les plus utiles qu’un canevas peut générer :

Motif géométrique Item Tkinter
segment de droite, flèche create_line
rectangle carré create_rectangle
cercle ellipse create_oval
texte create_text
image png, gif create_image
polygone create_polygon
arcs de cercle, d’ellipse create_arc

Création d’un canevas

Voici un code qui crée un simple canevas :

1
2
3
4
5
6
from tkinter import Tk, Canvas

root=Tk()
cnv=Canvas(root, width=600, height=400, bg="ivory")
cnv.pack(padx=50, pady=50)
root.mainloop()

ce qui affiche :

../../../_images/canevas.png

On crée une surface canevas avec le constructeur Canvas (c’est une classe). Comme pour tout widget, il faut lui indiquer comme premier argument le widget qui va contenir le canevas à créer, dans l’exemple ci-dessus, c’est la fenêtre appelée root.

On lui indique les dimensions du canevas avec les arguments nommés width et height. On peut indiquer une couleur de fond avec l’argument nommé bg (qui signifie background). Si on ne met rien, par défaut c’est une couleur grise qui se confond avec la couleur par défaut du widget parent, ce qui ne permet pas de bien distinguer la surface (et voilà pourquoi, en principe, je placerai une couleur de fond dans mes canevas).

Repérage dans un canevas

Soit le code suivant construisant un canevas :

from tkinter import Tk, Canvas

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

Un canevas (défini par le constructeur Canvas) est muni d’un système de repérage (invisible) dans lequel chaque point du canevas a deux coordonnées :

../../../_images/canevas_repere1.png

Attention, le système de repérage est orienté différemment du système utilisé en mathématiques :

  • l’origine est le coin en haut à gauche
  • l’axe des abscisses est, comme en math, l’axe horizontal orienté de gauche à droite
  • l’axe des ordonnées est, comme en math, un axe vertical mais il est orienté vers le bas (assez logique vue la configuration, non ?).

Les coordonnées repèrent des pixels. Pour être plus précis, le premier pixel est numéroté 0, le suivant 1, etc. Si un canevas est créé avec l’option width=200 le pixel le plus à droite du canevas est numéroté 199.

Bords d’un canevas

Le contenu de ce paragraphe peut être réservé à un approfondissment.

Lorsqu’on définit un canevas, on peut lui fournir deux options highlightthickness et bd qui prennent chacune des pixels :

  • l’option bd est la largeur d’une bande autour du canevas et qui a même couleur de fond ; l’option borderwidth est synonyme de bd ;
  • l’option highlightthickness est la largeur d’une bande autour du canevas (et de son bord) et qui est activé lorsque le canevas a le focus (cad que le canevas est susceptible d’enregistrer des touches au clavier ou des activités de la souris comme cela est utile dans un jeu) ; par défaut, cette bande d’activation du canevas est blanche et de deux pixels de large.

La plupart du temps, on peut ignorer ces options et si vous débutez complètement vous pouvez ignorer ce qui suit. Mais, parfois le comportement par défaut de ces options, justifie qu’on les modifie. La documentation n’explique pas clairement comment ces options affectent la zone active du canevas (celle où on peut dessiner), avec pour conséquence que parfois, certaines zones, prêts des bords en particulier, sont rendues invisibles.

Soit le code suivant

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

root=Tk()

# Largeur du canevas
W=300

# Hauteur du canevas
H=200

# Largeur du bord
B=50

# Largeur de la zone de focus
F=10

cnv=Canvas(root, width=W, height=H, bg="ivory",
           bd=B,
           highlightthickness=F,
           highlightbackground="green")

cnv.pack(side="left", padx=20, pady=20)

# Partie active du canevas
z=B+F
w=W-1
h=H-1
cnv.create_rectangle(z,z,z+w,z+h, fill="orange")

root.mainloop()

Il produit la fenêtre suivante :

../../../_images/focus_border.png

que l’on va décrire en liaison avec le code :

  • l’origine (0,0) du canevas se trouve dans la zone verte, au coin en haut à gauche ;

  • en vert, la bande d’activation du focus. Son épaisseur est highlightthickness (ligne 19). La couleur provient de l’option highlightbackground donnée au canevas ;

  • en ivoire, le bord du canevas, de couleur bg="ivory", cf. ligne 17. La largeur du bord est donnée par bd (ligne 18)

  • en orange, un rectangle dont les dimensions sont calculées ici pour recouvrir exactement la totalité de la zone du canevas sur laquelle on peut dessiner de manière visible ; en dehors de cette zone, tout item généré (rectangle, ligne, etc) sera caché.

  • le rectangle orange (lignes 25-28) a les dimensions déclarées dans le contructeur Canvas ligne 17 c’est-à-dire width=W et height=H.

  • Noter le comportement non intuitif suivant :

    • un item placé en un point (x,y)\(\mathtt{0< x <width}\) et \(\mathtt{0< y < height}\) peut être invisible ;
    • un item placé en un point (x,y) ne vérifiant pas \(\mathtt{0< x <width}\) ou \(\mathtt{0< y < height}\) peut être visible ;

La plupart du temps, le comportement n’utilisant pas les options highlightthickness et bd convient car les valeurs par défaut des options sont de 1 ou 2 pixels, ce qui est peu visible.

Mais si on veut faire des dessins très précis sur un canevas, et un comportement plus intuitif selon lequel les coordonnées visibles sont exactement range(W) et range(H)W et H sont les dimensions données au canevas pour width et height, je conseille de mettre highlightthickness et bd à 0.

Voici le même dessin avec ces options placées à zéro :

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

root=Tk()

# Largeur du canevas
W=300

# Hauteur du canevas
H=200

# Largeur du bord
B=0

# Largeur de la zone de focus
F=0

cnv=Canvas(root, width=W, height=H, bg="ivory",
           bd=B,
           highlightthickness=F)

cnv.pack(side="left", padx=20, pady=20)

# Partie active du canevas
z=B+F
w=W-1
h=H-1
cnv.create_rectangle(z,z,z+w,z+h, fill="orange")

root.mainloop()

et la sortie :

../../../_images/position_canevas_ok.png

Limites des objets dessinés sur le canevas

Les positions données lors de la construction d’objets tels que des rectangles, etc sont toujours comprises au sens large. Par exemple, un rectangle créé par

1
cnv.create_rectangle((10, 20), (100, 200))

commence en largeur au pixel du canevas numéroté 10 et se termine au pixel numéroté 100, ce pixel étant inclus. Donc les dimensions du rectangle sont, si on inclut les bords, de 91 pixels.

Sauvegarder le contenu du canevas

Vous avez dessiné quelque chose sur un canevas et vous souhaitez le sauvegarder dans un format image, png par exemple. Sous Tkinter, la seule sortie possible est le format eps (du postscript encapsulé) qui peut se lire sous Gimp facilement et se convertir en png par exemple.

On a besoin de la méthode postscript du canevas. Voici un exemple d’utilisation :

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

SIDE=300
SEP=50

def print_cnv():
    cnv.postscript(file="rect.eps", colormode='color')

master=Tk()

cnv=Canvas(master, width=SIDE, height=SIDE, bg='lavender')
cnv.pack()

cnv.create_rectangle(SEP, SEP, SIDE-SEP, SIDE-SEP, fill="gray")
cnv.create_text(SIDE/2, SIDE/2, text="Tkinter",
                fill="white",
                font="Times 30 bold")

cnv.after(1000, print_cnv)

master.mainloop()
  • Ligne 7 : la méthode postscript du canevas permet d’exporter au format eps. On désigne un fichier de sortie avec l’argument nommé file.
  • Lignes 14-15 : on peut capturer les items de Tkinter comme des rectangles ou du texte. La capture d’image Photoimage ne fonctionne pas.
  • Ligne 11 : la couleur de fond du canevas (qui n’est pas un item) n’est pas capturé.
  • Ligne 19 : il faut attendre que le canevas soit dessiné.

Voici la fenêtre et son image capturée (et convertie en png) :

../../../_images/rect_ps_window.png
../../../_images/rect_ps.png

Items par catégories

Dessiner un segment, une ligne brisée

Un segment est généré par la méthode create_line du Canvas :

../../../_images/create_line.png

Voici le code qui trace un segment d’extrémités les points A et B :

from tkinter import Tk, Canvas

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

A=(50, 60)
B=(150, 60)
cnv.create_line(A, B)
root.mainloop()

On peut aussi utiliser la syntaxe moins lisible cnv.create_line(50, 60, 150, 60).

Pour la couleur du trait (noire par défaut), utiliser l’option fill et l’épaisseur de la ligne (1 pixel par défaut), utiliser l’option width :

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

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

cnv.create_line(50, 60,150, 60, width=8, fill="blue")
root.mainloop()
../../../_images/create_line_options.png

Plus généralement, create_line permet de tracer une ligne polygonale :

from tkinter import Tk, Canvas

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

A=50, 60
B=150, 100
C= 30,140
D=150, 180

cnv.create_line(A, B, C, D)
root.mainloop()
../../../_images/create_multiline.png

On peut utiliser la syntaxe moins lisible

cnv.create_line(50, 60,150, 100, 30,140, 150, 180)

Dessiner un rectangle

Pour dessiner un rectangle sur un canevas cnv, on appelle la méthode cnv.create_rectangle :

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

root = Tk()
cnv = Canvas(root, width=500, height=250, bg='ivory')
cnv.pack()

cnv.create_rectangle(30, 70, 190, 170)
cnv.create_rectangle((280, 40), (400, 200), fill='orange')

root.mainloop()
../../../_images/rectangle.png

Pour le positionnement du rectangle sur le canevas, il faut donner les coordonnées des extrémités d’une des deux diagonales du rectangle :

  • soit en les plaçant côte à côte (ligne 7)
  • soit en plaçant les coordonnées entre parenthèses (ligne 8), ce qui est plus lisible.

Un rectangle peut être dessiné « rempli » avec l’argument nommé fill prenant un nom de couleur (ligne 8). Par défaut, un rectangle est toujours délimité par des côtés qui sont formés d’une ligne noire d’un pixel d’épaisseur. Si on veut supprimer cette bordure, on donne l’argument outline=''. Si on veut colorier par exemple en bleu cette bordure, on écrira outline="blue" :

../../../_images/bord_rectangle.png
from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=500, height=250, bg='ivory')
cnv.pack()

cnv.create_rectangle(30, 70, 190, 170, fill="orange", outline="")
cnv.create_rectangle((280, 40), (400, 200),
                     fill='orange',
                     outline="blue",
                     width=6)

root.mainloop()

Ordre des sommets et create_rectangle

Si on construit un rectangle par un appel cnv.create_rectangle((a, b), (c, d)), il n’est pas nécessaire que \(\mathtt{a\leq c}\) ou que \(\mathtt{b\leq d}\) ; en fait Tkinter construit un rectangle de diagonales les points de coordonnées (a, b) et (c,d) ce qui bien sûr, permet de tracer le rectangle.

from tkinter import Tk, Canvas

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

cnv.create_rectangle((50, 50), (150, 100), fill='red', outline='')
cnv.create_rectangle((250, 150), (350, 50), fill='green')

cnv.create_rectangle((150, 300), (50, 250), fill='orange')
cnv.create_rectangle((350, 250), (250, 350), fill='brown', outline='')

root.mainloop()

Une précision toutefois. Par convention,

  • le bord gauche et le bord supérieur du rectangle ainsi identifiés sont considérés comme faisant partie du rectangle
  • le bord droit et le bord inférieur est considéré comme NE faisant PAS partie du rectangle bien que ces bords soient déssinés (si l’option outline n’est pas marquée outline='').

Dessiner un cercle, un disque

Contrairement à ce qu’on s’attendrait, un cercle n’est pas construit en donnant son centre et son rayon. En fait, le cercle est produit comme si on suivait les trois étapes ci-dessous :

../../../_images/create_circle.png

On donne à Tkinter deux points \(\mathtt{A}\) et \(\mathtt{B}\) qui sont deux sommets opposés du carré ayant leurs côtés parallèles aux axes et circonscrit au cercle que l’on veut dessiner.

Voici un code de dessin :

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

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

diam=400
A=(a,b)=(50, 80)
B=(a+diam, b+diam)

cnv.create_oval(A, B, fill='light blue', outline='red', width=2)

root.mainloop()
../../../_images/creer_disque.png
  • Lignes 8-10: le côté du carré et le diamètre du cercle ont même longueur.

  • Ligne 12 :

    • l’option fill détermine la couleur de remplissage ; en l’absence de cette option, un cercle plutôt qu’un disque est dessiné (absence de remplissage);
    • l’option outline définit la couleur du cercle ; en l’absence de cette option, un cercle de 1 pixel d’épaisseur entoure le disque ;
    • l’option width détermine, en pixel, l’épaisseur du bord.

Noter que les deux espacements, vertical et horizontal, entre \(\mathtt{A}\) et \(\mathtt{B}\) doivent être égaux. Cet espacement est le diamètre du cercle (dans le code, c’est diam). Le centre du cercle est le milieu des points \(\mathtt{A}\) et \(\mathtt{B}\).

Si les espacements ne sont pas identiques, on obtient plus un cercle mais une ellipse :

../../../_images/ellipse.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from tkinter import Tk, Canvas
SIDE=500

root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE//1.5, bg="ivory")
cnv.pack()

da=400
db=180
A=(a,b)=(50, 80)
B=(a+da, b+db)

cnv.create_oval(A, B, fill='light blue', outline='red', width=2)

root.mainloop()

Disque de centre et de rayon donnés

Tkinter ne dispose pas de fonction de tracé de cercle mais il est facile d’en créer une :

 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 *

def dot(cnv, C, R=6, color='red'):
    xC, yC=C
    A=(xC-R, yC-R)
    B=(xC+R, yC+R)
    return cnv.create_oval(A,B, fill=color, outline=color)

PAD=50
DIM=200
WIDTH=DIM+PAD
HEIGHT=DIM+PAD

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

C = (WIDTH//2, HEIGHT//2)

dot(cnv, C, R=DIM//2, color="pink")

root.mainloop()

ce qui produit :

../../../_images/disque_centre_rayon.png

Quelques explications :

  • Lignes 5-6 : si le centre du cercle est \(\mathtt{C=(xC, yC)}\) et que son rayon est \(\mathtt{R}\), c’est que le sommet \(\mathtt{A}\) en haut à gauche du carré extérieur au cercle est \(\mathtt{A=(xC-R, yC-R)}\) et de même, le sommet \(\mathtt{B}\) en bas à droite est \(\mathtt{B=(xC+R, yC+R)}\);
  • Ligne 3 : la fonction dot recevra comme argument le canevas (cnv) sur lequel le disque sera dessiné, comme cela la fonction facilement réutilisable ; on donne, bien sûr, le centre \(\mathtt{C}\) et le rayon R et une couleur de fond, par défaut rouge ;
  • Ligne 7 : on utilise la méthode create_oval pour dessiner le disque ; le bord de cercle (outline) a même couleur que le fond (ça évite un liseré noir comme bord).
  • Ligne 10 : DIM sera le diamètre du disque, PAD est une marge pour que le disque ne soit pas trop collé au bord du canevas.
  • Ligne 18 : les coordonnées du centre choisi. On a centré le point dans le canevas.
  • Ligne 20 : dessin d’un disque.

Placer du texte dans le canevas

De la même façon que l’on peut créer des figures géométriques sur un canevas (rectangles, etc), on peut créer des items de texte :

../../../_images/create_text.png

qui est produit par le code suivant :

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

WIDTH=400
HEIGHT=300

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

cnv.create_text(C, anchor =W,
                text ="Coucou !", fill ="blue", font="Arial 30 bold")

root.mainloop()

On indique en premier argument de create_text (point C de la ligne 11 qui est défini en ligne 9) un point de référence (une ancre) par rapport auquel le texte sera positionné sur le canevas.

On peut donner une option d’ancre (anchor) qui indique à quel point cardinal le point de référence se trouve par rapport au texte. Le point cardinal est le nord, sud, est, ouest, etc mais abrégé en anglais et en majuscule ou sinon entre quotes. Dans l’exemple, le point de référence est placé à l’ouest du texte comme on peut le voir sur la sortie ci-dessous où le point de référence est marqué en rouge :

../../../_images/create_text_anchor.png

C’est assez peu intuitif : c’est le point indiqué avec ses coordonnées (ici C) qui est à l’ouest du texte et non pas le contraire !

En l’absence d’option anchor le texte est centré au point de référence.

Le contenu du texte (une chaîne de caractères) est placé dans l’argument nommé text. Parmi les options se trouvent :

  • le remplissage (fill affecté à un nom de couleur comme 'blue' dans l’exemple)
  • le choix de la fonte avec l’option font (ligne 12).

Si le texte est trop long par rapport à la surface du canevas, il sera coupé et sortira (donc invisible).

Il existe une option angle permettant d’incliner le texte (ligne 13 ci-dessous) :

../../../_images/text_angle.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from tkinter import *

WIDTH=400
HEIGHT=300

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

cnv.create_text(C, anchor =W,
                text ="Coucou !", fill ="blue", font="Arial 30 bold",
                angle=45)

root.mainloop()

Inclusion d’images sur le canevas

Le canevas supporte le placement d’images présentes sur une unité de stockage. Seuls les formats

  • png (depuis Python 3.4 sous Windows et peut-être Python 3.6 pour Mac Os)
  • gif

sont pris en charge. Le format jpeg ne semble pas pris en charge, cf. ce message Tkinter error: Couldn’t recognize data in image file.

Exemple de code :

inclusion_images.py

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

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

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

for i in range(5):
    centre= (randrange(SIDE),randrange(SIDE))
    cnv.create_image(centre, image=logo)

root.mainloop()

et qui affiche

../../../_images/inclusion_images1.png

Le code ci-dessus place aléatoirement 5 images sur un canevas.

Avant de pouvoir utiliser une image (ligne 13), il faut la convertir dans un format propre à Tkinter. Pour cela (ligne 9), on doit utiliser la classe PhotoImage en lui précisant l’adresse du fichier image sur le disque. Une fois l’image convertie par appel à PhotoImage, on peut l’incorporer dans un canevas en utilisant la méthode du canevas appelée create_image. Comme les dimensions d’une image sont invariables, il suffit de donner à create_image les coordonnées du point où on souhaite que le centre de l’image se trouve sur le canevas.

Remarquer qu’on a besoin de créer juste une seule instance de PhotoImage même si elle sert à la création de plusieurs images.

L’image ne s’adapte pas automatiquement au canevas où l’image est placée, si l’image est plus large que le canevas, elle ne sera qu’en partie visible dans le canevas.

Noter qu’il existe une syntaxe peut-être moins lisible pour désigner les coordonnées du centre de l’image : au lieu d’écrire cnv.create_image(c, image=logo) après avoir défini la variable c=(x,y), on peut écrire cnv.create_image(x, y, image=logo).

Pour le support du png sous macOS, voir IDLE and tkinter with Tcl/Tk on macOS.

Dessiner un segment fléché

Pour dessiner un segment avec une extrémité en forme de flèche, utiliser l’argument nommé arrow de la méthode cnv.create_line :

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

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

cnv.create_line(50, 60,150, 60, width=5, arrow='last')
root.mainloop()
../../../_images/fleche_tkinter.png

L’option 'last' signifie que c’est l’extrémité donnée en 2e qui est fléchée. Pour que ce soit l’origine qui soit fléché, l’option à donner est "first" et pour les deux extrémités soient fléchées, utiliser la valeur "both". La largeur de la flèche s’adapte à la largeur du segment (width). Il semble qu’on ne puisse générer des flèches que pour des segments, pas pour des acs.

Dessiner un unique pixel

Pour se repérer (lorsqu’on débogue un code par exemple), on peut avoir besoin de placer un point ayant une taille d’un unique pixel :

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

root = Tk()
cnv = Canvas(root, width=200, height=200)
cnv.pack()
cnv.create_rectangle(100, 100, 100, 100, fill='red', outline='')

root.mainloop()

ce qui, agrandi, donne :

../../../_images/pixel_canevas_agrandi.png

Tracer un polygone

Un polygône est tracé par la méthode create_polygon à qui on donne les coordonnées des sommets :

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

WIDTH=300
HEIGHT=200

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

cnv.create_polygon(50,50, 100, 120, 150, 10)

root.mainloop()

ce qui poduit :

../../../_images/polygon.png

Par défaut, les polygones sont remplis en noir et sans bordure, ie fill="black" et outline='', cf. code ci-dessous pour une alternative.

Le dernier sommet est relié au premier.

Il est aussi possible de placer un sommet par son couple de coordonnées ; le code est alors plus lisible :

 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, sample

WIDTH=300
HEIGHT=200

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

A=50,50
B=100, 120
C=150, 10

triangle=cnv.create_polygon(A, B, C, fill='lavender', outline='red')
L=cnv.coords(triangle)

root.mainloop()

La suite des coordonnées des sommets détermine le caractère convexe ou concave du polygone :

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

WIDTH=300
HEIGHT=200

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

cnv.create_polygon((50,50), (100, 120), (150, 10), (40,150))

root.mainloop()

ce qui produit

../../../_images/polygone_concave.png

Bord d’un rectangle

Par défaut, un bord noir de 1 pixel d’épaisseur est rajouté à chaque rectangle. On peut agir sur la couleur du bord avec l’option bd pour outline :

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

root = Tk()
cnv = Canvas(root, width=480, height=120, bg='ivory')
cnv.pack()

cnv.create_rectangle(10, 10, 110, 110, fill='light blue')
cnv.create_rectangle(120, 10, 220, 110, fill='light blue', outline='')
cnv.create_rectangle(230, 10, 340, 110, fill='light blue', outline='red')
cnv.create_rectangle(350, 10, 460, 110, fill='light blue',
                     outline='light blue')

root.mainloop()

On voit donc qu’on peut faire disparaître de deux façons le bord d’un rectangle : soit en plaçant outline='' (ligne 8), soit en confondant sa couleur avec la couleur de fond (ligne 11).

Bord, intérieur et dimensions exactes d’un rectangle

Le contenu de cette unité peut être examiné en seconde lecture.

Le premier pixel en haut à gauche d’un rectangle est toujours dessiné.

Par défaut, un bord de 1 pixel d’épaisseur est rajouté à chaque rectangle mais de manière dissymétrique :

  • le bord en bas et le bord à droite sont extérieurs au rectangle,
  • le bord en haut et le bord à gauche sont intérieurs au rectangle et viennent recouvrir la couleur de fond.

Ainsi, si un côté a pour abscisse \(a\) et le côté parallèle a pour abscisse \(b\), la largeur par défaut du rectangle est \(|a-b|+1\) et de \(|a-b|\) si le bord est exclu. Ne pas oublier toutefois que si \(a=b\) le pixel est quand même dessiné.

Voci un code et l’image agrandie qui en résulte :

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

root = Tk()
cnv = Canvas(root, width=130, height=250, bg='ivory')

cnv.pack()

cnv.create_rectangle(10, 10, 110, 110, fill='light blue')
cnv.create_rectangle(10, 113, 110, 223, fill='light blue', outline='')

root.mainloop()
../../../_images/bords_rectangles.png

On voit bien le décalage de 1 pixel entre la version avec bord et la version sans bord.

Lorsque le bord est retiré, les dimensions d’un rectangle sont faciles à calculer : c’est la différence entre les abscisses et la différence entre les ordonnées. Par exemple ci-dessous le rectangle est de largeur 100 et de hauteur 50 :

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

root = Tk()
cnv = Canvas(root, width=120, height=60, bg='ivory')

cnv.pack()

cnv.create_rectangle(10, 10, 110, 60, fill='light blue', outline='')


root.mainloop()

Créer un arc

Voici un exemple de tracé de secteur angulaire dans une ellipse :

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

WIDTH=400
HEIGHT=300

root = Tk()
cnv = Canvas(root, width=WIDTH, height=HEIGHT, background="ivory")
cnv.pack()
A=(50,50)
B=(350, 250)

cnv.create_oval(A, B)
cnv.create_arc(A, B, outline="red", extent=210, start=-120,
               fill="light blue",  width=4)
root.mainloop()

ce qui produit

../../../_images/arc_plein.png
  • Ligne 12-14 : le secteur est tracé sur la base de l’ellipse qui contient le secteur. on appelera \(O\) le centre de l’ellipse et on reprend les noms de sommets \(A\) et \(B\).

  • Ligne 13-14 :

    • option extent : la totalité de l’angle \((OA,OB)\), en degrés ;
    • option start : l’angle en degrés \((u, OA)\)\(u\) représente une demi-droite horizontale dirigée vers l’Est et \(A\) le point de départ sur l’ellipse ;

Les trois styles possibles de fermeture du secteur sont :

 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 *

WIDTH=400
HEIGHT=150

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

x=30
y=20
COTEx=100
COTEy=180

cnv.create_arc(x,y, x+COTEx, y+COTEy, extent=210, start=-20, width=2)
x+=20+COTEx
cnv.create_arc(x,y, x+COTEx, y+COTEy, extent=210, start=-20, width=2,
               style=ARC)

x+=20+COTEx
cnv.create_arc(x,y, x+COTEx, y+COTEy, extent=210, start=-20, width=2,
               style=CHORD)

root.mainloop()

ce qui produit

../../../_images/trois_arcs_possibles.png

Bord d’un cercle

Un cercle possède par défaut un bord noir d’une épaisseur width de 1 pixel. La question se pose de savoir

1
cnv.create_line(a, b, a+2*R, b+2*R)

s’étend sur 2R+1 pixels, coupant la verticale numérotée a et la verticale numérotée a+2*R. Si l’option width est placée à 0 alors le cercle ne possède plus de bord visible et est situé strictement entre les deux droites. Si width est mise à 2 alors, de chaque côté, on a un pixel à l’intérieur et 1 pixel sur les droites. Ensuite, ça se répartit équitablement entre l’intérieur et l’extérieur.

Pour remplir, option fill, par exemple fill='red'.

 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

R=400
x=20
SIDE=2*(R+x)

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

u=x
v=x


# le bord du cercle passe sur les droites
cnv.create_line(u,0,u, SIDE)
cnv.create_line(u+2*R,0,u+2*R, SIDE)
cnv.create_oval(u,v,u+2*R, v+2*R, fill='green', outline='red', width=2)

root.mainloop()

Ci-dessous un cercle de centre le point (100, 100) et de rayon 5. Bords compris, le diamètre du cercle s’étend sur 11 pixels :

  • le centre : 1 pixel
  • le diamètre intérieur (hors centre) : 4 pixels
  • le bord : 1 pixel

Les bords du cercle sont sur les droites d’équations x=xC+R, x=xC-R. Si on passe l’option fill='' alors la seule différence est que le bord est supprimé.

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

WIDTH=200
HEIGHT=200

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

xC, yC= WIDTH//2, HEIGHT//2
R=5

# cercle de centre C et de rayon R
cnv.create_rectangle(xC, yC, xC, yC, fill='black', outline='')

cnv.create_oval(xC-R,yC-R,xC+R, yC+R,  outline='red')

root.mainloop()

Les deux points donnés à la construction sont les extrémités de la diagonale d’un rectangle qui contient le disque.

Attention, la boite est à côtés parallèles aux axes ce qui peut poser problème en cas de transformation par rotation..

Dessiner une courbe

On peut être intéressé par tracer une courbe passant par certains points.

Si la courbe est connue par son équation, on peut utiliser Matplotlib qui s’interface assez bien avec Tkinter.

Voici une solution utilisant des segments, d’après draw a smooth curve :

from tkinter import Canvas, Tk
#from tkinter import * # For Python 3.2.3 and higher.
root = Tk()
root.title('Smoothed line')
cw = 250 # canvas width
ch = 200 # canvas height
canvas_1 = Canvas(root, width=cw, height=ch, background="pink")
canvas_1.grid(row=0, column=1)

x1 = 50
y1 = 10
x2 = 50
y2 = 180
x3 = 180
y3 = 180

canvas_1.create_line(x1,y1, x2,y2, x3,y3, smooth="true", width= 2)
root.mainloop()

ce qui produit :

../../../_images/courbe.png

Voici une autre possibilité, utilisant create_arc :

from tkinter import Canvas, mainloop, Tk

def circle(canvas, x, y, r, width):
    return canvas.create_oval(x+r, y+r, x-r, y-r, width=width)

def circular_arc(canvas, x, y, r, t0, t1, width):
    return canvas.create_arc(x-r, y-r, x+r, y+r, start=t0,
                             extent=t1-t0, style='arc', width=width)

def ellipse(canvas, x, y, r1, r2, width):
    return canvas.create_oval(x+r1, y+r2, x-r1, y-r2, width=width)

def elliptical_arc(canvas, x, y, r1, r2, t0, t1, width):
    return canvas.create_arc(x-r1, y-r2, x+r1, y+r2, start=t0,
                             extent=t1-t0, style='arc', width=width)

def line(canvas, x1, y1, x2, y2, width, start_arrow=0, end_arrow=0):
    arrow_opts = start_arrow << 1 | end_arrow
    arrows = {0b10: 'first', 0b01: 'last', 0b11: 'both'}.get(
        arrow_opts, None)
    return canvas.create_line(x1, y1, x2, y2, width=width,
                              arrow=arrows)

def text(canvas, x, y, text):
    return canvas.create_text(x, y, text=text, font=('bold', 20))


w = Canvas(Tk(), width=1000, height=600, bg='white')

circle(w, 150, 300, 70, 3)  # q0 outer edge
circle(w, 150, 300, 50, 3)  # q0 inner edge
circle(w, 370, 300, 70, 3)  # q1
circle(w, 640, 300, 70, 3)  # q2
circle(w, 910, 300, 70, 3)  # q3

# Draw arc from circle q3 to q0.
midx, midy = (150+910) / 2, 300
r1, r2 = 910-midx, 70+70
elliptical_arc(w, midx, midy, r1, r2, 30, 180-30, 3)

line(w,  10, 300,  80, 300, 3, end_arrow=1)
line(w, 220, 300, 300, 300, 3, end_arrow=1)
line(w, 440, 300, 570, 300, 3, end_arrow=1)
line(w, 710, 300, 840, 300, 3, end_arrow=1)

text(w, 150, 300, 'q0')
text(w, 370, 300, 'q1')
text(w, 640, 300, 'q2')
text(w, 910, 300, 'q3')

w.pack()
mainloop()

ce qui produit

../../../_images/courbe2.png

Dessiner une ligne en pointillé

Il s’agit plutôt de tirets que de pointillés. Il faut utiliser l’option dash. L’exemple ci-dessous provient du site de Fredrick lundh :

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

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

cnv.create_line(0, 100, 200, 0, fill="red", dash=(4, 4))

root.mainloop()

qui affiche :

../../../_images/pointilles.png

Dessiner un pixel invisible

Créer un pixel caché, cela peut-être utile pour déboguer un code : le pixel n’est pas visible mais l’item existe sur le canevas, il a donc une id :

 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, sample

WIDTH=300
HEIGHT=200

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

point=cnv.create_polygon(50,50, fill='', outline='')
print(cnv.coords(point))

cnv.move(point, 100, 100)
print(cnv.coords(point))

L=cnv.coords(point)
root.mainloop()

qui affiche :

1
2
[50.0, 50.0]
[150.0, 150.0]

Modifier le profil de la flèche

On souhaite parfois modifier l’allure de la pointe d’une flèche. C’est possible avec l’option arrowshape :

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

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

cnv.create_line(50, 60,150, 60, width=8, arrow='last', arrowshape=(18,30, 8))
root.mainloop()
../../../_images/forme_fleche_tkinter.png

Voici la description de l’option arrowshape=(h, b, c) :

  • h est la distance à l’axe de l’aile de la flèche,
  • b est la largeur de la base de la flèche (la partie qui est sur l’axe),
  • c est la longueur de la partie extérieure de la flèche.
../../../_images/fleche_options.png

Extrémité, jonction de segments

L’option capstyle de create_line permet de gérer le raccordement de segments, en particulier de l’arrondir :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from tkinter import Tk, Canvas, ROUND
WIDTH=400
HEIGHT=200

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

cnv.create_line(20, 20, 180, 20, fill='blue', width=16)
cnv.create_line(180, 20, 180, 180, fill='blue', width=16)

cnv.create_line(380, 20, 220, 20, fill='blue', width=16, capstyle=ROUND)
cnv.create_line(220, 20, 220, 180, fill='blue', width=16, capstyle=ROUND)

root.mainloop()
../../../_images/jonction_segments.png

Voir la documentation pour d’autres possibilités.

Image vs rectangle : positionnement

On fera attention que pour la méthode create_image, les arguments de dimensions n’ont pas même signification que pour, par exemple, create_rectangle. Ainsi,

  • cnv.create_image(a, a, image=test) va afficher le centre de l’image au point (a, a) ;
  • cnv.create_rectangle(a, a, a+50, a+50, fill="red") va afficher le coin supérieur gauche du rectangle au point (a, a).

Ainsi le code suivant :

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

side=120
root = Tk()
cnv = Canvas(root, width=2*side, height=2*side, bg="lavender")
cnv.pack()

a=side
test = PhotoImage(file="python.gif")
cnv.create_image(a, a, image=test)
cnv.create_rectangle(a, a, a+50, a+50, fill="red")

root.mainloop()

affiche-t-il

../../../_images/image_vs_rectangle.png

On peut remédier à cela en donnant l’argument nommé anchor=nw à create_image :

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

side=120
root = Tk()
cnv = Canvas(root, width=2*side, height=2*side, bg="lavender")
cnv.pack()

a=side
test = PhotoImage(file="python.gif")
cnv.create_image(a, a, image=test, anchor=NW)
cnv.create_rectangle(a, a, a+50, a+50, fill="red")

root.mainloop()

qui affiche :

../../../_images/image_vs_rectangle_anchor.png

On voit alors que les coins supérieurs gauches du carré et de l’image, cette fois, coïncident.

Image qui n’apparaît pas

Les items de type PhotoImage souffrent d’une anomalie très curieuse, illustrée par le code suivant (il faut disposer dans le répertoire courant d’une image python.gif qui est un logo du langage Python)

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

def run():
    root = Tk()
    cnv = Canvas(root, width=200, height=200)
    cnv.pack()
    photo(cnv)
    root.mainloop()

def photo(cnv):
    test =PhotoImage(file="python.gif")
    cnv.create_image(100, 100, image=test)
    cnv.create_rectangle(100, 100, 150,150,fill="orange")

run()

qui affiche

../../../_images/image_invisible.png

Quand le code ci-dessus s’exécute, l’image de la ligne 12 reste invisible tandis que le rectangle à la ligne 13 lui est bien visible. Pourquoi ? Parce que test est une variable locale à la fonction photo et qu’elle est ensuite éliminée par le garbage collector de Python. En réalité, c’est peut-être même plus compliqué puisque le code suivant ne contient aucune variable vers PhotoImage(file="python.gif") et pourtant l’image n’est pas affichée :

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

def run():
    root = Tk()
    cnv = Canvas(root, width=200, height=200)
    cnv.pack()
    photo(cnv)
    root.mainloop()

def photo(cnv):
    cnv.create_image(100, 100, image=PhotoImage(file="python.gif"))
    cnv.create_rectangle(100, 100, 150,150,fill="orange")

run()

Pour y remédier, il suffit de garder une référence vers l’image, par exemple en définissant l’image comme attribut du canevas :

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

def run():
    root = Tk()
    cnv = Canvas(root, width=200, height=200)
    cnv.pack()
    photo(cnv)
    root.mainloop()

def photo(cnv):
    test =PhotoImage(file="python.gif")
    cnv.test=test
    cnv.create_image(100, 100, image=test)
    cnv.create_rectangle(100, 100, 150,150,fill="orange")

run()

qui affiche

../../../_images/image_visible.png

Cetta anomalie est référencée, par exemple voir le message de furas sur StackOverFlow ou le site de Fredrik Lunth. Voir aussi cette partie de la documentation officielle et qui explique le comportement : * Tk will not keep a reference to the image*.

Les items et les tags

Identifiant d’items du canevas

Tout objet placé sur un canevas Tkinter est créé avec une méthode du canevas dont le nom commence par create, comme create_rectangle. Lors de la création, chaque objet reçoit une id unique. Cette id est un identifiant entier strictement positif. Il permet de garder trace de l’objet créé et de le modifier dynamiquement.

Exemple :

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

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

t=SIDE//2
xA, yA=A=(randrange(t),randrange(t))
xB, yB=B=(randrange(t),randrange(t))

rect1=cnv.create_rectangle(A, (xA+t,yA+t), fill='blue')
rect2=cnv.create_rectangle(B, (xB+t,yB+t), fill='red')

print(rect1)
print(rect2)

root.mainloop()
20
21
1
2

et qui produit :

../../../_images/notion_id.png

Dans ce code, on fait apparaître, à des endroits aléatoires (lignes 10 et 11 les points A et B), deux rectangles sur le canevas. L’appel à cnv.create_rectangle (lignes 16 et 17) est récupéré dans une variable et qui contient alors l’id du rectangle créé. Les ids sont alors affichées (ligne 16 et 17). On remarque (lignes 20-21) que les id ne sont pas des entiers aléatoires mais qu’il s’agit d’entiers consécutifs croissants.

Identifiant d’image

Tout image placée sur un canevas Tkinter et créée avec la méthode du canevas create_image, lors de la création, reçoit une id unique. Cette id est un identifiant entier strictement positif. Il permet de garder trace de l’image créée.

Exemple :

id_images.py

from tkinter import *
from random import randrange

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

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

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

root.mainloop()
1
2
3
4
5

qui produit :

../../../_images/plusieurs_id1.png

Dans ce code, on fait apparaître, à des endroits aléatoires, 5 images sur le canevas. L’appel à cnv.create_image est récupéré dans une variable et qui contient alors l’id de l’image créée. Les ids sont alors affichées. On remarque que les id ne sont pas des entiers aléatoires mais qu’il s’agit d’entiers consécutifs croissants.

La méthode delete

Le canevas offre une méthode permettant de supprimer des items présents sur le canevas. Supprimer signifie que les items ne sont plus visibles mais aussi que la mémoire qu’ils occupaient est libérée.

Soit l’animation minimale suivante :

../../../_images/delete.gif

qui crée deux carrés (lignes 7 et 8 ci-dessous) dans un canevas et, au bout de deux secondes, fait disparaître un des deux carrés. Voici le code :

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

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

cnv.create_rectangle(20, 20, 80, 80, fill='red', outline='')
rect=cnv.create_rectangle(100, 20, 160, 80, fill='red', outline='')

def effacer(ident):
    cnv.delete(ident)

cnv.after(2000, effacer, rect)

root.mainloop()

C’est à la ligne 11 qu’on observe l’effacement et à la ligne 13 qu’on observe la temporisation d’une seconde.

L’effacement se fait en appelant la méthode delete du canevas sur l’identificateur de l’item que l’on souhaite supprimer.

Pour tout effacer tous les items présents sur le canevas, utiliser un appel de la forme Canvas.delete('all'). C’est une technique fréquente lorsqu’on cherche à rafraichir le plateau d’un jeu (on efface et on redessine).

Le pemier usage de delete est lorsqu’on veut faire disparaître visuellement un item, par exemple un projectile qui a atteint sa cible.

Toutefois, il n’y a pas que la disparition visuelle : imaginons un missile qui se déplace puis sort de la zone du canevas ; il n’est plus visible du joueur mais est toujours référencé comme item. Si de tels missiles étaient très nombreux, ils pourraient utiliser inutilement des ressources : calcul (détermination de la nouvelle position du missile) et mémoire (car un item nécessite une allocation mémoire effectué par la bibliothèque TCL/Tk sous-jacente).

Pour déplacer un objet et donc le faire disparaître d’une position pour le faire apparaître à une autre, on n’utilise pas la méthode delete mais les méthodes de Canvas du nom de move ou de coords.

Suppression d’images du canevas

Il est possible de supprimer n’importe quelle image créée sur le canevas. Pour cela, il suffit de connaître l’id de l’image à supprimer et d’utiliser la méthode delete du canevas.

Exemple :

suppression_image.py

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

NB_IMG=8
SIDE=100
WIDTH=SIDE*NB_IMG
X0=Y0=SIDE//2

root = Tk()
logo = PhotoImage(file="python.gif")
cnv = Canvas(root, width=WIDTH, height=SIDE, bg="ivory")
cnv.pack(pady=100)

ids=[]
for k in range(NB_IMG):
    id_image=cnv.create_image(X0+k*SIDE, Y0, image=logo)
    ids.append(id_image)

j=randrange(NB_IMG)
print(j)
mon_id=ids[j]

cnv.delete(mon_id)

root.mainloop()
26
5
../../../_images/suppression_image1.png

On utilise une même image python.gif carrée de taille SIDE=100. Dans la boucle (lignes 15-17) :

  • on dessine côte à côte NB_IMG=8 images
  • on mémorise l’id de chaque image dans une liste ids

Puis, on choisit au hasard une id d’image (ligne 19 et ligne 20 pour l’affichage de l’indice) et on supprime du canevas l’objet ayant cet id (ligne 23). Comme on le voit sur la copie d’écran, il y a bien un trou dans l’alignement des images à l’indice choisi.

La liste de tous les items du canevas

Il est possible de récupérer toutes les id des objets créés sur un canevas (disons cnv). Cette liste s’obtient en appelant la méthode cnv.find_all.

Ci-dessous, on tire un entier aléatoire N entre 1 et 10 et on dessine sur le canevas N carrés aléatoires. Puis on demande l’affichage de tous les items :

../../../_images/find_all.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tkinter import *
from random import randrange

WIDTH=200
HEIGHT=200

COTE=20

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

N=randrange(10)
print(N)

for _ in range(N):
    x=randrange(WIDTH)
    y=randrange(HEIGHT)
    cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="purple")

print(cnv.find_all())

root.mainloop()
24
25
4
(1, 2, 3, 4)
  • Lignes 13-14 et 24 : le nombre aléatoire de carrés à dessiner.
  • Lignes 16-19 : le dessin des carrés
  • Lignes 21 et 25 : la liste de toutes les id des objets présents sur le canevas. ici, il n’y a que les N carrés construits.

La génération d’identifiants d’items du canevas

Les identifiants des items générés d’un canevas donné sont uniques, y compris si on supprime des items. Les identifiants générés sont des entiers consécutifs croissants à partir de 1.

Cela signifie que par exemple les identifiants 1, 2, 3, etc sont générés. Si l’item d’identifiant par exemple 42 est supprimé, l’identifiant 42 ne sera pas réutilisé.

Pour observer le phénomène, soit le programme 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from tkinter import *
from random import randrange, sample

COLORS=["orange", "pink", "green", "yellow", "lavender", "lightblue"]
NCOLORS=len(COLORS)

WIDTH=600
HEIGHT=600
COTE=20

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

N=100

items_ids=[]

for i in range(N):
    x=randrange(WIDTH)
    y=randrange(HEIGHT)
    color=COLORS[randrange(NCOLORS)]
    rect=cnv.create_rectangle(x, y, x+COTE, y+COTE, fill=color, outline="")
    items_ids.append(rect)

print(items_ids==list(range(1, N+1)))


M=sample(items_ids, N//2)

for m in M:
    cnv.delete(m)

new_items_ids=[]

for i in range(N):
    x=randrange(WIDTH)
    y=randrange(HEIGHT)
    color=COLORS[randrange(NCOLORS)]
    rect=cnv.create_rectangle(x, y, x+COTE, y+COTE, fill=color, outline="")
    new_items_ids.append(rect)

print(new_items_ids==list(range(N+1, 2*N+1)))

root.mainloop()

qui affiche ceci :

../../../_images/id_items.png

Le programme génère 100 carrés placés et colorés aléatoirement (lignes 23 et 22) dont les id sont stockés dans une liste (ligne 24). Ensuite, on vérifie (ligne 26) que les id générés sont les entiers de 1 à 100, dans cet ordre.

Puis on supprime (à la fois de la mémoire du canevas et visuellement) au hasard la moitié des items (ligne 29). Et on recrée 100 items dont les id sont stockés dans une liste (ligne 41).

On vérifie alors (ligne 43) que les id générés sont dans l’ordre, les entiers de 101 à 200.

Lire les options d’un item du canevas

La méthode itemconfigure du canevas permet de lire les options d’un item :

../../../_images/item_configure.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
om 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='green')

options=cnv.itemconfigure(rect)
print(*options.items(), sep='\n')

root.mainloop()
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
('activedash', ('activedash', '', '', '', ''))
('activefill', ('activefill', '', '', '', ''))
('activeoutline', ('activeoutline', '', '', '', ''))
('activeoutlinestipple', ('activeoutlinestipple', '', '', '', ''))
('activestipple', ('activestipple', '', '', '', ''))
('activewidth', ('activewidth', '', '', '0.0', '0.0'))
('dash', ('dash', '', '', '', ''))
('dashoffset', ('dashoffset', '', '', '0', '0'))
('disableddash', ('disableddash', '', '', '', ''))
('disabledfill', ('disabledfill', '', '', '', ''))
('disabledoutline', ('disabledoutline', '', '', '', ''))
('disabledoutlinestipple', ('disabledoutlinestipple', '', '', '', ''))
('disabledstipple', ('disabledstipple', '', '', '', ''))
('disabledwidth', ('disabledwidth', '', '', '0.0', '0'))
('fill', ('fill', '', '', '', 'red'))
('offset', ('offset', '', '', '0,0', '0,0'))
('outline', ('outline', '', '', 'black', 'green'))
('outlineoffset', ('outlineoffset', '', '', '0,0', '0,0'))
('outlinestipple', ('outlinestipple', '', '', '', ''))
('state', ('state', '', '', '', ''))
('stipple', ('stipple', '', '', '', ''))
('tags', ('tags', '', '', '', ''))
('width', ('width', '', '', '1.0', '1.0'))
  • Ligne 9 : les options du rectangle (ligne 7) apparaissent comme un dictionnaire.
  • Lignes 10 et 13-35 : les couples (clé, valeur) du dictionnaire sont affichés grâce à la méthode items. Par exemple, on lit la couleur de remplissage ligne 27.

La méthode itemcget permet de lire une option particulière (lignes 9 et 13) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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='green')

color=cnv.itemcget(rect, 'fill')
print(color)

root.mainloop()
13
red

Modifier les options d’un item du canevas

Parfois, on cherche à modifier les propriétés d’un item, par exemple changer du texte, changer la couleur de fond ou les dimensions sans pour autant recréer complètement l’item. La méthode itemconfigure (ou itemconfig) qui permettait déjà de lire les options permet aussi de les modifier.

Le programme visible ci-dessous :

../../../_images/modif_item_canevas.gif

crée d’abord un carré rouge dans un canevas puis, au bout de deux secondes, change en bleu la couleur du carré :

Le code correspondant :

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

def action():
    cnv.itemconfigure(rect, fill="lavender")


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

rect=cnv.create_rectangle(30, 30, 450, 450, fill="pink", outline='green')


cnv.after(1000, action)
root.mainloop()

La modification de la couleur est lancée par la ligne 14 et est effectuée ligne 4 où l’option fill est passée à "lavender" sur l’item du rectangle par la méthode itemconfigure.

Noter qu’on ne peut pas modifier ainsi la position de l’item sur le canevas puisque itemconfigure donne seulement accès aux options ayant un nom comme fill. Pour déplacer ou agrandir un item, utiliser les méthodes move ou coords.

Cacher/montrer des items sur le canevas

On peut avoir besoin de faire disparaître provisoirement un item présent sur le canevas, par exemple un rectangle. Il serait possible de l’effacer avec la méthode delete du canevas puis de le recréer quand on en a à nouveau besoin. En réalité, il est possible de le faire disparaître et de le faire réapparaître sans le détruire ni le reconstruire. Il suffit pour cela d’utiliser l’option state de l’item et qui prend deux valeurs :

  • hidden : pour cacher l’item
  • normal : pour rendre visible l’item.

Pour modifier cette option, il suffit d’utiliser la méthode du canevas itemconfigure. Voici un exemple :

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

def hide_my_rect():
    cnv.itemconfigure(rect, state="hidden")

def show_my_rect():
    cnv.itemconfigure(rect, state="normal")

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

rect=cnv.create_rectangle(30, 30, 270, 270, fill="red")

cnv.after(1500, hide_my_rect)
cnv.after(3000, show_my_rect)

root.mainloop()

Le canevas contient un rectangle rouge. Au bout d’une seconde et demi, le rectangle est caché. Une seconde et demi après, le rectangle réapparaît.

L’options state peut aussi être utile si on veut créer avant exécution un certain nombre d’items qu’on aura plus qu’à afficher le moment venu. Dans un jeu , il est souvent préférable de créer des objets à l’initialisation du jeu plutôt qu’en cours de jeu car la création d’objets peut être source de ralentissments.

Déplacer un item avec la méthode move

On peut déplacer un item du canevas en utilisant la méthode move :

../../../_images/move.gif

Pour cela on suppose que l’on connait le vecteur de déplacement. Voici un exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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='gray')

def deplacer(x):
    cnv.move(rect, x, 0)

cnv.after(1000, deplacer, 150)

root.mainloop()
  • Ligne 7 : le rectangle a une id appelée rect
  • Ligne 10 : le déplacement se fait en utilisant de l’id de l’item. Le vecteur de déplacement est (x, 0) et à l’appel, x vaudra 150 (cf. ligne 12).
  • Ligne 12 : le déplacement se fait une seconde après l’ouverture de la fenêtre.
  • Ligne 10 : le déplacement est instantané, c’est plutôt une translation qu’un déplacement.

Ce qui suit est d’importance secondaire, en particulier si vous êtes débutant. Bien comprendre qu’un déplacement d’un item est en fait une translation d’un item. Et une translation dépend d’un vecteur. Si vous voulez bouger un item du point \(\mathtt{A=(x_A, y_A)}\) vers le point \(\mathtt{B=(x_B, y_B)}\) alors la translation est de vecteur \(\mathtt{\vec{AB}=(x_B-x_A, y_B -y_A)}\) (pensez à la formule « extrémité moins origine »). Donc pour pouvoir bouger un item sur le canevas, au lieu d’écrire

v=(xB - xA, yB - yA)
cnv.move(item, v[0], v[1])

qui n’est pas très parlant, on aimerait pouvoir écrire le vecteur directement :

v=(xB - xA, yB - yA)
cnv.move(item, v)

Mais ce code ne sera pas accepté car la méthode move attend 3 arguments et pas 2. On peut toutefois écrire un code assez proche :

v=(xB - xA, yB - yA)
cnv.move(item, *v)

En effet, l”« opérateur » * en Python permet de décompacter les éléments d’un tuple (comme v ci-dessus) et de placer ces éléments comme arguments d’un appel de fonction.

Déplacer un item avec la méthode coords

On peut déplacer un item du canevas en utilisant la méthode coords :

../../../_images/coords.gif

Cela suppose que l’on connaisse la position finale de l’objet. Voici un exemple avec pour objet un triangle :

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

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

A=(70, 30)
B=(30, 160)
C=(150, 120)

tr=cnv.create_polygon(A, B, C, fill='gray')
print(cnv.coords(tr))

def deplacer(x, y):
    cnv.coords(tr, A[0]+x, A[1]+y,  B[0]+x, B[1]+y, C[0]+x, C[1]+y)

cnv.after(1000, deplacer, 150,50)

root.mainloop()
  • Ligne 11 : le triangle a une id appelée tr
  • Ligne 12 : les coordonnées des sommets du triangle.
  • Ligne 15 : le déplacement se fait à partir de l’id de l’item. La position finale des 3 sommets du triangle est indiquée dans l’appel. Ici, on a décalé chaque abscisse de x et chaque ordonnée de y.
  • Ligne 17 : le déplacement se fait une seconde après l’ouverture de la fenêtre.
  • Ligne 15 : le déplacement est instantané, c’est plutôt une translation qu’un déplacement.
  • Lignes 12 et 15 : on notera les deux rôles possibles de coords (informer ou déplacer).

Contour d’un item

On a parfois besoin de connaître la position précise d’un item sur le canevas, surtout si cet item est mobile.

Un appel du type cnv.coords(item) renvoie une liste de coordonnées de points qui entourent l’item. La liste renvoyée dépend du type d’item. Dans le cas d’un item de type rectangle, ellipse, ligne, ou une image, une liste [a, b, c, d] de 4 nombres flottants est renvoyée et qui correspond aux coordonnées des extrémités d’une diagonale du rectangle qui entoure l’item ; plus précisément, (a, b) est le coin supérieur gauche et (c, d) est le coin inférieur droit. Voici un exemple d’utilisation :

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

WIDTH=400
HEIGHT=300

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

ellipse= cnv.create_oval(200, 250, 350, 100)

print(cnv.coords(ellipse))

root.mainloop()
15
[200.0, 100.0, 350.0, 250.0]

et qui produit :

../../../_images/coords.png

Pour un segment, ce sera les coordonnées des deux extrémités. Si l’item est du texte, ce sera les coordonnées du centre du texte. Si l’item est un arc, l’appel cnv.coords(item) renvoie les sommets du rectangle ayant servi de support à la création de l’arc. Si l’item est un polygone, l’appel cnv.coords(item) renvoie les sommets du polygone :

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

WIDTH=400
HEIGHT=300

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

triangle = cnv.create_polygon(50,50, 300, 250, 390, 10)

print(cnv.coords(triangle))

root.mainloop()
15
[50.0, 50.0, 300.0, 250.0, 390.0, 10.0]

Il est aussi possible d’utiliser la méthode bbox (bounding box) :

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

SIDE=400
item=None
root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE, background="ivory")
cnv.pack()

item = cnv.create_text(SIDE/2, SIDE/2, text ="2024", font="Arial 100 bold")
print(cnv.bbox(item))

root.mainloop()
13
(51, 125, 349, 276)

Items touchant une zone rectangulaire

La méthode find_overlapping permet une forme de détection de collisions (autrement dit, le fait que deux items se touchent). Dans le programme ci-dessous

../../../_images/collision_tkinter.png

produit par le code suivant

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

WIDTH=400
HEIGHT=400

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

# rectangle de référence
a, b=U=(200, 120)
c, d=V=(350, 350)
cnv.create_rectangle(U, V)

COTE=50

# carré bleu
x=y=40
cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="blue", outline='')

# carré vert
x=180
y=80
cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="green", outline='')

# carré rouge
x=240
y=260
cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="red", outline='')

print(cnv.find_all())
print(cnv.find_overlapping(a+1, b+1, c-1, d-1))

root.mainloop()
35
36
(1, 2, 3, 4)
(3, 4)

on crée

  • un rectangle de référence, de diagonale ABA=(a, b) et B=(c,d), non coloré et recevant l’id numéro 1 (ligne 13)
  • trois carrés de couleur d’id 2, 3 et 4 (cf. lignes 17-29 et lignes 31 et 35 pour l’affichage des id)

et on demande à la méthode find_overlapping (ligne 32) de donner les items du canevas qui touchent l’intérieur du rectangle. La figure montre bien que le carré rouge et le carré vert touchent l’intérieur du rectangle mais que c’est pas le cas du bleu. Et la méthode find_overlapping renvoie les id 3 et 4 correspondant au vert et au rouge (ligne 36).

Noter que cnv.find_overlapping ne renvoie pas l’id 1 du rectangle de référence car en écrivant a+1, b+1, c-1, d-1 à la ligne 32, on est garanti, à cause de +1 et -1, que la zone est strictement à l’intérieur du rectange de diagonale AB.

Capture de l’item le plus proche

Le canevas dispose d’une méthode permettant de connaître l’id de l’item placé sur le canevas et le plus proche d’un point donné. Par exemple, soit le programme suivant où on peut défaire à la souris les segments d’une grille :

../../../_images/closest.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
from tkinter import *

COTE=30
WIDTH=20*COTE
HEIGHT=20*COTE

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

n=WIDTH//COTE

for i in range(n):
    for j in range(n):
        cnv.create_line(i*COTE, j*COTE, i*COTE + COTE, j*COTE,
                        fill='red', width=4)
        cnv.create_line(j*COTE, i*COTE,  j*COTE, i*COTE + COTE,
                        fill='red', width=4)

def suppr(event):
    clic=event.x, event.y
    bord = cnv.find_closest(*clic)
    cnv.delete(bord)

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

root.mainloop()

Voici une description du programme : on dispose d’une grille dont chaque cellule constitué de 4 segments rouges indépendants (lignes 15-16). Chaque clic de souris de l’utilisateur (cf. ligne 25) déclenche l’exécution de la fonction suppr (lignes 20-23) ; la fonction suppr récupère alors les coordonnées clic (ligne 21) du point où l’utilisateur a cliqué puis détermine (ligne 22), avec la méthode find_closest, l’item le plus proche de ce point. L’item trouvé est ensuite effacé (ligne 23).

Superposition des items sur le canevas

Lorsque des items sont créés sur un canevas, certains nouveaux items peuvent en partie intersecter des items déjà présents ; les nouveaux items sont toujours placés au-dessus des anciens. Les nouveaux items viennent alors cacher (masquer) ceux sur lesquels il se superposent.

Soit le programme suivant :

../../../_images/raise_dessous.png
../../../_images/raise_dessus.png

correspondant au code 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
28
29
30
31
32
33
34
35
from tkinter import *
from random import randrange, sample

WIDTH=600
HEIGHT=600

COTE=150

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

rect1=cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="red",
                           outline='')
rect2=cnv.create_rectangle(x+1.25*COTE, y, x+2*COTE, y+2*COTE,
                           fill="gray", outline='')
x=x+COTE/2
y=y+COTE/2
rect3=cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="green",
                           outline='')
x=x+COTE/2
y=y+COTE/2
rect4=cnv.create_rectangle(x, y, x+COTE, y+COTE, fill="blue",
                           outline='')

print(cnv.find_all())

def f():
    cnv.tag_raise(rect2, rect4)
    print(cnv.find_all())

cnv.after(1000, f)

root.mainloop()
36
37
(1, 2, 3, 4)
(1, 3, 4, 2)

On crée un carré rouge puis, à sa droite un rectangle gris puis un carré vert et un carré bleu qui se superposent au-dessus du rectangle et viennent donc le cacher en partie. Au bout de 1,5 seconde, le rectangle gris est placé au-dessus des deux derniers carrés. Noter que la position relative des carrés elle ne change pas.

Expliquons le code.

  • Ligne 27 : avant l’animation. La méthode find_all renvoie les items en commençant par les plus profonds et en remontant dans la pile des items.
  • Ligne 33 : l’animation est déclenchée avec la méthode after qui exécute la fonction up
  • ligne 30 : la méthode tag_raise est appelée. Le rectangle rect2, de couleur grise est déplacé dans la pile des items, au-dessus de l’item rect4 qui est le dernier carré, celui de couleur bleue.
  • Lignes 31 et 33 : on affiche à nouveau la pile des items sur le canevas. On voit bien que l’item 2 a été placé au-dessus de l’item d’id 4. L’ordre relatif des autres items dans la pile lui n’a pas bougé.

On peut appeler tag_raise sans le second argument et dans ce cas, l’item est placé en dernière position dans la liste d’affichage des items du canevas et donc au-dessus de tous les autres.

Les items plus récents sont au-dessus. En pratique, un item qu’on déplace doit être visible ce qui peut nécessiter de le hisser.

La méthode inverse de tag_raise est tag_lower. Ainsi, une instruction du type cnv.tag_lower(mon_item) va placer mon_item tout en bas de la pile des items.

Tag sur un item

Chaque item créé sur un canevas peut recevoir un tag sous forme d’une chaîne de caractères. Le canevas permet ensuite d’agir sur tous les items ayant un tag donné.

A titre d’illustration, soit par exemple le programme suivant

../../../_images/echange.gif

où on place des petits carrés aléatoires sur un canevas, des bleus à droite et des rouges à gauche ; les carrés à gauche sont marqués avec le tag "left" et ceux de droite sont marqués avec le tag "right" ; au bout de deux secondes, tous les carrés bleus se déplacent dans la zone de gauche et les rouges dans la zone de droite.

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

WIDTH=400
HEIGHT=400

COTE=20

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

for i in range(40):
    x=randrange(WIDTH)
    y=randrange(HEIGHT)
    if x>WIDTH//2:
        cnv.create_rectangle(x, y, x+COTE, y+COTE, fill='light blue',
                             outline='', tag='right')
    else:
        cnv.create_rectangle(x, y, x+COTE, y+COTE, fill='pink',
                             outline='', tag='left')

def move():
    cnv.move("right", -WIDTH//2, 0)
    cnv.move("left", WIDTH//2, 0)

cnv.after(1000, move)

root.mainloop()
  • Ligne 18 : les items à droite sont marqués "right" et initialement de couleur bleu.
  • Ligne 21 : les items à gauche sont marqués "left" et initialement de couleur rouge.
  • Ligne 27 : lancement dfe l’animation au bout de 1 seconde.
  • Ligne 24 : les items qui sont marqués du tag "right" bougent vers la gauche. de la moitié de la largeur du canevas.
  • Ligne 25 : les items qui sont marqués du tag "left" bougent vers la croite.

Il est possible de placer une option tags (au pluriel) associant plusieurs tags à un item. La syntaxe est :

1
cv. create_rectangle(80, 80, 100, 100, tags=("clickable", "right"))

Ci-dessous, on a utilisé un tuple mais on pourrait utiliser une liste.

Les tags permettent de filtrer des items et de lier sélectivement certains items portant un tag à des événements.

Lecture de l’option tags

Les tags attribués un item sont accessibles via l’option tags à laquelle on peut accéder avec la méthode itemconfig.

Cependant, quand on appelle cette option, au lieu de récupérer un tuple des différents tags, on récupère une chaîne qui est la concaténation, séparée par des espaces, des tags donnés en options.

Exemple :

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

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

rect=cnv.create_rectangle(100, 100, 150, 150, fill='blue', tags=["blue", 42])

options=cnv.itemconfig(rect)
print(*options.items(), sep='\n')

root.mainloop()
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
('activedash', ('activedash', '', '', '', ''))
('activefill', ('activefill', '', '', '', ''))
('activeoutline', ('activeoutline', '', '', '', ''))
('activeoutlinestipple', ('activeoutlinestipple', '', '', '', ''))
('activestipple', ('activestipple', '', '', '', ''))
('activewidth', ('activewidth', '', '', '0.0', '0.0'))
('dash', ('dash', '', '', '', ''))
('dashoffset', ('dashoffset', '', '', '0', '0'))
('disableddash', ('disableddash', '', '', '', ''))
('disabledfill', ('disabledfill', '', '', '', ''))
('disabledoutline', ('disabledoutline', '', '', '', ''))
('disabledoutlinestipple', ('disabledoutlinestipple', '', '', '', ''))
('disabledstipple', ('disabledstipple', '', '', '', ''))
('disabledwidth', ('disabledwidth', '', '', '0.0', '0'))
('fill', ('fill', '', '', '', 'blue'))
('offset', ('offset', '', '', '0,0', '0,0'))
('outline', ('outline', '', '', 'black', 'black'))
('outlineoffset', ('outlineoffset', '', '', '0,0', '0,0'))
('outlinestipple', ('outlinestipple', '', '', '', ''))
('state', ('state', '', '', '', ''))
('stipple', ('stipple', '', '', '', ''))
('tags', ('tags', '', '', '', 'blue 42'))
('width', ('width', '', '', '1.0', '1.0'))
  • Ligne 7 : on passe sous forme de liste les tags tags=["blue", 42] au rectangle.
  • Lignes 9 et 34 : on accède au dictionaire des options et la liste de tags est devenue la chaîne de caractères "blue 42".

Récupérer les tags d’un item

La méthode gettags appliquée à un item du canevas permet de récupérer les tags que l’on a affectés à cet item. Exemple :

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

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

rect1=cnv.create_rectangle(10, 10, 50, 50, fill='red', tags=("red", 24))
rect2=cnv.create_rectangle(100, 100, 150, 150, fill='blue', tags=["blue", 42])

print(cnv.gettags(rect2))

root.mainloop()
13
('blue', '42')
../../../_images/recuperer_tag.png
  • Lignes 7-8 : on affecte des tags (une couleur, un numéro) à chaque rectangle.
  • Lignes 10 et 13 : on récupère les tags de l’item d’id rect2.

Déplacement multiple avec tags et move

Les tags permettent d’agir simultanément sur plusieurs items ayant un tag donné. L’exemple ci-dessous montre le déplacement de tous les items du canevas dont un tag est la chaîne "rect" :

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

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

cnv.create_rectangle(30, 30, 130, 130, fill='gray', tags=("A", "rect"))
cnv.create_rectangle(30, 30+150, 130, 130+150, fill='gray', tags=("rect", "B"))
cnv.create_rectangle(140, 30, 170, 130, fill='orange', tags=("small rect",))

def deplacer(x):
    cnv.move("rect", x, 0)

cnv.after(1000, deplacer, 150)

root.mainloop()
  • Ligne 12 : tout item dont un des tag est la chaîne "rect" sera déplacé vers la droite de x pixels.
  • Ligne 14 : le déplacement est activé une seconde et demi après ouverture du programme.
  • Lignes 7-8 : les deux deux rectangles ont un tag "rect" : ils seront déplacés.
  • Ligne 9 : ce rectangle orange a un tag qui est une chaîne contenant le mot "rect" mais sans être exactement le mot "rect" donc il ne sera pas déplacé.

On pourrait également effectuer des suppressions multiples avec la méthode delete du canevas.

Événement associé à un tag

Il est possible de faire en sorte qu’un événement de la souris ne soit associé qu’à certains items du canevas portant un tag donné.

Dans le programme ci-dessous :

../../../_images/mobile_bleu_rouge.gif

un carré fait l’aller et retour entre le bord gauche et la bord droit du canevas. Lorsque l’utilisateur clique sur le carré, il change de couleur

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

milieu=HEIGHT/2-COTE/2
debut=10
fin = WIDTH-debut

cnv.create_rectangle(debut, milieu, debut+COTE, milieu+COTE, fill="blue",
                     outline='', tag="mobile")

def animate(v):
    a,b,c,d=cnv.coords("mobile")
    if a< debut or c> fin:
        v=-v
    cnv.move("mobile", v, 0)
    cnv.after(25, animate, v)

def changer_couleur(event):
    print("click !")
    color=cnv.itemconfigure("mobile", "fill")[-1]
    if color=="red":
        color="blue"
    else:
        color="red"
    cnv.itemconfigure("mobile", fill=color)

cnv.tag_bind("mobile", "<Button-1>", changer_couleur)

animate(v=3)
root.mainloop()
  • Un carré portant un tag « mobile » est créé lignes 15-16.
  • L’animation est lancée (ligne 36) avec une vitesse valant 3.
  • Un événement de la souris est défini ligne 34 mais il est attaché uniquement à un clic sur un item portant un tag « mobile ». Si on clique ailleurs que sur un tel objet, la fonction changer_couleur n’est pas lancée et l’affichage ligne 25 n’apparaîtra pas.
  • Si on clique sur l’objet mobile, la fonction changer_couleur est lancée : après avoir affiché « click ! » dans la console (ligne 26), elle examine la couleur de l’objet cliqué (ligne 28) et la change (lignes 28-32).

Barres de défilement

La zone scrollregion d’un canevas

La partie visible d’un canevas peut, en fait, être incluse dans une zone plus étendue :

../../../_images/scrollregion.png

Lorsque le canevas est construit, cette zone est donnée par une option scrollregion :

cnv=Canvas(root,  scrollregion =(-100, 200, 600, 400),
           width=300, height=200, bg='ivory')

Ici, cnv est un canevas, s’étendant sur une zone rectangulaire de diagonale les points (-100, 200) et (600, 400). Le sens et la direction des axes sont comme dans toute interface graphique :

  • axe des abscisses horizontal et orienté de gauche à droite,
  • axe des ordonnées vertical orienté de haut en bas.

L’origine des axes s’en déduit par intersection. Il en résulte que l’origine des axes n’est pas forcément en haut à gauche de la région.

Je parlerai de zone visible pour le rectangle du canevas et de zone de défilement pour la zone définie par scrollregion.

Ici, la zone visible (le canevas à proprement parler) est un rectangle de largeur width=300 et de hauteur height=200 inclus dans la zone de défilement. Par défaut, la zone visible et la zone de défilement ont des coins supérieurs gauches qui coïncident.

Voici un code complet définissant une zone de défilement :

from tkinter import *
from random import randrange

def dot(cnv, C, R=6, color='red'):
    xC, yC=C
    A=(xC-R, yC-R)
    B=(xC+R, yC+R)
    return cnv.create_oval(A,B, fill=color, outline=color)

root=Tk()

COLORS=["orange", "blue", "green", "purple"]
R=20

cnv=Canvas(root,  scrollregion =(-100, 200, 600, 400),
           width=300, height=200, bg='ivory')
cnv.pack()

for _ in range(100):
    A=[randrange(600) for _ in range(2)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])
dot(cnv, (10, 10))

root.mainloop()
../../../_images/demoscrollregion.png

Le code ci-dessus ne montre pas véritablement l’intérêt de l’option scrollregion. Néanmoins, il faut savoir qu’autour du canevas visible, le canevas se poursuit. L’intérêt provient du fait que des méthodes du canevas permettent d’explorer (xview et yview) la totalité de la zone, avec barres de défilement (scrollbar, d’où le nom de scrollregion) ou même, sans barre de défilement.

On pourra observer qu’un canevas peut admettre une zone de défilement extrêmement grande tout en restant fluide (si on essayait de l’explorer). Le programme ci-dessous crée une zone de défilement gigantesque :

from tkinter import *
from random import randrange

root=Tk()

COLORS=["orange", "blue", "green", "purple"]
R=20

cnv=Canvas(root,  scrollregion =(0, 0, 10**6, 10**6),
           width=600, height=600, bg='ivory')
cnv.pack()

for _ in range(10**6):
    A=[randrange(600) for _ in range(2)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])

root.mainloop()

La taille potentielle est de 1000 milliards de pixels. Seul le temps de chargement des items (la boucle for dans le code ci-dessus) est un peu long.

Changement d’origine, d’unité

Tkinter ne permet pas de changer les unités sur les axes, en particulier, on ne peut pas changer de sens l’axe des ordonnées (pour obtenir le même sens qu’en mathématiques).

En revanche, l’existence d’une zone de défilement scrollregion paramétrable permet de changer l” origine des axes. Voici un exemple :

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

def dot(cnv, C, R=6, color='red'):
    xC, yC=C
    A=(xC-R, yC-R)
    B=(xC+R, yC+R)
    return cnv.create_oval(A,B, fill=color, outline=color)

root=Tk()

SIDE=200

cnv=Canvas(root,  scrollregion =(-SIDE, -SIDE, SIDE, SIDE),
           width=2*SIDE, height=2*SIDE, bg='ivory')
cnv.pack()

dot(cnv, (0,0))
dot(cnv, (SIDE//4,0), color="blue")
dot(cnv, (0,SIDE//2), color="green")

root.mainloop()
../../../_images/new_origin.png

La fonction dot permet de dessiner un disque, par défaut rouge, de 6 pixels de rayon et centré au point C donné.

Comme le montre ligne 17, on dessine un disque rouge à l’origine (0,0) et on observe que le disque n’est pas placé en haut à gauche du canevas mais au centre de celui-ci : on a donc bien changé l’origine du repère. Le placement des deux autres points montre que les axes ont leurs direction et sens habituels.

En effet, la définition de scrollregion contraint la position de l’origine. Ici, comme scrollregion =(-200, -200, 200, 200), c’est que l’origine (0,0) est au centre de cette région. Comme, la fenêtre est de dimension 400 x 400 (même dimension que la zone de défilement), c’est que l’origine est au centre du canevas visible.

Scroller un canevas sans barre de défilement

On dispose d’un canevas ayant une option scrollregion (zone de défilement) dont on voudrait explorer le contenu, typiquement :

cnv=Canvas(root,  scrollregion =(-SIDE, -SIDE, SIDE, SIDE),
           width=2*SIDE, height=2*SIDE, bg='ivory')

On rappelle que, par défaut, le canevas à proprement parler permet juste de voir le rectangle

  • de largeur width
  • de hauteur height
  • placé dans le coin supérieur gauche de la zone scrollregion.

Pour voir un autre rectangle, il suffit de translater le canevas dans la zone scrollregion ; pour cela on utilise les méthodes xview_scroll (pour un décalage horizontal) et yview_scroll (pour un décalage vertical) de Canvas.

Dans le programme ci-dessous

../../../_images/anim_scroll.gif

initialement la zone de défilement est remplie de carrés colorés aléatoires. Une seconde après le début du programme, une animation permet de parcourir la zone de défilement de gauche à droite par pas de 100 pixels.

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

root=Tk()

COLORS=["orange", "blue", "green", "purple"]
R=20

cnv=Canvas(root,  scrollregion =(0, 0, 800, 800),
           width=200, height=200, bg='ivory')
cnv.pack()

cnv["xscrollincrement"]=100

for _ in range(500):
    A=[randrange(800) for _ in range(2)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])

def xscroll():
    cnv.xview_scroll(1, "units")
    cnv.after(1500, xscroll)

xscroll()

root.mainloop()
  • Ligne 10 : le canevas visible est de taille 200 x 200
  • Lignes 15-18 : toute la zone de défilement (actuellement visible ou pas) est remplie de carrés aléatoires colorés.
  • Lignes 20-22 : l’animation du défilement : un décalage toutes les 1 seconde 1/2.
  • Ligne 21 : chaque scroll décale d’une unité, l’unité étant indiquée par la chaîne "units". Il semblerait, d’après la documentation de Tcl/Tk que l’unité par défaut soit 1/10 de la largeur du canevas.

Il est aussi possible de modifier le décalage de scroll (l’unité) par défaut. Par exemple, pour un décalage horizontal de 100 pixels, il suffit pour cela de définir l’option "xscrollincrement" :

cnv=Canvas(root,  scrollregion =(0, 0, 800, 800),
           width=200, height=200, bg='ivory',
           xscrollincrement=100)

et de continuer à déclarer le décalage par

cnv.xview_scroll(1, "units")

Il est même possible d’effectuer un décalage dynamique en modifiant cnv["xscrollincrement"].

Fonctionnement d’une barre de défilement

Une barre de défilement permet de rendre accessible une zone a priori invisible. Plus précisément, une barre de défilement s’utilise lorsqu’on dispose de deux zones rectangulaires

  • une zone « utile » (par exemple, du texte, du dessin)
  • une zone « de vue » (un canevas par exemple)
../../../_images/zones.png

et que la zone utile déborde de la zone de vue et donc que certaines parties de la zone utile ne sont pas visibles. Une barre de défilement peut être horiontale ou verticale. Pour visualiser la zone utile dans sa largeur, une barre de défilement horizontale permet de déplacer la zone de vue suivant un couloir de la gauche vers la droite de la zone utile. De même si on dispose d’une barre de défilement verticale, on peut explorer la zone utile de haut en bas et sur un couloir. Avec deux barres, on peut donc explorer toute la zone utile.

Pour fixer les idées, prenons le cas d’une barre de défilement horizontale. Elle possède un curseur de défilement qui se déplace de la gauche à la droite de la barre. Ce curseur de défilement se présente sous la forme d’une segment mobile en relief :

../../../_images/curseurs_defil.png

Comment la barre de défilement corrèle-t-elle la zone de vue et la zone utile ? Réponse :

  • la totalité de la barre représente la largeur de la zone utile ;
  • le curseur représente la largeur de la vue ;
  • la position du curseur dans la barre représente proportionnellement la position du début de la vue par rapport à la longueur de la barre ;
  • la largeur du curseur est proportionnelle à la largeur de la vue.
../../../_images/fonctionnement_barre.png

Ainsi, pour fixer les idées, considérons une zone utile de 1000 pixels de large admettant une vue de 200 pixels de large. La barre de défilement, collée sous la zone de vue, est aussi de 200 pixels de large. Comme il y a un rapport de 1 à 5 entre la vue et l’utile, la vue sera représentée dans la barre de défilement par un segment mobile de longueur 200/5=40 pixels. Si le côté gauche de la vue commence au pixel 600 de la zone utile (c’est-à-dire aux 3/5), c’est que le segment mobile commencera aux 3/5 de la barre de défilement et donc sera positionné à partir du début de la barre entre le pixel 120 et le pixel 120+40=160.

Sous Tkinter, une barre de défilement est créée avec le widget Scrollbar. Ce widget est, dans un premier temps, construit de manière indépendante du widget qu’il visualisera (par exemple un canevas ou une zone de texte). La tâche essentielle d’un widget Scrollbar est de gérer la position du segment mobile de déplacement de la vue. En interne, il le gère par deux valeurs qui sont deux ratios représentant la vue par rapport à la dimension de la zone utile. Dans l’exemple précédent, le curseur est déterminé par son ratio gauche qui est de \(\mathtt{120/200 = 0.6}\) et son ratio droit qui est \(\mathtt{160/200=0.8}\).

Barres de défilement autour d’un canevas

Assez classiquement, on peut entourer un canevas de barres de défilement, une barre horizontale, une barre verticale. Mais on va voir que ce n’est pas aussi immédiat que la création d’autres widgets. En guise d’illustration, un logo du langage Python en taille 1000 x 1000 :

../../../_images/logo_python.png

on va placer une (grosse) image logo.png de taille 1000 x 1000 dans un canevas de même taille. La vue du canevas sera de taille 400 x 400 et deux barres de défilement permettront d’explorer la totalité de l’image :

../../../_images/curseurs_defil_logo.gif

Le code final sera le suivant :

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

root=Tk()
SIZE=1000
size=400

cnv=Canvas(root,  scrollregion =(0, 0, SIZE, SIZE),
           width=size, height=size, bg='ivory')

cnv.grid(row=0,column=0)

xscroll=ttk.Scrollbar(root, orient=HORIZONTAL)
yscroll=ttk.Scrollbar(root, orient=VERTICAL)


xscroll.grid(row=1, column=0, sticky=E+W)
yscroll.grid(row=0, column=1,  sticky=S+N)

xscroll["command"]=cnv.xview
yscroll["command"]=cnv.yview
cnv['xscrollcommand']=xscroll.set
cnv['yscrollcommand']=yscroll.set

logo=PhotoImage(file="logo.png")

cnv.create_image((SIZE//2, SIZE//2), image=logo)

root.mainloop()

Dans ce qui suit, on décrit ce code. D’abord, pour des raisons esthétiques, je n’ai pas utilisé les barres de défilement par défaut de Tkinter mais les barres utilisables par son extension standard Ttk. Il faut donc importer Ttk comme montré dans les deux premières lignes. D’autre part, on a une zone scrollregion de taille 1000 x 1000 dans un canevas de taille visible 400 x 400 :

SIZE=1000
size=400

cnv=Canvas(root,  scrollregion =(0, 0, SIZE, SIZE),
           width=size, height=size, bg='ivory')

Une barre de défilement dans un canevas est un widget de type Scrollbar qui, dans un premier temps, est construit indépendamment du canevas. On construit deux barres, une horizontale et l’autre verticale :

1
2
3
4
5
6
xscroll=ttk.Scrollbar(root, orient=HORIZONTAL)
yscroll=ttk.Scrollbar(root, orient=VERTICAL)


xscroll.grid(row=1, column=0, sticky=E+W)
yscroll.grid(row=0, column=1,  sticky=S+N)

Comme j’ai choisi d’utiliser l’extension Ttk de Tkinter, le nom de la classe est ici ttk.Scrollbar.

On utilise la méthode grid de placement des widgets, elle est ici assez naturelle. Pour ajuster les barres, on utilise l’option d’étirement sticky. Les barres de défilement sont alors bien présentes mais inactives. Si on voulait placer les barres autrement, par exemple une en haut au lieu d’en bas, il suffit de modifier en conséquence l’emplacement des widgets dans les cellules de la grille (grid) utilisée.

Il faut maintenant :

  • connecter chaque barre au canevas ;
  • connecter le canevas à chaque barre ;

Les connexions se font en définissant des options pour les widgets. Rappelons que si un widget, disons mon_widget, admet une option portant le nom mon_option, alors Tkinter permet d’initialiser ou de modifier l’option en affectant mon_widget[mon_option] à ce qui est souhaité. Par exemple, si on veut changer en rouge la couleur de fond d’un canevas cnv alors on écrira cnv["background"]="red". C’est avec cette syntaxe que l’on va modifier les options du canevas et des barres. Par ailleurs, on va limiter les explications au cas de la barre horizontale, nommée xscroll ci-dessus, le cas de la barre verticale étant analogue.

Lorsque le curseur de défilement de la barre est modifié par l’utilisateur, une fonction référencée par l’option command de la barre de défilement xscroll est automatiquelent appelée. Cette fonction va transmettre le déplacement au canevas ; le scroll horizontal du canevas est géré par la méthode xview ; d’où la « connexion » suivante :

xscroll["command"]=cnv.xview

Mais le canevas doit lui-même avertir la barre de comment il va scroller (par exemple, le scroll pourrait être bloqué et seul le canevas le sait). Pour cela, le canevas dispose d’une option xscrollcommand ; enfin, une barre de défilement dispose d’une méthode qui lui permet de définir la position de son curseur de défilement, la méthode set. D’où la connexion suivante :

cnv['xscrollcommand']=xscroll.set

Barre de défilement et canevas : performances

Les barres permettent de défiler une zone extrêmement grande sans perte de performance. Ci-dessous, le canevas est de taille un rectangle de longueur un million de pixels et de 800 de hauteur et rempli avec un million de carrés aléatoires :

../../../_images/perf_defil.gif

Une fois le canevas visible (ce qui prend un peu de temps à cause de la création aléatoire des items), la barre de défilement permet de le parcourir de manière très fluide.

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

root=Tk()

SIDE=10**7
WINDOW=800
nitems=10**6
COLORS=["orange", "blue", "green", "purple"]
R=40

cnv=Canvas(root,  scrollregion =(0, 0, SIDE, WINDOW),
           width=WINDOW, height=WINDOW, bg='ivory')
cnv.grid(row=0,column=0)

xscroll=Scrollbar(root, orient=HORIZONTAL, command=cnv.xview)
xscroll.grid(row=1, column=0, sticky=E+W)

cnv['xscrollcommand']=xscroll.set

for _ in range(nitems):
    A=[randrange(SIDE), randrange(WINDOW)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])

root.mainloop()

Il serait même possible de choisir deux barres de défilement sur une aire virtuelle d’un million par un million et d’obtenir un déplacement encore très fluide.

Barre de défilement et canevas : les fonctions de commande

Voici un code de placement d’une unique barre de défilement sous un canevas :

from tkinter import *
from random import randrange

root=Tk()

SIDE=1000
WINDOW=500
nitems=300
COLORS=["orange", "blue", "green", "purple"]
R=20

cnv=Canvas(root,  scrollregion =(0, 0, 1000, 1000),
           width=WINDOW, height=WINDOW, bg='ivory')
cnv.grid(row=0,column=0)

scroll=Scrollbar(root, orient=HORIZONTAL, command=cnv.xview)
scroll.grid(row=1, column=0, sticky=E+W)

cnv['xscrollcommand']=scroll.set

for _ in range(300):
    A=[randrange(1000) for _ in range(2)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])

root.mainloop()

On va ici décrire les arguments transmis lors des appels automatiques des fonctions de rappel des widgets ; pour le widget Canvas, on va décrire :

  • l’option xscrollcommand
  • la méthode xview

et pour le widget Scrollbar :

  • l’option command
  • la méthode set

Lorsqu’un élément d’une barre de défilement est modifié, son option command est appelée. Dans le code ci-dessus, cette option pointe vers la méthode xview du canevas. Lors de l’appel, command transmet à xview la nature du mouvement (scroll ou moveto) et un flottant entre 0 et 1 représentant le ratio entre la nouvelle position du curseur et la longueur de la barre de défilement (par exemple, 0.5 si le début du curseur est au milieu de la barre). Mais, le canevas doit examiner si le déplacement proposé est valide (par exemple, le curseur peut être bloqué). Le canevas appelle donc son option xscrollcommand ; elle pointe ici vers la méthode set de la barre. L’option ne transmet rien si le curseur est bloqué et sinon, elle transmet deux valeurs à la méthode set lui permettant de positionner le début et la fin du curseur.

Pour bien observer le comportement des appels automatiques, il pourra être utile de les intercepter comme ceci :

1
2
scroll=Scrollbar(root, orient=HORIZONTAL, command=f)
cnv['xscrollcommand']=ff

où on définira préalablement les fonctions interceptrices par :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def f(*z):
    print("f")
    print(*z)
    print("---------------")
    cnv.xview(*z)

def ff(*z):
    print("ff")
    print(*z)
    print("---------------")
    scroll.set(*z)

Défilement du canevas avec le clavier

Il est possible de scroller un canevas en utilisant les flèches du clavier :

../../../_images/defil_clavier.gif

Le principe est assez simple. On associe (méthode bind) les flèches du clavier à une fonction qui, selon la direction choisie, appelle les méthodes de scroll xview_scroll et xview_scroll. 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
from tkinter import Tk, Canvas
from random import randrange

root=Tk()

COLORS=["orange", "blue", "green", "purple"]
R=20
SIDE=1200
wcnv=400
hcnv=300

cnv=Canvas(root,  scrollregion =(0, 0, SIDE, SIDE),
           width=wcnv, height=hcnv, bg='ivory')
cnv.pack()
cnv.focus_set()

for _ in range(400):
    A=[randrange(SIDE) for _ in range(2)]
    B=A[0]+R, A[1]+R
    cnv.create_rectangle(A, B, fill=COLORS[randrange(4)])

def scroll(event):
    arrow=event.keysym
    if arrow=='Right':
        cnv.xview_scroll(1, "units")
    elif arrow=='Left':
        cnv.xview_scroll(-1, "units")
    elif arrow=='Down':
        cnv.yview_scroll(1, "units")
    elif arrow=='Up':
        cnv.yview_scroll(-1, "units")

for arrow in ["Left", "Right", "Down", "Up"]:
    cnv.bind('<%s>' %arrow, scroll)

root.mainloop()
  • Ligne 12 : on définit une zone de défilement carrée de côté 1200 pixels.
  • Ligne 17-20 : on dessine des carrés aléatoires colorés pour remplir la zone de défilement.
  • Ligne 15 : on donne le focus au canevas sinon, il n’écoute pas le canevas.
  • Lignes 33-34 : les flèches du clavier sont associées à la fonction scroll de la ligne 22.
  • Lignes 23-31 : selon la direction de flèche qui est repérée, une méthode xview_scroll ou yview_scroll est appelée ; cette méthode décale d’une demi-fenêtre la vue. Arrivée en fin de canevas, la vue est bloquée. Si la valeur de décalage est 1, le décalage a lieu vers la droite ou vers le bas. Si c’est -1, c’est dans le sens inverse.