Du libre, du code, des idées, du mélange d’ArraKISS…

De l’épice pour la pensée

Alors que je bricole 2-3 trucs sur mes sessions, je râle à chaque fois qu'un script doit utiliser zenity. C'est censé être de petits outils rapides, mais la moindre petite fenêtre met 3 plombes à s'ouvrir le temps que toutes les libs GTK soient chargées.

J'ai donc eu envie de refaire un zenity à ma façon à partir de la librairie tkinter, et ça donne ZeniTK.

L'outil ne propose pas encore autant d'options que zenity, et je ne suis pas certain d'en rajouter sauf si j'en ai besoin ou si des contributions sont proposées.

On peut déjà profiter des éléments suivants :

  • Alertes habituelles :

  • Entrer et récupérer ce que l'utilisateur a écrit :
  • Sélection de fichier ou répertoire avec éventuellement filtre d'extensions :
  • Barre de progression :
  • Afficher et éditer du texte :

Ce dernier permet de faire un éditeur de texte tout bête pour prendre des notes par exemple :

#!/bin/sh

DIR=~/Documents/notes
mkdir -p $DIR

N=$(zeniTK --title="Nouvelle note" --text="Nom de la nouvelle note : " --entry)

if [ -n "$N" ]; then
        RES=$(cat "$DIR/$N" | zeniTK --text-info)
        if [ -n "$RES" ]; then
                echo $RES > "$DIR/$N"
        fi
fi
exit
  • Sélecteur de couleur

Ne vous fiez pas aux couleurs dans les captures d'écran, j'ai modifié des fichiers système par erreur mais ne parviens plus à les retrouver pour remettre à la normale. Oui, je suis un boulet :)

Contrôler son GUI avec le clavier, c'est un jeu d'enfant avec tkinter.

Vous vous souvenez lorsqu'on a associé des fonctions aux boutons ? Eh bien la démarche va être quasiment identique pour relier un raccourci clavier à une fonction.
De façon générale, cela se fait ainsi :

w.bind("<Key>", fonction)

On a ici "w" qui est le widget, "" qui est le code de la touche appuyée et enfin "fonction" qui est la fonction appelée.

Comment connaître le code ?

Ça peut être pratique c'est sûr. Pour ça, le plus simple reste à les essayer au lieu de lire toute la documentation. Essayez donc de lancer le code ci-dessous puis d'appuyer sur des touches :

#!/usr/bin/env python3.4
# -*- coding:Utf-8 -*- 
# print pressed key
from tkinter import *

def key(event):
    print(event.keysym)

w = Tk()
w.title("get key") 
w.bind("<Key>", key)
w.mainloop()

Ce petit bout de code va afficher la touche appuyée, dont la valeur sera à mettre à la place de pour utiliser cette touche.

Notez que la fonction appelée par ".bind" reçoit en argument l'évènement. Il faudra y penser lorsqu'on utilisera lambda.
Dans notre visionneuse, nous allons donc associer les flèches pour faire défiler les images, et la combinaison "ctrl-O" pour ouvrir une nouvelle image :

# Raccourcis clavier
w.bind("<Left>", lambda e: defile_img(img_widget, "prev"))
w.bind("<Right>", lambda e: defile_img(img_widget, "next"))
w.bind("<Control - o>", lambda e: chg_img(img_widget))

Et voilà !

Avec tous ces TP, vous avez dévantage de billes en poche pour vous amuser avec tkinter.
Cette fois, je dépose le code à cette adresse, avec quelques modifications supplémentaires si vous souhaitez améliorer cette visionneuse : http://git.yeuxdelibad.net/tkv

Bon code ;)

Jusqu'à présent, nous ne nous sommes pas occupés de l'apparence de notre application. Et vous l'avez sans doute remarqué, ce n'est pas très joli.
On peut heureusement améliorer légèrement les choses avec la bibliothèque ttk.

On ajoute alors ceci au début de notre code :

from tkinter import ttk

Nous allons ainsi pouvoir utiliser des widgets améliorés. Pour cela, rien de compliqué, on a juste à ajouter ttk devant. Ainsi, Button devient ttk.Button.

Pour activer un autre thème, il faut insérer ce bout de code pour choisir entre "clam", "alt", "default" et "classic" :

#('clam', 'alt', 'default', 'classic')
style = ttk.Style()
style.theme_use("clam")

Vous pouvez voir ci-dessous ce que ça change avec les interfaces avant/après côte à côte.

Notez que l'on peut aller encore plus loin en modifiant les couleurs, polices et bordures. Cela devient vite compliqué, mais c'est faisable. Par exemple :

theme = {
        'disabledfg':"#eeeeee",
        'dark': "#777777",
        'darker': "#333333",
        'darkest': "#777777",
        'lighter': "#777777",
        'lightest': "#ffffff",
        'selectbg': "#41B1FF",
        'selectfg': "#ffffff",
        'foreground': "#111111",
        'background': "#dddddd",
        'borderwidth': 1,
        'font': ("Droid Sans", 10)
        }

    style.configure(".", padding=5, relief="flat", 
            background=theme['background'],
            foreground=theme['foreground'],
            bordercolor=theme['darker'],
            indicatorcolor=theme['selectbg'],
            focuscolor=theme['selectbg'],
            darkcolor=theme['dark'],
            lightcolor=theme['lighter'],
            troughcolor=theme['darker'],
            selectbackground=theme['selectbg'],
            selectforeground=theme['selectfg'],
            selectborderwidth=theme['borderwidth'],
            font=theme['font']
            )

    style.map(".",
        foreground=[('pressed', theme['darkest']), ('active', theme['selectfg'])],
        background=[('pressed', '!disabled', 'black'), ('active', theme['lighter'])]
        )

    style.configure("TButton", relief="flat")
    style.map("TButton", 
        background=[('disabled', theme['disabledfg']), ('pressed', theme['selectbg']), ('active', theme['selectbg'])],
        foreground=[('disabled', theme['disabledfg']), ('pressed', theme['selectfg']), ('active', theme['selectfg'])],
        bordercolor=[('alternate', theme['selectbg'])],
        )

Ça ressemble maintenant à ça :

J'ai tenté de rassembler les couleurs ensemble au cas où vous voudriez bricoler. Afin d'aller plus loin, une bonne idée est d'aller fouiller dans le fichier de thème par défaut. Pour moi, il s'agit de /usr/local/lib/tcl/tk8.5/ttk/clamTheme.tcl.

Même si ce n'est pas notre priorité, on voit qu'il est possible de faire un peu tout ce que l'on veut au niveau de l'apparence.

Ça sera tout pour aujourd'hui. La prochaine fois, on parlera des évènements afin de contrôler la visionneuse au clavier.

Et voici le code final (avec un petit bug pour le défilement d'images corrigé).

#!/usr/bin/env python3.4
# -*- coding:Utf-8 -*- 

import os
import sys
import mimetypes
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
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.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):
    # 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":
            newpos = pos + 1
            if newpos > len(img_list) -1: # fin de liste
                newpos = 0
        elif sens == "prev":
            newpos = pos - 1 # début de liste
            if newpos < 0:
                newpos = len(img_list) - 1
        open_img(img_container, img_list[newpos])

def change_colors(style):
    theme = {
        'disabledfg':"#eeeeee",
        'dark': "#777777",
        'darker': "#333333",
        'darkest': "#777777",
        'lighter': "#777777",
        'lightest': "#ffffff",
        'selectbg': "#41B1FF",
        'selectfg': "#ffffff",
        'foreground': "#111111",
        'background': "#dddddd",
        'borderwidth': 1,
        'font': ("Droid Sans", 10)
        }

    style.configure(".", padding=5, relief="flat", 
            background=theme['background'],
            foreground=theme['foreground'],
            bordercolor=theme['darker'],
            indicatorcolor=theme['selectbg'],
            focuscolor=theme['selectbg'],
            darkcolor=theme['dark'],
            lightcolor=theme['lighter'],
            troughcolor=theme['darker'],
            selectbackground=theme['selectbg'],
            selectforeground=theme['selectfg'],
            selectborderwidth=theme['borderwidth'],
            font=theme['font']
            )

    style.map(".",
        foreground=[('pressed', theme['darkest']), ('active', theme['selectfg'])],
        background=[('pressed', '!disabled', 'black'), ('active', theme['lighter'])]
        )

    style.configure("TButton", relief="flat")
    style.map("TButton", 
        background=[('disabled', theme['disabledfg']), ('pressed', theme['selectbg']), ('active', theme['selectbg'])],
        foreground=[('disabled', theme['disabledfg']), ('pressed', theme['selectfg']), ('active', theme['selectfg'])],
        bordercolor=[('alternate', theme['selectbg'])],
        )


### tkv ###

# Notre fenêtre principale
w = Tk()
w.title("tkv : visionneuse d'images") # Un titre
w.configure(background='#000000')     # Fond noir

#('clam', 'alt', 'default', 'classic')
style = ttk.Style()
style.theme_use("clam")
change_colors(style)

# Un conteneur dans la fenêtre
mainframe = ttk.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 = ttk.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 = ttk.Frame(mainframe)
btnbox.pack()

# Bouton ouvrir
b_open = ttk.Button(btnbox, text="Ouvrir", width=12, command=lambda: chg_img(img_widget))

# Boutons suivant et précédent
b_next = ttk.Button(btnbox, text="Suivant →", width=12, command=lambda: defile_img(img_widget, "next"))
b_prev = ttk.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)

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 :

  • Lister les fichiers dans le répertoire de l'image actuelle,
  • Ne garder que les images,
  • Les mettre en ordre alphabétique,
  • Trouver l'emplacement de l'image actuelle dans la liste,
  • Prendre l'image précédente ou suivante selon le cas.

Ç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)

Nous sommes aujourd'hui réunis suite à un terrible constat : notre visionneuse tkv craint un max.
C'est pourquoi nous allons ajouter une toute petite option nous permettant de choisir l'image que l'on souhaite visualiser sur notre ordinateur. Cela nous permettra par la même occasion de nous débarasser de certains morceaux qui ne sont plus utiles.

Profitons des dialogues déjà prêts dans la bibliothèque de tkinter en important celle qui nous intéresse :

from tkinter import filedialog

Nous pouvons à la place du téléchargement de l'image appeler une fenêtre de dialogue pour demander l'image à ouvrir :

# Ouverture de l'image
img_path = filedialog.askopenfilename(\
        initialdir=(os.path.expanduser("~")),\
        title="Image à ouvrir",\
        parent=w)

Notez ici plusieurs options :

  • initialdir : Le répertoire de départ où on propose des fichiers à ouvrir. Là, je me sert de os pour afficher le répartoire personnel de l'utilisateur repéré par ~ sous UNIX.
  • title : Le titre de notre fenêtre.
  • parent : La fenêtre dont dépend le dialogue.

Cela ne sufit pas, il faudrait proposer seulement les fichiers qui sont des images. On va donc indiquer les motifs qui nous intéressent (pattern). En gros, ce sont juste les extensions :

# Ouverture de l'image
img_path = filedialog.askopenfilename(\
        initialdir=(os.path.expanduser("~")),\
        filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
        title="Image à ouvrir",\
        parent=w)

On ajoute quand même un petit test pour vérifier que on a bien sélectionné un fichier valide. On utilise la bibliothque "mimetypes" qui est livrée par défaut avec python, et on regarde si le mot "image" apparait dans le type du fichier.
Pour afficher l'erreur, on utilise une fenêtre de dialogue livré avec tkinter après avoir importé la partie qui nous intéresse :

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.")

Et voilà !

Remarquez qu'il existe un tas de fenêtres de dialogue déjà toutes prêtes pour afficher des avertissements (showwarning) ou des informations (showinfo) ou de poser une simple question (askquestion). Regardez par ici pour plus de détails : http://effbot.org/tkinterbook/tkinter-standard-dialogs.htm .

Je vais rajouter quelques lignes pour afficher une image si elle est passée en argument (par exemple en faisant tkv /home/bibi/image.jpg) :


# Ouverture de l'image
if len(sys.argv) == 2:
    # On a une image en agument
    img_path = sys.argv[1]
else:
    # On va chercher une image sur le disque
    img_path = filedialog.askopenfilename(\
    # ...
    # ...

Il reste encore quelques points à améliorer, mais ça reste déjà plus pratique. ^^
Ça sera tout pour aujourd'hui.
La prochaine fois, nous ajouterons quelques boutons à notre visionneuse d'images :)

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


# 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 = filedialog.askopenfilename(\
            initialdir=(os.path.expanduser("~")),\
            filetypes=[('Images', ('.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF')), ('Tout', '.*')],\
            title="Image à ouvrir",\
            parent=w)

# 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)

# Ouverture de l'image
image = Image.open(img_path)
# Dimensions de l'écran : 
gap = 100 # marge par rapport aux bordes 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)

# Insertion de l'image dans le conteneur.
img_widget = Label(mainframe, image=img)
img_widget.pack()

# Démarrage du programme
w.mainloop()

sys.exit(0)