PROJET AUTOBLOG


Planet-Libre

source: Planet-Libre

⇐ retour index

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

mercredi 25 janvier 2017 à 14:24

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)

Gravatar de Thuban
Original post of Thuban.Votez pour ce billet sur Planet Libre.