Rendez-vous sur Arrakis

C'est lorsque nous croyons savoir quelque chose qu'il faut justement réfléchir un peu plus profondément. F. Herbert

Un GUI en python : TP 4, l'apparence du GUI

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)
 

le 01/02/2017 à 10:56:21, trefix a dit :

Coucou (express).
Elle va être sympa, cette visionneuse kiss ;)
Merci pour les TP.

le 01/02/2017 à 12:45:59, thuban a dit :

@trefix : Coucou :)
Il reste du boulot. Et puis, sxiv, qiv ou gpicview le font déjà très bien ^^

le 01/02/2017 à 13:18:11, trefix a dit :

Sans doute, mais j'ai encore trop besoin de sommeil pour lire tout ce qui m'intéresse :/
Et du retard en matière de g33queries, tu as du le voir…

le 02/02/2017 à 16:05:15, Starsheep a dit :

Tu arrives même à rendre Tk sexy… Je note l'exploit. :D

le 02/02/2017 à 20:03:54, thuban a dit :

@Starsheep : Coucou :)
Pas à ce point quand même :)
Mais y a moyen de faire un truc sympa en y prenant l'arrêt :D

le 02/02/2017 à 20:54:58, trefix a dit :

[…] truc sympa en y prenant la raie :D

Mmmh, c'est louche, votre truc. Du langage codé, je parie…

/me => []

le 02/02/2017 à 20:59:24, thuban a dit :

@trefix : Zut, nous sommes des couverts !

le 17/01/2018 à 20:36:59, bendia a dit :

Salut, c'est l'archéologue de service :-)

Comme il se trouve que je m'amuse avec Tkinter en ce moment, et que j'en suis globalement au point de rendre le truc plus joli à regarder. Donc j'ai ressorti ce TP.

Juste une petite précision, l'instanciation de ttk, c'est à faire après celle de Tk, sinon, deux fenêtres sont créées :-/ C'est juste pour préciser ce que signifie au début de notre code :p

le 17/01/2018 à 21:34:47, thuban a dit :

@bendia : À noter qu'on peut effectivement bien personnaliser avec ttk, mais parfois, c'est tout aussi joli avec l'environnement par défaut ^^

Bon courage, la doc sur ce point est un peu obscure, et il faut pas mal tester (cf le code du tkmenu)