Affichage successif d'images sur tkinter dans même cavnas
Résolufrom 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.
7 réponses
-
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 dusleep
).
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 codeCanvas
est une classe,canvas_logo
est une instance de cette classe, etcreate_rectangle
est une méthode de la classeCanvas
.
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éthodecreateWidgets
qui place les composants de la fenêtre. Celle-ci appelle immédiatementonUpdate
(dans ton cas, cela te permettra d'afficher la première image). Note que les noms des méthodes (createWidgets
etonUpdate
) 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 fonctiononUpdate
: l'instructionself.after(1000, self.onUpdate)
permet de rappeler dans 1000ms (donc dans 1s) la méthodeself.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 -
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 classeApplication
. 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 fonctionfind
qui liste les fichiers.gif
du dossierIMAGE_DIR
(et dans ses sous-dossiers).
Ensuite on crée une instance de notre classeApplication
à laquelle je passe le pointeurroot
(que tu appelaiswin
). 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 atteindraroot.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, unsleep
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 viasuper()
), puis première différence avec ton programme on fait un self.pack(expand=True, fill="both"). Celui-ci fait en sorte que notretk.Frame
occupe toute la fenêtre. Ici on devrait selon moi créer untk.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 attributself.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 destk.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 globalevar
, j'utilise un attribut de classeself.entries_y
. C'est l'un des points qu'il faudrait améliorer.
Les dimensions de certains widgets sont calculés à partir dehe
etwi
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 tatk.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 unwhile 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 fonctionon_update
se déclenche au bout d'une seconde et réarme ce timer à chaque fois à l'aide deself.after
. Au passage elle met à jour l'image. Pour cela on charge directement le bon objettk.Image
(toutes nos images sont stockées dansself.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 siself.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'instructionself.image_container = self.canvas_logo.create_image(wi / 2, he / 2, image=image)
, c'est le premier paramètre attendus parself.canvas_logo.itemconfigure(...)
.
Bonne chance -
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. -
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++++-
Re bonjour pycarpe,
Concernant la version avec unwhile 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
-
-
Vous n’avez pas trouvé la réponse que vous recherchez ?
Posez votre question -
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() -
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 classeApplication
, sans quoi elle ne parviendra pas à afficher les images. C'est pourquoi dans mon cas je les stockais dansself.images
. Si tu n'avais eu qu'une image tu aurais pu directement avoir un attributself.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 classeApplication
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 -
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+++