Rendez-vous sur Arrakis, Le site perso d'un hacker libriste curieux crêpophile étourdi
Le 11/09/2019 à 14:53 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)