Infoforall

Python 29 : Savoir où on clique

Cette activité propose de revenir sur les applications Tkinter en mettant l'accent sur la position de la souris.

C'est toujours pratique de savoir exactement où on clique sur l'écran : dans le vide, sur un widget, lequel, et où sur celui-ci.

Plusieurs méthodes de travail sont proposées.

Nous allons également en profiter pour faire un rappel sur Tkinter et la création de fenêtre, en intégrant les nouveaux outils que nous avons vu : les listes, les dictonnaires, les tuples et les objets.

PS : Merci à Alexandre F. et Thomas G. d'avoir créer les cartes.

1 - Création de l'interface

Nous allons créer une interface qui permet d'afficher quelques cartes dans une fenêtre.

Le but sera de parvenir à trouver la position de la souris quelque soit l'endroit où on clique.

Voici son code :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


from tkinter import *

from PIL import Image as Img

from PIL import ImageTk


c1 = { 'imgVerso' : 'carte_1.jpg', 'x' : 20, 'y' : 20 }

c2 = { 'imgVerso' : 'carte_2.jpg', 'x' : 220, 'y' : 20 }

c3 = { 'imgVerso' : 'carte_3.jpg', 'x' : 420, 'y' : 20 }

c4 = { 'imgVerso' : 'carte_4.jpg', 'x' : 620, 'y' : 20 }

listeImages = (c1,c2,c3,c4)

cartes = []


class Carte:

    def __init__(self, boss, coordX=0, coordY=0, fichierImageCarte="", fichierImageDos=""):

        self.verso = Img.open(fichierImageCarte)

        self.versoTk = ImageTk.PhotoImage(self.verso)

        self.labelCarte = Label(boss, image = self.versoTk)

        self.labelCarte.place(x=coordX, y=coordY)


def coordonnees(event):

    xe = event.x

    ye = event.y

    print("On clique en x = {} et y = {}".format(xe,ye))


# ###################### #

# Programme principal

# ###################### #

fen_princ = Tk()

fen_princ.title("Où clique-t-on ?")

fen_princ.geometry("900x600")


for x in range(len(listeImages)):

    donnees = listeImages[x]

    cartes.append(Carte(fen_princ, donnees['x'],donnees['y'],donnees['imgVerso'],"dos_des_cartes.jpg"))


fen_princ.bind('<Button-1>',coordonnees)


fen_princ.mainloop()

Vous aurez besoin également de télécharger certaines images. Voici la structure à recréer sur votre poste :

02° Télécharger le fichier js et les 5 images. Placez le tout dans un même dossier.

03° Tester pour vérifier que cela fonctionne.

04° Comment se nomment les structures de données suivantes ? Comment accéder aux valeurs stockées ?

c1 = { 'imgVerso' : 'carte_1.jpg', 'x' : 20, 'y' : 20 }

c2 = { 'imgVerso' : 'carte_2.jpg', 'x' : 220, 'y' : 20 }

c3 = { 'imgVerso' : 'carte_3.jpg', 'x' : 420, 'y' : 20 }

c4 = { 'imgVerso' : 'carte_4.jpg', 'x' : 620, 'y' : 20 }

...CORRECTION...

Il s'agit de dictionnaires, reconnaissables en Python à cause des accolades..

On accède à l'une des valeurs en tapant par exemple c1['imgVerso'].

05° Comment se nomme la structure de données suivante ? Comment accéder aux valeurs stockées ?

listeImages = (c1,c2,c3,c4)

...CORRECTION...

Il s'agit d'un tuple, reconnaissable en Python à l'aide des parenthèses.

On crée donc un objet non modifiable. Pour contre, on pourra modifier le contenu de c1, c2, c3 ou c4. Mais pas les références elles-mêmes.

On peut accéder aux éléments du tuple à l'aide de l'index : listeImages[1] renvoie ainsi c2 car le premier élément possède l'indice 0.

06° Comment se nomme la structure de données suivante ? Comment accéder aux valeurs stockées ?

cartes = []

...CORRECTION...

Il s'agit d'une liste, reconnaissable en Python à cause des crochets.

On accède à l'une des valeurs en tapant par exemple cartes[0] pour accèder au premier élément.

Ici, la liste est vide initialement.

On crée donc ici une liste contenant des tuples.

Chaque tuple contient lui un dictionnaire qui va nous permettre d'afficher des cartes : on a le fichier-image et les coordonnées x et y à utiliser.

Continuons l'exploration de ce code :

class Carte:

    def __init__(self, boss, coordX=0, coordY=0, fichierImageCarte="", fichierImageDos=""):

        self.verso = Img.open(fichierImageCarte)

        self.versoTk = ImageTk.PhotoImage(self.verso)

        self.labelCarte = Label(boss, image = self.versoTk)

        self.labelCarte.place(x=coordX, y=coordY)

07° Comment se nomme cette structure ? A quoi sert la fonction __init__ ? Doit-on transmettre un argument correspondant au paramètre self ?

...CORRECTION...

Il s'agit d'une classe nommée Carte, avec une majuscule.

Cette classe n'hérite d'aucune autre structure pour l'instant.

On n'a pas besoin de transmettre le self : self va référence à l'objet lui-même lorsqu'on tente d'y accéder depuis son propre code. Plusieurs objets de type Carte seront créés. Utiliser self permet d'agir sur l'objet actif sans avoir à connaitre son nom.

J'espère que vous remarquerez que le code manque de commentaires. On ne sait pas exactement à quoi correspondent les paramètres. Veillez à être plus précis sur les scripts de votre projet.

Expliquons donc ici à quoi correspondent les paramètres :

08° Que fait-on avec ces paramètres ?

        self.verso = Img.open(fichierImageCarte)

        self.versoTk = ImageTk.PhotoImage(self.verso)

        self.labelCarte = Label(boss, image = self.versoTk)

        self.labelCarte.place(x=coordX, y=coordY)

...CORRECTION...

On crée une variable d'instance verso qui contient un objet-image PIL créé à partir du fichier-image à l'aide de la méthode open.

On crée une variable d'instance versoTk pour rendre verso (issue de PIL) compatible avec Tk.

On crée un Label image dans Tk en l'associant à la fenêtre boss et dans lequel on place l'image versoTk.

On positionne le Label (visible sous forme de la carte donc) à l'aide de la méthode place et des valeurs coordX et coordY.

Nous allons maintenant détaillé le court programme principal :

# ###################### #

# Programme principal

# ###################### #

fen_princ = Tk()

fen_princ.title("Où clique-t-on ?")

fen_princ.geometry("900x600")


for x in range(len(listeImages)):

    donnees = listeImages[x]

    cartes.append(Carte(fen_princ, donnees['x'],donnees['y'],donnees['imgVerso'],"dos_des_cartes.jpg"))


fen_princ.bind('<Button-1>',coordonnees)


fen_princ.mainloop()

09° A quoi sert fen_princ = Tk() ?

10° Quelle est la valeur renvoyée par len(listeImages) ? Enumérer alors les valeurs prises par x pendant la boucle.

...CORRECTION...

Tk() est la méthode-constructeur des fenêtres Tk. La variable fen_princ contient donc la référence de la fenêtre qu'on vient de créer.

La fonction native len permet de connaitre le nombre d'éléments contenant dans un objet itérable : ici, on obtient donc 4 car cela contient c1,c2,c3 et c4.

Le for va donc simplement commencer à x=0 et va augmenter de 1 tant que x < 4, strictement.

On a donc 0, 1, 2 et 3.

11° La méthode append permet de rajouter des éléments dans une liste. Que rajoute-t-on au fur et à mesure dans cartes ?

...CORRECTION...

On rajoute une à une les références des objets de classe Carte.

La méthode Carte correspond à la méthode-constructeur de cette classe.

Derniers bouts de code (qu'on rassemble pour plus de clarté)

def coordonnees(event):

    xe = event.x

    ye = event.y

    print("On clique en x = {} et y = {}".format(xe,ye))


fen_princ.bind('<Button-1>',coordonnees)

12° Que fait la méthode bind ? Que devrait-il se passer ?

...CORRECTION...

Bind permet de créer une surveillance d'événements dans la fen_princ.

Lorsqu'on appuie avec le bouton gauche de la souris dans la fenêtre, on active la fonction coordonnees.

Cette fonction est un peu particulière car elle possède un paramètre qu'on ne transmet pas directement : le paramètre event qui contient des informations sur l'environnement au moment où l'événement a eu lieu.

La fonction va chercher les coordonnées x et y contenues dans event et les affiches dans la console.

2 - Observation des résultats de la fonction-événement

13° Activer le script. Cliquer sur la fenêtre, parfois dans le 'vide', parfois sur une carte. Lire les coordonnées. Cela vous semble-t-il normal ?

...CORRECTION...

C'est étrange : tant qu'on reste sur la fenêtre elle-même, les coordonnées semblent correctes.

Par contre, lorsqu'on clique sur une image, les coordonnées ne semblent plus correspondre aux coordonnées de la fenêtre ...

Pourquoi ? Simplement car event.x et event.y correspondent aux coordonnées locales de la souris sur le widget qui a reçu l'événement.

Si vous cliquez dans le 'vide', c'est la fenêtre fen_princ qui recoit l'événement : le point (0,0) correspond ainsi au point en haut à gauche de la fenêtre.

Par contre, si vous cliquez sur une carte, vous obtenez les coordonnées de la souris sur la carte : le point (0,0) correspond au point en haut à gauche de l'image-carte.

La fonction va chercher les coordonnées x et y contenues dans event et les affiches dans la console.

Pas très pratique pour savoir où on clique ...

14° Rajouter la ligne suivante à la fin de la fonction-événement. Vous devriez parvenir à distinguer qu'on clique sur telle ou telle carte.

print(event.widget)

Par contre, on obtient simplement la référence interne sous forme d'une chaîne de caractères qui ne nous parle pas vraiment.

Par contre, on peut utiliser cette référence pour modifier le widget : on va ainsi retourner la carte.

Pour cela, commençons par créer une imageTk directement dans le corps du programme. Cela nous évitera de devoir jouer avec le ramasse-miette.

15° Rajouter ceci dans le corps du programme, sous fen_princ.geometry("900x600") par exemple :

image_retournee = ImageTk.PhotoImage(Img.open("dos_des_cartes.jpg"))

Il nous reste à utiliser la méthode config pour modifer certains attributs de mon widget cliqué :

def coordonnees(event):

    xe = event.x

    ye = event.y

    print("On clique en x = {} et y = {}".format(xe,ye))

    event.widget.config(image=image_retournee)


Et voilà, c'est magique : on peut agir comme on veut sur le widget qui a subi l'événement !

3 - Coordonnées globales

C'est bien beau tout ça, mais je ne peux toujours pas faire une action spécifique lorsqu'on clique plutôt sur le haut de la fenêtre ou sur le bas de la fenêtre. On choisir de faire des actions différentes sur on clique à droite ou à gauche d'une carte.

Alors ? Comment faire ?

Vous vous doutez bien qu'il existe un moyen simple de faire cela : il ne faut pas faire appel à event.x mais à event.x_root.

Root comme racine : on obtient alors les coordonnées de l'événement dans la fenêtre-racine : ici c'est fen_princ.

16° Modifier le code dans le print pour afficher les coordonnées dites absolues.

Voilà. Il vous reste à exploiter ceci.

17° Utiliser notre interface pour créer un petit jeu de cartes. Pensez à rajouter du hasard lors du placement initial.

Attention néanmoins : les coordonnées globales ne fonctionnent bien que si votre interface est maximisée sur l'écran. On vous donne les coordonnées de votre événement sur l'écran, pas sur l'interface.

4 - FAQ

Question : comment tester la classe du widget sur lequel on se trouve ?

Question intéressante pour savoir si on doit agir de telle ou telle façon en fonction de la nature du widget.

La façon la plus facile est d'utiliser la fonction native isinstance :

Comment fonctionne-t-elle ? isinstance(object, Classinfo) renvoie True si object est bien une instance de la classe Classinfo.

Exemple d'utilisation :

if isinstance(event.widget, Label):

Question : comment mélanger les éléments d'une liste ?

Vous pourriez en avoir besion pour mélanger une liste de noms de fichiers par exemple :

Exemple d'utilisation :

random.shuffle(maListe)

Avec ceci maListe = ['A','B','C','D'] pourrait se transformer en ['B','D','A','C'].

Attention, pensez à importer random. Sinon, ça risque de vous déclarer une erreur.