Tkinter
Créer des interfaces graphiques natives en Python — widgets, layouts, événements, formulaires, dialogues, Canvas et architecture POO. Inclus dans Python, aucune installation requise.
Introduction & fenêtre principale
Tkinter est la bibliothèque GUI standard de Python — elle est incluse dans toute installation Python, aucun pip install nécessaire. Elle fournit des widgets natifs (look Windows/macOS/Linux) et une boucle d'événements intégrée.
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
# Fenêtre principale
root = tk.Tk()
root.title("Mon Application")
root.geometry("600x400") # largeur x hauteur
root.minsize(400, 300) # taille minimale
root.resizable(True, True) # redimensionnable H/V
# Centrer la fenêtre sur l'écran
w, h = 600, 400
x = (root.winfo_screenwidth() - w) // 2
y = (root.winfo_screenheight() - h) // 2
root.geometry(f"{w}x{h}+{x}+{y}")
# Icône (fichier .ico Windows)
# root.iconbitmap("icon.ico")
# Boucle d'événements — TOUJOURS EN DERNIER
root.mainloop()
mainloop() est la boucle d'événements — elle garde la fenêtre ouverte et attend les actions de l'utilisateur. Tout le code avant mainloop() est de l'initialisation. Tout le code après ne s'exécute qu'à la fermeture de la fenêtre.
Tkinter utilise le look natif de l'OS. Pour un look plus moderne et cohérent entre plateformes, utiliser ttk (Themed Tk) qui sera couvert en section 10.
Widgets de base
# Label
lbl = tk.Label(root, text="Nom :", font=("Arial", 12),
fg="#333", bg="#f0f0f0")
# Button
btn = tk.Button(root, text="Valider", command=on_submit,
bg="#0078d4", fg="white", relief="flat",
padx=10, pady=5, cursor="hand2")
# Entry (champ texte)
entry = tk.Entry(root, width=30, show="") # show="*" = mot de passe
entry.insert(0, "Texte par défaut")
valeur = entry.get()
# Text (multi-lignes)
txt = tk.Text(root, width=40, height=8, wrap=tk.WORD)
txt.insert(tk.END, "Contenu initial\n")
contenu = txt.get("1.0", tk.END) # "ligne.colonne"
# Checkbutton
var_cb = tk.BooleanVar(value=False)
cb = tk.Checkbutton(root, text="Accepter les CGU", variable=var_cb)
if var_cb.get(): pass # lire la valeur
# Radiobutton
choix = tk.StringVar(value="option1")
tk.Radiobutton(root, text="Option A", variable=choix, value="option1")
tk.Radiobutton(root, text="Option B", variable=choix, value="option2")
# Listbox avec scrollbar
frame_lb = tk.Frame(root)
scrollbar = tk.Scrollbar(frame_lb)
listbox = tk.Listbox(frame_lb, yscrollcommand=scrollbar.set,
selectmode=tk.SINGLE, height=6)
scrollbar.config(command=listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
for item in ["Alice", "Bob", "Charlie"]:
listbox.insert(tk.END, item)
selected = listbox.get(listbox.curselection()) # élément sélectionné
# Scale (curseur)
scale = tk.Scale(root, from_=0, to=100,
orient=tk.HORIZONTAL, label="Volume")
val = scale.get()
# Combobox (ttk)
combo = ttk.Combobox(root, values=["Python", "Java", "C++"],
state="readonly")
combo.current(0) # sélectionner le premier
lang = combo.get()
# Menu barre principale
menubar = tk.Menu(root)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="Ouvrir", command=open_file, accelerator="Ctrl+O")
file_menu.add_separator()
file_menu.add_command(label="Quitter", command=root.quit)
menubar.add_cascade(label="Fichier", menu=file_menu)
root.config(menu=menubar)
Layouts — pack, grid, place
# side : TOP (défaut), BOTTOM, LEFT, RIGHT
# fill : NONE, X, Y, BOTH
# expand : True = prendre l'espace disponible
tk.Label(root, text="En-tête").pack(
side=tk.TOP, fill=tk.X, padx=5, pady=5)
frame = tk.Frame(root)
frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
sidebar = tk.Frame(frame, width=150, bg="#2d2d2d")
sidebar.pack(side=tk.LEFT, fill=tk.Y)
content = tk.Frame(frame, bg="white")
content.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
tk.Label(root, text="Barre de statut").pack(
side=tk.BOTTOM, fill=tk.X)
# Position absolue
btn.place(x=100, y=200)
# Position relative (0.0 → 1.0)
btn.place(relx=0.5, rely=0.5, # centre
anchor=tk.CENTER) # point d'ancrage
# Taille relative
entry.place(relx=0.1, rely=0.3,
relwidth=0.8, height=30)
# row, column — index de ligne/colonne (commence à 0)
# sticky — N S E W ou combinaisons (comme CSS)
# columnspan / rowspan — fusion de cellules
# padx/pady — espacement externe
# ipadx/ipady — espacement interne
for i, (label, var) in enumerate([
("Prénom :", "prenom"),
("Nom :", "nom"),
("Email :", "email"),
]):
tk.Label(root, text=label).grid(
row=i, column=0, sticky=tk.E, padx=5, pady=3)
tk.Entry(root, width=25).grid(
row=i, column=1, sticky=tk.EW, padx=5, pady=3)
# Fusionner sur 2 colonnes
btn.grid(row=3, column=0, columnspan=2, pady=10)
# Pondération — colonne 1 s'étire au redimensionnement
root.columnconfigure(1, weight=1)
root.rowconfigure(4, weight=1) # ligne 4 s'étire
Ne jamais mélanger pack() et grid() dans le même conteneur — cela provoque une erreur au runtime. On peut en revanche utiliser grid() dans un Frame et pack() dans un autre Frame.
Variables Tkinter
# Les variables Tkinter sont des "observables" —
# un widget lié se met à jour automatiquement
nom_var = tk.StringVar(value="")
age_var = tk.IntVar(value=0)
actif_var = tk.BooleanVar(value=False)
note_var = tk.DoubleVar(value=0.0)
# Lier à un widget
entry_nom = tk.Entry(root, textvariable=nom_var)
label_val = tk.Label(root, textvariable=nom_var) # miroir live !
scale = tk.Scale(root, variable=age_var, from_=0, to=120)
# Lire / écrire
nom = nom_var.get()
nom_var.set("Alice")
# Trace — réagir à un changement de valeur
def on_nom_change(name, index, mode):
print(f"Nom changé : {nom_var.get()}")
nom_var.trace_add("write", on_nom_change)
# Modes de trace : "write" | "read" | "unset"
Les variables Tkinter créent un lien bidirectionnel entre la donnée et le widget. Modifier la variable avec .set() met à jour immédiatement l'affichage du widget lié, et inversement. C'est le mécanisme de base pour le data-binding en Tkinter.
root = tk.Tk()
texte = tk.StringVar()
tk.Label(root, text="Saisir :").pack()
tk.Entry(root, textvariable=texte).pack()
# Ce label se met à jour à chaque frappe
tk.Label(root, textvariable=texte,
font=("Arial", 18), fg="#818cf8").pack(pady=10)
root.mainloop()
Événements & callbacks
# Syntaxe : widget.bind("<Événement>", callback)
# Le callback reçoit un objet Event
def on_click(event):
print(f"Clic en ({event.x}, {event.y})")
def on_key(event):
print(f"Touche : {event.keysym} char : {event.char}")
label.bind("<Button-1>", on_click) # clic gauche
label.bind("<Button-3>", on_rclick) # clic droit
entry.bind("<KeyPress>", on_key) # toute touche
entry.bind("<Return>", on_submit) # touche Entrée
entry.bind("<FocusIn>", on_focus) # focus reçu
entry.bind("<FocusOut>", on_blur) # focus perdu
root.bind("<Escape>", lambda e: root.quit())
root.bind("<Control-s>", save) # Ctrl+S
# Survol souris
widget.bind("<Enter>", lambda e: widget.config(bg="lightblue"))
widget.bind("<Leave>", lambda e: widget.config(bg="white"))
# Redimensionnement de la fenêtre
root.bind("<Configure>", lambda e: on_resize(e.width, e.height))
# Exécuter une fonction après N millisecondes
root.after(2000, on_timeout) # une seule fois
# Boucle récurrente (animation, horloge, polling…)
def update_clock():
from datetime import datetime
lbl_time.config(text=datetime.now().strftime("%H:%M:%S"))
root.after(1000, update_clock) # se reprogramme
update_clock() # démarrer la boucle
# Annuler une tâche planifiée
task_id = root.after(5000, some_func)
root.after_cancel(task_id)
# Mise à jour de l'interface depuis un thread
# (ne jamais modifier les widgets depuis un thread secondaire)
root.after(0, lambda: label.config(text="Terminé"))
| Événement | Description |
|---|---|
<Button-1/2/3> | Clic gauche / milieu / droit |
<Double-Button-1> | Double-clic |
<Motion> | Mouvement souris (bouton enfoncé) |
<MouseWheel> | Molette (event.delta) |
<Return> | Touche Entrée |
<Escape> | Touche Échap |
<Control-z> | Ctrl+Z (modifier au besoin) |
Formulaires & validation
import re
class FormFrame(tk.Frame):
def __init__(self, parent):
super().__init__(parent, padx=20, pady=20)
self.vars = {
"nom": tk.StringVar(),
"email": tk.StringVar(),
"age": tk.StringVar(),
}
self._build()
def _build(self):
champs = [("Nom :", "nom"), ("Email :", "email"), ("Âge :", "age")]
self.entries = {}
self.errors = {}
for i, (label, key) in enumerate(champs):
tk.Label(self, text=label, anchor="e").grid(
row=i*2, column=0, sticky=tk.E, pady=(4,0))
e = tk.Entry(self, textvariable=self.vars[key], width=28)
e.grid(row=i*2, column=1, sticky=tk.EW, pady=(4,0))
self.entries[key] = e
err = tk.Label(self, text="", fg="red", font=("Arial",8))
err.grid(row=i*2+1, column=1, sticky=tk.W)
self.errors[key] = err
tk.Button(self, text="Valider", command=self.validate).grid(
row=6, column=0, columnspan=2, pady=10)
self.columnconfigure(1, weight=1)
def validate(self):
rules = {
"nom": lambda v: len(v) >= 2 or "Min. 2 caractères",
"email": lambda v: bool(re.match(r".+@.+\..+", v)) or "Email invalide",
"age": lambda v: v.isdigit() or "Entier requis",
}
valid = True
for key, rule in rules.items():
result = rule(self.vars[key].get())
if result is True:
self.errors[key].config(text="")
self.entries[key].config(bg="white")
else:
self.errors[key].config(text=result)
self.entries[key].config(bg="#fff0f0")
valid = False
if valid:
messagebox.showinfo("Succès", "Formulaire valide !")
# Validation en temps réel — empêche certains caractères
def only_digits(new_val):
return new_val.isdigit() or new_val == ""
vcmd = root.register(only_digits)
entry_age = tk.Entry(
root,
validate="key", # valider à chaque frappe
validatecommand=(vcmd, "%P") # %P = valeur après saisie
)
Fenêtres secondaires & dialogues
from tkinter import messagebox, filedialog, simpledialog, colorchooser
# Boîtes de message
messagebox.showinfo( "Titre", "Message d'information")
messagebox.showwarning("Titre", "Attention !")
messagebox.showerror( "Titre", "Une erreur est survenue")
# Confirmation
ok = messagebox.askyesno("Confirmer", "Vraiment quitter ?")
if ok: root.quit()
# Dialogues de fichier
path = filedialog.askopenfilename(
title="Ouvrir un fichier",
filetypes=[("Fichiers texte", "*.txt"), ("Tous", "*.*")]
)
save_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Texte", "*.txt")]
)
folder = filedialog.askdirectory(title="Choisir un dossier")
# Saisie simple
nom = simpledialog.askstring("Saisie", "Votre nom ?")
age = simpledialog.askinteger("Saisie", "Votre âge ?", minvalue=0)
# Sélecteur de couleur
color = colorchooser.askcolor(title="Choisir une couleur")
# → ((r, g, b), "#rrggbb")
class SettingsDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Paramètres")
self.geometry("350x200")
self.resizable(False, False)
self.result = None
# Rendre modale (bloque la fenêtre parent)
self.transient(parent)
self.grab_set()
self._build()
# Attendre la fermeture avant de rendre la main
parent.wait_window(self)
def _build(self):
self.theme_var = tk.StringVar(value="sombre")
tk.Label(self, text="Thème :").pack(pady=(15,5))
for t in ["clair", "sombre"]:
tk.Radiobutton(self, text=t.capitalize(),
variable=self.theme_var, value=t).pack()
tk.Button(self, text="OK", command=self.on_ok).pack(pady=15)
def on_ok(self):
self.result = self.theme_var.get()
self.destroy()
# Utilisation
dlg = SettingsDialog(root)
if dlg.result:
print(f"Thème choisi : {dlg.result}")
Canvas & dessin
canvas = tk.Canvas(root, width=500, height=400,
bg="white", cursor="crosshair")
canvas.pack(fill=tk.BOTH, expand=True)
# Toutes les méthodes retournent un ID d'élément
rect_id = canvas.create_rectangle(
50, 50, 150, 100,
fill="#818cf8", outline="#4f46e5", width=2)
cercle_id = canvas.create_oval(
200, 50, 280, 130,
fill="#f97316", outline="")
ligne_id = canvas.create_line(
0, 200, 500, 200, fill="gray", dash=(4, 2))
texte_id = canvas.create_text(
250, 30, text="Mon Canvas",
font=("Arial", 16, "bold"), fill="#333")
img = tk.PhotoImage(file="icon.png")
img_id = canvas.create_image(400, 100, image=img, anchor=tk.NW)
# Modifier un élément par son ID
canvas.itemconfig(rect_id, fill="red")
canvas.move(cercle_id, 10, 0) # déplacer de (dx, dy)
canvas.coords(rect_id, 60, 60, 160, 110) # changer les coords
canvas.delete(ligne_id) # supprimer
canvas.delete("all") # tout effacer
# Bind sur un élément canvas
canvas.tag_bind(rect_id, "<Button-1>",
lambda e: canvas.itemconfig(rect_id, fill="green"))
class DrawApp:
def __init__(self, root):
self.canvas = tk.Canvas(root, bg="white",
cursor="crosshair")
self.canvas.pack(fill=tk.BOTH, expand=True)
self.last_x = self.last_y = None
self.color = "black"
self.width = 3
self.canvas.bind("<ButtonPress-1>", self.start)
self.canvas.bind("<B1-Motion>", self.draw)
self.canvas.bind("<ButtonRelease-1>", self.stop)
def start(self, e):
self.last_x, self.last_y = e.x, e.y
def draw(self, e):
if self.last_x:
self.canvas.create_line(
self.last_x, self.last_y, e.x, e.y,
fill=self.color, width=self.width,
capstyle=tk.ROUND, smooth=True)
self.last_x, self.last_y = e.x, e.y
def stop(self, e):
self.last_x = self.last_y = None
def clear(self):
self.canvas.delete("all")
Architecture POO — Application structurée
class App(tk.Tk):
"""Fenêtre principale — hérite de Tk."""
def __init__(self):
super().__init__()
self.title("Mon App")
self.geometry("800x600")
self._center()
self._build_menu()
# Conteneur pour toutes les vues
container = tk.Frame(self)
container.pack(fill=tk.BOTH, expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (HomeView, EditorView, SettingsView):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(HomeView)
def show_frame(self, view_class):
"""Afficher une vue et masquer les autres."""
frame = self.frames[view_class]
frame.tkraise()
def _center(self):
w, h = 800, 600
x = (self.winfo_screenwidth() - w) // 2
y = (self.winfo_screenheight() - h) // 2
self.geometry(f"{w}x{h}+{x}+{y}")
def _build_menu(self):
menubar = tk.Menu(self)
nav = tk.Menu(menubar, tearoff=0)
nav.add_command(label="Accueil", command=lambda: self.show_frame(HomeView))
nav.add_command(label="Éditeur", command=lambda: self.show_frame(EditorView))
nav.add_command(label="Paramètres",command=lambda: self.show_frame(SettingsView))
menubar.add_cascade(label="Navigation", menu=nav)
self.config(menu=menubar)
class HomeView(tk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
tk.Label(self, text="Accueil", font=("Arial", 24)).pack(pady=50)
tk.Button(self, text="Ouvrir l'éditeur",
command=lambda: controller.show_frame(EditorView)).pack()
if __name__ == "__main__":
app = App()
app.mainloop()
Le pattern App + Frame par vue empile toutes les vues dans un même conteneur et utilise tkraise() pour afficher la vue active. Les vues reçoivent une référence au controller (l'App) pour pouvoir naviguer entre elles.
Style personnalisé global :
style = ttk.Style()
style.theme_use("clam") # alt|clam|classic|default
style.configure("TButton",
background="#818cf8", foreground="white",
font=("Arial", 11), padding=8, relief="flat")
style.map("TButton",
background=[("active", "#6366f1")])
style.configure("TEntry",
fieldbackground="#f8f8ff", borderwidth=1)
style.configure("TFrame", background="#1e1e2e")
ttk — Widgets modernes
# Notebook — onglets
notebook = ttk.Notebook(root)
notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
tab1 = ttk.Frame(notebook)
tab2 = ttk.Frame(notebook)
notebook.add(tab1, text="Général")
notebook.add(tab2, text="Avancé")
# Détecter le changement d'onglet
notebook.bind("<<NotebookTabChanged>>",
lambda e: print(notebook.index(notebook.select())))
# Treeview — tableau / arbre
tree = ttk.Treeview(root, columns=("nom", "age", "ville"),
show="headings", height=8)
tree.heading("nom", text="Nom")
tree.heading("age", text="Âge")
tree.heading("ville", text="Ville")
tree.column("nom", width=120)
tree.column("age", width=60, anchor=tk.CENTER)
tree.column("ville", width=120)
data = [("Alice", 28, "Paris"), ("Bob", 34, "Lyon")]
for row in data:
tree.insert("", tk.END, values=row)
# Lire la sélection
def on_select(e):
item = tree.selection()[0]
vals = tree.item(item, "values") # tuple
tree.bind("<<TreeviewSelect>>", on_select)
# Trier en cliquant sur l'en-tête
def sort_col(col):
items = [(tree.set(k, col), k) for k in tree.get_children()]
for i, (_, k) in enumerate(sorted(items)):
tree.move(k, "", i)
# Progressbar déterminée
progress = ttk.Progressbar(root, length=300,
mode="determinate",
maximum=100)
progress.pack(pady=10)
progress["value"] = 65 # 65%
# Progressbar indéterminée (chargement)
busy = ttk.Progressbar(root, mode="indeterminate")
busy.start(50) # intervalle en ms
busy.stop()
# Séparateur horizontal
ttk.Separator(root, orient=tk.HORIZONTAL).pack(
fill=tk.X, padx=10, pady=5)
# PanedWindow — zones redimensionnables
paned = ttk.PanedWindow(root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True)
left = ttk.Frame(paned, width=200)
right = ttk.Frame(paned)
paned.add(left, weight=1)
paned.add(right, weight=3)
Préférer les widgets ttk aux widgets tk classiques dès que possible — ils s'adaptent mieux au thème de l'OS et permettent une stylisation cohérente via ttk.Style.
Projet — Gestionnaire de tâches
Application complète : liste de tâches avec ajout, suppression, marquage "fait", persistance JSON, et interface structurée en POO.
todo_app/
├── main.py ← point d'entrée
├── app.py ← classe App (Tk)
├── views/
│ ├── task_view.py ← vue principale
│ └── add_dialog.py ← dialogue ajout
├── models/
│ └── task.py ← dataclass Task
└── data/
└── tasks.json ← persistance
import json, tkinter as tk
from tkinter import ttk, messagebox
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class Task:
title: str
done: bool = False
class TaskView(tk.Frame):
DATA_FILE = Path("data/tasks.json")
def __init__(self, parent):
super().__init__(parent, padx=15, pady=15)
self.tasks: list[Task] = []
self._build()
self._load()
def _build(self):
# Barre d'ajout
top = tk.Frame(self)
top.pack(fill=tk.X, pady=(0, 10))
self.entry_var = tk.StringVar()
entry = tk.Entry(top, textvariable=self.entry_var, width=35)
entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
entry.bind("<Return>", lambda e: self.add_task())
tk.Button(top, text="+ Ajouter", command=self.add_task).pack(
side=tk.LEFT, padx=(5, 0))
# Liste des tâches
self.tree = ttk.Treeview(self,
columns=("status", "title"),
show="headings", height=15)
self.tree.heading("status", text="✓")
self.tree.heading("title", text="Tâche")
self.tree.column("status", width=40, anchor=tk.CENTER)
self.tree.column("title", width=350)
self.tree.pack(fill=tk.BOTH, expand=True)
self.tree.bind("<Double-Button-1>", lambda e: self.toggle_task())
# Boutons d'action
btns = tk.Frame(self)
btns.pack(fill=tk.X, pady=(10, 0))
tk.Button(btns, text="✓ Fait", command=self.toggle_task).pack(side=tk.LEFT)
tk.Button(btns, text="🗑 Supprimer", command=self.delete_task).pack(side=tk.LEFT, padx=5)
tk.Button(btns, text="💾 Sauvegarder", command=self._save).pack(side=tk.RIGHT)
def _refresh(self):
self.tree.delete(*self.tree.get_children())
for t in self.tasks:
tag = "done" if t.done else ""
self.tree.insert("", tk.END,
values=("✓" if t.done else "○", t.title), tags=(tag,))
self.tree.tag_configure("done", foreground="gray")
def add_task(self):
title = self.entry_var.get().strip()
if not title: return
self.tasks.append(Task(title))
self.entry_var.set("")
self._refresh()
def toggle_task(self):
sel = self.tree.selection()
if not sel: return
idx = self.tree.index(sel[0])
self.tasks[idx].done = not self.tasks[idx].done
self._refresh()
def delete_task(self):
sel = self.tree.selection()
if not sel: return
if messagebox.askyesno("Confirmer", "Supprimer ?"):
self.tasks.pop(self.tree.index(sel[0]))
self._refresh()
def _save(self):
self.DATA_FILE.parent.mkdir(exist_ok=True)
self.DATA_FILE.write_text(
json.dumps([asdict(t) for t in self.tasks], indent=2))
messagebox.showinfo("Sauvegardé", "Tâches enregistrées.")
def _load(self):
if self.DATA_FILE.exists():
data = json.loads(self.DATA_FILE.read_text())
self.tasks = [Task(**d) for d in data]
self._refresh()
Cheat Sheet Tkinter
🪟 Fenêtre & boucle
tk.Tk() | Fenêtre principale |
.geometry("WxH+X+Y") | Taille et position |
.title("...") | Titre de la fenêtre |
.mainloop() | Démarrer la boucle |
.after(ms, fn) | Exécution différée |
tk.Toplevel() | Fenêtre secondaire |
🧩 Widgets courants
tk.Label(p, text=) | Texte statique |
tk.Button(p, command=) | Bouton cliquable |
tk.Entry(p, textvariable=) | Saisie 1 ligne |
tk.Text(p, wrap=WORD) | Saisie multi-lignes |
ttk.Combobox(p, values=) | Liste déroulante |
ttk.Treeview(p, columns=) | Tableau de données |
📐 Layouts
.pack(side=, fill=, expand=) | Empilement |
.grid(row=, column=, sticky=) | Grille |
.place(x=, y=) | Absolu |
columnconfigure(i, weight=1) | Colonne extensible |
sticky="nsew" | Remplir la cellule |
columnspan=2 | Fusionner colonnes |
⚡ Événements
.bind("<Return>", fn) | Touche Entrée |
.bind("<Button-1>", fn) | Clic gauche |
.bind("<Control-s>", fn) | Ctrl+S |
StringVar().trace_add() | Réagir au changement |
messagebox.askyesno() | Dialogue Oui/Non |
filedialog.askopenfilename() | Ouvrir un fichier |