logo oujood
🔍

Projet Tkinter : mini application complète

Un éditeur de texte minimaliste mais complet : barre de menus, ouverture et sauvegarde de fichiers, compteur de mots en temps réel et bascule thème clair/sombre.

OUJOOD.COM

Ce dernier projet assemble tous les concepts du cours dans une application réelle : un éditeur de texte minimaliste avec barre de menus, ouverture et sauvegarde de fichiers, compteur de mots en temps réel, bascule thème clair/sombre et protection contre la fermeture sans sauvegarde. Une synthèse pratique avant de passer à des projets plus ambitieux.

Fonctionnalités couvertes

L'éditeur intègre : barre de menus avec raccourcis clavier, filedialog pour ouvrir et sauvegarder, widget Text avec Scrollbar, compteur de mots via after(), thème sombre basculable, et interception de la fermeture via WM_DELETE_WINDOW.

Code complet

📋 Copier le code

import tkinter as tk
from tkinter import filedialog, messagebox
import os

THEMES = {
    "clair": {
        "bg": "#ffffff", "fg": "#212121",
        "barre_bg": "#f5f5f5", "status_fg": "#777"
    },
    "sombre": {
        "bg": "#1e1e1e", "fg": "#d4d4d4",
        "barre_bg": "#2d2d2d", "status_fg": "#aaa"
    }
}

class Editeur(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title("Éditeur — sans titre")
        self.geometry("720x500")
        self._fichier_actuel = None
        self._modifie        = False
        self._theme_actuel   = "clair"
        self._creer_interface()
        self._creer_menus()
        self._lier_raccourcis()
        self.protocol("WM_DELETE_WINDOW", self._quitter)
        self._mise_a_jour_compteur()

    def _creer_interface(self):
        # Zone de texte + scrollbar
        frame = tk.Frame(self)
        frame.pack(fill="both", expand=True)

        self._scrollbar = tk.Scrollbar(frame)
        self._scrollbar.pack(side="right", fill="y")

        self._texte = tk.Text(
            frame, wrap="word", font=("Courier New", 12),
            undo=True, yscrollcommand=self._scrollbar.set
        )
        self._texte.pack(fill="both", expand=True)
        self._scrollbar.config(command=self._texte.yview)
        self._texte.bind("<>", self._sur_modification)

        # Barre de statut
        self._barre_statut = tk.Label(
            self, text="0 mot | 0 caractère",
            anchor="e", padx=10, pady=3
        )
        self._barre_statut.pack(fill="x", side="bottom")

    def _creer_menus(self):
        barre = tk.Menu(self)
        self.config(menu=barre)

        # Menu Fichier
        m_fichier = tk.Menu(barre, tearoff=0)
        barre.add_cascade(label="Fichier", menu=m_fichier)
        m_fichier.add_command(label="Nouveau",          accelerator="Ctrl+N", command=self._nouveau)
        m_fichier.add_command(label="Ouvrir…",          accelerator="Ctrl+O", command=self._ouvrir)
        m_fichier.add_separator()
        m_fichier.add_command(label="Enregistrer",      accelerator="Ctrl+S", command=self._enregistrer)
        m_fichier.add_command(label="Enregistrer sous…",                       command=self._enregistrer_sous)
        m_fichier.add_separator()
        m_fichier.add_command(label="Quitter",          accelerator="Ctrl+Q", command=self._quitter)

        # Menu Édition
        m_edition = tk.Menu(barre, tearoff=0)
        barre.add_cascade(label="Édition", menu=m_edition)
        m_edition.add_command(label="Annuler",  accelerator="Ctrl+Z", command=lambda: self._texte.edit_undo())
        m_edition.add_command(label="Rétablir", accelerator="Ctrl+Y", command=lambda: self._texte.edit_redo())
        m_edition.add_separator()
        m_edition.add_command(label="Tout sélectionner", accelerator="Ctrl+A",
                              command=lambda: self._texte.tag_add("sel", "1.0", tk.END))

        # Menu Affichage
        m_affichage = tk.Menu(barre, tearoff=0)
        barre.add_cascade(label="Affichage", menu=m_affichage)
        m_affichage.add_command(label="Basculer thème clair/sombre",
                                accelerator="Ctrl+T", command=self._basculer_theme)

    def _lier_raccourcis(self):
        self.bind("", lambda e: self._nouveau())
        self.bind("", lambda e: self._ouvrir())
        self.bind("", lambda e: self._enregistrer())
        self.bind("", lambda e: self._quitter())
        self.bind("", lambda e: self._basculer_theme())

    def _nouveau(self):
        if self._confirmer_abandon():
            self._texte.delete("1.0", tk.END)
            self._fichier_actuel = None
            self._modifie = False
            self.title("Éditeur — sans titre")

    def _ouvrir(self):
        if not self._confirmer_abandon():
            return
        chemin = filedialog.askopenfilename(
            filetypes=[("Fichiers texte", "*.txt"), ("Tous", "*.*")]
        )
        if chemin:
            with open(chemin, "r", encoding="utf-8") as f:
                contenu = f.read()
            self._texte.delete("1.0", tk.END)
            self._texte.insert("1.0", contenu)
            self._fichier_actuel = chemin
            self._modifie = False
            self.title(f"Éditeur — {os.path.basename(chemin)}")

    def _enregistrer(self):
        if self._fichier_actuel:
            self._ecrire(self._fichier_actuel)
        else:
            self._enregistrer_sous()

    def _enregistrer_sous(self):
        chemin = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Fichiers texte", "*.txt"), ("Tous", "*.*")]
        )
        if chemin:
            self._ecrire(chemin)
            self._fichier_actuel = chemin
            self.title(f"Éditeur — {os.path.basename(chemin)}")

    def _ecrire(self, chemin):
        with open(chemin, "w", encoding="utf-8") as f:
            f.write(self._texte.get("1.0", tk.END).rstrip("\n"))
        self._modifie = False
        self.title(self.title().replace(" *", ""))

    def _confirmer_abandon(self):
        if not self._modifie:
            return True
        choix = messagebox.askyesnocancel("Modifications non sauvegardées",
                                          "Sauvegarder avant de continuer ?")
        if choix is True:
            self._enregistrer()
            return True
        elif choix is False:
            return True
        return False

    def _sur_modification(self, event=None):
        if self._texte.edit_modified():
            if not self._modifie:
                self._modifie = True
                titre = self.title()
                if not titre.endswith(" *"):
                    self.title(titre + " *")
            self._texte.edit_modified(False)

    def _mise_a_jour_compteur(self):
        contenu = self._texte.get("1.0", tk.END).strip()
        mots = len(contenu.split()) if contenu else 0
        chars = len(contenu)
        self._barre_statut.config(text=f"{mots} mot{'s' if mots != 1 else ''} | {chars} caractère{'s' if chars != 1 else ''}")
        self.after(500, self._mise_a_jour_compteur)

    def _basculer_theme(self):
        self._theme_actuel = "sombre" if self._theme_actuel == "clair" else "clair"
        t = THEMES[self._theme_actuel]
        self._texte.config(bg=t["bg"], fg=t["fg"],
                           insertbackground=t["fg"])
        self._barre_statut.config(bg=t["barre_bg"], fg=t["status_fg"])

    def _quitter(self):
        if self._confirmer_abandon():
            self.destroy()

if __name__ == "__main__":
    app = Editeur()
    app.mainloop()

Ce que ce projet résume

Ce projet assemble les briques fondamentales dans un outil utilisable : la structure en classes centralise l'état, after() met à jour le compteur sans bloquer l'interface, les callbacks sont des méthodes bien nommées, et les dialogues natifs de filedialog et messagebox gèrent les interactions critiques. C'est le socle sur lequel on peut greffer un correcteur orthographique, une fonction de recherche/remplacement, ou une intégration avec Pandas pour l'analyse de contenu.

Par carabde | Mis à jour le 30 avril 2025