Accueil

Traduction

Tutoriel Python - sommaire

Tutoriel Python - recherche

L'auteur : Patrick Darcheville

Vous pouvez me contacter via Facebook pour questions & suggestions : Page Facebook relative à mon site

Belles interfaces graphiques à la place du shell

Nous avons déjà réalisé de nombreux programmes avec des traitements de plus en plus complexes. Mais nos entrées et sorties dans le shell (via les fonctions input() & print()) ne sont pas ergonomiques.
Les usagers exigent des interfaces graphiques comme ils ont l'habitude sur la "toile".
Dans Python, c'est possible à condition de faire appel à tkinter.

Tkinter est un module intégré à la bibliothèque standard de Python ; une simple importation suffit.
Grâce aux fonctions de ce module nous créons une fenêtre comprenant des "widgets" : cadres, zones de texte (monoligne ou multiligne) cases à cocher, boutons radio,listes, bouton de commande, barres de menus.
Les saisies et l'affichage des résultatas se font alors dans l'interface graphique (et non plus dans le shell).

Créez un nouveau dossier dans C: et nommez le "python_interfaces".
Tous les scripts de ce chapitre et suivant seront stockés dans "c:\python_interfaces".

Premier programme : "tkinter_connexion.py"

Le code

# nom programme : tkinter_connexion.py
from tkinter import *

# Création d'un objet "fenêtre"
fen = Tk()  
fen.title("Connexion ")
fen.geometry("600x400")

# pour saisir identifiant
Label(fen, text = "Votre identifiant  ? ").pack()
zt1 = Entry(fen, width=30)
zt1.pack()

# pour saisir mot de passe
Label(fen, text = "Votre mot de passe ? ").pack()
zt2 = Entry(fen, width=30)
zt2.pack
# Bouton de commande 
Button(fen, text = "connexion").pack()

# Affichage de la fenêtre
fen.mainloop()

Il faut importer le module tkinter.
fen = Tk(): création d'une fenêtre tkinter, nommée "fen" avec le constructeur Tk()
fen.title("Connexion ") : on personnalise le titre de cette fenêtre
fen.geometry("600x400") : on fixe les dimensions de cette fenêtre

Certaines méthodes réclament deux arguments
Les méthodes du module sont sensibles à la casse : Tk(), Label(), Entry(), Button(), ...
Pour les widgets de type Entry il ne faut jamais définir et positionner dans la même instruction. Explication plus loin.

Le positionnement

Le positionnement des éléments avec la méthode pack() est basique. Les widgets se placent les uns en dessous des autres et sont centrés horizontalement dans la fenêtre.

Le rendu de ce programme

module tkinter

Les widgets sont collés car on n'a pas prévu de marges.
La couleur de fond de la fenêtre est un gris triste.

Deuxième programme : tkinter_renseignements

le programme

Les contrôles seront positionnés dans une grille : des lignes et des colonnes.

# nom programme : tkinter_renseignements.py
# objet : présentation de widgets moins connus
from tkinter import *

# Création d'un objet "fenêtre"
fenetre = Tk()  
fenetre.title("Fiche de renseignements")
fenetre.geometry("900x600")
fenetre.config(bg ="skyblue")

# la grille de saisie
for i in range(1,6):
    fenetre.rowconfigure(i,pad =15)
for i in range(1,4):
    fenetre.columnconfigure(i,pad = 20)
    
# spinbox
Label(fenetre, text = "Votre âge ? ").grid(row =1, column =1)
spinbox1 = Spinbox(fenetre, from_=20, to=90, increment=1).grid(row =1, column =2)

#scale
Label(fenetre, text = "Votre poids ? ").grid(row =2, column =1)
scale1 = Scale(fenetre, from_=40, to=100, showvalue=True,  /
	label='kilos' ,orient='h').grid(row =2, column =2)

#radiobutton 
Label(fenetre, text = "Votre sexe ?  ").grid(row =3, column =1)
rb1 = Radiobutton(fenetre, text="femme" , value ="f").grid(row =3, column =2)
rb2 = Radiobutton(fenetre, text="homme", value ="m").grid(row =3, column =3)


#checkbutton ; cc
Label(fenetre, text = "Jeux de cartes pratiqués  ?  ").grid(row =4, column =1)
cc1 = Checkbutton(fenetre, text="belote").grid(row =4, column =2)
cc2 = Checkbutton(fenetre, text ="tarot").grid(row =4, column =3)
cc3 = Checkbutton(fenetre, text ="bridge").grid(row =4, column =4)

# liste simple
Label(fenetre, text = "sport premier pratiqué  ?  ")
liste1 = Listbox(fenetre, height=6)
for item in ("foot", "vélo", "natation", "sieste","pétanque","aucun"):
    liste1.insert(END, item)
liste1.grid(row =5, column =2)

# Affichage de la fenêtre
fenetre.mainloop()

Commentaire

Il faut donc deux boucles pour créer cette grille de 5 lignes et 3 colonnes.
Attention la première ligne a l'indice 1 ! la première colonne a l'indice 1 aussi.
Attention pour que les widgets ne soient pas collés, prévoyez des marges entre lignes et colonnes avec la propriété "pad".

Le rendu

tutoriel tkinter

Observez bien l'apparence d'un widget de type "spinbox" (toupie) et un widget de type "scale" (curseur) pour saisir respectivement âge et poids.

Essayez de saisir "boof" respectivement dans les widgets liés aux étiquettes "Votre âge" et "Votre poids".
Dans un Spinbox c'est possible mais pas dans un Scale. Dans ce dernier type de widget la saisie via une frappe au clavier est impossible. Un contrôle "curseur" propose donc un contrôle de saisie efficace.

Les frames

On peut diviser la fenêtre en différentes zones appelés "frames" (cadres). Chaque cadre contient des widgets.

Thème : page d'accueil d'une application pour laquelle l'utilisateur doit être inscrit.
L'utilisateur doit donc se connecter OU s'inscrire.

Le code

 
# nom programme : tkinter_connexion_inscription.py
# objet : fenêtre avec deux frames
from tkinter import *

# Création d'un objet "fenêtre"
fenetre = Tk()  # nouvelle instance de Tk
fenetre.title("Connexion / inscription ")
fenetre.geometry("600x600")
fenetre.config(bg ="white")

# création de deux frames
f1 =LabelFrame(fenetre,bd=2, text ="connexion", bg ="skyblue")
f2= LabelFrame(fenetre,bd=2, text ="inscription", bg ="skyblue",)
f1.pack(side =LEFT, padx = 10, pady =10)
f2.pack(side =RIGHT,padx = 10, pady =10)

Label(f1, text ="identifiant  ? ").pack(padx =5, pady =5)
zt1 = Entry(f1,width =50)
zt1.pack(padx =5, pady =5)
Label(f1, text ="mot de passe ?  ").pack(padx =5, pady =5)
zt2 = Entry(f1,width =50)
zt2.pack(padx =5, pady =5)

Label(f2, text ="identifiant ?  ").pack(padx =5, pady =5)
zt3 = Entry(f2,width =50)
zt3.pack(padx =5, pady =5)

Label(f2, text ="confirmer identifiant ! ").pack(padx =5, pady =5)
zt4 = Entry(f2,width =50)
zt4.pack(padx =5, pady =5)

Label(f2, text ="mot de  passe ? ").pack(padx =5, pady =5)
zt5 = Entry(f2,width =50)
zt5.pack(padx =5, pady =5)

Label(f2, text =" confirmer mot de  passe !  ").pack(padx =5, pady =5)
zt6 = Entry(f2,width =50)
zt6.pack(padx =5, pady =5)

fenetre.mainloop()

Analyse du code

f1 =LabelFrame(fenetre,bd=2, text ="connexion", bg ="skyblue", relief="groove") : définition d'un cadre, enfant de "fenetre", borduré avec l'étiquette "connexion", couleur de fond : "skyblue".
f1.pack(side =LEFT, padx =10, pady=10) : le frame "f1" est positionné à gauche avec des marges par rapport à son parent.
Label(f1, text ="identifiant ? ").pack(padx =5, pady=5) : création d'une étiquette dans le frame "f1".
zt1 = Entry(f1,width =50) : création d'une zone de texte nommée "zt1".
zt1.pack(padx =5, pady =5) : positionnement de cette zone de texte.

Rappel : ne jamais définir et positionner un widget de type Entry dans la même instruction.

Le rendu

tutoriel tkinter

Récupérer les données saisies dans les widgets

Arrivé à ce stade, vous devez vous dire : c'est bien gentil tout ça. Mais à quoi ça sert d'effectuer des saisies via une belle interface si il n'y a aucun traitement de ces données en aval ???
Et bien j'y arrive !

Thème : via une interface, l'utilisateur saisit X et N dans des Entry et en retour il y a affichage de X à la puissance N dans une troisième Entry qui doit être en lecture seule.

Le code de "tkinter_puissance.py"

def fpuissance():
    x = zt1.get()
    n = zt2.get()
    if x.isdigit() and n.isdigit() : 
        resultat = math.pow(eval(x),eval(n))
        zt3.configure(state ='normal')
        zt3.delete(0,END)
        zt3.insert(0,resultat)
        zt3.configure(state='disabled')
    else :
        msg.showinfo("Erreur","Il faut saisir deux entiers !")
#-----------------------------
# nom programme : tkinter_puissance.py
# objet du programme : élever x à la puisssance n
from tkinter import *
import math
import tkinter.messagebox as msg

# Création d'un objet "fenêtre"
fenetre = Tk()  # nouvelle instance de Tk

# mise en forme fenêtre & widgets
fonte1 = "{courier new} 14 bold"
fonte2 ="{courier new} 14 italic"
fenetre.config(bg ="lime")
fenetre.title("Elever x à la puissance n ")
fenetre.geometry("600x400")

# pour saisir x
Label(fenetre, text = " valeur de x ? ", font = fonte2).pack(padx=0,pady=10)
zt1= Entry(fenetre,width=30, font =fonte1)
zt1.pack()
# pour saisir n
Label(fenetre, text = "valeur de n  ?  ", font=fonte2).pack(padx=0,pady=10)
zt2= Entry(fenetre,width=30, font=fonte1)
zt2.pack(padx = 0, pady=10)

# Bouton de commande 
Button(fenetre,text = "Calcul X à la puissance N",  /
	font = fonte1, command=fpuissance).pack(padx=0, pady=10)

# affichage du résultat
zt3 = Entry(fenetre,width=30, font = fonte2)
zt3.pack(padx = 0, pady=10)

fenetre.mainloop()

Les entrées et sorties se font dans l'interface graphique ; le shell n'est plus utilisé. Vous avez créé une micro application locale Python.

Analyse de la routine principale

import math : on a besoin de la fonction pow().
import tkinter.messagebox as msg : importation du sous module messagebox de tkinter sous l'alias 'msg'. Ainsi on pourra indiquer les erreurs via des boites de dialogue.
fonte1 = "{courier new} 14 bold" : la variable "fonte1" redéfinit le style par défaut du texte (police, taille,poids). Même remarque pour "fonte2".
Label(fenetre, text = " valeur de x ? ", font = fonte2) : définition d'une étiquette ; notez l'option font = fonte2
zt1= Entry(fenetre,width=30, font =fonte1) : un Entry doit être nommé car son contenu est manipulé dans le cadre d'un traitement.
Button(fenetre,text = "Calcul X à la puissance N", font = fonte1, command=fpuissance) : notez l'option command = nomFonction. Donc sur clic appel d'une fonction. On fait donc de la programmation évènementielle.

À défaut d'instruction basée sur la méthode delete() les résultats successifs seraient mis bout à bout dans l'Entry "zt3".

Je rappelle qu'il ne faut jamais définir et positionner un Entry dans la même instruction.
En effet si vous écrivez zt1 = tk.Entry(fen, ...).pack() alors "zt1" vaut "none" et donc l'instruction zt1.get() 'plante'.
Il faut toujours deux instructions : une pour définir le widget Entry : zt1 = tk.Entry(fen, ...) puis une deuxième instruction pour le positionner.

Le rendu

J'ai saisi "1O" au lieu de "10". Le contrôle de saisie fonctionne parfaitement rt je suis averti par une boite de dialogue de l'erreur.

Etes vous conscient de la révolution programmatique effectuée avec ce programme ?
Le shell n'est plus utilisé ; les saisies et les affichages se font dans le cadre de l'interface graphique pour une bien meilleure expérience utilisateur.

Amélioration de "tkinter_puissance.py"

Si on veut que X puisse être un nombre décimal et N un entier positif.
Il suffit de modifier la fonction :

def fpuissance():
    try : 
        x = float(zt1.get())
        n = int(zt2.get())
        resultat = math.pow(x,n)
        zt3.configure(state ='normal')
        zt3.delete(0,END)
        zt3.insert(0,resultat)
        zt3.configure(state='disabled')
    except:
        msg.showinfo("Erreur","Attention le séparateur décimal est le point. \n N doit être un entier ! ")

On doit recourir à la gestion des exceptions.
Notez que la boite de dialogue affiche un message sur 2 lignes.

Exercice

Thème : vous devez concevoir une calculatrice basique : les quatre opérations + élévation à la puissance.

Le rendu

Extraits du programme

Le programme doit être structuré : fonctions & programme principal.

Les fonctions (extraits)

def saisies() :
    a = eval(zt1.get())
    b = eval(zt2.get())
    return a,b

def fadd() :
    a,b = saisies()
    resultat = a+b
    zt3.delete(0,END)
    zt3.insert(0,resultat)
...
def fdivi():
    a,b = saisies()
    resultat = a / b
    zt3.delete(0,END)
    zt3.insert(0,resultat)
...

Il y a une nouveauté syntaxique : appel d'une fonction dans une fonction.
En effet les fonctions fadd(), fsous(), fmulti(), fdivi(), fexp() appellent chacune la fonction saisies()
celle-ci retourne après conversion, et sous forme d'un tuple, les saisies dans les zones de texte zt1 & zt2

Le programme principal (extraits)

#programme principal : tkinter_calculatrice.py 
from tkinter import *
# Création d'une interface graphique
fenetre = Tk() 
fenetre.title("Calculatrice ")
fenetre.geometry("900x400")
fenetre.resizable(False, False) #l'interface n'est pas redimensionnable
fenetre.config(bg ="lime")

# grille de 5 lignes par 4 colonnes
for i in range(1,6):
    fenetre.rowconfigure(i, pad =10)
for i in range(1,5):
    fenetre.columnconfigure(i, pad =15)
mafonte = "{courier new} 14"

# pour saisir A
e1 = Label(fenetre, text = " saisir A", font =mafonte)
e1.grid(row=1, column=1)
zt1 = Entry(fenetre, width=15,font =mafonte)
zt1.grid(row=1, column=2)

# pour saisir B
e2 = Label(fenetre, text = "saisir B", font =mafonte)
e2.grid(row=2, column=1)
zt2 = Entry(fenetre, width=15,font =mafonte)
zt2.grid(row=2, column=2)

# Boutons de commande 
b1 = Button(fenetre, text = "A + B ", command=fadd, font =mafonte)
b1.grid(row=3, column=1)
b2 = Button(fenetre, text = "A - B ", command=fsous, font =mafonte)
b2.grid(row=3, column=2)
...
# il manque le code de trois boutons de commande : 
# b3,b4,b5 pour multiplier, diviser, exponentiel. 
...
# pour afficher le résultat
e3 = Label(fenetre, text = "Résultat ",font =mafonte)
e3.grid(row=4, column=1)
zt3 = Entry(fenetre, width=20,font =mafonte)
zt3.grid(row=4, column=2)

fenetre.mainloop()

Dans le programme principal, notez l'instruction fenetre.resizable(False, False). La fenêtre ne peut être redimensionnée ni en hauteur, ni en largeur.
mafonte = "{courier new} 14" : la variable "mafonte" rédéfinit la police, taille et poids du texte.
e1 = Label(fenetre, text = " saisir A", font =mafonte) : à chaque widget j'ajoute l'option "font = mafonte" pour personnaliser légendes, saisies et résultats.

Un script perfectible

Ce programme ne gère pas les exceptions (erreurs de saisie de l'utilisateur, si vous préférez).
Comme on n'utilise plus le shell pour les saisies et les affichages, ces exeptions ne sont pas bloquantes. Mais il n'empêche que des messages d'erreur sont affichées dans le shell.
Dans le chapitre 20 vous retrouverez une version améliorée de "calculatrice.py" : Créer et distribuer des applications locales

Récupérer les contenus des autres widgets

Attention cette partie est un peu plus délicate à appréhender ...
Si vous n'envisagez pas d'utiliser des boutons radio et des cases à cocher dans vos interfaces graphiques, vous pouvez ignorer ce paragraphe dans un premier temps. Vous y reviendrez plus tard.

Variables de contrôle

Le script

Reprenons le thème du questionnaire dans lequel on demande le sexe, le poids, le jeu de cartes préféré et le sport pratiqué.
Le code comprend un programme principal pour la description de l'interface et une fonction pour le traitement.

Le programme principal

nom programme : tkinter_infos_traitements.py
# programme principal avec design de la fenêtre
from tkinter import *
# définition de la fenêtre
fenetre = Tk()  # nouvelle instance de Tk
fenetre.title("Fiche de renseignements")
fenetre.geometry("900x600")
fenetre.config(bg ="skyblue")
for i in range(1,6):
    fenetre.rowconfigure(i, pad =20)

# un scale
vc1 =  IntVar()
e2 = Label(fenetre, text = "Votre poids ?  ")
scale1 = Scale(fenetre, from_=40, to=100, showvalue=True, 
	\label='kilos' ,orient='h', variable =vc1)
e2.grid(row =2, column =1)
scale1.grid(row =2, column =2)

# deux radiobutton
vc2 =StringVar()
e3 = Label(fenetre, text = "Votre sexe ?  ")
rb1 = Radiobutton(fenetre, text="femme",value ="féminin", variable = vc2)
rb2 = Radiobutton(fenetre, text="homme",value ="masculin",variable = vc2)
e3.grid(row =3, column =1)
rb1.grid(row =3, column =2)
rb2.grid(row =3, column =3)

# trois cases à cocher
vc3 =IntVar()
vc4 =IntVar()
vc5 =IntVar()
e4 = Label(fenetre, text = "Jeux de cartes pratiqués  ?  ")
cc1 = Checkbutton(fenetre, text="belote", variable =vc3)
cc2 = Checkbutton(fenetre, text ="tarot", variable =vc4)
cc3 = Checkbutton(fenetre, text ="bridge",variable =vc5)
e4.grid(row =4, column =1)
cc1.grid(row =4, column =2)
cc2.grid(row =4, column =3)
cc3.grid(row =4, column =4)

# une zone de liste
e5 = Label(fenetre, text = "sport premier pratiqué  ?  ")
liste1 = Listbox(fenetre)
contenu = ['aucun', 'tennis','vélo','foot','natation', 'autre']
for item in contenu:
    liste1.insert(END, item)
e5.grid(row =5, column =1)
liste1.grid(row =5, column =2)

#  un bouton de commande 
bouton1 = Button(fenetre, text = "Récupérez les valeurs saisies", command=mafonction)
bouton1.grid(row=6, column=2)

fenetre.mainloop()

La fonction

def  mafonction():
    monpoids = vc1.get()
    monsexe =vc2.get()
    belote = vc3.get()
    tarot = vc4.get()
    bridge = vc5.get()
    print ("mon poids :", monpoids)
    print("mon sexe : ", monsexe)
    print("belote : ", belote)
    print("tarot : ", tarot)
    print("bridge : ", bridge)
    index = liste1.curselection()
    item= liste1.get(index)
    print ("sport : ", index, item)

L'interface graphique

tutoriel tkinter

La fenêtre Shell de l'IDLE

mon poids : 68
mon sexe :  masculin
belote :  0
tarot :  1
bridge :  0
sport :  (4,) natation

Amélioration du programme

Extrait du programme principal amélioré

Le shell n'est plus utilisé !

...
mafonte = "{courier new} 10 bold "
...
e1 = Label(fenetre, text = "Fiche de renseignements ", font = mafonte)
e1.grid(row =1, column =2)
...
scale1 = Scale(fenetre, from_=40, to=100, showvalue=True, /
label='kilos' ,orient='h', variable =vc1, font=mafonte)
...
# trois cases à cocher
vc3 =StringVar()
vc4 =StringVar()
vc5 =StringVar()
e4 = Label(fenetre, text = "Jeux de cartes pratiqués  ?  ")
cc1 = Checkbutton(fenetre, text="belote", onvalue = "belote",offvalue =" ", variable =vc3)
cc2 = Checkbutton(fenetre, text="tarot", onvalue = "tarot",offvalue =" ", variable = vc4)
cc3 = Checkbutton(fenetre, text="bridge", onvalue = "bridge",offvalue =" ",variable = vc5)
...
# pour afficher le résultat
e6 = Label(fenetre, text = "vos informations   : ")
e6.grid(row=6, column=1)
zt1 = Entry(fenetre, width=100)
zt1.grid(row=6, column=2)

mafonte = "{courier new} 12 bold " : je définis un objet "fonte".
e1 = Label(fenetre, text = "Fiche de renseignements ", font = mafonte) : à chaque widget j'ajoute l'option font = mafonte.

Donc les trois variables de contrôle associées aux "Checkbutton" doivent changer de type puisqu'elles contiennent désormais une chaine !

zt1 = Entry(fenetre, width=50, font =mafonte) : ajout d'une zone de texte large qui va afficher la chaine retournée par la fonction.

Extrait de la fonction

Le rendu

tutoriel tkinter

Le shell n'est plus du tout utilisé.

Enrichir l'interface

Afin de vérifier si vous avez bien compris la syntaxe, je vous conseille de récréer ce script et de l'enrichir : ajoutez un Scale pour saisir l'âge du questionné. Modifiez la fonction en conséquence.

Le widget Text

Alors que Entry est une zone de texte monoligne, Text est une zone de texte multiligne.
C'est l'équivalent donc de l'élément textarea dans un formulaire HTML.
Or vous savez qu'un textarea est souvent en lecture seule ...

Exemple

Thème : possibilité de modifier le texte initial d'un contrôle Text puis de la transférer vers un autre Text qui lui est en lecture seule.

Le script

def recup_texte():
	t2.configure(state='normal')
    contenu =t1.get("1.0", "end")
    t2.delete("1.0",END)
    t2.insert("1.0", contenu)
    t2.configure(state='disabled')
#----------------------------
# tkinter_text.py
from tkinter import *

root = Tk()
root.title("Le widget Text")

# Création du widget Text
t1 = Text(root, font="Arial 12", height=5, width=100)
t1.pack(padx = 10, pady=10)

# Insertion de texte dans t1
t1.insert("1.0", "Une première ligne de texte\n")
t1.insert("2.0", "Une seconde ligne de texte\n")
t1.insert("3.0", "Une troisième ligne de texte\n")

# Création d'un bouton pour transférer le texte
btn = Button(root, text="Transfert", width=20, command=recup_texte)
btn.pack(padx = 10, pady=10)

# création d'un widget Text
t2 = Text(root, font="Arial 12", height=5, width=100)
t2.pack(padx = 10, pady=10)

root.mainloop()

Quelques instructions de la routine principale :
t1 = Text(root, font="Arial 12", height=5, width=100) : création d'une zone de texte multiligne.
t1.insert("1.0", "Une première ligne de texte\n") : insertion de texte au début de la zone.

La fonction :
t2.configure(state='normal') : ce widget est éditable (on peut écrire dedans).
contenu =t1.get("1.0", "end") : récupération du contenu de "t1"
t2.delete("1.0",END) : vidage du contenu de "t2"
t2.insert("1.0", contenu) : écrire dans "t2".
t2.configure(state='disabled') : ce widget n'est plus éditable (en lecture seule)

Le rendu

J'ai modifié le texte initial puis j'ai transféré vers un Entry non éditable (sauf au moment du transfert).

Une barre de menus

La barre de menus est un widget "Menu" qui sert de conteneur pour d’autres menus (sous‑menus).

Exemple

Thème : je vous propose une interface qui vous permet de visiter tous mes tutoriels en ligne sur la programmation web côté client et côté serveur.

Le rendu

Aucun menu n'a été déroulé.

La routine principale

La barre de menus comprend 5 menus.
Chaque menu comprend 2 ou 3 items.
Le dernier menu illustre l'usage des boites de messages.

# nom programme : tkinter_barre_menus.py
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import messagebox
import webbrowser
import time

fen = tk.Tk()
fen.title("Mes tuto sur la programmation web")
fen.geometry("600x500")
fen.resizable(False,False)
fen.config(bg ="skyblue")

ma_barre = tk.Menu(fen) # nouvelle instance de Menu 
ma_barre.config(font=('Arial', 20))

menu1 = tk.Menu(ma_barre, tearoff=0)
ma_barre.add_cascade(label="HTML & CSS", menu=menu1,)
menu1.add_command(label="débuter en HTML & CSS",command =fonction1)
menu1.add_command(label="HTML version 5",command =fonction2)
menu1.add_command(label="CSS version 3", command =fonction3)

menu2=tk.Menu(ma_barre,tearoff=0)
ma_barre.add_cascade(label="Desssins vectoriels avec SVG", menu=menu2)
menu2.add_command(label="Langage SVG",command =fonction4)
menu2.add_command(label="Logiciel Inkscape", command =fonction5)

menu3=tk.Menu(ma_barre,tearoff=0)
ma_barre.add_cascade(label="Univers JavaScript", menu=menu3)
menu3.add_command(label="JavaScript natif",command =fonction6)
menu3.add_command(label="Frameworks jQuery & Vue",command =fonction7)
menu3.add_command(label="API Canvas",command =fonction8)

menu4=tk.Menu(ma_barre,tearoff=0)
ma_barre.add_cascade(label="Prog. web côté serveur", menu=menu4)
menu4.add_command(label="PHP & MYSQL",command =fonction9)
menu4.add_command(label="Framework Python-flask",command =fonction10)

menu5=tk.Menu(ma_barre,tearoff=0)
ma_barre.add_cascade(label="Tests messagbox", menu=menu5)
menu5.add_command(label="item1",command =fonction11)
menu5.add_command(label="item2",command =fonction12)
menu5.add_command(label="Quitter",command =fonction13)

fen.config(menu=ma_barre) #lie la barre à la fentêtre tkinter
fen.mainloop()

ma_barre = tk.Menu(fen) : création d'une barre de menus (instance de la classe Menu) nommée "ma_barre".
menu1 = tk.Menu(ma_barre, tearoff=0) : création d'un menu dans la barre nommée "menu1"
ma_barre.add_cascade(label="HTML & CSS", menu=menu1,) : titre de "menu1"
menu1.add_command(label="débuter en HTML & CSS",command =fonction1) : création d'un item dans "menu1".

Le paramètre tearoff contrôle la possibilité de détacher (ou "tirer hors") le menu de la fenêtre principale.
tearoff =0 : le menu ne peut être détaché.
L'option est ici obligatoire car la valeur par défaut est 1 (détachable).

Les fonctions du programme

def fonction1():
    # tuto d'initiation au HTML & CSS
    webbrowser.open("https://darchevillepatrick.info/debutant/debuter0.php")
    time.sleep(5)
    fen.lift()
    fen.focus_force() 
def fonction2():
    # approfondissements HTML
    webbrowser.open("https://darchevillepatrick.info/html/html0.php")
    time.sleep(5)
    fen.lift()
    fen.focus_force()
def fonction3():
    # approfondissements CSS 
    webbrowser.open("https://darchevillepatrick.info/css/css0.php")
   ...
def fonction4():
    # format SVG
    webbrowser.open("https://darchevillepatrick.info/svg/svg0.php#debut")
   ...
def fonction5():
    # logiciel Inkscape pour générer SVG
    webbrowser.open("https://darchevillepatrick.info/inkscape/ink0.php")
  ...
def fonction6():
    # javascript natif
    webbrowser.open("https://darchevillepatrick.info/js/js0.php")
  ...
def fonction7():
    # frameworks jquery et vue
    webbrowser.open("https://darchevillepatrick.info/js/js20.php")
  ...
def fonction8():
    # API Canvas
    webbrowser.open("https://darchevillepatrick.info/canvas/canvas0.php")
    ...
def fonction9():
    # tuto PHP & MYSQL
    webbrowser.open("https://darchevillepatrick.info/debutant/debuter20.php")
   ...
def fonction10():
    # tuto sur framework Python-flask
    webbrowser.open("https://darchevillepatrick.info/python/python21.php")
   ...
def fonction11():
    messagebox.showinfo("Item1","Fonctionnalité pas disponible")
def fonction12():
    messagebox.showinfo("Item2","Fonctionnalité pas disponible")
def fonction13():
     reponse =messagebox.askquestion("Quitter application", "Voulez vous quitter application ? ")
     if reponse == "yes":
         quit()

Étudions la fonction "fonction11()":
messagebox.showinfo("Item1","Fonctionnalité pas disponible"): Affichage d'une boite de dialogue avec un seul bouton "OK", un titre ("Item1") et un message principal.

Étudions maintenant la fonction "fonction13()":
reponse =messagebox.askquestion("Quitter application", "Voulez vous quitter application ? "): Affichage d'une boite de dialogue avec un bouton "OUI" et un bouton "NON".
Si vous cliquez sur "OUI" la variable "reponse" récupère "yes" sinon "no".

Donc selon l'option de messagebox : showinfo() / askquestion() l'aspect de la boite de dialogue est différent.

Les limites de tkinter

Mais heureusement le sous module tkinter.ttk corrige ces lacunes.