python projet 2

Identification

Infoforall

31 - Projet avec tkinter


Cette activité présente un début de projet guidé utilisant une Interface Graphique Tkinter en tant qu'Interface Homme Machine.

Le programme va être séparé en deux parties distinctes :

  • La partie qui traite de l'interaction avec l'IHM Tkinter
  • La partie qui gère les données et sans lien direct avec l'interface graphique

Prérequis :

  • l'activité Données : encodage des matrices
  • l'activité Python : les dictionnaires
  • l'article FICHE Tkinter

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

1 - Création de l'application graphique

RAPPEL

Contrairement au système d'axes Oxy classique en mathématiques, dans le système d'axes de Tkinter, l'axe y est orienté vers le bas et le point d'origine (0,0) correspond au point en haut à gauche.

systeme de coorcodes
Rappel des notions vues dans l'article Tkinter

La FICHE sur Tkinter contient les explications sur les notions suivantes. Si cela ne vous dit vraiment plus rien, commencez par lire l'article en activant les programmes au fur et à mesure de façon à bien comprendre l'utilisation de ces différentes notions.

FICHE Tkinter

  1. Partie 1 : Créer la fenêtre
    • Constructeur Tk
    • Méthode geometry
    • Méthode title
    • Méthode configure pour modifier après création
    • Méthode mainloop pour lancer la surveillance des événements
  2. Partie 2 : Créer, modifier et lire les attributs des widgets
    • Constructeur Label pour créer un widget Texte
    • Méthode pack pour afficher et placer simplement le widget
    • Méthode place pour afficher et placer à des coordonnées précises
    • Méthode configure pour modifier après création
    • Méthode cget pour récupérer la valeur d'un attribut
  3. Partie 3 : Utiliser le gestionnaire d'événements
    • Méthode bind pour utiliser le gestionnaire d'événements
    • Description des différents événements
    • Fonction événementielle et paramètre event pour récupérer les informations de l'événement
  4. Partie 4 : Placer précisement les widgets
    • Méthode place pour afficher et placer le widget à des coordonnées précises
  5. Partie 5 : Animer automatiquement
    • Méthode after pour programmer un appel retardé à une fonction
  6. Partie 6 : Utilisation avancée
    • Fonction lambda pour envoyer l'adresse d'une fonction créée à la volée (hors programme, à n'utiliser que dans le cadre des interfaces graphiques)

Nous allons voir comment réaliser une fenêtre graphique dans laquelle nous allons placer nos éléments graphiques. Par exemple ceci :

Une application graphique avec des cases colorées

Nous prendrons comme support de travail le plateau de jeu que nous avions modélisé dans l'activité Matrice de la partie Données.

Pour travailler proprement,nous allons clairement séparer :

  1. la partie "Interface" : les variables et les fonctions qui dépendent de l'Interface Homme Machine (IHM) (l'interface graphique Tkinter ici)
  2. la partie "Données" : les variables et les fonctions qui contiennent les données brutes sur le plateau de jeu (tout ce qui n'aura pas à être modifié si on décide de passer de l'interface Tkinter à l'interface Qt ou autre par exemple)

Nous allons construire peu à peu une interface de ce type dans cette activité. L'activité suivante consiste à sa "transformation" de façon à en faire un véritable jeu. Commençons par la création de :

  • la fenêtre servant d'écran principal fenetre
  • la matrice qui contient les données encodant les cases du plateau (sous forme d'un simple entier)
  • d'un dictionnaire infos contenant des informations complémentaires diverses sur le jeu (quel joueur, l'état actuel du jeu...).

01° Lancer simplement le programme suivant pour visualiser qu'il parvient bien à construire une interface graphique (noire et vide pour le moment).

Inutile de l'analyser, c'est l'objet des questions suivantes.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
# ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # ----- 3 - FONCTIONS liées à la partie DONNEES ---------------- # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def creer_fenetre(): '''Renvoie la référence d'une nouvelle fenêtre :: return (tkinter.Tk) :: la référence de la nouvelle fenêtre ''' fe = tk.Tk() # on crée la nouvelle fenêtre configurer_fenetre(fe) # on configure sa apparence return fe # on renvoie sa référence def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python, modifie fe par effet de bord ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # Une seule fonction ? doctest.run_docstring_examples(initialiser_les_donnees, globals()) # --- Données du jeu # informations diverses infos = {"joueur": -1, "état": 0} # matrice encodant le contenu des cases du plateau matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ] # --- Création de la fenêtre principale fenetre = creer_fenetre() # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

Que fait le programme ?

Ligne 3 : importation du module sous l'alias tk

3
import tkinter as tk

Lignes 14-33 : passons pour l'instant sur la partie déclaration des fonctions. Nous verrons ce qu'elles font lorsque nous aurons besoin de le savoir.

Lignes 41-32 dans le programme principal : on commence par l'utilisation des docstrings en tant que jeu de tests :

38 39 40 41 42
if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod()

Lignes 45-58 : créations de 2 variables globales.

45 46 47 48 49 50 51 52 53 54 55 56 57 58
# --- Données du jeu # informations diverses infos = {"joueur": -1, "état": 0} # matrice encodant le contenu des cases du plateau matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ]
  • Un dictionnaire infos contenant des données générales sur l'état du jeu
    • La clé "joueur" permet d'accéder au joueur actif : 1, 2 ou -1 si aucun
    • La clé "état" permet d'accéder à l'état du jeu : 0 pour inactif, 1 pour actif et 2 pour fini par exemple.
  • Une matrice donnees qui encode le contenu des cases du plateau de jeu. Dans la version présentée ici :
    • 0 veut dire que la case contient une case vide
    • 1 veut dire que la case contient un mur
    • d'autres valeurs pour l'aventurier, le dragon...

    J'utilise un integer ici mais, dans votre projet, vous pourrez y mettre un tuple, un string, un autre tableau...

Tableaux et dictionnaires étant muables dans Python , nous pourrons modifier le contenu de ces variables depuis les fonctions en leur transférant la référence-mémoire.

02° Que renvoie matrice[2][4] ?

...CORRECTION...

On se souviendra que l'indice de la première ligne est 0.

matrice[2] est donc le contenu de la case-ligne d'indice 2.

51 52 53 54 55 56 57 58
matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ]

matrice[2][4] : il ne reste qu'à chercher la colonne d'indice 4 sur cette ligne : elle fait référence à la valeur 3.

51 52 53 54 55 56 57 58
matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ]

03° Que renvoie len(matrice) ?

...CORRECTION...

On parvient à trouver le nombre de lignes en cherchant la longueur de matrice. Si vous regarder ci-dessous, vous pourrez constatez qu'on dénombre 6 éléments car il y a 6 tableaux-lignes.

51 52 53 54 55 56 57 58
matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ]

04° Que renvoie len(matrice[0]) ou len(matrice[1]) ?

...CORRECTION...

matrice[2] renvoie le tableau encodant la première ligne.

La longueur de ce tableau correspond donc au nombre de colonnes sur cette ligne.

Quelque soit la ligne transmise, on obtient 6 colonnes.

83 84 85 86 87 88 89 90
donnees = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ]

Lignes 61 : création de la fenêtre graphique.

61
fenetre = creer_fenetre()

Pour comprendre comment cela fonctionne, il faut bien entendu aller voir la déclaration de la fonction creer_fenetre(), qui utilise d'ailleurs à l'interne la fonction configurer_fenetre().

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
def creer_fenetre(): '''Renvoie la référence d'une nouvelle fenêtre :: return (tkinter.Tk) :: la référence de la nouvelle fenêtre ''' fe = tk.Tk() # on crée la nouvelle fenêtre configurer_fenetre(fe) # on configure sa apparence return fe # on renvoie sa référence def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python, modifie fe par effet de bord ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black")
  • Ligne 20 : Création d'un objet de classe Tk et stockage de sa référence dans la variable locale fe.
  • Ligne 21 : Appel à configurer_fenetre qui reçoit ici l'argument fe et le place dans son paramètre qui se nomme également fe.
  • Ligne 31 : geometry permet de fournir les dimensions en pixels.
  • Ligne 32 : title permet de faire de même pour le titre de la fenêtre
  • Ligne 33 : configure permet de modifier la couleur de fond (bg) à qui on affecte la couleur "black"
  • Ligne 22 : On renvoie la référence de la nouvelle fenêtre stockée dans fe.

05° Lancer l'interface pour visualiser ce que cela donne. Modifier quelques valeurs de la fonction configurer_fenetre() pour voir le résultat.

On rappelle que les couleurs d'une image sont définies en RVB (ou RGB en anglais). On peut donc soit donner le nom d'une couleur prédéfinie ou fournir les valeurs RGB en hexadécimal. Chaque composante peut prendre une valeur allant de 00 à FF.

Exemples :

"#000000" équivalent à "black"

"#FF0000" équivalent à "red"

"#00FF00" équivalent à "green"

"#FFFF00" équivalent à "yellow"

A partir de là, on peut lancer l'interface graphique avec cette dernière partie :

Activation de la surveillance en boucle sur l'interface graphique jusqu'à fermeture

63 64
# --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

2 - Les carrés colorés

Voyons maintenant comment créer des carrés colorés sur cette interface. Ici :

  • le carré numéro 0 est aux coordonnées x = 0 et y = 0
  • le carré numéro 1 est aux coordonnées x = 1 et y = 0
  • le carré numéro 5 est aux coordonnées x = 1 et y = 1

Dans ce programme,

  • x correspondra donc à l'indice de la colonne (0 est la colonne de gauche)
  • y correspondra à l'indice de la ligne (0 est la ligne du haut).

05° Lancer le code ci-dessous pour voir la nouveauté qu'il apporte. Lire ensuite la suite des explications.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
# ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # ----- 3 - FONCTIONS liées à la partie DONNEES ---------------- # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def creer_fenetre(): '''Renvoie la référence d'une nouvelle fenêtre :: return (tkinter.Tk) :: la référence de la nouvelle fenêtre ''' fe = tk.Tk() # on crée la nouvelle fenêtre configurer_fenetre(fe) # on configure sa apparence return fe # on renvoie sa référence def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python, modifie fe par effet de bord ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") def creer_cases(fe, m): '''Renvoie une matrice ayant les mêmes dimensions que m mais contenant des widgets Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param m(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: une matrice contenant des tkinter.Label ''' nbl = len(m) # Nombre de lignes nbc = len(m[0]) # Nombre de colonnes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) # Affichage et placement du widget contenu dans w[y][x] px = 20 + x*90 # position px en pixels py = 20 + y*85 # position py en pixels w[y][x].place(x=px, y=py) # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y # Option : modification du widget (juste pour montrer qu'on peut !!) if x == 1: # nous sommes sur la 2e colonne (indice 1) w[y][x].configure(bg='red') elif y == 2: # nous sommes sur la 3e ligne (indice 2) w[y][x].configure(bg='#AA0000') # On renvoie la matrice return w # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # Une seule fonction ? doctest.run_docstring_examples(initialiser_les_donnees, globals()) # --- Données du jeu # informations diverses infos = {"joueur": -1, "état": 0} # matrice encodant le contenu des cases du plateau matrice = [ [1, 0, 0, 1, 1, 0], [1, 0, 3, 1, 1, 0], [1, 0, 0, 0, 3, 0], [8, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 0], [1, 0, 1, 0, 0, 0] ] # --- Création de la fenêtre principale fenetre = creer_fenetre() # --- Création des widgets représentant graphiquement la matrice de données widgets = creer_cases(fenetre, matrice) # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

Création de la matrice de stockage des Labels

Ligne 107, on crée (à l'aide de la fonction creer_cases()) la nouvelle matrice nommée widgets qui contient les différents widgets colorés.

L'appel à la fonction se fait ainsi :

107
widgets = creer_cases(fenetre, matrice)

On envoie donc les argmuments fenetre et matrice.

35 36 37 38 39 40 41 42 43 44 45 46 47 48
def creer_cases(fe, m): '''Renvoie une matrice ayant les mêmes dimensions que m mais contenant des widgets Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param m(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: une matrice contenant des tkinter.Label ''' nbl = len(m) # Nombre de lignes nbc = len(m[0]) # Nombre de colonnes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] ...

Ligne 107 puis 35 : le paramètre fe reçoit donc fenetre et le paramètre m reçoit matrice.

Lignes 43 et 44 : on récupère le nombre de lignes nbl et le nombre de colonnes nbc de la matrice m.

Ligne 47 : on crée une matrice puisqu'on crée un tableau contenant des tableaux.

47
w = [ [None for x in range(nbc)] for y in range(nbl) ]

06° QCM : Cette matrice w (pour dire widgets représentant graphiquement les cases du plateau) est construite par :

  • A : Compréhension
  • B : Déception
  • C : Incompréhension
  • D : Extension et ajouts successifs

...CORRECTION...

  • A : Compréhension
  • Création des cases et remplissage de cases

    Nous avons donc une belle matrice vide nommée w. Voyons comment nous parvenons à la remplir.

    Lignes 50-51 : double boucle permettant d'accéder aux coordonnées des cases de la matrice dans l'ordre habituel de lecture : de gauche à droite et avec un passage à la ligne suivante :

    .. 49 50 51 52 53 54 55 56 57 ..
    ... # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) ...

    On va donc boucler plusieurs fois et obtenir la suite de valeurs suivantes pour la ligne y et la colonne x :

    Si on prend l'exemple d'une petite matrice 3x3 avec des indices valant donc 0, 1 ou 2, les indices successifs pris par les variables de boucle y et x sont donc :
    (y=0, x=0) puis (y=0, x=1) puis (y=0, x=2) puis
    (y=1, x=0) puis (y=1, x=1) puis (y=1, x=2) puis
    (y=2, x=0) puis (y=2, x=1) puis (y=2, x=2) et c'est fini.

    07° QCM : Cela revient donc à parcourir les cases du plateau :

    • A : En commençant en haut à gauche et en partant d'abord vers la droite
    • B : En commençant en haut à gauche et en partant d'abord vers le bas
    • C : En commençant en bas à gauche et en partant d'abord à droite
    • D : En commençant en bas à gauce et en partant d'abord en haut

    ...CORRECTION...

  • A : En commençant en haut à gauche et en partant d'abord vers la droite
  • La question n'était pas difficile puisque les cases sont numérotées de base dans l'ordre de création !

    Reprenons la création du Label en ligne 57 :

    57
    w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5)

      On place donc dans w[y][x] la référence d'un widget-Label. On lui transmet différents paramètres nommés, dont on peut trouver la description dans la documentation de Tkinter :

      • Un paramètre obligatoire : la réference d'une fenêtre Tkinter qui contiendra ce Label : fe ici
      • text, le texte qu'on veut voir s'afficher dans le widget Label
      • fg pour foreground, fond du premier plan, le texte.
      • bg pour background, fond coloré.
      • width pour width, la largeur (en largeur de caractères puisqu'il s'agit d'un texte)
      • height pour height, la hauteur (en hauteur de caractères puisqu'il s'agit d'un texte).

    08° Répondre aux deux questions en analysant la ligne 57.

    • Quel est le texte qu'on veut voir s'afficher dans notre carré ?
    • Quelle devrait être la couleur de fond de tous nos carrés ?

    ...CORRECTION...

    On veut afficher le numéro de la case : text=numero

    Toutes les cases devraient être grises : bg='grey'

    Remarque

    Nous avons ceci :

    57
    w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5)

    Nous aurions pu l'écrire en plusieurs étapes si vous préférez :

    57 58 59 60 61 62
    w[y][x] = tk.Label(fe) w[y][x].configure(text=numero) w[y][x].configure(fg="white") w[y][x].configure(bg='grey') w[y][x].configure(width=10) w[y][x].configure(height=5)

    Avantage : plus clair.

    Désavantage : moins concis.

    Les widgets sont maintenant créés et stockés dans la matrice w mais si nous arrêtions ici, ils n'apparaitraient pas à l'écran : il faut encore dire précisement où les placer dans l'interface.

    Placement des widgets

    Voici donc pourquoi nous calculons leurs coordonnées en pixels px et py en fonction des positions théoriques x et y et que nous faisons appel à la méthode place() pour placer les widgets.

    59 60 61 62
    # Affichage et placement du widget contenu dans w[y][x] px = 20 + x * 90 # position px en pixels py = 20 + y * 85 # position py en pixels w[y][x].place(x=px, y=py)

    Si les écartements entre cases ne vous conviennent pas, il suffit de modifier les valeurs.

    Rajout de quelques informations

    On rajoute ensuite quelques informations liées à la position de la case qui vont être assez utiles faire le lien entre la matrice widgets et la matrice matrice : son numéro de case, son numéro de ligne et de colonne. L'explication profonde du mécanisme n'arrivera qu'en Terminale. Mais c'est facile à lire :

    64 65 66 67
    # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y

    Ainsi si on veut récupérer le numéro de ligne d'un Label sur lequel on vient de cliquer et dont la référence est stockée

    • dans case : il suffit de taper case.ligne.
    • dans w[y][x] : il suffit de taper w[y][x].ligne.

    Pratique, non ?

    Quelques modifications optionnelles supplémentaires

    On trouve enfin une modification des caractéristiques de la case. Nous aurions pu les placer directement lors de la création : elles ne servent qu'à vous montrer comment modifier avec configure() les attributs d'un Label qui existe déjà.

    69 70 71 72 73
    # Option : modification du widget (juste pour montrer qu'on peut !!) if x == 1: # nous sommes sur la 2e colonne (indice 1) w[y][x].configure(bg='red') elif y == 2: # nous sommes sur la 3e ligne (indice 2) w[y][x].configure(bg='#AA0000')

    Comme nous l'avons vu, les carrés devraient être gris. Or, ils ne sont pas tous gris. Pourquoi ?

    • Ligne 70 : on teste si notre case se situe sur la colonne d'indice 1
    • Ligne 71 : si c'est le cas, on utilise la méthode configure() qui modifie le fond coloré (bg) en rouge vif (red).
    • Ligne 72 : sinon, on teste sur notre case se situe sur la ligne d'indice 2
    • Ligne 73 : si c'est le cas, on utilise la méthode configure() pour modifier le fond coloré en rouge foncé.

    09° Modifier le script pour afficher le texte 'C1' dans les carrés de la colonne 1.

    ...CORRECTION...

    69 70 71 72 73 74
    # Option : modification du widget (juste pour montrer qu'on peut !!) if x == 1: # nous sommes sur la 2e colonne (indice 1) w[y][x].configure(bg='red') w[y][x].configure(text='C1') elif y == 2: # nous sommes sur la 3e ligne (indice 2) w[y][x].configure(bg='#AA0000')

    10° Pourquoi la case de colonne 1 et de ligne 2 a-t-elle la couleur imposée par sa colonne alors que le test de la ligne est vrai 

    59 60 61 62 63
    # Option : modification du widget (juste pour montrer qu'on peut !!) if x == 1: # nous sommes sur la 2e colonne (indice 1) w[y][x].configure(bg='red') elif y == 2: # nous sommes sur la 3e ligne (indice 2) w[y][x].configure(bg='#AA0000')

    ...CORRECTION...

    Justement parce que le test de la ligne se fera APRES celui de la colonne et SI le bloc de la colonne n'a pas été activé : dans une séquence IF-ELIF-ELSE, on n'effectue que le premier bloc détécté comme valide.

    Et une fois que nous sommes sortis des deux boucles FOR et avons stocké tous les labels dans la nouvelle matrice, il faut la renvoyer puisqu'il s'agit d'une variable locale : si on ne la renvoie pas, elle disparaitrait dès qu'on sort de cette fonction.

    65 66
    # On renvoie la matrice return w
    Faciliter la lecture du code

    Les variables globales n'ont pas vocation à être directement lues ou modifiées depuis une fonction si on peut faire autrement.

    Les transmettre en arguments et les placer dans des paramètres est la meilleur possibilité. Par contre, changer le nom des paramètres à chaque fois est ennuyeux pour la compréhension du code.

    Vous remarquerez que j'ai fait le choix que garder constamment les mêmes noms pour les paramètres contenant des références vers des variables globales.

    1. fe dans le paramètre de fonction qui doit recevoir la variable globale fenetre
    2. d comme variable locale associée à la variable globale donnees : il s'agit de la matrice contenant les données encodant le contenu des cases.
    3. w comme variable locale associée à la variable globale widgets : il s'agit de la matrice contenant les widgets représentant graphiquement les cases.
    4. c comme variable locale associée à la variable globale complement : il s'agit du tableau contenant les informations complémentaires sur le jeu.

    On pourrait leur donner exactement le même nom mais on évitera : celui permet de créer momins de confusion entre variable globale et variable locale.

    Variable d'une seule lettre ?

    Attention aux variables d'une seule lettre.

    Pratiques

    • car elles allègent la lecture.

    Dangereuses

    • car on aura vite fait de créer une autre variable portant le même nom et écrasant l'ancien contenu.
    • car elles ne sont pas vraiment explicites.

    3 - Partie Données vers partie Interface graphique

    Constantes

    Nous allons utilisé ici des CONSTANTES, des "variables" dont le contenu ne sera pas variable mais constant : on affecte un contenu au début et ensuite il ne changera jamais lors du déroulement du programme.

    Par convention, on utilise uniquement des majuscules dans le nom des constantes. Elles indiquent clairement qu'il ne faudra pas les modifier ensuite.

    En Python, elles n'existent pas vraiment : nos constantes ne sont en réalité que des variables dont le nom est composé de majuscules. Rien ne vous empêche de modifier le contenu après initialisation du contenu. Il existe par contre des langages qui distinguent variables et constantes et empêchent les modifications ultérieures.

    Dans nos programmes, nous nous autoriserons à LIRE les valeurs des constantes globales depuis les fonctions plutôt que de les transférer via des paramètres.

    Pour l'instant l'affichage n'a aucun rapport avec ce qu'on a placé dans la matrice donnees. Nous allons ici nous intéresser à la liaison DONNEES vers INTERFACE : on veut que les données stockées dans la matrice donnees soient visibles à l'écran. Il faudrait ainsi que les 0, 1, 3 et 8 stockés dans donnees soient transformés en effet visible à l'écran. Voici le début du nouveau code.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
    # ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # Codification des contenus COULOIR = 0 MUR = 1 AVENTURIER = 3 DRAGON = 8 # texte à afficher - couleur - couleur au survol APPARENCE = { COULOIR: ('', '#EEEEEE', '#DDDDDD'), MUR: ('', 'black', '#111111'), AVENTURIER: ('A', '#8888FF', '#AAAAFF'), DRAGON: ('D', '#FF5555', '#FF7777') }

    11° Mettre ce code en mémoire : il place en mémoire les 5 constantes, dont APPARENCE qui est une CONSTANTE-DICTIONNAIRE.

    Lancer ensuite ces évaluations dans la console.

    Questions

    1. Quel est le type des clés du dictionnaire ?
    2. Quel est l'intérêt de taper DRAGON plutôt que juste 8 ?
    3. Quel est le type de valeurs du dictionnaire ?
    4. Que va renvoyer la dernière évaluation ?
    >>> DRAGON 8 >>> APPARENCE[DRAGON] ('D', '#FF5555', '#FF7777') >>> APPARENCE[8] ('D', '#FF5555', '#FF7777') >>> APPARENCE[AVENTURIER][1]

    ...CORRECTION...

    1. Les clés sont des integers puisque les constantes MUR, COULOIR... sont des integers.
    2. Utiliser les constantes plutôt que les entiers directement permet de centraliser les valeurs des entiers. Si on décide de changer et de mettre 7 pour DRAGON, il suffit de modifier la ligne 12 et pas TOUTES les lignes où le 8 apparaît.
    3. Les valeurs du dictionnaire sont toutes des puplets (tuples) de 3 valeurs. Des triplets.
    4. Lorsqu'on tape APPARENCE[AVENTURIER], on récupère le triplet ('A', '#8888FF', '#AAAAFF')
    5. Lorsqu'on tape APPARENCE[AVENTURIER][1], on récupère donc l'élément d'indice 1 du triplet ('A', '#8888FF', '#AAAAFF') et donc '#8888FF'

    Nous utiliserons aussi les constantes dans la matrice donnees. Mais comme les noms des constantes sont longs, nous allons utiliser des alias :

    1 2 3 4 5 6 7 8 9 10 11 12 13
    # matrice encodant les données du plateau, C par défaut C = COULOIR M = MUR A = AVENTURIER D = DRAGON donnees = [ [M, C, C, M, M, C], [M, C, D, M, M, C], [M, C, C, C, D, C], [A, C, C, C, M, M], [M, C, M, C, M, C], [M, C, M, C, C, C] ]

    Nous voudrions maintenant créer la fonction modifier_apparence_cases. Cette fonction devra

    1. regarder chaque valeur contenue dans donnees et
    2. modifier en conséquence l'apparence du widget correspondant contenu dans widgets.

    Comme il s'agit de modifier l'interface à partri des données, il s'agit bien d'une fonction à placer dans la partie Interface. Nous allons partir du principe "une tâche - une fonction":

    • modifier_apparence_case se charge de modifier l'apparence d'un widget en fonction d'une valeur de contenu qu'on lui transmet (0 ou MUR par exemple).
    • modifier_apparence_cases se charge de le faire sur toutes les cases, en utilisant modifier_apparence_case.

    12° Mettre ce code en mémoire pour vérifier qu'il fonctionne sur votre système.

    Vous devriez obtenir ceci :

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
    # ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # Codification des contenus COULOIR = 0 MUR = 1 AVENTURIER = 3 DRAGON = 8 # texte à afficher - couleur - couleur au survol APPARENCE = { COULOIR: ('', '#EEEEEE', '#DDDDDD'), MUR: ('', 'black', '#111111'), AVENTURIER: ('A', '#8888FF', '#AAAAFF'), DRAGON: ('D', '#FF5555', '#FF7777') } # ----- 3 - FONCTIONS liées à la partie DONNEES ------------- # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") def creer_cases(fe, m): '''Renvoie une matrice ayant les mêmes dimensions que m et contenant des widgets-Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param m(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: une matrice ayant les mêmes dimensions que m mais contenant des tkinter.Label ''' nbl = len(m) # Nombre de lignes nbc = len(m[0]) # Nombre de lignes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) # Affichage et placement du widget contenu dans w[y][x] px = 20 + x * 90 # position px en pixels py = 20 + y * 85 # position py en pixels w[y][x].place(x=px, y=py) # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y # Option : modification du widget (juste pour montrer qu'on peut !!) #if x == 1 : # nous sommes sur la 2e colonne (indice 1) # w[y][x].configure(bg='red') #elif y == 2 : # nous sommes sur la 3e ligne (indice 2) # w[y][x].configure(bg='#AA0000') # On renvoie la matrice return w def modifier_apparence_case(case, v): '''Modifie l'apparence du widget en fonction de la valeur v :: param case (tkinter.Label) :: la référence du widget-case à modifier :: param v (int) :: le code du contenu à afficher dans la case :: return (None) :: "procédure" Python .. effet de bord :: modifie le widget case ''' if v in APPARENCE.keys(): case.configure(text=APPARENCE[v][0]) case.configure(bg=APPARENCE[v][1]) def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets modifier_apparence_case(w[5][0], AVENTURIER) modifier_apparence_case(w[0][0], d[0][0]) # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # --- Données du jeu # informations diverses : complement = [-1, 0] # Indice 0 contiendra le joueur actif : -1 (aucun), 1 , 2 ... # matrice encodant les données du plateau, C par défaut C = COULOIR M = MUR A = AVENTURIER D = DRAGON donnees = [ [M, C, C, M, M, C], [M, C, D, M, M, C], [M, C, C, C, D, C], [A, C, C, C, M, M], [M, C, M, C, M, C], [M, C, M, C, C, C] ] # --- Création de la fenêtre principale fenetre = tk.Tk() configurer_fenetre(fenetre) # --- Création des widgets représentant graphiquement les cases widgets = creer_cases(fenetre, donnees) modifier_apparence_cases(widgets, donnees) # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

    13° L'appel à la fonction modifier_apparence_cases avec un s se fait ligne 146 :

    146
    modifier_apparence_cases(widgets, donnees)

    Et voici la fonction modifier_apparence_cases pour le moment :

    95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
    def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets modifier_apparence_case(w[5][0], AVENTURIER) modifier_apparence_case(w[0][0], d[0][0])

    Questions

    1. Dans quel paramètre est stocké l'argument widgets envoyé en ligne 146 ?
    2. Dans quel paramètre est stocké l'argument donnees envoyé en ligne 146 ?
    3. Sur quelle ligne de la fonction modifier_apparence_cases demande-t-on de modifier en l'apparence du widget ligne 5 et colonne 0 ?
    4. Sur quelle ligne de la fonction modifier_apparence_cases demande-t-on de modifier en l'apparence du widget ligne 5 et colonne 0 ?
    5. Sur quelle ligne de la fonction modifier_apparence_cases demande-t-on de modifier en l'apparence du widget ligne 0 et colonne 0 ?
    6. Que renvoie l'évaluation d'AVENTURIER ?
    7. Que renvoie l'évaluation de d[0][0] ?

    ...CORRECTION...

    1. Dans quel paramètre est stocké l'argument widgets envoyé en ligne 146 ?
    2. Dans le premier paramètre, celui qui se nomme w

    3. Dans quel paramètre est stocké l'argument donnees envoyé en ligne 146 ?
    4. Dans le deuxième paramètre, celui qui se nomme d

    5. Sur quelle ligne de la fonction modifier_apparence_cases demande-t-on de modifier en l'apparence du widget ligne 5 et colonne 0 ?
    6. 109
      modifier_apparence_case(w[5][0], AVENTURIER)
    7. Sur quelle ligne de la fonction modifier_apparence_cases demande-t-on de modifier en l'apparence du widget ligne 0 et colonne 0 ?
    8. 110
      modifier_apparence_case(w[0][0], d[0][0])
    9. Quelle valeur est obtenue par l'évaluation d'AVENTURIER ?
    10. AVENTURIER rest évaluée à 8.

    11. Que valeur est obtenue par l'évaluation de d[0][0] ?
    12. d[0][0] veut dire d'aller voir la ligne 0 puis la colonne 0 dans la matrice d qui est un alias pour la matrice globale donnees. On y trouve M qui vaut 1. d[0][0] est donc évalué à 1.

    14° Regardons maintenant pourquoi les deux cases s'affichent pour l'une en noir et pour l'autre en bleu ciel.

    Voici les deux appels à modifier_apparence_case, sans s :

    109 110
    modifier_apparence_case(w[5][0], AVENTURIER) modifier_apparence_case(w[0][0], d[0][0])

    Et voici le code de la fonction modifier_apparence_case :

    82 83 84 85 86 87 88 89 90 91 92 93
    def modifier_apparence_case(case, v): '''Modifie l'apparence du widget en fonction de la valeur v :: param case (tkinter.Label) :: la référence du widget-case à modifier :: param v (int) :: le code du contenu à afficher dans la case :: return (None) :: "procédure" Python .. effet de bord :: modifie le widget case ''' if v in APPARENCE.keys(): case.configure(text=APPARENCE[v][0]) case.configure(bg=APPARENCE[v][1])

    Question

    1. Lors de l'appel ligne 109, quel est le widget reçu dans le paramètre case : celui de la ligne 0 du haut ou celui de la ligne 5 du bas ?
    2. Lors de l'appel ligne 109, quelle est la valeur associée au paramètre v ?
    3. Que contient le tableau obtenu lorsqu'on utilise APPARENCE.keys ?
    4. Que veut dire en français if v in APPARENCE.keys ?
    5. Expliquer alors les modifications faites au widget lors de l'appel ligne 109.
    6. Expliquer alors les modifications faites au widget lors de l'appel ligne 110.

    ...CORRECTION...

    1. Lors de l'appel ligne 109, quel est le widget reçu dans le paramètre case : celui de la ligne 0 du haut ou celui de la ligne 5 du bas ?
    2. Celui en bas à gauche. C'est celui qui est bleu ciel.

    3. Lors de l'appel ligne 109, quelle est la valeur associée au paramètre v ?
    4. 8 puisqu'on transmet AVENTURIER.

    5. Que contient le tableau obtenu lorsqu'on utilise APPARENCE.keys ?
    6. On obtient un tableau qui contient toutes les clés disponibles dans le dictionnaire.

      [0, 1, 3, 8].

    7. Que veut dire en français if v in APPARENCE.keys ?
    8. On teste si la valeur v reçue (8) est bien l'une des clés du dictionnaire-constant APPARENCE. Puisque c'est vrai ici, le test est validé et on effectuera les actions visibles sur les lignes 92 et 93.

    9. Expliquer alors les modifications faites au widget lors de l'appel ligne 109.
    10. Ligne 92 : on modifie le texte de ce widget par le texte présent à l'indice 0 de la valeur associée à la clé 8. On y trouve un 'A'.

      Ligne 93 : on modifie la couleur de fond de ce widget par la couleur présente à l'indice 1 de la valeur associée à la clé 8. On y trouve un '#8888FF' : Rouge et Vert à 88 et le bleu à fond sur FF. On obtient donc du bleu ciel.

    11. Expliquer alors les modifications faites au widget lors de l'appel ligne 110.
    12. L'appel montre qu'on envoie le widget présent en ligne 0 et colonne 0 et que la valeur associé est celle de d[0][0] qui se trouve être 1 (voir la question précédente).

      Ligne 92 : on modifie le texte de ce widget par le texte présent à l'indice 0 de la valeur associée à la clé 1. On y trouve un ''.

      Ligne 93 : on modifie la couleur de fond de ce widget par la couleur présente à l'indice 1 de la valeur associée à la clé 1. On y trouve un 'black'.

    15° Modifier maintenant la fonction modifier_apparence_cases pour qu'elle fasse ce qu'on lui demande en réalité : lire la matrice d case par case et imposer la bonne apparence aux widgets stockés dans la matrice w.

    Il faudra bien entendu utiliser deux boucles imbriquées. Vous noterez que j'ai déjà récupéré le nombre de lignes et le nombre de colonnes.

    Résultat attendu de ce type :

    ...CORRECTION...

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
    def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets for y in range(nbl): for x in range(nbc): modifier_apparence_case(w[y][x], d[y][x])

    Vous savez maintenant créer une interface (vous avez au moins un exemple)

    Et vous savez maintenant comment afficher les données vers la partie Interface puis vers l'IHM de façon à informer l'utilisateur.

    Prochaine partie : récupérer les choix de l'utilisateur via l'IHM, les transmettre à votre partie Interface et transmettre ces choix à la partie Données.

    Nous allons donc retrouver une gestion des événements.

    4 - Evénements

    Tout d'abord : qu'est-ce qu'un événement pour l'interface ? Il s'agit d'une modification de l'environnement extérieur : une touche de clavier sur laquelle on appuie, le déplacement de la souris, l'appui sur une touche de la souris...

    Rappel de l'article Tkinter : gestionnaire d'événement et fonction-événement Tkinter

    C'est ce gestionnaire qui va donner l'ordre d'activer une fonction particulière si cet événement survient.

    Pour faire la liaison événement à surveiller ↦ fonction à réaliser, on utilise la méthode bind, appliquée au widget qu'on surveille.

    Codification de la liaison avec bind

    • On active sur le widget voulu (fenetre ici, mais c'est souvent un label directement)
    • la surveillance d'une utilisation de la flèche Gauche, identifiée par '<KeyPress-Left>'
    • qui provoque l'appel à la fonction nommée evt_fleche_g
      ...
      fenetre.bind("<KeyPress-Left>", evt_fleche_g) # Lorsqu'on appuie sur la flèche Gauche

    Fonction événementielle

    La fonction evt_fleche_g est une fonction normale si ce n'est que :

    1. c'est le gestionnaire d'événements qui la lance lorsqu'il détecte que l'événement est présent.
    2. elle doit avoir un premier paramètre (souvent nommé event) permettant d'accueillir l'événement.

    Que peut faire cette fonction ?

    Ce qu'on veut. C'est d'ailleurs pour cela qu'on a le fameux paramètre event. On peut notamment récupérer l'identifiant du widget ayant déclenché l'événement en tapant event.widget.

    ... ... ... ...
    def evt_fleche_g(event): reference_widget = event.widget print("Vous venez d'appuyer sur la flèche de gauche") reference_widget.configure(bg='#440000')

    Rien d'original : lorsqu'on détecte l'appui sur la FLECHE GAUCHE du clavier alors que reference_widget est actif, on affiche un petit message dans la console et on modifie la couleur du fond du widget.

    On notera qu'on ne place par les parenthèses de la fonction : on ne donne que son adresse interne. Sous cette forme, on ne peut donc pas fournir d'arguments.

    Le transfert d'arguments est possible mais à travers quelques notions que nous n'aborderons pas ici. Voir FAQ si vous voulez plus de détails.

    Liste d'événements

    Pas de blabla. Juste la description de l'événement et la façon de le gérer.

    Evénements liés à la souris

    1 2 3 4 5 6 7 8 9 10 11
    ref_widget.bind('<Button-1>', agir1) # Appui sur clic-gauche sur ce widget ref_widget.bind('<Button-2>', agir2) # Appui sur clic-molette sur ce widget ref_widget.bind('<Button-3>', agir3) # Appui sur clic-droit sur ce widget ref_widget.bind('<ButtonRelease-1>', agir4) # Relachement après clic-gauche ref_widget.bind('<ButtonRelease-2>', agir5) # Relachement après clic-molette ref_widget.bind('<ButtonRelease-3>', agir6) # Relachement après clic-droit ref_widget.bind('<Enter>', agir7) # Pointeur souris rentre sur ce widget ref_widget.bind('<Leave>', agir8) # Pointeur souris sort de ce widget ref_widget.bind('<Motion>', agir9) # Pointeur souris sort de ce widget

    Si vous voulez que la détection soit appliquée sur toute votre fenêtre d'application, il faut lier l'événement à votre fenêtre principale (fenetre dans le code de l'activité présentée ici).

    Evénements liés aux touches du clavier

    On notera qu'il faut cette fois lié l'événement à un widget-fenêtre. C'est par exemple le widget fenetre de notre activité.

    Voici pour les flèches.

    1 2 3 4 5 6 7 8 9
    fenetre.bind('<KeyPress-Left>', agir1) # Appui sur la flèche Gauche fenetre.bind('<KeyPress-Right>', agir2) # Appui sur la flèche Droit fenetre.bind('<KeyPress-Up>', agir3) # Appui sur la flèche Haut fenetre.bind('<KeyPress-Down>', agir4) # Appui sur la flèche Bas fenetre.bind('<KeyRelease-Left>', agir5) # Relachement de la flèche Gauche fenetre.bind('<KeyRelease-Left>', agir6) # Relachement de flèche Droit fenetre.bind('<KeyRelease-Up>', agir7) # Relachement de flèche Haut fenetre.bind('<KeyRelesase-Down>', agir8) # Relachement de la flèche Bas

    On peut aussi demander à agir si n'importe quelle touche est activée :

    1
    fenetre.bind('<Any-KeyPress>', agir9) # Appui sur n'importe quelle touche

    De façon générale pour les autres touches du clavier, cette utilisation basique demande de faire attention à l'utilisation conjointe de majuscule ou pas :

    1 2 3 4 5 6 7 8 9
    fenetre.bind('<KeyPress-a>', agir10) # Appui sur la touche A sans majuscule activée fenetre.bind('<KeyPress-b>', agir11) # Appui sur la touche B sans majuscule activée fenetre.bind('<KeyPress-c>', agir12) # Appui sur la touche C sans majuscule activée fenetre.bind('<KeyPress-d>', agir13) # Appui sur la touche D sans majuscule activée fenetre.bind('<KeyPress-A>', agir14) # Appui sur la touche A avec majuscule activée fenetre.bind('<KeyPress-B>', agir15) # Appui sur la touche B avec majuscule activée fenetre.bind('<KeyPress-C>', agir16) # Appui sur la touche C avec majuscule activée fenetre.bind('<KeyPress-D>', agir17) # Appui sur la touche D avec majuscule activée

    Si vous voulez d'autres explications :

    Passons à la pratique : nous allons rajouter une partie à notre programme de test : après la création de widgets, il est temps de surveiller quelques événements sur ceux-ci.

    16° Mettre le programme en mémoire.

    Questions :

    1. Sur quelle ligne se trouve la mise en place de la surveillance de certains événements ?
    2. Quel est l'événement surveillé ? Sur quels widgets ?
    3. Que provoque l'événement ?
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
    # ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # Codification des contenus COULOIR = 0 MUR = 1 AVENTURIER = 3 DRAGON = 8 # texte à afficher - couleur - couleur au survol APPARENCE = { COULOIR: ('', '#EEEEEE', '#DDDDDD'), MUR: ('', 'black', '#111111'), AVENTURIER: ('A', '#8888FF', '#AAAAFF'), DRAGON: ('D', '#FF5555', '#FF7777') } # ----- 3 - FONCTIONS liées à la partie DONNEES ------------- # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") def creer_cases(fe, d): '''Renvoie une matrice ayant les mêmes dimensions que dc et contenant des widgets Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param d(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: matrice ayant les dimensions d'entrée, contenant des tkinter.Label ''' nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) # Affichage et placement du widget contenu dans w[y][x] px = 20 + x * 90 # position px en pixels py = 20 + y * 85 # position py en pixels w[y][x].place(x=px, y=py) # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y # Option : modification du widget (juste pour montrer qu'on peut !!) #if x == 1 : # nous sommes sur la 2e colonne (indice 1) # w[y][x].configure(bg='red') #elif y == 2 : # nous sommes sur la 3e ligne (indice 2) # w[y][x].configure(bg='#AA0000') # On renvoie la matrice return w def modifier_apparence_case(case, v): '''Modifie l'apparence du widget en fonction de la valeur v :: param case (tkinter.Label) :: la référence du widget-case à modifier :: param v (int) :: le code du contenu à afficher dans la case :: return (None) :: "procédure" Python .. effet de bord :: modifie le widget case ''' if v in APPARENCE.keys(): case.configure(text=APPARENCE[v][0]) case.configure(bg=APPARENCE[v][1]) def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets for y in range(nbl): for x in range(nbc): modifier_apparence_case(w[y][x], d[y][x]) def colorier(event): event.widget.configure(bg="#444444") # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # --- Données du jeu # informations diverses : complement = [-1, 0] # Indice 0 contiendra le joueur actif : -1 (aucun), 1 , 2 ... # matrice encodant les données du plateau, C par défaut C = COULOIR M = MUR A = AVENTURIER D = DRAGON donnees = [ [M, C, C, M, M, C], [M, C, D, M, M, C], [M, C, C, C, D, C], [A, C, C, C, M, M], [M, C, M, C, M, C], [M, C, M, C, C, C] ] # --- Création de la fenêtre principale fenetre = tk.Tk() configurer_fenetre(fenetre) # --- Création des widgets représentant graphiquement les cases widgets = creer_cases(fenetre, donnees) modifier_apparence_cases(widgets, donnees) # --- Création des événements nbl = len(widgets) nbc = len(widgets[0]) for y in range(nbl): for x in range(nbc): widgets[y][x].bind('<Enter>', colorier) # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

    ...CORRECTION...

    1. Sur quelle lignes se trouve la mise en place de la surveillance de certains événements ?
    2. Ligne 156

    3. Quel est l'événement surveillé ? Sur quels widgets ?
    4. L'événement correspond à l'entrée de la souris dans un widget ('<Enter>'). La déclaration s'applique sur tous les widgets contenus dans la matrice widgets à l'aide de la double boucle.

    5. Que provoque l'événement ?
    6. Il faut aller voir le code de la fonction colorier. Lignes 113-114, on voit qu'elle ne fait que colorier le widget survoler en gris.

    Nous voulons maintenant rendre la couleur des widgets plus claire lorsqu'on les survole :

    17° Cette fois, la fonction-événementielle eclaircir est plus complexe car on a besoin de recevoir les matrices donnees et widgets de façon à pouvoir :

    1. Obtenir l'indice de ligne y et de colonne x du widget qu'on vient de survoler
    2. D'aller lire la donnée stockée dans donnees[y][x].
    3. De retrouver la couleur éclaircie à appliquer en cherchant dans APPARENCE (en utilisant la clé trouvée à l'étape précédente et en lisant l'indice 2 du tuple obtenu)
    4. D'appliquer cette couleur sur le widget en utilisant la méthode configure.

    Voici le prototype de la fonction eclaircir :

    113
    def eclaircir(e, w, d):

    Et la façon dont on lie cette fonction à un événement :

    183
    widgets[y][x].bind('<Enter>', lambda event: eclaircir(event, widgets, donnees))

    Questions liées au code ci-dessous (notamment le code surligné)

    1. Pourquoi a-t-on dû utiliser une fonction lambda lors de la déclaration de la fonction-événement ?
    2. Quelle est la fonction qui gère au final l'événement 'entrée dans la case' ?
    3. Quelle est la fonction qui gère au final l'événement 'sortie hors de la case' ?
    4. Sur quelles lignes avait-on créé les attributs ligne et colonne que nous parvenons ensuite à lire sur les lignes ... et ...
    5. Pourquoi aller chercher l'indice 2 du triplet stocké dans le dictionnaire ?
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    # ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # Codification des contenus COULOIR = 0 MUR = 1 AVENTURIER = 3 DRAGON = 8 # texte à afficher - couleur - couleur au survol APPARENCE = { COULOIR: ('', '#EEEEEE', '#FFFFFF'), MUR: ('', 'black', '#111111'), AVENTURIER: ('A', '#8888FF', '#AAAAFF'), DRAGON: ('D', '#FF5555', '#FF7777') } # ----- 3 - FONCTIONS liées à la partie DATA ---------------- # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") def creer_cases(fe, d): '''Renvoie une matrice ayant les mêmes dimensions que dc et contenant des widgets Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param d(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: matrice ayant les dimensions d'entrée, contenant des tkinter.Label ''' nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) # Affichage et placement du widget contenu dans w[y][x] px = 20 + x * 90 # position px en pixels py = 20 + y * 85 # position py en pixels w[y][x].place(x=px, y=py) # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y # Option : modification du widget (juste pour montrer qu'on peut !!) #if x == 1 : # nous sommes sur la 2e colonne (indice 1) # w[y][x].configure(bg='red') #elif y == 2 : # nous sommes sur la 3e ligne (indice 2) # w[y][x].configure(bg='#AA0000') # On renvoie la matrice return w def modifier_apparence_case(case, v): '''Modifie l'apparence du widget en fonction de la valeur v :: param case (tkinter.Label) :: la référence du widget-case à modifier :: param v (int) :: le code du contenu à afficher dans la case :: return (None) :: "procédure" Python .. effet de bord :: modifie le widget case ''' if v in APPARENCE.keys(): case.configure(text=APPARENCE[v][0]) case.configure(bg=APPARENCE[v][1]) def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets for y in range(nbl): for x in range(nbc): modifier_apparence_case(w[y][x], d[y][x]) def eclaircir(e, w, d): '''Modifie la case en utilisant la couleur claire :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' case = e.widget y = case.ligne x = case.colonne code = d[y][x] if code in APPARENCE: nouvelle_couleur = APPARENCE[code][2] case.configure(bg=nouvelle_couleur) def assombrir(e, w, d): '''Modifie la case en utilisant la couleur sombre :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' pass # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # --- Données du jeu # informations diverses : complement = [-1, 0] # Indice 0 contiendra le joueur actif : -1 (aucun), 1 , 2 ... # matrice encodant les données du plateau, C par défaut C = COULOIR M = MUR A = AVENTURIER D = DRAGON donnees = [ [M, C, C, M, M, C], [M, C, D, M, M, C], [M, C, C, C, D, C], [A, C, C, C, M, M], [M, C, M, C, M, C], [M, C, M, C, C, C] ] # --- Création de la fenêtre principale fenetre = tk.Tk() configurer_fenetre(fenetre) # --- Création des widgets représentant graphiquement les cases widgets = creer_cases(fenetre, donnees) modifier_apparence_cases(widgets, donnees) # --- Création des événements nbl = len(widgets) nbc = len(widgets[0]) for y in range(nbl): for x in range(nbc): widgets[y][x].bind('<Enter>', lambda event: eclaircir(event, widgets, donnees)) widgets[y][x].bind('<Leave>', lambda event: assombrir(event, widgets, donnees)) # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

    ...CORRECTION...

    Rappel

    Les fonctions lambda ne sont pas au programme : nous ne les utiliserons que comme moyen commode de transmettre des paramètres lors de l'utilisation de la méthode bind ou after.

    Hors du contexte "Projet Tkinter", inutile d'essayer de placer une solution qui les utilise. Ce n'est pas attendu.

    18° En vous basant sur la fonction précédente, créer la fonction assombrir.

    ...CORRECTION...

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
    def assombrir(e, w, d): '''Modifie la case en utilisant la couleur sombre :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' case = e.widget y = case.ligne x = case.colonne code = d[y][x] if code in APPARENCE: nouvelle_couleur = APPARENCE[code][1] case.configure(bg=nouvelle_couleur)

    Imaginons maintenant qu'on ne veuille pas animer les murs au survol. Les murs ne bougeront pas dans notre jeu. On peut donc juste récupérer la valeur du fond coloré de la case et s'il correspond à celui d'un mur, on ne fait rien. Pour cela, il faut utiliser la méthode cget.

    Méthode cget

    C'est une méthode propre aux widgets de Tkinter. Elle n'est donc pas à connaître. Elle permet de récupérer l'une des valeurs graphiques d'un widget : couleur du fond, couleur du texte, texte...

    Un exemple : on récupère la couleur du fond du widget dont la référence est stockée dans w_get.

    1
    couleur = case.cget('bg')

    Du coup, on peut l'utiliser pour modifier la couleur d'une des cases lorsqu'on la survole.

    19° Remplacer la fonction par la version ci-dessous. Expliquer ce que fait la ligne 127.

    113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    def eclaircir(e, w, d): '''Modifie la case en utilisant la couleur claire :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' case = e.widget y = case.ligne x = case.colonne code = d[y][x] if case.cget('bg') != APPARENCE[MUR][1] and code in APPARENCE: nouvelle_couleur = APPARENCE[code][2] case.configure(bg=nouvelle_couleur)

    Question supplémentaire : quel est le type de MUR ?

    ...CORRECTION...

    On réalise l'éclaircissement SI :

    1. Le fond coloré de la case graphique n'est pas noir (APPARENCE[MUR][1])
    2. La clé fournie existe bien dans le dictionnaire APPARENCE

    MUR est une constante faisant référence à l'entier 1. Voir ligne 10. Il ne s'agit pas d'un string attention.

    Bien. Nous avons révisé comment gérer les événements dans Tkinter. Vous avez maintenant le squelette d'un plateau qui vous permettra de réaliser votre propre jeu. Avant cela, voyons comment gérer une modification dans la partie Donnée depuis la partie Interface.

    5 - Très Mini projet décrit : déplacer l'aventurier

    Avant de commencer, réfléchissons à la manière de procéder lorsque réalise un projet basé sur une IHM (Interface Homme Machine). Vous allez avoir besoin d'appliquer cette méthode de réflexion pendant votre projet.

    Un bon moyen de faire un projet en équipe est de :

    1. En groupe : réflechir avec un texte écrit en français qui décrit le déroulé des actions. On y trouve le nom des fonctions et ce qu'elles doivent faire
    2. En groupe : créer le schéma du projet pour vérifier qu'il est bien conçu (la partie Interface n'agit que sur l'Interface et la partie Données n'agit que sur les Données). Les interactions entre les deux catégories sont encapsulées dans certaines fonctions bien ciblées.
    3. En groupe : Réaliser les prototypages
    4. Individuel : Travailler sur sa fonction, créer sa documentation et des tests si possible.

    Un exemple - Parvenir à déplacer l'aventurier :

    On commence par réflechir aux fonctions qu'on désire, à ce qu'elle doive recevoir et à ce qu'elle doit faire ou répondre.

    1. Partie Données : on veut une fonction coordonnees_aventurier qui renvoie les x et y de la case-donnée contenant l'aventurier dans donnees.
    2. 1 2 3 4 5 6 7 8
      def coordonnees_aventurier(d): '''Renvoie un tuple (x,y) contenant les coordonnes de l'aventurier :: param d(list(list)) :: la matrice contenant les données des cases :: return (tuple(int,int)) :: (x,y) ''' return (0,0) # Fausse réponse qui permet néanmoins de travailler
    3. Partie Données : on veut une fonction est_case_proche qui renvoie True si les coordonnées-paramètres x et y correspondent à un élément adjacent verticalement ou horizontalement à la case-donnée cible (celle de l'aventurier ici).
    4. 1 2 3 4 5 6 7 8 9 10 11 12
      def est_case_proche(d, x, y, xA, yA): '''Renvoie True si d[y][x] est proche de la case cible d[yA][xA] :: param d(list(list)) :: la matrice contenant les données des cases :: param x(int) :: l'indice de la colonne de la case étudiée :: param y(int) :: l'indice de la ligne de la case étudiée :: param xA(int) :: l'indice de la colonne de la case cible :: param yA(int) :: l'indice de la ligne de la case cible :: return (bool) :: True si proche, False sinon ''' return True # Fausse réponse qui permet de travailler en attendant
    5. Partie Données : on veut une fonction intervertir qui intervertit les contenus de deux éléments de donnees dont on fournit les quatre coordonnées x1, y1, x2 et y2.
    6. 1 2 3 4 5 6 7 8 9 10 11 12 13
      def intervertir(d, x1, y1, x2, y2): '''Intervertit les contenus de d[y1][x1] et d[y2][x2] :: param d(list(list)) :: la matrice contenant les données des cases :: param x1(int) :: l'indice de la colonne de la 1er case :: param y1(int) :: l'indice de la ligne de la 1er case :: param x2(int) :: l'indice de la colonne de la 2e case :: param y2(int) :: l'indice de la ligne de la 2e case :: return (None) :: "Procédure" python .. effet de bord :: Modifie la matrice d ''' pass
    7. Partie Données : on crée une fonction tentative_deplacement qui reçoit des coordonnées x et y, qui vérifie si elles sont bien proches de l'aventurer et qui intervertit les éléments dans ce cas. Elle renvoie True si on fait une interversion.
    8. 1 2 3 4 5 6 7 8 9 10 11
      def tentative_deplacement(d, x, y): '''Tente de déplacer l'aventurier et renvoie True si cela fonctionne :: param d(list(list)) :: la matrice contenant les données des cases :: param x(int) :: l'indice de la colonne de la case étudiée :: param y(int) :: l'indice de la ligne de la case étudiée :: return (bool) :: True si déplacement possible et effectué .. effet de bord :: Modifie la matrice d si déplacement possible ''' return True # Fausse réponse qui permet de travailler en attendant
    9. Partie Interface : on crée une fonction gerer_clic_case liée aux clics sur l'une des cases et qui
      • récupère la référence de la case-widget, sa ligne et sa colonne
      • envoie cette ligne et cette colonne à la fonction tentative_deplacement appartenant à la partie Données.
      • si tentative_deplacement répond True, on actualise l'affichage de l'interface avec la fonction modifier_apparence_cases (déja codée)
      1 2 3 4 5 6 7 8 9 10 11
      def gerer_clic_case(e, w, d): '''Tente de faire bouge l'aventurier sur le plateau :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w et la matrice d ''' modifier_apparence_cases(w, d) # en attendant mieux

    Si on représente la répartition entre partie Interface et partie Données, cela donne :

    Description du projet Aventurier

    Pour travailler en équipe, vous allez donc devoir avoir au moins le prototype des fonctions (avec la documentation précises des préconditions et postconditions).

    Chaque personne du groupe peut ensuite commencer à réfléchir sur sa propre fonction.

    20° Quel est l'intérêt de placer de "fausses réponses" dans les fonctions devant répondre plutôt qu'un simple pass ?

    ...CORRECTION...

    Cela permet simplement de commencer à travailler même si on a besoin d'une réponse de l'une de ces fonctions. On sait que la réponse est fausse mais on n'a pas à entendre la version finale pour avancer.

    Voici le code final du très mini-projet Déplacement Aventurier. Vous pouvez le lancer, le tester et voir comment je propose de résoudre les problèmes. Bien entendu, il a d'autres manières de coder une même fonction.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
    # ----- 1 - IMPORTATIONS ------------------------------------ import tkinter as tk # ----- 2 - CONSTANTES -------------------------------------- # Codification des contenus COULOIR = 0 MUR = 1 AVENTURIER = 3 DRAGON = 8 # texte à afficher - couleur - couleur au survol APPARENCE = { COULOIR: ('', '#EEEEEE', '#FFFFFF'), MUR: ('', 'black', '#111111'), AVENTURIER: ('A', '#8888FF', '#AAAAFF'), DRAGON: ('D', '#FF5555', '#FF7777') } # ----- 3 - FONCTIONS liées à la partie DATA ---------------- def coordonnees_aventurier(d): '''Renvoie un tuple (x,y) contenant les coordonnes de l'aventurier :: param d(list(list)) :: la matrice contenant les données des cases :: return (tuple(int,int)) :: (x,y) ''' nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes for y in range(nbl): for x in range(nbc): if d[y][x] == AVENTURIER: return (x,y) def est_case_proche(d, x, y, xA, yA): '''Renvoie True si d[y][x] est proche de la case cible d[yA][xA] :: param d(list(list)) :: la matrice contenant les données des cases :: param x(int) :: l'indice de la colonne de la case étudiée :: param y(int) :: l'indice de la ligne de la case étudiée :: param xA(int) :: l'indice de la colonne de la case cible :: param yA(int) :: l'indice de la ligne de la case cible :: return (bool) :: True si proche, False sinon ''' if (x-1) == xA and y == yA: return True elif (x+1) == xA and y == yA: return True elif x == xA and (y-1) == yA: return True elif x == xA and (y+1) == yA: return True else: return False def intervertir(d, x1, y1, x2, y2): '''Intervertit les contenus de d[y1][x1] et d[y2][x2] :: param d(list(list)) :: la matrice contenant les données des cases :: param x1(int) :: l'indice de la colonne de la 1er case :: param y1(int) :: l'indice de la ligne de la 1er case :: param x2(int) :: l'indice de la colonne de la 2e case :: param y2(int) :: l'indice de la ligne de la 2e case :: return (None) :: "Procédure" python .. effet de bord :: Modifie la matrice d ''' temp = d[y1][x1] d[y1][x1] = d[y2][x2] d[y2][x2] = temp def tentative_deplacement(d, x, y): '''Tente de déplacer l'aventurier et renvoie True si cela fonctionne :: param d(list(list)) :: la matrice contenant les données des cases :: param x(int) :: l'indice de la colonne de la case étudiée :: param y(int) :: l'indice de la ligne de la case étudiée :: return (bool) :: True si déplacement possible et effectué .. effet de bord :: Modifie la matrice d si déplacement possible ''' (xA, yA) = coordonnees_aventurier(d) if est_case_proche(d, x, y, xA, yA): intervertir(d, x, y, xA, yA) return True else: return False # ----- 4 - FONCTIONS liées à la partie INTERFACE ----------- def configurer_fenetre(fe): '''Modifie les paramètres de la fenêtre reçue en paramètre :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: return (None) :: "procédure" Python ''' fe.geometry("1200x600") fe.title("Mon super jeu") fe.configure(bg="black") def creer_cases(fe, d): '''Renvoie une matrice ayant les mêmes dimensions que dc et contenant des widgets Labels :: param fe(tkinter.Tk) :: la référence d'une fenêtre de classe Tk :: param d(list(list)) :: la matrice contenant les données des cases :: return (list(list)) :: matrice ayant les dimensions d'entrée, contenant des tkinter.Label ''' nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On crée une matrice w qui servira à contenir les widgets représentant les cases w = [ [None for x in range(nbc)] for y in range(nbl) ] # On crée les Labels un par un et on les stocke dans la matrice for y in range(nbl): for x in range(nbc): # Calcul du numéro de la case numero = x + y * nbc # Création et stockage d'un Label w[y][x] = tk.Label(fe, text=numero, fg="white", bg='grey', width=10, height=5) # Affichage et placement du widget contenu dans w[y][x] px = 20 + x * 90 # position px en pixels py = 20 + y * 85 # position py en pixels w[y][x].place(x=px, y=py) # Option : rajout d'attributs pour les récupérer facilement w[y][x].numero = numero w[y][x].colonne = x w[y][x].ligne = y # Option : modification du widget (juste pour montrer qu'on peut !!) #if x == 1 : # nous sommes sur la 2e colonne (indice 1) # w[y][x].configure(bg='red') #elif y == 2 : # nous sommes sur la 3e ligne (indice 2) # w[y][x].configure(bg='#AA0000') # On renvoie la matrice return w def modifier_apparence_case(case, v): '''Modifie l'apparence du widget en fonction de la valeur v :: param case (tkinter.Label) :: la référence du widget-case à modifier :: param v (int) :: le code du contenu à afficher dans la case :: return (None) :: "procédure" Python .. effet de bord :: modifie le widget case ''' if v in APPARENCE.keys(): case.configure(text=APPARENCE[v][0]) case.configure(bg=APPARENCE[v][1]) def modifier_apparence_cases(w, d): '''Modifie les widgets pour refléter les données du plateau de jeu :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' # On récupére les dimensions de la matrice d nbl = len(d) # Nombre de lignes nbc = len(d[0]) # Nombre de lignes # On lit les données une par une et on modifie les widgets for y in range(nbl): for x in range(nbc): modifier_apparence_case(w[y][x], d[y][x]) def eclaircir(e, w, d): '''Modifie la case en utilisant la couleur claire :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' case = e.widget y = case.ligne x = case.colonne code = d[y][x] if case.cget('bg') != APPARENCE[MUR][1] and code in APPARENCE: nouvelle_couleur = APPARENCE[code][2] case.configure(bg=nouvelle_couleur) def assombrir(e, w, d): '''Modifie la case en utilisant la couleur sombre :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w ''' case = e.widget y = case.ligne x = case.colonne code = d[y][x] if case.cget('bg') != APPARENCE[MUR][1] and code in APPARENCE: nouvelle_couleur = APPARENCE[code][1] case.configure(bg=nouvelle_couleur) def gerer_clic_case(e, w, d): '''Tente de faire bouge l'aventurier sur le plateau :: param e (tkinter.Event) :: contient les informations sur l'événement :: param w (list(list)) :: matrice contenant les références des widgets :: param d (list(list)) :: matrice contenant les données des cases :: return (None) :: "procédure" Python .. effet de bord :: modifie la matrice w et la matrice d ''' case = e.widget x = case.colonne y = case.ligne if tentative_deplacement(d, x, y): modifier_apparence_cases(w, d) # ----- 5 - PROGRAMME PRINCIPAL ------------------------------ if __name__ == '__main__': # --- Activation des tests sur les fonctions du module import doctest doctest.testmod() # --- Données du jeu # informations diverses : complement = [-1, 0] # Indice 0 contiendra le joueur actif : -1 (aucun), 1 , 2 ... # matrice encodant les données du plateau, C par défaut C = COULOIR M = MUR A = AVENTURIER D = DRAGON donnees = [ [M, C, C, M, M, C], [M, C, D, M, M, C], [M, C, C, C, D, C], [A, C, C, C, M, M], [M, C, M, C, M, C], [M, C, M, C, C, C] ] # --- Création de la fenêtre principale fenetre = tk.Tk() configurer_fenetre(fenetre) # --- Création des widgets représentant graphiquement les cases widgets = creer_cases(fenetre, donnees) modifier_apparence_cases(widgets, donnees) # --- Création des événements nbl = len(widgets) nbc = len(widgets[0]) for y in range(nbl): for x in range(nbc): widgets[y][x].bind('<Enter>', lambda event: eclaircir(event, widgets, donnees)) widgets[y][x].bind('<Leave>', lambda event: assombrir(event, widgets, donnees)) widgets[y][x].bind('<Button-1>', lambda event: gerer_clic_case(event, widgets, donnees)) # --- Activation de la surveillance sur l'application graphique fenetre.mainloop()

    Si vous voulez plus d'informations, vous pouvez faire des recherches sur le Web.

    Vous trouverez également quelques fiches sur ce site, comme celle-ci par exemple.

    FICHE Tkinter

    Activité publiée le 29 11 2019
    Dernière modification : 13 12 2020
    Auteur : ows. h.