logo oujood
🔍

Projet Tkinter : gestionnaire de tâches

Un gestionnaire de tâches avec ajout, suppression, filtrage et sauvegarde JSON. Ce projet combine Listbox, Entry, Checkbutton, filedialog et organisation en classes.

OUJOOD.COM

Ce deuxième projet est plus orienté données que la calculatrice. Il introduit la persistance (sauvegarde JSON), le filtrage d'une liste, et l'organisation en classes avec plusieurs responsabilités bien séparées. Les concepts utilisés : Entry, Listbox, StringVar, messagebox et bind().

Fonctionnalités

L'application gère des tâches avec un statut "fait / à faire". Elle permet d'ajouter une tâche, de la marquer comme faite d'un double-clic, de la supprimer et de filtrer l'affichage. Les tâches sont sauvegardées dans un fichier JSON au même endroit que le script.

Code complet

📋 Copier le code

import tkinter as tk
from tkinter import messagebox
import json
import os

FICHIER = "taches.json"

class GestionnaireTaches(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title("Gestionnaire de tâches")
        self.geometry("480x420")
        self.resizable(False, False)
        self.protocol("WM_DELETE_WINDOW", self._quitter)
        self._taches = []          # [{"texte": str, "faite": bool}]
        self._filtre  = tk.StringVar(value="toutes")
        self._charger()
        self._creer_interface()
        self._rafraichir_liste()

    def _creer_interface(self):
        # Barre de saisie
        frame_top = tk.Frame(self, bg="#f5f5f5")
        frame_top.pack(fill="x", padx=10, pady=10)

        self._champ = tk.Entry(frame_top, font=("Arial", 12), width=32)
        self._champ.pack(side="left", ipady=4)
        self._champ.bind("", lambda e: self._ajouter())

        tk.Button(frame_top, text="Ajouter", command=self._ajouter,
                  bg="#1565c0", fg="white", relief="flat",
                  padx=12, pady=4).pack(side="left", padx=6)

        # Filtres
        frame_filtres = tk.Frame(self, bg="#f5f5f5")
        frame_filtres.pack(fill="x", padx=10)

        for val, texte in [("toutes","Toutes"), ("a_faire","À faire"), ("faites","Faites")]:
            tk.Radiobutton(
                frame_filtres, text=texte, variable=self._filtre,
                value=val, command=self._rafraichir_liste,
                bg="#f5f5f5"
            ).pack(side="left", padx=6)

        # Liste avec scrollbar
        frame_liste = tk.Frame(self)
        frame_liste.pack(fill="both", expand=True, padx=10, pady=8)

        scrollbar = tk.Scrollbar(frame_liste)
        scrollbar.pack(side="right", fill="y")

        self._listbox = tk.Listbox(
            frame_liste, font=("Arial", 11), height=12,
            yscrollcommand=scrollbar.set,
            selectbackground="#bbdefb"
        )
        self._listbox.pack(side="left", fill="both", expand=True)
        scrollbar.config(command=self._listbox.yview)
        self._listbox.bind("", lambda e: self._basculer())

        # Boutons d'action
        frame_btn = tk.Frame(self)
        frame_btn.pack(pady=6)

        tk.Button(frame_btn, text="✓ Marquer faite",
                  command=self._basculer, width=16).pack(side="left", padx=5)
        tk.Button(frame_btn, text="✕ Supprimer",
                  command=self._supprimer,
                  bg="#ef5350", fg="white", relief="flat", width=12).pack(side="left", padx=5)

        # Compteur
        self._label_compteur = tk.Label(self, text="", fg="#777")
        self._label_compteur.pack(pady=4)

    def _ajouter(self):
        texte = self._champ.get().strip()
        if not texte:
            return
        self._taches.append({"texte": texte, "faite": False})
        self._champ.delete(0, tk.END)
        self._sauvegarder()
        self._rafraichir_liste()

    def _basculer(self):
        indices = self._listbox.curselection()
        if not indices:
            return
        idx_reel = self._indices_visibles()[indices[0]]
        self._taches[idx_reel]["faite"] = not self._taches[idx_reel]["faite"]
        self._sauvegarder()
        self._rafraichir_liste()

    def _supprimer(self):
        indices = self._listbox.curselection()
        if not indices:
            return
        idx_reel = self._indices_visibles()[indices[0]]
        texte = self._taches[idx_reel]["texte"]
        if messagebox.askyesno("Confirmer", f"Supprimer « {texte} » ?"):
            del self._taches[idx_reel]
            self._sauvegarder()
            self._rafraichir_liste()

    def _indices_visibles(self):
        filtre = self._filtre.get()
        return [
            i for i, t in enumerate(self._taches)
            if filtre == "toutes"
            or (filtre == "faites"  and t["faite"])
            or (filtre == "a_faire" and not t["faite"])
        ]

    def _rafraichir_liste(self):
        self._listbox.delete(0, tk.END)
        for i in self._indices_visibles():
            t = self._taches[i]
            prefixe = "✓ " if t["faite"] else "○ "
            self._listbox.insert(tk.END, prefixe + t["texte"])
            if t["faite"]:
                self._listbox.itemconfig(tk.END, fg="#aaa")

        total  = len(self._taches)
        faites = sum(1 for t in self._taches if t["faite"])
        self._label_compteur.config(
            text=f"{faites}/{total} tâche(s) terminée(s)"
        )

    def _sauvegarder(self):
        with open(FICHIER, "w", encoding="utf-8") as f:
            json.dump(self._taches, f, ensure_ascii=False, indent=2)

    def _charger(self):
        if os.path.exists(FICHIER):
            try:
                with open(FICHIER, "r", encoding="utf-8") as f:
                    self._taches = json.load(f)
            except (json.JSONDecodeError, KeyError):
                self._taches = []

    def _quitter(self):
        self._sauvegarder()
        self.destroy()

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

Ce que ce projet illustre

La séparation entre les données (self._taches), l'affichage (_rafraichir_liste()) et les actions (_ajouter, _supprimer) est le schéma MVC simplifié — une action modifie les données puis appelle le rafraîchissement. La liste filtrée est calculée à la volée via _indices_visibles() plutôt que stockée séparément, ce qui évite les désynchronisations. Ce type d'architecture est réutilisable pour n'importe quelle liste d'objets — y compris des données chargées depuis un CSV avec Pandas.

Par carabde | Mis à jour le 30 avril 2025