Affichage successif d'images sur tkinter dans même cavnas

Résolu/Fermé
pycarpe Messages postés 16 Date d'inscription lundi 24 janvier 2022 Statut Membre Dernière intervention 9 août 2022 - 24 janv. 2022 à 14:00
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 - 26 janv. 2022 à 14:16
from tkinter import *
from time import *
var = 30
 
path = "C:\\XX\\XY\\XXY\\Y\\"
 
def change_img():
    tab_img = [f"{path}image1.gif",f"{path}image2.gif"] # images gif en 90x90
    for i in tab_img:
        photo = PhotoImage(file = i)
        canvas_logo.create_image(wi/2, he/2 ,image=photo)  
        canvas_logo.itemconfigure('image', image=photo)
        canvas_logo.update()
        sleep(1) # sûrement le fautif.
    win.after(1000, change_img)
 
 
win = Tk()
 
 
win.geometry("1000x720")
win.config(background='#7720B9')
 
 
 
 
def ajout_():
    global var
    canvas1 = Canvas(win, width= 200, height= 50)
    canvas1.create_rectangle(0, 0, 200, 50, fill="red")
 
 
    canvas2 = Canvas(win, width= 200, height= 50)
    canvas2.create_rectangle(0, 0, 200, 50, fill="yellow")
    canvas1.place(x=0, y=var)
    canvas2.place(x=200, y=var)
 
    myentry1 = Entry(canvas1, bd = 3)
    myentry1.place(x = 5, y = 12.5)
    myentry2 = Entry(canvas2, bd = 3)
    myentry2.place(x = 5, y = 12.5)
 
    var = var + 50
 
wi = 90
he  = 90
canvas_logo = Canvas(win, width= wi, height= he)
photo = PhotoImage(file=f"{path}image1.gif")
canvas_logo.create_image(wi/2, he/2 ,image=photo)
canvas_logo.place(x=600, y=0)
 
bu_ajout = Button(win, text = "Ajout", command = ajout_)
bu_ajout.place(x = 0, y = 0)
 
 
win.after(1000, change_img)
 
 
win.mainloop()


Ce qui se passe c'est que pendant le changement d'image, l'application freeze un peu. Je sais que le fautif c'est le sleep. Mais si je ne le met pas la transition se fait trop vite. Idem si je met la fonction change_img en threading. Avez-vous une solution svp? Si oui, j''aimerais bien un exemple.

Merci par avance.
A voir également:

7 réponses

mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
Modifié le 24 janv. 2022 à 14:21
Bonjour,

Peu importe le framework utilisé (ici Tk), l'idée générale consiste à un timer qui lorsqu'il expire, lève un événement que tu rattrapes à l'aide d'une fonction dédiée (appelée callback). C'est cette callback qui doit mettre à jour ton image. Ce faisant l'application n'est pas gelée (à cause du
sleep
).

Tu peux t'inspirer de cette discussion qui montre comment faire une horloge en Tk :

#!/usr/bin/env python3

# Display UTC.
# started with https://docs.python.org/3.4/library/tkinter.html#module-tkinter

import tkinter as tk
import time

def current_iso8601():
    """Get current date and time in ISO8601"""
    # https://en.wikipedia.org/wiki/ISO_8601
    # https://xkcd.com/1179/
    return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())

class Application(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        self.now = tk.StringVar()
        self.time = tk.Label(self, font=('Helvetica', 24))
        self.time.pack(side="top")
        self.time["textvariable"] = self.now

        self.QUIT = tk.Button(self, text="QUIT", fg="red",
                                            command=root.destroy)
        self.QUIT.pack(side="bottom")

        # initial time display
        self.onUpdate()

    def onUpdate(self):
        # update displayed time
        self.now.set(current_iso8601())
        # schedule timer to call myself after 1 second
        self.after(1000, self.onUpdate)

root = tk.Tk()
app = Application(master=root)
root.mainloop()


Dans cet exemple, la fenêtre est réalisée à l'aide d'une classe. Ce n'est pas obligatoire (comme le montre ton code), mais c'est la manière classique de procéder, donc je te recommande de faire pareil, car ça permet d'avoir un code mieux organiser.

Si tu n'as pas encore vu les classes, c'est juste une manière de déclarer un type personnalisé auquel est attaché un ensemble de fonctions (appelées méthodes). En fait, tu utilises déjà des classes sans le savoir (par exemple dans ton code
Canvas
est une classe,
canvas_logo
est une instance de cette classe, et
create_rectangle
est une méthode de la classe
Canvas
.

Quand tu crées une instance d'un objet (e.g.
canvas = Canvas()
), tu appelles le constructeur (méthode
__init__()
, qui selon la classe, peut attendre ou non des paramètres). Dans l'exemple de code que j'ai reporté, c'est dans le constructeur appelle la méthode
createWidgets
qui place les composants de la fenêtre. Celle-ci appelle immédiatement
onUpdate
(dans ton cas, cela te permettra d'afficher la première image). Note que les noms des méthodes (
createWidgets
et
onUpdate
) sont arbitraires, tu peux renommer ces méthodes si tu le souhaites.

La partie intéressante qui réalise cette notion de "timer" est réalisé dans la fonction
onUpdate
: l'instruction
self.after(1000, self.onUpdate)
permet de rappeler dans 1000ms (donc dans 1s) la méthode
self.onUpdate
, et donc dans ton cas d'afficher l'image suivante. L'avantage c'est qu'entre temps, ton application n'est pas endormie et donc reste réactive.

Par rapport à ton code, l'idéal serait de passer le chemin (
path
) au constructeur. Ça t'éviterait d'avoir une variable globale (qui est une mauvaise habitude de programmation). De même, ce que tu as appelé
var
pourrait être une variable interne à ta classe.

Bonne chance
1
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
Modifié le 26 janv. 2022 à 16:32
Bonjour,

Allez ça m'a permis de découvrir un peu Tk (et ses nombreux écueils :p, personnellement je fais rarement des interfaces graphiques et à choisir je préfère Qt) voici à quoi ça pourrait ressembler :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
import time
import os

IMAGE_DIR = "/home/mando/Downloads"
 
def find(dirname :str):
    return (
        os.path.join(root, name)
        for (root, dirs, files) in os.walk(dirname, topdown=False)
        for name in files
    )

class Application(tk.Frame):
    def __init__(self, filenames=None, master=None):
        tk.Frame.__init__(self, master)
        self.pack(expand=True, fill="both")
        self.entries_y = 30
        print(filenames)
        self.images = [
            tk.PhotoImage(file=filename, master=self if master is None else master)
            for filename in filenames
        ]
        self.image_index = 0
        self.create_widgets()

        # Prepare initial timer
        self.on_update()

    def create_widgets(self):  
        if self.images:
            wi = 90
            he  = 90
            image = self.images[self.image_index]
            self.canvas_logo = tk.Canvas(self, width = wi, height = he)
            self.canvas_logo.place(x = wi / 2, y = 0)
            #self.canvas_logo.create_rectangle(0, 0, 200, 50, fill="green")
            self.image_container = self.canvas_logo.create_image(wi / 2, he / 2, image=image)
            self.canvas_logo.pack()
        
        self.bu_ajout = tk.Button(self, text = "Ajout", command = self.add_entries)
        self.bu_ajout.pack(side="top")

        self.quit = tk.Button(self, text="Quitter", fg="red", command=root.destroy)
        self.quit.pack(side="bottom")

    def add_entries(self):
        self.add_entry("red")
        self.add_entry("yellow")
        
    def add_entry(self, color):
        canvas = tk.Canvas(self, width=200, height=50)
        canvas.create_rectangle(0, 0, 200, 50, fill=color)
        canvas.place(x=0, y=self.entries_y)
        myentry = tk.Entry(canvas, bd = 3)
        myentry.place(x = 5, y = 12.5)
        self.entries_y += 50

    def update_image(self):
        if self.images:
            image = self.images[self.image_index]
            self.canvas_logo.itemconfigure(self.image_container, image=image)
            self.canvas_logo.pack()
        
    def on_update(self):
        if self.images:
            self.image_index += 1
            self.image_index %= len(self.images)
            self.update_image()
            self.after(1000, self.on_update)

root = tk.Tk()
root.geometry("1000x720")
#root.config(background='#7720B9')
app = Application(
    filenames = [
        f
        for f in find(IMAGE_DIR)
        if f.endswith(".gif")
    ],
    master = root
)
root.mainloop()


Ici j'en ai profité pour améliorer un peu le design de ton code même si le code que je partage est selon mes critères encore améliorable (voir fin du message). Par ailleurs, j'importe un peu différemment les objets Tk (c'est plus propre comme je fais car on garde l'espace de nommage et on évite les potentielles collisions entre les noms de classe Tk et les autres).

Explication du programme principal

On commence par regarder le programme principal, après la classe
Application
. Ici je suis exactement l'exemple que je t'ai donné auparavant. Plutôt que de coder en dur le chemin des fichiers, j'ai fait une petite fonction
find
qui liste les fichiers
.gif
du dossier
IMAGE_DIR
(et dans ses sous-dossiers).

Ensuite on crée une instance de notre classe
Application
à laquelle je passe le pointeur
root
(que tu appelais
win
). On en aura besoin au moment de charger les images afin d'éviter l'erreur présentée lis cette discussion. Dans l'idée, les images doivent être chargées par "le bon interpréteur".

Synchrone vs asynchrone

il est primordial de comprendre que pendant toute la construction, l'application n'est pas encore lancée et le programme fonctionne donc de manière synchrone (il exécute les instructions le plus vite possible les unes à la suite des autres).

Ce n'est que quand on atteindra
root.mainloop()
que l'application sera réellement lancée et réactive. Cette instruction lance "la boucle d'exécution". On bascule alors en mode asynchrone, c'est à dire que le programme ne "travaille" que lorsque les événements (prévus pour être rattrapés par ton application) sont déclenchés (par exemple, sur un clic de bouton ou dans ton cas, quand le timer expire). Ça veut aussi dire qu'en asynchrone, un
sleep
est catastrophique car il gèle pendant tout ce temps l'application. Il est donc à proscrire. Une fois qu'on est en mode asynchrone, il ne faut raisonner qu'en réagissant à des événements (on parle de programmation événementielle, voir cette page -- rien à voir avec le concert de ton groupe préféré).

Création de l'application (partie synchrone)

Regardons la partie construction (
Application.__init__
), donc avant que la boucle d'exécution ne soit lancée.

Au moment de créer notre application, on entre dans
__init__
. On appelle le constructeur de la classe mère (qu'on récupère via
super()
), puis première différence avec ton programme on fait un self.pack(expand=True, fill="both"). Celui-ci fait en sorte que notre
tk.Frame
occupe toute la fenêtre. Ici on devrait selon moi créer un
tk.Canvas
principal qui pemettrait d'organiser tous les widgets de ta fenêtre. C'est recommandé pour avoir une interface graphique "responsive", c'est à dire dont les composants se réorganisent correctement quand la géométrie de la fenêtre change. Bref, ça vaudrait le coup que tu jettes un œil à tout ça et c'est un des points d'amélioration de ce script.

Ensuite je crée un attribut
self.images
dans lequel je charge tous les images (formats valides : png, bmp, gif). Si tu veux considérer d'autres images (genre .jpg) il faudra utiliser PIL (voir cette discussion).

On prépare aussi le bouton "Ajouter" qui te permettra de voir que ton programme n'est pas gelé par notre timer. Ici aussi, il serait plus judicieux d'utiliser des
tk.Canvas
dans lequel on empilerait les différents widgets que tu crées. J'ai gardé ton code pour que tu t'y retrouves, mais au lieu d'utiliser ta variable globale
var
, j'utilise un attribut de classe
self.entries_y
. C'est l'un des points qu'il faudrait améliorer.

Les dimensions de certains widgets sont calculés à partir de
he
et
wi
qui n'ont plus lieu d'être des variables globales. C'est donc un peu plus propre mais pas encore parfait. Il faudrait idéalement les calculer par rapport aux dimensions de ta
tk.Frame
. C'est un point que je te laisse améliorer.

Enfin, on termine le constructeur en armant le premier timer conformément à l'exemple de mon message précédent. À mon avis, c'est un peu bancale car la boucle d'exécution n'est pas encore lancée, donc dire de faire quelque chose dans 1s n'a pas de sens (et d'ailleurs ça lève un message d'avertissement). Ça mériterait de regarder si on ne peut pas faire plus propre et c'est l'une des améliorations à apporter à ce programme.

Une fois que tout ceci est fait, la prochaine instruction du programme principale lance la boucle d'exécution...

Mettre à jour l'image toute les secondes

Une fois dans la boucle d'exécution, le programme attend qu'un événement qui le concerne soit levé. Parmi eux, il y a le clic sur le bouton "Quitter", celui sur le bouton "Ajouter". L'énorme intérêt c'est qu'une boucle d'exécution quand elle est bien faite (et c'est le cas de Tk) n'est pas un
while True
et ne consomme donc pas 100% de CPU en passant son temps à examiner s'il y a eu une activité. Et c'est là toute la force de la programmation événementielle : le programme attend sagement que quelque chose se passe.

En particulier, la fonction
on_update
se déclenche au bout d'une seconde et réarme ce timer à chaque fois à l'aide de
self.after
. Au passage elle met à jour l'image. Pour cela on charge directement le bon objet
tk.Image
(toutes nos images sont stockées dans
self.images
) en faisant un itérant sur nos images (d'où l'incrément) et en repartant en début de liste quand on les a toutes traitées (d'où le modulo) -- on aurait pu aussi remettre le compteur à 0 si
self.image_index
est supérieur ou égale à
len(self.images)
(ici, ça revient au même, mais ces deux stratégies ont un comportement différent si l'incrément est plus grand que 1).

Tu noteras que j'ai un peu simplifié la mise à jour de l'image en me basant sur cette discussion. Il est important de garder trace du sous-canevas qui a été implicitement lors de l'instruction
self.image_container = self.canvas_logo.create_image(wi / 2, he / 2, image=image)
, c'est le premier paramètre attendus par
self.canvas_logo.itemconfigure(...)
.

Bonne chance
1
pycarpe Messages postés 16 Date d'inscription lundi 24 janvier 2022 Statut Membre Dernière intervention 9 août 2022
Modifié le 24 janv. 2022 à 14:55
Merci pour ta diligence. Du coup, si j'ai bien compris, il faut que je joue avec l'horloge de mon pc pour temporiser l'affichage de chaque image c'est bien ca?

Ps : Merci pour le petit rappel pour les class. Mais j'ai pas voulu en utiliser pour cette exemple simple. Mais merci quand même vraiment ca change des autres forums qui ont 0 pédagogies.
0
pycarpe Messages postés 16 Date d'inscription lundi 24 janvier 2022 Statut Membre Dernière intervention 9 août 2022
Modifié le 24 janv. 2022 à 17:58
Alors déjà Merci infiniment pour le temps que tu as pris pour détailler les choses. Je ne suis pas sûr d'avoir tout compris mais je vais me pencher dessus jusqu'à que je sois à l'aise avec tout ca.
En effet, j'étais en train de gruger le truc avec un while mais ma consommation était en modéré contre très faible avec ta méthode.

Un grand merci à toi encore. Je n'hésiterai pas à revenir vers toi si j'ai des questions pour ma compréhension si tu as le temps biensûr.

Ps : Ce forum est génial comparé à d'autres que j'ai pu voir. C'est motivant de voir des gens qui se mettent à la place des autres. Car malheureusement certains pensent qui si c'est évident pour eux alors ca doit l'être pour les autres. Ce qui n'est pas forcément le cas.
Donc mille merci. A++++
0
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
24 janv. 2022 à 18:06
Re bonjour pycarpe,

Concernant la version avec un
while True
c'était modéré car ton programme n'utilisait qu'un CPU parmi tous ceux dont ta machine dispose (mais probablement, saturait ce CPU à 100%), donc si tu as disons 8 CPU tu n'as l'impression d'utiliser "que" 12.5% de CPU :p (ce qui est en réalité énorme !). Et tu comprends bien entendu qu'on peut difficilement se permettre de sacrifier un CPU par application (surtout quand celle-ci ne fait pas de calcul !). Bref, si tu dois retenir une chose, c'est que la programmation événementielle permet précisément d'éviter ça.

Ensuite, un grand merci pour tes compliments qui me vont droit au cœur. C'est le genre de message qui fait bien plaisir et qui donne envie de continuer à être altruiste :-)
  • Je bascule ton sujet en résolu, car le problème posé me paraît résolu. Sache qu'en tant qu'auteur de la discussion, c'est quel que chose que tu faire (voir ce lien).
  • Si tu as besoin de plus de précisions sur cette discussion, n'hésite pas à poser tes questions. Et si tu rencontres de nouveaux problèmes indépendants, crée une nouvelle discussion.


Bonne continuation
1

Vous n’avez pas trouvé la réponse que vous recherchez ?

Posez votre question
pycarpe Messages postés 16 Date d'inscription lundi 24 janvier 2022 Statut Membre Dernière intervention 9 août 2022
25 janv. 2022 à 19:17
Hello, j'espère que tu vas bien?

j'ai pris le temps d'analyser ta méthode pour essayer de bien tout comprendre. Et là je me suis dit "tiens et si je faisais une petite modification pour voir si j'ai bien compris", donc j'ai juste enlevé la fonction find et j'ai juste mis les chemins des fichiers en dur juste pour voir si j'avais le même résultat et j'ai lié l'index à file dans Photoimage au lieu de Photoimage tout court... Je me suis dit que cela allait revenir au même mais à ma grande surprise les images ne se chargent pas et le fait que je ne comprenne pas pourquoi prouve que je n'ai pas tout saisi finalement...
Si tu peux m'éclaircir chef, je pense que cela m'aiderait dans ma recherche de compréhension. Merci d'avance :)


Ci dessous le code modifié :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
import time
import os



class Application(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.pack(expand=True, fill="both")
        self.entries_y = 30
        self.tab = ["C:\\Users\\blabla\\Documents\\python\\images\\image1.gif", "C:\\Users\\blabla\\Documents\\python\\images\\image2.gif"]
        print(self.tab)
        
        self.tab_index = 0
        self.create_widgets()

        # Prepare initial timer
        self.on_update()

    def create_widgets(self):  
        
        if self.tab:
            wi = 90
            he  = 90
            image = tk.PhotoImage(file=self.tab[self.tab_index], master=self)
            self.canvas_logo = tk.Canvas(self, width = wi, height = he)
            self.canvas_logo.place(x = wi / 2, y = 0)
            
            #self.canvas_logo.create_rectangle(0, 0, 200, 50, fill="green")
            self.image_container = self.canvas_logo.create_image(wi / 2, he / 2, image=image)
            self.canvas_logo.pack()
        
        self.bu_ajout = tk.Button(self, text = "Ajout", command = self.add_entries)
        self.bu_ajout.pack(side="top")

        self.quit = tk.Button(self, text="Quitter", fg="red", command=root.destroy)
        self.quit.pack(side="bottom")

    def add_entries(self):
        self.add_entry("red")
        self.add_entry("yellow")
        
    def add_entry(self, color):
        canvas = tk.Canvas(self, width=200, height=50)
        canvas.create_rectangle(0, 0, 200, 50, fill=color)
        canvas.place(x=0, y=self.entries_y)
        myentry = tk.Entry(canvas, bd = 3)
        myentry.place(x = 5, y = 12.5)
        self.entries_y += 50

    def update_image(self):
    
        if self.tab:
            image = tk.PhotoImage(file=self.tab[self.tab_index], master=self)
            self.canvas_logo.itemconfigure(self.image_container, image=image)
            self.canvas_logo.pack()
        
    def on_update(self):
        if self.tab:
            self.tab_index += 1
            self.tab_index %= len(self.tab) # permet de rester entre index 0 et et len self.images non inclus.
            self.update_image()
            print(self.tab_index)# pourtant l'index change bien à chaque appel
            self.after(500, self.on_update)

root = tk.Tk()
root.geometry("1000x720")
#root.config(background='#7720B9')
app = Application(
    master = root
)
root.mainloop()
0
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
Modifié le 26 janv. 2022 à 12:19
Bonjour,

Alors en fait l'erreur n'est pas algorithmique, mais lié à une subtilité de Tk dont j'ai peu parlé brièvement dans mon message précédent :

Ensuite je crée un attribut self.images dans lequel je charge tous les images (formats valides : png, bmp, gif).

Si tu lis ce lien, tu verras qu'il faut garder une référence vers l'image chargée par ta classe
Application
, sans quoi elle ne parviendra pas à afficher les images. C'est pourquoi dans mon cas je les stockais dans
self.images
. Si tu n'avais eu qu'une image tu aurais pu directement avoir un attribut
self.image
.

Même si j'ai bien compris que tu retapais le code pour voir si tu le maîtrisais, garde à l'esprit qu'en terme de design de classe, il est mieux que ta classe
Application
prennent en paramètre la liste des fichiers plutôt que de les coder en dur en interne. Ainsi elle est utilisable si les fichiers sont dans un autre dossier et/ou dispersées dans plusieurs dossiers.

Bonne chance
0
pycarpe Messages postés 16 Date d'inscription lundi 24 janvier 2022 Statut Membre Dernière intervention 9 août 2022
26 janv. 2022 à 12:31
J'ai compris pourquoi ma modif ne fonctionnait pas. Désolé du dérangement. J'ai simplement oublié de déclarer l'attribut représentant les images dans l'instance app = Application() ce qui devient :

tab = ["C:\\Users\\blabla\\Documents\\python\\images\\image1.gif", "C:\\Users\\blabla\\Documents\\python\\images\\image2.gif"] 
root = tk.Tk()
root.geometry("1000x720")
#root.config(background='#7720B9')
app = Application(
    filenames = [
        f
        for f in tab
        
    ],
    master = root
)
root.mainloop()


Et là ca marche. Evidemment c'était juste pour tester en mettant les images en dur juste pour voir si j'avais bien compris. Merci encore a+++
0
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
26 janv. 2022 à 14:16
Pas de soucis, bonne continuation, et si tu as d'autres questions, n'hésite pas.
0