Objectifs
- Comprendre les options pour créer une interface (GUI) en Python.
- Savoir choisir un toolkit (tkinter, PySide6/Qt, Kivy, etc.) selon votre besoin.
- Construire des interfaces robustes (layouts, événements, threads, validation, styles).
- Tester et empaqueter votre application pour la distribution.
- Exploiter VS Code + Copilot Chat pour accélérer le développement.
Prérequis : Notions de base en Python (fonctions, modules), Python 3.10+ recommandé, VS Code (ou IDE équivalent).
1) Choisir son toolkit GUI en Python
Décision rapide :
- Débutant / standard / sans dépendances externes → tkinter (inclus avec Python).
- Look & feel moderne, très puissant, multiplateforme → PySide6 (Qt for Python) (ou PyQt6).
- Cibler desktop + mobile (Android/iOS) → Kivy.
- tkinter mais avec thèmes modernes → customtkinter (surcouche moderne de tkinter).
Remarque licences :
- tkinter : livré avec Python, très permissif.
- PySide6 : LGPL (souvent plus simple pour des apps distribuées).
- PyQt6 : GPL/commercial (bien vérifier l’adéquation à votre projet).
- Kivy : MIT (très permissif).
2) Préparer l’environnement
Créer un environnement virtuel
Python:
python -m venv .venv
Windows
.venv\Scripts\activate
macOS / Linux
source .venv/bin/activate
Installer selon votre choix
Python:
Tkinter : déjà inclus (sur macOS/Linux, il peut falloir installer Tk via le gestionnaire de paquets)
pip install customtkinter # (optionnel) pour des thèmes modernes
pip install PySide6 # Qt for Python (recommandé)
ou
pip install PyQt6
pip install kivy
3) Créer une GUI avec tkinter (base standard)
3.1 Hello, World (tkinter)
Python:
import tkinter as tk
root = tk.Tk()
root.title("Hello tkinter")
root.geometry("320x150")
label = tk.Label(root, text="Bonjour, tkinter !")
label.pack(pady=10)
def on_click():
label.config(text="Bouton cliqué !")
btn = tk.Button(root, text="Clique-moi", command=on_click)
btn.pack(pady=5)
root.mainloop()
Explications :
- Tk() : crée la fenêtre principale.
- pack() : place les widgets en pile (simple). Alternatives : grid() (grille), place() (position absolue).
- command= : lie un callback Python à un bouton.
Python:
pack : simple empilement vertical/horizontal
label.pack(side="top", fill="x", padx=10, pady=5)
grid : gestion en lignes/colonnes
label.grid(row=0, column=0, padx=10, pady=5, sticky="w")
entry.grid(row=0, column=1, padx=10, pady=5, sticky="ew")
root.grid_columnconfigure(1, weight=1) # colonne qui s'étire
3.3 Variables de contrôle et validation
Python:
import tkinter as tk
root = tk.Tk()
root.title("Validation simple")
var = tk.StringVar(value="0")
def inc():
try:
n = int(var.get())
except ValueError:
n = 0
var.set(str(n + 1))
tk.Label(root, text="Compteur :").grid(row=0, column=0, padx=5, pady=5, sticky="e")
entry = tk.Entry(root, textvariable=var, width=10)
entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
tk.Button(root, text="+1", command=inc).grid(row=1, column=0, columnspan=2, pady=10)
root.mainloop()
3.4 Longues tâches : threading pour éviter le gel de l’UI
Python:
import tkinter as tk
import threading, time
root = tk.Tk()
root.title("Threading avec tkinter")
status = tk.StringVar(value="Prêt")
tk.Label(root, textvariable=status).pack(pady=8)
def long_task():
status.set("Traitement en cours...")
time.sleep(3) # Simule une tâche lourde
# Revenir au thread GUI via .after
root.after(0, lambda: status.set("Terminé !"))
def start_task():
threading.Thread(target=long_task, daemon=True).start()
tk.Button(root, text="Lancer", command=start_task).pack(pady=8)
root.mainloop()
Points clés : Ne bloquez jamais le thread principal (celui de mainloop). Pour mettre à jour l’UI depuis un thread, utilisez .after().
4) Créer une GUI avec PySide6 (Qt for Python)
4.1 Hello, World (PySide6)
Python:
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout
import sys
app = QApplication(sys.argv)
win = QWidget()
win.setWindowTitle("Hello PySide6")
label = QLabel("Bonjour, Qt !")
btn = QPushButton("Clique-moi")
def on_click():
label.setText("Bouton cliqué !")
btn.clicked.connect(on_click)
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(btn)
win.setLayout(layout)
win.resize(320, 150)
win.show()
sys.exit(app.exec())
Explications :
- Signals/slots : clicked → on_click.
- Layouts (QVBoxLayout, QHBoxLayout, QGridLayout) gèrent proprement les redimensionnements.
Python:
from PySide6.QtWidgets import (QApplication, QWidget, QLineEdit, QPushButton,
QListWidget, QVBoxLayout, QHBoxLayout)
import sys
class Todo(QWidget):
def init(self):
super().init()
self.setWindowTitle("Todo (PySide6)")
self.input = QLineEdit()
self.input.setPlaceholderText("Nouvelle tâche...")
self.btn_add = QPushButton("Ajouter")
self.list = QListWidget()
top = QHBoxLayout()
top.addWidget(self.input)
top.addWidget(self.btn_add)
layout = QVBoxLayout(self)
layout.addLayout(top)
layout.addWidget(self.list)
self.btn_add.clicked.connect(self.add_item)
self.input.returnPressed.connect(self.add_item)
def add_item(self):
text = self.input.text().strip()
if text:
self.list.addItem(text)
self.input.clear()
if name == "main":
app = QApplication(sys.argv)
w = Todo()
w.resize(400, 300)
w.show()
sys.exit(app.exec())
4.3 Longues tâches : QThread + signaux
Python:
from PySide6.QtCore import QObject, QThread, Signal
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QProgressBar
import sys, time
class Worker(QObject):
progress = Signal(int)
finished = Signal()
def run(self):
for i in range(101):
time.sleep(0.02)
self.progress.emit(i)
self.finished.emit()
class Window(QWidget):
def init(self):
super().init()
self.setWindowTitle("QThread & Progress")
self.label = QLabel("Prêt")
self.bar = QProgressBar()
self.btn = QPushButton("Lancer")
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.bar)
layout.addWidget(self.btn)
self.btn.clicked.connect(self.start)
def start(self):
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.progress.connect(self.bar.setValue)
self.worker.finished.connect(self.on_finished)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.label.setText("En cours…")
self.thread.start()
def on_finished(self):
self.label.setText("Terminé !")
if name == "main":
app = QApplication(sys.argv)
w = Window()
w.resize(360, 180)
w.show()
sys.exit(app.exec())
5) Créer une GUI avec Kivy (option mobile)
5.1 Hello, World (Kivy)
Python:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class MyApp(App):
def build(self):
root = BoxLayout(orientation='vertical', padding=10, spacing=10)
self.label = Label(text="Bonjour, Kivy !")
btn = Button(text="Clique-moi")
btn.bind(on_press=self.on_click)
root.add_widget(self.label)
root.add_widget(btn)
return root
def on_click(self, _):
self.label.text = "Bouton cliqué !"
MyApp().run()
Python:
fichier: myui.kv
BoxLayout:
orientation: "vertical"
padding: 10
spacing: 10
Label:
id: lbl
text: "Bonjour, Kivy !"
Button:
text: "Clique-moi"
on_press: app.on_click()
Python:
main.py
from kivy.app import App
from kivy.lang import Builder
class MyApp(App):
def build(self):
return Builder.load_file("myui.kv")
def on_click(self):
self.root.ids.lbl.text = "Bouton cliqué !"
MyApp().run()
6) Architecture & bonnes pratiques
6.1 Séparer l’UI et la logique (ex. MVC simplifié)
Python:
mon_app/
app.py # point d'entrée
ui/ # vues
main_window.py
core/ # logique métier
services.py
data/ # accès aux données (fichiers, DB)
repository.py
assets/ # icônes, images
Python:
core/services.py
def calc_tva(montant_ht: float, taux: float = 0.2) -> float:
return round(montant_ht * (1 + taux), 2)
Python:
ui/main_window.py
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QLabel
from core.services import calc_tva
class MainWindow(QWidget):
def init(self):
super().init()
self.setWindowTitle("Calcul TVA")
self.inp = QLineEdit(placeholderText="Montant HT")
self.out = QLabel("Résultat : -")
layout = QVBoxLayout(self)
layout.addWidget(self.inp)
layout.addWidget(self.out)
self.inp.textChanged.connect(self.on_change)
def on_change(self, _):
try:
ht = float(self.inp.text())
self.out.setText(f"Résultat : {calc_tva(ht)} €")
except ValueError:
self.out.setText("Résultat : -")
Python:
app.py
from PySide6.QtWidgets import QApplication
import sys
from ui.main_window import MainWindow
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
- Évitez de mettre la logique métier dans les callbacks UI.
- Centralisez la configuration (constantes, chemins, i18n).
- Ajoutez des logs (module logging) pour diagnostiquer.
7) Thèmes & Styles
tkinter
- Utilisez ttk pour des widgets plus modernes.
- customtkinter apporte un look moderne (dark mode, sliders, etc.).
Python:
import customtkinter as ctk
ctk.set_default_color_theme("dark-blue")
ctk.set_appearance_mode("dark")
app = ctk.CTk()
app.title("Modern tkinter")
ctk.CTkLabel(app, text="Bonjour !").pack(pady=10)
ctk.CTkButton(app, text="Action").pack(pady=10)
app.mainloop()
- Styles intégrés (Fusion, Windows, etc.) + Stylesheets (type CSS) pour personnaliser.
Python:
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
import sys
app = QApplication(sys.argv)
app.setStyle("Fusion")
w = QWidget()
btn = QPushButton("Accent")
w.setStyleSheet("""
QWidget { background: #1e1e1e; color: #ddd; }
QPushButton { background: #3a86ff; color: white; border-radius: 6px; padding: 6px 12px; }
QPushButton:hover { background: #2f6adf; }
""")
layout = QVBoxLayout(w)
layout.addWidget(btn)
w.resize(300,120)
w.show()
sys.exit(app.exec())
8) Internationalisation & Accessibilité
i18n (Qt) : utilisez QTranslator + fichiers .qm.
tkinter : centralisez les chaînes dans un module et chargez-les selon la locale.
Accessibilité :
- Fournissez des labels explicites et des raccourcis clavier (Alt+Lettre).
- Respectez le contraste, permettez le redimensionnement des polices.
- Ordre de tabulation logique (Qt : setTabOrder).
9) Tests (exemple rapide avec PySide6 + pytest-qt)
Optionnel si vous utilisez Qt[/i]
Python:
test_todo.py
def test_add_item(qtbot):
from app import Todo # votre classe QWidget
w = Todo()
qtbot.addWidget(w)
w.show()
w.input.setText("Tâche A")
qtbot.mouseClick(w.btn_add, Qt.LeftButton)
assert w.list.count() == 1
assert w.list.item(0).text() == "Tâche A"
10) Empaquetage (créer un .exe / .app / binaire)
PyInstaller (générique)
Python:
pip install pyinstaller
pyinstaller --name MonAppli --noconsole --onefile app.py
ou en dossier avec ressources :
pyinstaller --name MonAppli --noconsole --add-data "assets:assets" app.py
- Testez sur l’OS cible (Windows/macOS/Linux) — les binaires ne sont pas portables entre OS.
- Sur macOS, signature et notarisation peuvent être nécessaires.
- Pour Qt, incluez les plugins requis (PyInstaller le gère souvent automatiquement).
11) VS Code + Copilot Chat : accélérer votre développement
Configuration rapide
- Installez l’extension Python (Microsoft) et sélectionnez l’interpréteur de votre venv.
- Démarrez un Run & Debug simple (bouton ▶) pour tester votre app.
- Ouvrez Copilot Chat (panneau latéral) pour converser avec l’IA dans votre contexte de code.
Objectif : Générer une fenêtre tkinter minimale avec un compteur et un bouton +1
Prompt à Copilot Chat :
« Crée une application tkinter avec une fenêtre 320x150, un label "Compteur : 0" et un bouton "+1" qui incrémente le compteur sans bloquer l’UI. Utilise grid et une StringVar. »
Réponse attendue (extrait plausible) :
Python:
import tkinter as tk
root = tk.Tk()
root.title("Compteur")
root.geometry("320x150")
val = tk.StringVar(value="0")
def incr():
val.set(str(int(val.get()) + 1))
tk.Label(root, text="Compteur :").grid(row=0, column=0, padx=8, pady=10, sticky="e")
tk.Label(root, textvariable=val).grid(row=0, column=1, padx=8, pady=10, sticky="w")
tk.Button(root, text="+1", command=incr).grid(row=1, column=0, columnspan=2, pady=10)
root.mainloop()
Objectif : Refactoriser en séparant logique & UI (PySide6)
Prompt à Copilot Chat :
« Refactorise ce fichier PySide6 pour séparer la logique de calcul de la TVA dans un module core/services.py et importe la fonction. Écris le code structuré (app.py, ui/main_window.py, core/services.py). »
Objectif : Ajouter un thread pour une tâche longue (PySide6)
Prompt à Copilot Chat :
« Dans cette fenêtre PySide6, ajoute un Worker sur QThread qui met à jour une QProgressBar (0→100) et remet le label à 'Terminé !' à la fin. Donne le code complet et libère correctement le thread. »
Objectif : Générer un style sombre (Qt Stylesheet)
Prompt à Copilot Chat :
« Propose un stylesheet Qt sombre avec bouton primaire bleu, hover plus foncé, texte contrasté et fond #1e1e1e. Applique-le à l’ensemble de la fenêtre. »
Astuce : Collez un extrait de votre code dans Copilot Chat et demandez « Explique ce que fait ce code », « Propose des tests unitaires », ou « Optimise ce layout sans changer le comportement ».Objectif : Aide au débogage
Prompt à Copilot Chat :
« Mon app PyInstaller sous Windows affiche 'failed to execute script' ou 'could not find Qt platform plugin'. Liste les causes probables et donne la commande PyInstaller corrigée avec --add-data/--hidden-import si nécessaire. »
12) Foire aux problèmes courants
tkinter
- L’UI « gèle » → une fonction lourde bloque le thread principal. Solution : threading + .after().
- TclError: no display name (Linux/CI) → serveur X manquant. Lancez sur une machine avec affichage ou utilisez xvfb.
- Plusieurs Tk() → n’avoir qu’un seul Tk(), utiliser Toplevel() pour des fenêtres supplémentaires.
- Qt platform plugin "xcb" non trouvé (Linux) → installer dépendances système (libxcb, etc.).
- Crash à la fermeture → Destroy order : relier finished → thread.quit, deleteLater sur worker/thread.
- Path des ressources → utilisez QResource ou chemins relatifs embarqués via PyInstaller --add-data.
- Dépendances SDL2/GLEW manquantes → suivez la doc d’installation spécifique à l’OS.
- Affichage vide → testez un exemple minimal pour vérifier l’accélération graphique.
13) Check-list de publication
- Désactivez les prints inutiles, activez des logs de niveau INFO/ERROR.
- Ajoutez un écran « À propos » (version, licence, crédits).
- Testez les tailles d’écran, thèmes clair/sombre, accessibilité (tab, contrastes).
- Packager via PyInstaller (ou Briefcase/Flet selon la stack choisie) et testez sur l’OS cible.
Conclusion
Vous pouvez démarrer très vite avec tkinter pour un utilitaire simple, ou choisir PySide6 si vous souhaitez un rendu moderne et des fonctionnalités avancées. Kivy devient pertinent si vous visez desktop + mobile avec la même base.
Vous voulez que je vous génère un projet squelette clé-en-main (structure de dossiers, code de base, scripts de lancement) pour tkinter ou PySide6 ? Dites-moi :
- Le toolkit choisi (tkinter / PySide6 / Kivy / customtkinter)
- Votre OS cible principal (Windows / macOS / Linux)
- Un besoin concret (ex : « petit gestionnaire de tâches », « calcul TVA », « formulaire avec validation »)