Infoforall

Python 30 : Créer des menus déroulants dans vos applications Tkinter

Cette activité propose de finaliser un peu vos applications Tkinter : nous allons voir comment créer un menu. Cela fera tout de suite plus sérieux !

En réalité, cela n'est pas bien difficile et ne changera pas intégralement la structure de vos programmes Tkinter déjà réalisés. Il faudra simplement intégrer le code des menus.

Nous allons partir sur l'une des applications réalisées lors du chapitre sur les images et Tkinter : l'application Pixellisation.

Programme de pixellisation

PS : Merci à Bryan M. qui a réalisé l'application de base, que nous allons modifier ici.

1 - Création d'un premier menu

Commençons par créer une fenêtre pour y placer un menu comportant plusieurs zones de sélection possibles.

01° Utiliser le code suivant pour créer votre interface Tkinter basique :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# Création de la fenêtre

fen_princ = Tk()

fen_princ.title("Mon application à moi que j'ai")

fen_princ.geometry("900x600")


# Création du cadre-conteneur pour les menus

zoneMenu = Frame(fen_princ, borderwidth=3, bg='#557788')

zoneMenu.grid(row=0,column=0)


# Création de l'onglet Fichier

menuFichier = Menubutton(zoneMenu, text='Fichier', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuFichier.grid(row=0,column=0)


# Création de l'onglet Edition

menuEdit = Menubutton(zoneMenu, text='Editer', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuEdit.grid(row=0,column=1)


# Création de l'onglet Format

menuFormat = Menubutton(zoneMenu, text='Format', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuFormat.grid(row=0,column=2)


# Lancement de la surveillance sur la fenêtre

fen_princ.mainloop()

Vous devriez obtenir ceci :

Premier menu

Il est temps de voir comment cela fonctionne :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# Création de la fenêtre

fen_princ = Tk()

fen_princ.title("Mon application à moi que j'ai")

fen_princ.geometry("900x600")

On crée la fenêtre de l'application en utilisant le constructeur de la classe Tk. Puis :

Passons à la création du cadre (frame en anglais) qui va contenir notre menu.

# Création du cadre-conteneur pour les menus

zoneMenu = Frame(fen_princ, borderwidth=3, bg='#557788')

zoneMenu.grid(row=0,column=0)

On utilise la méthode-constructeur de la classe Frame en l'attachant à fen_princ.

On l'affiche ensuite à l'aide de la méthode grid en utilisant la case (0,0).

Du coup, ce n'est pas très beau. Nous allons remplir la ligne en entier en obligeant le widget à prendre toute la place sur la ligne.

02° Remplacer la méthode d'affichage par méthode pack(fill=X) pour étendre la ligne du menu.

Menu étendu

Il est temps de voir comment on crée et affiche les différents onglets du cadre contenant le menu :

# Création de l'onglet Fichier

menuFichier = Menubutton(zoneMenu, text='Fichier', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuFichier.grid(row=0,column=0)


# Création de l'onglet Edition

menuEdit = Menubutton(zoneMenu, text='Editer', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuEdit.grid(row=0,column=1)


# Création de l'onglet Format

menuFormat = Menubutton(zoneMenu, text='Format', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

menuFormat.grid(row=0,column=2)

On crée donc les widgets de classe Menubutton (avec b, pas B attention). Ces widgets ne sont pas que de simples labels ou boutons. En appuyant dessous, nous pourrons afficher un sous-menu que nous aurons configuré.

03° Comment se nomme le widget-conteneur ? Avec quelle méthode affiche-t-on les objets Menubutton dans ce conteneur ? Y-a-t'il un conflit sachant qu'on avait utilisé pack précédement ?

...CORRECTION...

Le widget conteneur est zoneMenu, le cadre créé pour cela juste avant.

On y fixe les Menubuttons à l'aide de la méthode grid pour placer les menus dans une grille.

On avait placé zoneMenu dans fen_princ avec pack.

Mais rien n'empêche de travailler avec une autre méthode pour placer les éléments dans zoneMenu.

04° Trouver les noms des attributs des widgets de classe Menubutton permettant de définir le texte affiché sur le sous-menu, la couleur du background et la couleur du background si la souris passe sur le widget.

05° Rajouter un onglet nommé Affichage par exemple.

4 onglets

Maintenant que nous savons comment créer les onglets, il nous reste à voir comment on crée les sous-menus.

2 - Création des sous-menus

Il va falloir créer les widgets correspondants au choix qu'on veut voir s'afficher lorsqu'on clique sur un onglet (qui est un widget de classe Menubutton.

Les différents widgets qu'on va attacher à notre Menubutton sont ici des widgets de classe Menu.

06° Rajouter le code suivant juste avant les deux dernières lignes, celles où lance la surveillance de la fenêtre.

# Création d'un menu défilant

menuDeroulant1 = Menu(menuAffichage)

menuDeroulant1.add_command(label='Petit format', command = petitFormat)

menuDeroulant1.add_command(label="Normal", command = formatNormal)

menuDeroulant1.add_command(label="Grand format", command = grandFormat)

menuDeroulant1.add_command(label="Fond clair", command = fondClair)

menuDeroulant1.add_command(label="Fond sombre", command = fondSombre)

# Attribution du menu déroulant au menu Affichage

menuAffichage.configure(menu=menuDeroulant1)

Vous devriez obtenir ceci (mais en fait vous allez avoir une belle erreur ) :

Effet voulu

Pourquoi ? Simplement parce que les attributs command désignent des fonctions qui n'existent pas. Forcément, ça coince.

Il va donc falloir les créer.

07° Rajouter le code suivant juste APRES l'importation de tkinter. Tester à nouveau le programme. Vous devriez maintenant déclencher des messages dans la console.

# Définitions des fonctions

def petitFormat():

    print("Petit format")


def formatNormal():

    print("Format Normal")


def grandFormat():

    print("Grand format")


def fondClair():

    print("Fond Clair")


def fondSombre():

    print("Fond Sombre")

Vous allez maintenant pouvoir créer les fonctions qui vont agir sur votre application.

08° Modifier les codes des fonctions qu'elles fassent ce qu'on attend d'elles :

Vous devez obtenir résultat visuel de ce type (correction en cliquant sur l'image) :

correction

Si vous cliquez sur l'onglet, vous devriez constater qu'il y a une sorte de pointillés entre l'onglet et les choix. En cliquant dessus, vous pouvez extraire le menu de la fenêtre, et rendre ainsi le menu 'indépendant' de la fenêtre. Testez pour voir ceci :

Menu indépendant

09° Remplacer la méthode constructeur des menus par ceci : Menu(menuAffichage, tearoff = 0).

Cela va vous permettre de mettre de côté cette fonctionnalité et d'avoir une interface qui ressemble aux interfaces habituelles.

Menu sans barre

Il existe de nombreuses options sur ces menus. Nous allons encore en voir une ici, mais si le sujet vous intéresse, pensez à la documentation officielle de Tkinter ou aux nombreux sites qui en parlent.

3 - Création d'options à sélectionner

La fameuse option que nous allons voir est celle des checkbuttons : un menu dans lequel on peut sélectionner une option.

Menu checkbutton

10° Rajouter un nouvel élément à menuDeroulant1 :

bTest = IntVar()

menuDeroulant1.add_checkbutton(label="Controles actifs", variable=bTest, onvalue=1, offvalue=0)

Nous avons déjà rencontré ce type de code dès la première activité Tkinter.

Nous créons bTest, un objet de la classe IntVar(). Il s'agit d'une classe propre à Tkinter qui a comme propriétés :

On rajoute ensuite un checkbutton à menuDeroulant1. On lui donne les attributs suivants :

Vous devriez avoir le visuel donné juste au dessus. Mais pour l'instant, nous n'utilisons pas du tout l'option.

Pour savoir si l'option est sélectionnée, il suffit de faire bTest.get(). Vous obtiendrez 0 si l'option n'est pas sélectionnée et 1 si l'option est sélectionnée.

11° Modifier le code des fonctions de façon à ce que les modifications de fond ou de format ne puissent se faire que si l'option est sélectionnée.

Correction en cliquant sur l'image :

correction

4 - Exemple d'interface réelle

Nous allons maintenant tenter d'utiliser notre menu sur un programme réel : le programme de pixellisation d'images que vous pouvez trouver ici :

correction

Le code est le suivant mais vous n'avez pas réellement besoin de le comprendre : il vous suffit de connaitre le nom des fonctions à appeller.

Attention : si vous êtes un étudiant à la recherche d'un code fonctionnel et correctement documenté, je vous conseille d'être très prudent si vous décidez de copier ce code : il a été réalisé par un élève n'ayant pas encore assez de recul ou de connaissances sur certains points. Cela se voit. Un simple copier/coller dans vos projets risquent de vous poser quelques soucis lors de votre oral !

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

from tkinter.filedialog import *

from PIL import Image as Img

from PIL import ImageTk


def ouverture_de_l_image():

    """

    Permet de modifier l'image à traiter en modifiant la variable presentation.

    Aucun argument à transmettre.

    Presentation est déclarée global car on veut lui affecter une nouvelle valeur.

    *

    La variable presentation contient la référence de l'objet Img qui contient l'image à traiter.

    La variable filepath est locale à la fonction.

    La variable nomImage fait référence à un objet de classe StringVar du programme principal

    La variable labelphoto fait référence à un objet de classe Label du programme principal.

    """

    global presentation

    #permet d'ouvrir l'image

    filepath = askopenfilename(title="Ouvrir une image",filetypes=[('jpg files','.jpg'),('png files','.png'),('all files','.*')])

    nomImage.set(filepath)

    #change l'image de base de presentation avec l'image choisie

    presentation = Img.open(filepath)

    presentationTk = ImageTk.PhotoImage(presentation)

    # la ligne ci-dessous permet de placer presentationTk (créée dans la fonction) comme image de labelphoto

    labelphoto.configure (image = presentationTk)

    # la ligne ci-dessous permet d'éviter le ramasse-miettes : la réf est dans un objet externe à la fonction

    labelphoto.image = presentationTk


def enregistrement_de_la_valeur_de_montexte():

    """

    Permet de modifier la valeur affichée dans le label sous le curseur.

    Elle correspond alors à la valeur de pixellisation du curseur.

    On modifie variable valeurCurseur qui correspond à un StringVar.

    Aucun argument à transmettre.

    *

    La variable valeurCurseur contient la référence de l'objet StringVar du programme principal.

    La variable valeur2 contient la référence de l'objet IntVar qui stocke la valeur du Scale (curseur) nommé curseur2.

    Cela permettra de l'enregistrer à l'aide d'une autre fonction.

    """

    valeurCurseur.set(valeur2.get())


def pixellisation():

    """

    Permet de pixelliser l'image stockée en réduisant sa taille puis en l'agrandissant.

    Aucun argument à transmettre.

    *

    On utilise valeur2, un IntVar du programme principal qui fait stocke la valeur de pixellisation voulue.

    La variable presentation est un objet Img qui contient l'image à traiter.

    La variable valeurCurseur contient la référence de l'objet StringVar du programme principal.

    La variable valeur2 contient la référence de l'objet IntVar qui stocke la valeur du Scale (curseur) nommé curseur2.

    La variable imageStockage permet de garder en mémoire la nouvelle image Img pixellisée une fois qu'on sort de la fonction.

    La variable labelphoto fait référence à un objet de classe Label du programme principal.

    """

    # Permet de modifier la variable imageStockage depuis la fonction

    global imageStockage

    #enregistrement de valeur2 dans la variable x

    x = valeur2.get()

    #enregistrement de la largeur et de la hauteur de l'image dans des variables

    # http://pillow.readthedocs.io/en/4.2.x/reference/ImageTk.html

    larg = presentation.width

    haut = presentation.height

    #taille de la redimension dans une variables

    larg2 = int(larg/x)

    haut2 = int(haut/x)

    #redimension de l'image

    z = presentation.resize((larg2,haut2))

    z2 = z.resize((larg,haut))

    #affichage de l'image pixeliser

    presentationTk = ImageTk.PhotoImage(z2)

    labelphoto.configure (image = presentationTk)

    labelphoto.image = presentationTk

    imageStockage = z2


def sauvegarder() :

    """

    Permet de sauvarder l'image Img stockée dans imageStockage.

    Aucun argument à transmettre.

    *

    On stocke sous le nom 'image-pixellisee.jpg'

    """

    imageStockage.save("image-pixellisee.jpg")


# - - - - - - - - - - -

# PROGRAMME PRINCIPAL

# - - - - - - - - - - -


#creation d'une fenetre

fen_princ = Tk()


#code pour faire la variable de l'image et afficher le nom de l'image choisi

nomImage = StringVar()

monAffichage = Label(fen_princ, textvariable = nomImage)

monAffichage.pack()

#creation des boutons

Button(fen_princ, text="ouvrir image", command=ouverture_de_l_image).pack()

Button(fen_princ, text="Afficher variable", command=enregistrement_de_la_valeur_de_montexte).pack()

Button(fen_princ, text="pixelliser", command=pixellisation).pack()

Button(fen_princ, text="sauvegarder", command=sauvegarder).pack()

#creation d'une image objet

presentation=Img.new("RGB", (50,50) , (0,0,0) )

presentationTk= ImageTk.PhotoImage(presentation)

labelphoto = Label(fen_princ, image=presentationTk)

labelphoto.pack()

#creation d'un curseur

valeur2 = IntVar()

curseur2 = Scale(fen_princ, from_=0, to=50, variable=valeur2)

curseur2.pack()

# création d'un label montrant la valeur du curseur

valeurCurseur=IntVar()

valeurCurseur.set(0)

monaffichage = Label(fen_princ, textvariable=valeurCurseur, width=35)

monaffichage.pack()

#creation d'une variable de stockage

imageStockage=0


#fin du programme pour garder la fenetre ouverte

fen_princ.mainloop()

12° Placer ce code (qui permet de pixelliser une image) et le votre (qui permet de créer un menu) dans un seul et même fichier.

Vous devrez faire attention à :

Vous devriez obtenir quelque chose ressemblant à ceci (correction en cliquant sur l'image, mais faites le d'abord vous même !):

correction

13° Rajouter des options dans le menu FICHIER qui permet d'ouvrir et enregistrer une image, en faisant référence aux mêmes fonctions que les boutons. Vous pourrez d'ailleurs supprimer ces boutons une fois le programme fonctionnel d'ailleurs.

14° Rajouter une possibilité pour QUITTER le programme.

Fermeture : Vous pouvez utiliser sur votre fenêtre deux méthodes :

correction

Et voilà. Vous savez à présent comment gérer vos menus sur les interfaces Tkinter.

S'il vous reste du temps, voyez comment rajouter des choix dans le menu EDITER et FORMAT. Pourquoi pas une gestion du format des images, du choix de l'image enregistrée ...

Sinon, la dernière partie est optionnelle mais vous permettra de gérer vos interfaces en créant des classes. Cela vous permettra de rendre vos bouts de programme plus facilement modulables et récupérables.

5 - Interface créée à l'aide de classes

Partie totalement optionnelle. Par contre, si vous désirez réaliser un programme en utilisant des objets pour gérer certains widgets, je ne peux que vous conseiller de voir ce qu'on y fait !

Nous allons ici réaliser la même interface mais nous allons en réalité créer :

Comme barreDesMenus et affichage sont affichés dans fen_princ, on dit que fen_princ est le widget père, maitre ou la fenêtre mère.

Les deux autre sont les widgets fils ou les fenêtres filles de fen_princ.

On trouve beaucoup de programme qui stockent l'adresse des fenêtres mères dans une variable qu'on nomme boss.

Normalement, nous aurions du directement créer nos classes. La modification à partir d'un programme qui n'avait pas été concu pour faire de la programmation objet n'est pas toujours évident. Le plus simple si vous voulez programmez ainsi et de toujours partir directement sur ce type de programmation.

Voici un bout de la version avec une classe BarreDeMenu :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


from tkinter import *

from tkinter.filedialog import *

from PIL import Image as Img

from PIL import ImageTk


# - - - - - - - - - - - -

# DECLARATION DES CLASSES

# - - - - - - - - - - - -

class BarreDeMenu(Frame):

    """Classe permettant d'intégrer facilement une barre de menu à un widget maitre."""


    # Methode constructeur

    def __init__(self, boss=None):

        Frame.__init__(self, borderwidth=3, bg='#557788')

        self.pack(fill=X)

        self.menuAffichage = Menubutton(self, text='Affichage', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

        self.menuAffichage.grid(row=0,column=2)

        # Création d'un menu défilant

        self.menuDeroulant1 = Menu(self.menuAffichage)

        self.menuDeroulant1.add_command(label='Petit format', command = self.petitFormat)

        # Attribution du menu déroulant au menu Affichage

        self.menuAffichage.configure(menu=self.menuDeroulant1)


    # Methodes rattachées au menu

    def petitFormat(self):

        self.master.geometry('600x400')


# - - - - - - - - - - -

# PROGRAMME DE TEST

# - - - - - - - - - - -


if __name__ == '__main__':

    # Création d'une fenêtre principale

    fen_princ = Tk()

    fen_princ.title("Mon application à moi que j'ai")

    fen_princ.geometry("900x600")

    # Création du menu via la classe BarreDeMenu

    barre = BarreDeMenu(fen_princ)

    # Lancement de la surveillance sur la fenêtre

    fen_princ.mainloop()

Alors, comment marche tout cela ?

Regardons comment on lance le programme :

if __name__ == '__main__':

    # Création d'une fenêtre principale

    fen_princ = Tk()

    fen_princ.title("Mon application à moi que j'ai")

    fen_princ.geometry("900x600")

    # Création du menu via la classe BarreDeMenu

    barre = BarreDeMenu(fen_princ)

    # Lancement de la surveillance sur la fenêtre

    fen_princ.mainloop()

Pour rappel, le test initial n'est vrai que si on lance ce programme en cliquant directement dessus. Si c'est un autre programme qui l'utilise, le test est faux et le code sous le if ne sera pas exécuté.

On voit qu'on y crée fen_princ puis qu'on créé barre, une instance de la classe BarreDeMenu.

Pour créer ce objet (issu de Tkinter), on lui envoie l'argument fen_princ qui correspond au widget dans lequel il doit s'afficher.

Regardons maintenant cette fameuse classe :

class BarreDeMenu(Frame):

    """Classe permettant d'intégrer facilement une barre de menu à un widget maitre."""


    # Methode constructeur

    def __init__(self, boss=None):

On voit qu'on se base sur la classe Frame : un cadre dans la synthaxe de Tkinter.

La méthode __init__ possède le paramètre obligatoire des méthodes (self par exemple) mais également un second paramètre, boss, car les méthodes-constructeurs des widgets Tkinter ont besoin de connaitre le maître du widget en cours de construction, de façon à savoir à qui le rattacher.

Si vous regardez le code du programme de test, vous verrez qu'on utilise l'argument fen_princ et que le paramètre boss contient donc (dans notre exemple) la référence à ce widget.

Regardons maintenant le contenu du constructeur :

    # Methode constructeur

    def __init__(self, boss=None):

        Frame.__init__(self, borderwidth=3, bg='#557788')

        self.pack(fill=X)

        self.menuAffichage = Menubutton(self, text='Affichage', width='20', borderwidth=2, bg='gray', activebackground='darkorange',relief = RAISED)

        self.menuAffichage.grid(row=0,column=2)

        # Création d'un menu défilant

        self.menuDeroulant1 = Menu(self.menuAffichage)

        self.menuDeroulant1.add_command(label='Petit format', command = self.petitFormat)

        # Attribution du menu déroulant au menu Affichage

        self.menuAffichage.configure(menu=self.menuDeroulant1)

On commence par lancer la méthode constructeur __init__ de la classe Frame. Nous sommes en train de redéfinir notre propre méthode __init__ et si vous oubliez cette ligne, tous les automatismes cachés liés aux Frames ne seraient correctement initialisés dans votre Classe.

On affiche ensuite le widget avec pack en utilisant self pour faire référence à lui-même.

Les lignes suivantes permettent de créer les attributs permettant de stocker les différentes variables qui permettront d'afficher ce qu'on désire dans notre cadre.

Ainsi, à chaque fois qu'on faisait référence dans le code sans objet à zoneMenu, on doit maintenant faire référence à self puisque nous sommes à l'intérieur du code de cette zone.

De la même façon, on stocke les références aux widgets contenus dans la zone en définissant des attributs à l'aide du point entre self et le nom de l'attribut : self.

Dernière chose : la commande fait référence à self.petitFormat car nous décidons de stocker l'effet dans une méthode de cette classe.

Regardons maintenant cette méthode :

    # Methodes rattachées au menu

    def petitFormat(self):

        self.master.geometry('600x400')

La méthode possède le paramètre obligatoire (qu'on nomme self ici). L'élément troublant via de self.master.

Si vous regardez la méthode constructeur, vous pourrez constater qu'aucun attribut master n'a été créé !

Vous devinerez que master fait référence au widget maître de notre classe, celui que nous avons reçu dans le paramètre boss.

Alors, l'attribut master s'est-il construit magiquement en étant de plus affecté du bon widget maitre ? Non bien entendu, cela vient de la ligne Frame.__init__(self, borderwidth=3, bg='#557788').

Lors de l'appel à la méthode constructeur de la classe Frame, le code de Tkinter a fait son travail : il a recupéré la valeur de boss pour créer un attribut automatique des widget Tkinter, l'attribut master. Et voilà. Il suffit de le savoir.

15° Créer les autres actions du menu AFFICHAGE.

Voilà. Je pense que vous avez compris l'esprit. Si vous voulez créer d'autres actions, agissant sur d'autres parties du programme, il va falloir trouver le moyen de signaler à votre classe sur quoi agir. Le mieux est donc de créer un ensemble de classes et de créer des méthodes qui leur permettent de communiquer.