Rendez-vous sur Arrakis, Le site perso d'un hacker libriste curieux crêpophile étourdi
Le 19/06/2019 à 15:43 dans /Journal/2017/

Un GUI en python : TP 3, la guerre des boutons

C'est mercredi, le jour du TP, youpi !

Notre visionneuse grandit doucement mais sûrement. Maintenant qu'elle affiche des dialogues et sait ouvrir les images sur le disque, elle est assez agée pour avoir ses premiers boutons ^^.

On va donc voir comment ajouter des boutons et comment relier une action à ces boutons.

Avec tkinter, un bouton se crée tout simplement ainsi :

monbouton = Button(parent, text="coucou", command=fonction)
 

Si on décompose, on voit un appel à Button. Jusque là, pas trop de surprises. On précise ensuite avec parent dans quel autre widget le bouton sera inséré, par exemple une Frame. Ensuite, on choisit le texte à mettre dans le bouton avec text="coucou". On peut aussi définir une image avec image=variable_image.
Enfin, on relie le bouton à une fonction avec command=fonction. Il faut noter qu'avec cette méthode, la fonction recevra en argument le bouton lui-même. Ça peut être pratique si on veut le modifier dans la fonction, mais la plupart du temps on se contentera d'utiliser lambda, qui permet d'exécuter une fonction plus simplement sans s'occuper des arguments. Par exemple, ça donnera :

monbouton = Button(parent, text="coucou", command=lambda: print("coucou")))
 

Je vous propose d'ajouter à notre visionneuse trois boutons : "Ouvrir une image", "Image précédente" et "Image suivante".
Nous mettrons les boutons en bas de la fenêtre. Afin de les contenir, on va créer une Frame rien que pour eux :

# Frame contenant les boutons en bas de la fenêtre : 
 btnbox = Frame(mainframe)
 btnbox.pack()
 

Commençons par le bouton pour ouvrir une image :

b_open = Button(btnbox, text="Ouvrir", command=lambda: pick_img())
 b_open.pack()
 

Ici, on appelle une fonction pick_img qu'il faut créer. Heureusement, nous avons déjà tout le code qui permet d'afficher un dialogue pour trouver une image.

def pick_img():
     img_path = filedialog.askopenfilename(\
                 initialdir=(os.path.expanduser("~")),\
                 filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
                 title="Image à ouvrir",\
                 parent=w)
     return img_path
 

Si vous lancez ce code, vous devriez voir votre zoli bouton :

En cliquant dessus, on a bien la fenêtre pour choisir un autre fichier qui apparaît.

Mais c'est tout pourri, ça ne change pas l'image quand j'en choisis une autre

En effet. Au lieu de seulement choisir une image, il faut aussi la modifier. On crée alors une fonction "open_img" qui appelera "pick_img".

La fonction open_img ressemble alors à :

def open_img(img_container, img_path):
     # Ouverture de l'image
     image = Image.open(img_path)
     # Dimensions de l'écran : 
     gap = 100 # marge par rapport aux bords de l'écran
     screen_width = w.winfo_screenwidth() - gap
     screen_height = w.winfo_screenheight() - gap
 
     if image.width > screen_width : 
         image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.ANTIALIAS)
     if image.height > screen_height :   
         image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.ANTIALIAS)
 
     # Chargement de l'image en mémoire
     img = ImageTk.PhotoImage(image)
 
     # On met l'image dans le conteneur
     img_container.configure(image = img)
     # On s'assure que l'image sera bien gardée en mémoire
     img_container.image = img
     # Ainsi que son emplacement
     img_container.path = img_path
 

J'en profite pour créer une fonction "chg_img" pour modifier l'image avec le bouton. Ce n'est pas obligatoire mais ça sera plus pratique :

def chg_img(img_container):
     i = pick_img()
     if i: # On a bien choisi une image
         open_img(img_container,i)
 

Actuellement, notre code fait bien ce qu'on attend de lui et ressemble à ça :

#!/usr/bin/env python
 # -*- coding:Utf-8 -*- 
 
 import os
 import sys
 import mimetypes
 from tkinter import *
 from tkinter import filedialog
 from PIL import Image, ImageTk
 
 
 ### Fonctions ###
 def pick_img():
     img_path = filedialog.askopenfilename(\
                 initialdir=(os.path.expanduser("~")),\
                 filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
                 title="Image à ouvrir",\
                 parent=w)
     return img_path
 
 def open_img(img_container, img_path):
     # Ouverture de l'image
     image = Image.open(img_path)
     # Dimensions de l'écran : 
     gap = 100 # marge par rapport aux bords de l'écran
     screen_width = w.winfo_screenwidth() - gap
     screen_height = w.winfo_screenheight() - gap
 
     if image.width > screen_width : 
         image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.ANTIALIAS)
     if image.height > screen_height :   
         image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.ANTIALIAS)
 
     # Chargement de l'image en mémoire
     img = ImageTk.PhotoImage(image)
 
     # On met l'image dans le conteneur
     img_container.configure(image = img)
     # On s'assure que l'image sera bien gardée en mémoire
     img_container.image = img
     # Ainsi que son emplacement
     img_container.path = img_path
 
 def chg_img(img_container):
     i = pick_img()
     if i: # On a bien choisi une image
         open_img(img_container,i)
 
 
 ### tkv ###
 
 # Notre fenêtre principale
 w = Tk()
 w.title("tkv : visionneuse d'images") # Un titre
 w.configure(background='#000000')     # Fond noir
 
 # Un conteneur dans la fenêtre
 mainframe = Frame(w)
 mainframe.pack(fill=BOTH,expand=True, padx=15, pady=15)
 
 # Ouverture de l'image
 img_path=""
 if len(sys.argv) == 2:
     # On a une image en agument
     img_path = sys.argv[1]
 
 if not os.path.isfile(img_path):
     # On va chercher une image sur le disque
     img_path = pick_img()
     if not img_path: # L'utilisateur n'a choisi aucune image, on quitte
         sys.exit(0)
 
     # Est-ce un fichier valide ?
 mimtyp = mimetypes.guess_type(img_path)[0] # i.e 'image/jpeg'
 if not mimtyp or "image" not in mimtyp :
     # Il n'y a pas le mot "image" dans le mimetype
     from tkinter import messagebox
     messagebox.showerror("Fichier invalide", "Le fichier demandé n'est pas une image.")
     sys.exit(1)
 
 # Conteneur de l'image
 img_widget = Label(mainframe)
 img_widget.pack()
 
 # Insertion de l'image dans le conteneur.
 open_img(img_widget, img_path)
 
 # Frame contenant les boutons en bas de la fenêtre : 
 btnbox = Frame(mainframe)
 btnbox.pack()
 
 # Bouton ouvrir
 b_open = Button(btnbox, text="Ouvrir", command=lambda: chg_img(img_widget))
 b_open.pack()
 
 # Démarrage du programme
 w.mainloop()
 
 sys.exit(0)
 

Entendez-vous cette petite voix du bon développeur ?

c'est pas malin, tu aurais pu utiliser une classe !

En effet, mais ça fera l'objet d'un autre TP. Na ! :p

On peut maintenant passer à nos boutons "précédent" et "suivant". Afin de trouver automatiquement les images suivantes ou précédentes, je vais suivre la méthode suivante :

Ça nous donne la fonction defile_img :

def defile_img(img_container, sens):
     """
     On fait défiler les images dans un sens ou dans l'autre
     sens == "prev" : précédent,
     sens == "next" : suivant,
 
     On a besoin de passer le conteneur de l'image
     en argument pour retrouver l'emplacement de l'image courante.
     """
     # Emplacement de l'image actuelle : 
     cur_path = img_container.path
     # Dossier de l'image actuelle : 
     d = os.path.dirname(cur_path)
     # Liste des images
     l = os.listdir(d)
 
     # on ne garde que les images
     # "{}/{}".format(d,i) : on met le chemin complet vers l'image 
     # for i in l :pour toutes les images dans la liste l
     # if os.path.splitext(i)[1] in img_extensions : 
     #     si l'extension de l'image est dans la liste des extensions
     img_list = [ "{}/{}".format(d,i) for i in l if os.path.splitext(i)[1] in img_extensions ]
 
     # On met dans l'ordre
     img_list = sorted(img_list)
 
     # On ne fait tourner que si il y a plusieurs images
     if len(img_list) > 1:
         # on retrouve la position de l'image actuelle
         pos = img_list.index(cur_path)
 
         if sens == "next":
             open_img(img_container, img_list[pos + 1])
         elif sens == "prev":
             open_img(img_container, img_list[pos - 1])
 

Et voilà !

On peut maintenant ajouter nos boutons. J'en profite pour leur définir une largeur afin de rendre l'interface plus cohérente :

# Bouton ouvrir
 b_open = Button(btnbox, text="Ouvrir", width=12, command=lambda: chg_img(img_widget))
 
 # Boutons suivant et précédent
 b_next = Button(btnbox, text="Suivant →", width=12, command=lambda: defile_img(img_widget, "next"))
 b_prev = Button(btnbox, text="← Précédent", width=12, command=lambda: defile_img(img_widget, "prev"))
 
 # On affiche les boutons dans la boîte les uns à côté des autres
 b_open.pack(side=LEFT)
 b_prev.pack(side=LEFT)
 b_next.pack(side=LEFT)
 

Nous avons finalement nos boutons qui permettent de visualiser les images plus simplement.

Ceux qui auraient voulu une classe s'apercevront qu'avec tkinter, il est très facile de s'en passer.

La semaine prochaine, nous verrons comment améliorer l'apparence de nos widgets tkinter.

Comme d'habitude, voici le code final :

#!/usr/bin/env python
 # -*- coding:Utf-8 -*- 
 
 import os
 import sys
 import mimetypes
 from tkinter import *
 from tkinter import filedialog
 from PIL import Image, ImageTk
 
 img_extensions = ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')
 
 ### Fonctions ###
 def pick_img():
     img_path = filedialog.askopenfilename(\
                 initialdir=(os.path.expanduser("~")),\
                 filetypes=[('Images', img_extensions), ('Tout', '.*')],\
                 title="Image à ouvrir",\
                 parent=w)
     return img_path
 
 def open_img(img_container, img_path):
     # Ouverture de l'image
     image = Image.open(img_path)
     # Dimensions de l'écran : 
     gap = 100 # marge par rapport aux bords de l'écran
     screen_width = w.winfo_screenwidth() - gap
     screen_height = w.winfo_screenheight() - gap
 
     if image.width > screen_width : 
         image = image.resize((screen_width, int(image.height * screen_width / image.width)), Image.NEAREST)
     if image.height > screen_height :   
         image = image.resize((int(image.width * screen_height / image.height), screen_height), Image.NEAREST)
 
     # Chargement de l'image en mémoire
     img = ImageTk.PhotoImage(image)
 
     # On met l'image dans le conteneur
     img_container.configure(image = img)
     # On s'assure que l'image sera bien gardée en mémoire
     img_container.image = img
     # Ainsi que son emplacement
     img_container.path = img_path
 
 def chg_img(img_container):
     # change l'image affichée
     i = pick_img()
     if i: # On a bien choisi une image
         open_img(img_container,i)
 
 def defile_img(img_container, sens):
     """
     On fait défiler les images dans un sens ou dans l'autre
     sens == "prev" : précédent,
     sens == "next" : suivant,
 
     On a besoin de passer le conteneur de l'image
     en argument pour retrouver l'emplacement de l'image courante.
     """
     # Emplacement de l'image actuelle : 
     cur_path = img_container.path
     # Dossier de l'image actuelle : 
     d = os.path.dirname(cur_path)
     # Liste des images
     l = os.listdir(d)
 
     # on ne garde que les images
     # "{}/{}".format(d,i) : on met le chemin complet vers l'image 
     # for i in l :pour toutes les images dans la liste l
     # if os.path.splitext(i)[1] in img_extensions : 
     #     si l'extension de l'image est dans la liste des extensions
     img_list = [ "{}/{}".format(d,i) for i in l if os.path.splitext(i)[1] in img_extensions ]
 
     # On met dans l'ordre
     img_list = sorted(img_list)
 
     # On ne fait tourner que si il y a plusieurs images
     if len(img_list) > 1:
         # on retrouve la position de l'image actuelle
         pos = img_list.index(cur_path)
 
         if sens == "next":
             open_img(img_container, img_list[pos + 1])
         elif sens == "prev":
             open_img(img_container, img_list[pos - 1])
 
 
 
 ### tkv ###
 
 # Notre fenêtre principale
 w = Tk()
 w.title("tkv : visionneuse d'images") # Un titre
 w.configure(background='#000000')     # Fond noir
 
 # Un conteneur dans la fenêtre
 mainframe = Frame(w)
 mainframe.pack(fill=BOTH,expand=True, padx=15, pady=15)
 
 # Ouverture de l'image
 img_path=""
 if len(sys.argv) == 2:
     # On a une image en agument
     img_path = sys.argv[1]
 
 if not os.path.isfile(img_path):
     # On va chercher une image sur le disque
     img_path = pick_img()
     if not img_path: # L'utilisateur n'a choisi aucune image, on quitte
         sys.exit(0)
 
     # Est-ce un fichier valide ?
 mimtyp = mimetypes.guess_type(img_path)[0] # i.e 'image/jpeg'
 if not mimtyp or "image" not in mimtyp :
     # Il n'y a pas le mot "image" dans le mimetype
     from tkinter import messagebox
     messagebox.showerror("Fichier invalide", "Le fichier demandé n'est pas une image.")
     sys.exit(1)
 
 # Conteneur de l'image
 img_widget = Label(mainframe)
 img_widget.pack()
 
 # Insertion de l'image dans le conteneur.
 open_img(img_widget, img_path)
 
 # Frame contenant les boutons en bas de la fenêtre : 
 btnbox = Frame(mainframe)
 btnbox.pack()
 
 # Bouton ouvrir
 b_open = Button(btnbox, text="Ouvrir", width=12, command=lambda: chg_img(img_widget))
 
 # Boutons suivant et précédent
 b_next = Button(btnbox, text="Suivant →", width=12, command=lambda: defile_img(img_widget, "next"))
 b_prev = Button(btnbox, text="← Précédent", width=12, command=lambda: defile_img(img_widget, "prev"))
 
 # On affiche les boutons dans la boîte les uns à côté des autres
 b_open.pack(side=LEFT)
 b_prev.pack(side=LEFT)
 b_next.pack(side=LEFT)
 
 # Démarrage du programme
 w.mainloop()
 
 sys.exit(0)