# Fichier: python_cheats/cheatsheets/PyQt5.txt
# Cheatsheet PyQt5 - Guide Complet pour Débutants


[OK] INTRODUCTION À PYQT5

# PyQt5 est un binding Python du framework Qt pour créer des interfaces graphiques (GUI)
# Qt est développé par Qt Company (anciennement Trolltech, Nokia)
# PyQt5 permet de créer des applications desktop cross-platform (Windows, Linux, macOS)

# Avantages:
# [OK] Cross-platform natif
# [OK] Widgets riches et personnalisables
# [OK] Performance native (C++)
# [OK] Designer graphique (Qt Designer)
# [OK] Documentation exhaustive
# [OK] Support de styles (look & feel)

# Alternatives:
# - Tkinter: Inclus avec Python, mais limité
# - wxPython: Alternative cross-platform
# - Kivy: Orienté mobile/tactile
# - PySide6: Binding officiel Qt (licence LGPL)


[OK] INSTALLATION

# Installation basique
pip install PyQt5

# Installation complète (avec Qt Designer et outils)
pip install PyQt5 PyQt5-tools

# Installation pour développement
pip install PyQt5 PyQt5-tools pyqt5-stubs

# Vérifier installation
python -c "from PyQt5 import QtWidgets; print(QtWidgets.QApplication([]))"
python -c "import PyQt5; print(PyQt5.__version__)"

# Versions
# PyQt5 5.15.x: Compatible Qt 5.15 (stable, recommandé)
# PyQt6: Nouvelle version majeure (API modernisée)

# Dépendances système (Linux)
# Ubuntu/Debian
sudo apt-get install python3-pyqt5
sudo apt-get install qttools5-dev-tools  # Qt Designer

# Fedora/RHEL
sudo dnf install python3-qt5
sudo dnf install qt5-designer


[OK] STRUCTURE DE BASE D'UNE APPLICATION PYQT5

# === Application minimale ===
import sys
from PyQt5.QtWidgets import QApplication, QWidget

# Créer l'application Qt (obligatoire, une seule instance)
app = QApplication(sys.argv)

# Créer la fenêtre principale
window = QWidget()
window.setWindowTitle('Ma Première App PyQt5')
window.setGeometry(100, 100, 400, 300)  # x, y, largeur, hauteur
window.show()

# Lancer la boucle d'événements
sys.exit(app.exec_())

# === Avec classe (recommandé) ===
from PyQt5.QtWidgets import QApplication, QWidget
import sys

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('Mon Application')
        self.setGeometry(100, 100, 800, 600)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

# === Avec QMainWindow (pour applications complexes) ===
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('Application avec Menu')
        self.setGeometry(100, 100, 1000, 700)
        
        # QMainWindow supporte:
        # - Barre de menu (menuBar)
        # - Barre d'outils (toolBar)
        # - Barre de statut (statusBar)
        # - Docks (dockWidget)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())


[OK] MODULES PYQT5 ESSENTIELS

# PyQt5.QtWidgets - Widgets et layouts
from PyQt5.QtWidgets import (
    QApplication, QWidget, QMainWindow,
    QLabel, QPushButton, QLineEdit, QTextEdit,
    QVBoxLayout, QHBoxLayout, QGridLayout,
    QMessageBox, QFileDialog, QInputDialog
)

# PyQt5.QtCore - Fonctionnalités core (signaux, slots, threads)
from PyQt5.QtCore import (
    Qt, QTimer, QThread, pyqtSignal, pyqtSlot,
    QObject, QEvent, QSize, QPoint, QRect
)

# PyQt5.QtGui - Graphiques, fonts, couleurs, icônes
from PyQt5.QtGui import (
    QIcon, QPixmap, QFont, QColor, QPalette,
    QPainter, QPen, QBrush, QImage
)

# PyQt5.QtMultimedia - Audio et vidéo
from PyQt5.QtMultimedia import (
    QMediaPlayer, QMediaContent, QAudioOutput
)

# PyQt5.QtWebEngineWidgets - Navigateur web intégré
from PyQt5.QtWebEngineWidgets import QWebEngineView

# PyQt5.QtNetwork - Réseau (HTTP, TCP, UDP)
from PyQt5.QtNetwork import (
    QNetworkAccessManager, QNetworkRequest, QNetworkReply
)

# PyQt5.QtSql - Base de données SQL
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel

# PyQt5.QtChart - Graphiques et charts
from PyQt5.QtChart import QChart, QChartView, QLineSeries


[OK] WIDGETS DE BASE

# === QLabel - Afficher texte ou image ===
from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QPixmap

# Texte simple
label = QLabel('Bonjour PyQt5')
label.setText('Nouveau texte')
text = label.text()

# Formatage HTML
label = QLabel('<h1>Titre</h1><p>Paragraphe avec <b>gras</b></p>')
label.setTextFormat(Qt.RichText)

# Alignement
label.setAlignment(Qt.AlignCenter)      # Centré
label.setAlignment(Qt.AlignLeft)        # Gauche
label.setAlignment(Qt.AlignRight)       # Droite
label.setAlignment(Qt.AlignTop)         # Haut
label.setAlignment(Qt.AlignBottom)      # Bas
label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)  # Centré H+V

# Image
label = QLabel()
pixmap = QPixmap('image.png')
label.setPixmap(pixmap)
label.setScaledContents(True)  # Redimensionner avec le label

# Taille
label.setFixedSize(200, 100)
label.setMinimumSize(100, 50)
label.setMaximumSize(400, 200)

# Style
label.setStyleSheet('QLabel { background-color: yellow; color: blue; }')

# === QPushButton - Bouton cliquable ===
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import QIcon

# Créer bouton
button = QPushButton('Cliquez-moi')
button.setText('Nouveau texte')

# Connecter au clic
button.clicked.connect(self.on_button_clicked)

def on_button_clicked(self):
    print('Bouton cliqué!')

# Bouton avec icône
button = QPushButton(QIcon('icon.png'), 'Ouvrir')

# Désactiver/activer
button.setEnabled(False)
button.setEnabled(True)
is_enabled = button.isEnabled()

# Taille icône
button.setIconSize(QSize(32, 32))

# Raccourci clavier
button.setShortcut('Ctrl+O')
button.setShortcut(Qt.Key_Return)  # Touche Entrée

# Style
button.setStyleSheet('''
    QPushButton {
        background-color: #4CAF50;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
    }
    QPushButton:hover {
        background-color: #45a049;
    }
    QPushButton:pressed {
        background-color: #3d8b40;
    }
''')

# Checkable (bouton toggle)
button.setCheckable(True)
button.setChecked(True)
is_checked = button.isChecked()

# === QLineEdit - Champ de saisie une ligne ===
from PyQt5.QtWidgets import QLineEdit

# Créer
line_edit = QLineEdit()
line_edit.setPlaceholderText('Entrez votre nom')

# Obtenir/définir texte
text = line_edit.text()
line_edit.setText('Valeur par défaut')
line_edit.clear()

# Mode de saisie
line_edit.setEchoMode(QLineEdit.Normal)      # Normal
line_edit.setEchoMode(QLineEdit.Password)    # Masqué (mot de passe)
line_edit.setEchoMode(QLineEdit.NoEcho)      # Rien affiché

# Longueur maximale
line_edit.setMaxLength(50)

# Lecture seule
line_edit.setReadOnly(True)

# Validation (nombres uniquement)
from PyQt5.QtGui import QIntValidator, QDoubleValidator
line_edit.setValidator(QIntValidator(0, 100))  # Entiers 0-100
line_edit.setValidator(QDoubleValidator(0.0, 100.0, 2))  # Décimaux

# Expression régulière
from PyQt5.QtCore import QRegExp
from PyQt5.QtGui import QRegExpValidator
regex = QRegExp('[A-Za-z0-9]+')  # Alphanumériques seulement
validator = QRegExpValidator(regex)
line_edit.setValidator(validator)

# Signaux
line_edit.textChanged.connect(self.on_text_changed)      # À chaque caractère
line_edit.returnPressed.connect(self.on_enter_pressed)   # Touche Entrée
line_edit.editingFinished.connect(self.on_editing_done)  # Focus perdu

def on_text_changed(self, text):
    print(f'Texte changé: {text}')

# Autocomplétion
from PyQt5.QtWidgets import QCompleter
completer = QCompleter(['Apple', 'Banana', 'Cherry', 'Date'])
line_edit.setCompleter(completer)

# === QTextEdit - Champ texte multiligne ===
from PyQt5.QtWidgets import QTextEdit

# Créer
text_edit = QTextEdit()
text_edit.setPlaceholderText('Entrez votre texte ici...')

# Texte brut
text = text_edit.toPlainText()
text_edit.setPlainText('Texte simple')
text_edit.clear()

# Texte HTML
html = text_edit.toHtml()
text_edit.setHtml('<h1>Titre</h1><p>Paragraphe</p>')

# Ajouter texte
text_edit.append('Nouvelle ligne')
text_edit.insertPlainText('Insérer à la position du curseur')

# Lecture seule
text_edit.setReadOnly(True)

# Accepter texte riche
text_edit.setAcceptRichText(True)  # HTML
text_edit.setAcceptRichText(False)  # Texte brut uniquement

# Line wrap (retour à la ligne automatique)
text_edit.setLineWrapMode(QTextEdit.WidgetWidth)  # Au bord du widget
text_edit.setLineWrapMode(QTextEdit.NoWrap)       # Pas de wrap

# Police
font = QFont('Arial', 12)
text_edit.setFont(font)
text_edit.setFontPointSize(14)
text_edit.setFontFamily('Courier')

# Curseur
cursor = text_edit.textCursor()
cursor.movePosition(cursor.End)  # Fin du document
text_edit.setTextCursor(cursor)

# === QCheckBox - Case à cocher ===
from PyQt5.QtWidgets import QCheckBox

checkbox = QCheckBox('Accepter les conditions')

# État
checkbox.setChecked(True)
is_checked = checkbox.isChecked()

# États (2 ou 3)
checkbox.setTristate(False)  # 2 états: coché/décoché
checkbox.setTristate(True)   # 3 états: coché/décoché/partiellement

# Vérifier état
state = checkbox.checkState()
# Qt.Unchecked (0)
# Qt.PartiallyChecked (1)
# Qt.Checked (2)

# Signal
checkbox.stateChanged.connect(self.on_checkbox_changed)

def on_checkbox_changed(self, state):
    if state == Qt.Checked:
        print('Coché')
    else:
        print('Décoché')

# === QRadioButton - Bouton radio (choix exclusif) ===
from PyQt5.QtWidgets import QRadioButton, QButtonGroup

# Créer boutons radio
radio1 = QRadioButton('Option 1')
radio2 = QRadioButton('Option 2')
radio3 = QRadioButton('Option 3')

# Sélectionner par défaut
radio1.setChecked(True)

# Grouper (automatique si même parent, sinon utiliser QButtonGroup)
button_group = QButtonGroup()
button_group.addButton(radio1, 1)  # ID 1
button_group.addButton(radio2, 2)  # ID 2
button_group.addButton(radio3, 3)  # ID 3

# Obtenir sélection
selected_button = button_group.checkedButton()
selected_id = button_group.checkedId()

# Signal
radio1.toggled.connect(self.on_radio_toggled)

def on_radio_toggled(self, checked):
    if checked:
        print('Radio 1 sélectionné')

# === QComboBox - Liste déroulante ===
from PyQt5.QtWidgets import QComboBox

combo = QComboBox()

# Ajouter items
combo.addItem('Option 1')
combo.addItem('Option 2')
combo.addItems(['Option 3', 'Option 4', 'Option 5'])

# Ajouter avec données associées
combo.addItem('France', 'FR')
combo.addItem('Belgique', 'BE')

# Sélection
combo.setCurrentIndex(0)        # Par index
combo.setCurrentText('Option 2')  # Par texte

# Obtenir sélection
index = combo.currentIndex()
text = combo.currentText()
data = combo.currentData()      # Données associées

# Obtenir item
item_text = combo.itemText(2)   # Texte de l'item à l'index 2
item_data = combo.itemData(2)   # Données de l'item

# Supprimer
combo.removeItem(2)             # Par index
combo.clear()                   # Tout supprimer

# Nombre d'items
count = combo.count()

# Éditable (saisie personnalisée)
combo.setEditable(True)

# Signal
combo.currentIndexChanged.connect(self.on_combo_changed)
combo.currentTextChanged.connect(self.on_combo_text_changed)

def on_combo_changed(self, index):
    print(f'Index sélectionné: {index}')

# === QSpinBox - Sélecteur numérique ===
from PyQt5.QtWidgets import QSpinBox, QDoubleSpinBox

# Entiers
spin = QSpinBox()
spin.setMinimum(0)
spin.setMaximum(100)
spin.setValue(50)
spin.setSingleStep(1)        # Incrément
spin.setPrefix('€ ')         # Préfixe
spin.setSuffix(' kg')        # Suffixe

# Obtenir valeur
value = spin.value()

# Signal
spin.valueChanged.connect(self.on_spin_changed)

def on_spin_changed(self, value):
    print(f'Valeur: {value}')

# Décimaux
double_spin = QDoubleSpinBox()
double_spin.setMinimum(0.0)
double_spin.setMaximum(100.0)
double_spin.setValue(50.5)
double_spin.setSingleStep(0.1)
double_spin.setDecimals(2)   # 2 décimales

# === QSlider - Curseur ===
from PyQt5.QtWidgets import QSlider

slider = QSlider(Qt.Horizontal)  # Horizontal
# slider = QSlider(Qt.Vertical)  # Vertical

slider.setMinimum(0)
slider.setMaximum(100)
slider.setValue(50)
slider.setSingleStep(1)
slider.setPageStep(10)  # Clic sur la barre

# Graduations
slider.setTickPosition(QSlider.TicksBelow)    # Sous le slider
slider.setTickPosition(QSlider.TicksAbove)    # Au-dessus
slider.setTickPosition(QSlider.TicksBothSides)  # Des deux côtés
slider.setTickInterval(10)  # Tous les 10

# Signal
slider.valueChanged.connect(self.on_slider_changed)
slider.sliderMoved.connect(self.on_slider_moved)      # Pendant le mouvement
slider.sliderReleased.connect(self.on_slider_released)  # Relâché

def on_slider_changed(self, value):
    print(f'Slider: {value}')

# === QProgressBar - Barre de progression ===
from PyQt5.QtWidgets import QProgressBar

progress = QProgressBar()
progress.setMinimum(0)
progress.setMaximum(100)
progress.setValue(50)

# Format texte
progress.setFormat('%p%')     # Pourcentage (défaut)
progress.setFormat('%v/%m')   # Valeur/Maximum
progress.setFormat('Loading: %p%')

# Cacher texte
progress.setTextVisible(False)

# Progression indéfinie (animation)
progress.setMinimum(0)
progress.setMaximum(0)  # Active le mode "busy"

# Orientation
progress.setOrientation(Qt.Horizontal)
progress.setOrientation(Qt.Vertical)

# Inverser direction
progress.setInvertedAppearance(True)


[OK] LAYOUTS (DISPOSITION DES WIDGETS)

# Les layouts gèrent automatiquement la position et taille des widgets

# === QVBoxLayout - Disposition verticale ===
from PyQt5.QtWidgets import QVBoxLayout

layout = QVBoxLayout()

# Ajouter widgets
layout.addWidget(widget1)
layout.addWidget(widget2)
layout.addWidget(widget3)

# Espacement
layout.addSpacing(20)       # Espace fixe
layout.addStretch()         # Espace flexible

# Marges
layout.setContentsMargins(10, 10, 10, 10)  # gauche, haut, droite, bas
layout.setSpacing(5)  # Espacement entre widgets

# Appliquer layout
window.setLayout(layout)

# === QHBoxLayout - Disposition horizontale ===
from PyQt5.QtWidgets import QHBoxLayout

layout = QHBoxLayout()
layout.addWidget(widget1)
layout.addWidget(widget2)
layout.addWidget(widget3)

# === QGridLayout - Grille ===
from PyQt5.QtWidgets import QGridLayout

grid = QGridLayout()

# Ajouter widgets (ligne, colonne)
grid.addWidget(label1, 0, 0)      # Ligne 0, colonne 0
grid.addWidget(line_edit1, 0, 1)  # Ligne 0, colonne 1
grid.addWidget(label2, 1, 0)      # Ligne 1, colonne 0
grid.addWidget(line_edit2, 1, 1)  # Ligne 1, colonne 1

# Span (fusionner cellules)
grid.addWidget(button, 2, 0, 1, 2)  # ligne, col, hauteur, largeur

# Espacement
grid.setSpacing(10)
grid.setColumnStretch(1, 1)  # Colonne 1 extensible
grid.setRowStretch(2, 1)     # Ligne 2 extensible

# === QFormLayout - Formulaire (label + champ) ===
from PyQt5.QtWidgets import QFormLayout

form = QFormLayout()
form.addRow('Nom:', line_edit_name)
form.addRow('Email:', line_edit_email)
form.addRow('Âge:', spin_age)

# === Layouts imbriqués ===
main_layout = QVBoxLayout()

# Layout horizontal pour boutons
button_layout = QHBoxLayout()
button_layout.addWidget(QPushButton('OK'))
button_layout.addWidget(QPushButton('Annuler'))

# Ajouter layout dans layout
main_layout.addWidget(QLabel('Titre'))
main_layout.addLayout(button_layout)

window.setLayout(main_layout)

# === QSplitter - Diviseur redimensionnable ===
from PyQt5.QtWidgets import QSplitter

splitter = QSplitter(Qt.Horizontal)  # Ou Qt.Vertical
splitter.addWidget(widget1)
splitter.addWidget(widget2)

# Tailles initiales
splitter.setSizes([200, 400])

# Collapsible
splitter.setCollapsible(0, False)  # Widget 0 non collapsible

# === QStackedWidget - Widgets empilés (un seul visible) ===
from PyQt5.QtWidgets import QStackedWidget

stack = QStackedWidget()
stack.addWidget(page1)
stack.addWidget(page2)
stack.addWidget(page3)

# Changer de page
stack.setCurrentIndex(1)      # Par index
stack.setCurrentWidget(page2)  # Par widget

# Signal
stack.currentChanged.connect(self.on_page_changed)

# === QTabWidget - Onglets ===
from PyQt5.QtWidgets import QTabWidget

tabs = QTabWidget()
tabs.addTab(widget1, 'Onglet 1')
tabs.addTab(widget2, 'Onglet 2')
tabs.addTab(widget3, QIcon('icon.png'), 'Onglet 3')

# Position onglets
tabs.setTabPosition(QTabWidget.North)   # Haut (défaut)
tabs.setTabPosition(QTabWidget.South)   # Bas
tabs.setTabPosition(QTabWidget.West)    # Gauche
tabs.setTabPosition(QTabWidget.East)    # Droite

# Fermeture onglets
tabs.setTabsClosable(True)
tabs.tabCloseRequested.connect(self.on_tab_close)

# Onglet courant
tabs.setCurrentIndex(1)
index = tabs.currentIndex()
widget = tabs.currentWidget()

# Compter onglets
count = tabs.count()

# === QScrollArea - Zone scrollable ===
from PyQt5.QtWidgets import QScrollArea

scroll = QScrollArea()
scroll.setWidget(large_widget)
scroll.setWidgetResizable(True)  # Widget s'adapte


[OK] DIALOGUES ET MESSAGES

# === QMessageBox - Boîtes de dialogue ===
from PyQt5.QtWidgets import QMessageBox

# Information
QMessageBox.information(self, 'Titre', 'Message d\'information')

# Avertissement
QMessageBox.warning(self, 'Attention', 'Ceci est un avertissement')

# Erreur
QMessageBox.critical(self, 'Erreur', 'Une erreur s\'est produite')

# Question (avec boutons)
reply = QMessageBox.question(
    self, 
    'Confirmation', 
    'Êtes-vous sûr?',
    QMessageBox.Yes | QMessageBox.No,
    QMessageBox.No  # Bouton par défaut
)

if reply == QMessageBox.Yes:
    print('Utilisateur a cliqué Oui')
else:
    print('Utilisateur a cliqué Non')

# Dialogue personnalisé
msg = QMessageBox()
msg.setIcon(QMessageBox.Question)
msg.setWindowTitle('Titre')
msg.setText('Texte principal')
msg.setInformativeText('Texte informatif supplémentaire')
msg.setDetailedText('Détails techniques...')
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
msg.setDefaultButton(QMessageBox.Ok)
result = msg.exec_()

# === QInputDialog - Saisie utilisateur ===
from PyQt5.QtWidgets import QInputDialog

# Texte
text, ok = QInputDialog.getText(
    self, 
    'Saisie', 
    'Entrez votre nom:',
    QLineEdit.Normal,
    'Valeur par défaut'
)
if ok:
    print(f'Nom: {text}')

# Nombre entier
number, ok = QInputDialog.getInt(
    self, 
    'Saisie', 
    'Entrez un nombre:',
    25,      # Valeur par défaut
    0,       # Minimum
    100,     # Maximum
    1        # Step
)

# Nombre décimal
number, ok = QInputDialog.getDouble(
    self,
    'Saisie',
    'Entrez un prix:',
    10.5,    # Valeur par défaut
    0.0,     # Minimum
    1000.0,  # Maximum
    2        # Décimales
)

# Liste déroulante
items = ['Option 1', 'Option 2', 'Option 3']
item, ok = QInputDialog.getItem(
    self,
    'Sélection',
    'Choisissez une option:',
    items,
    0,      # Index par défaut
    False   # Éditable
)

# === QFileDialog - Sélection de fichiers ===
from PyQt5.QtWidgets import QFileDialog

# Ouvrir un fichier
filename, _ = QFileDialog.getOpenFileName(
    self,
    'Ouvrir fichier',
    '',  # Dossier par défaut
    'Tous les fichiers (*);;Fichiers texte (*.txt);;Images (*.png *.jpg)'
)
if filename:
    print(f'Fichier sélectionné: {filename}')

# Ouvrir plusieurs fichiers
filenames, _ = QFileDialog.getOpenFileNames(
    self,
    'Ouvrir fichiers',
    '',
    'Fichiers Python (*.py);;Tous les fichiers (*)'
)

# Enregistrer fichier
filename, _ = QFileDialog.getSaveFileName(
    self,
    'Enregistrer sous',
    'document.txt',
    'Fichiers texte (*.txt);;Tous les fichiers (*)'
)

# Sélectionner dossier
directory = QFileDialog.getExistingDirectory(
    self,
    'Sélectionner dossier',
    ''
)

# Options avancées
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.AnyFile)        # Tout type
dialog.setFileMode(QFileDialog.ExistingFile)   # Fichier existant
dialog.setFileMode(QFileDialog.Directory)      # Dossier
dialog.setViewMode(QFileDialog.Detail)         # Vue détaillée
dialog.setViewMode(QFileDialog.List)           # Vue liste

# === QColorDialog - Sélection couleur ===
from PyQt5.QtWidgets import QColorDialog
from PyQt5.QtGui import QColor

color = QColorDialog.getColor(
    QColor(255, 0, 0),  # Couleur initiale (rouge)
    self,
    'Choisir une couleur'
)

if color.isValid():
    print(f'RGB: {color.red()}, {color.green()}, {color.blue()}')
    print(f'Hex: {color.name()}')

# === QFontDialog - Sélection police ===
from PyQt5.QtWidgets import QFontDialog

font, ok = QFontDialog.getFont(self)
if ok:
    widget.setFont(font)


[OK] MENUS ET BARRES D'OUTILS

# === Menu bar (QMainWindow) ===
from PyQt5.QtWidgets import QMainWindow, QAction

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        # Créer barre de menu
        menubar = self.menuBar()
        
        # Menu Fichier
        file_menu = menubar.addMenu('&Fichier')  # & pour raccourci Alt+F
        
        # Action Nouveau
        new_action = QAction(QIcon('new.png'), '&Nouveau', self)
        new_action.setShortcut('Ctrl+N')
        new_action.setStatusTip('Créer un nouveau fichier')
        new_action.triggered.connect(self.new_file)
        file_menu.addAction(new_action)
        
        # Action Ouvrir
        open_action = QAction('&Ouvrir', self)
        open_action.setShortcut('Ctrl+O')
        open_action.triggered.connect(self.open_file)
        file_menu.addAction(open_action)
        
        # Séparateur
        file_menu.addSeparator()
        
        # Action Quitter
        exit_action = QAction('&Quitter', self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)
        
        # Menu Édition
        edit_menu = menubar.addMenu('&Édition')
        edit_menu.addAction('Copier')
        edit_menu.addAction('Coller')
        
        # Sous-menu
        format_menu = edit_menu.addMenu('Format')
        format_menu.addAction('Gras')
        format_menu.addAction('Italique')
        
        # Menu Aide
        help_menu = menubar.addMenu('&Aide')
        help_menu.addAction('Documentation')
        help_menu.addAction('À propos')
    
    def new_file(self):
        print('Nouveau fichier')
    
    def open_file(self):
        print('Ouvrir fichier')

# Action checkable (avec coche)
view_statusbar = QAction('Barre de statut', self, checkable=True)
view_statusbar.setChecked(True)
view_statusbar.triggered.connect(self.toggle_statusbar)

def toggle_statusbar(self, checked):
    if checked:
        self.statusBar().show()
    else:
        self.statusBar().hide()

# === Toolbar (barre d'outils) ===
toolbar = self.addToolBar('Principale')
toolbar.setIconSize(QSize(32, 32))

# Ajouter actions
toolbar.addAction(new_action)
toolbar.addAction(open_action)
toolbar.addSeparator()
toolbar.addAction(exit_action)

# Ajouter widgets
toolbar.addWidget(QLabel('Recherche:'))
search_box = QLineEdit()
toolbar.addWidget(search_box)

# Position toolbar
toolbar.setMovable(True)   # Déplaçable
toolbar.setMovable(False)  # Fixe

# Plusieurs toolbars
toolbar2 = self.addToolBar('Secondaire')
self.addToolBarBreak()  # Nouvelle ligne

# === Status bar (barre de statut) ===
statusbar = self.statusBar()
statusbar.showMessage('Prêt', 5000)  # Message pendant 5 secondes

# Widget permanent dans statusbar
permanent_label = QLabel('Ligne: 1, Col: 1')
statusbar.addPermanentWidget(permanent_label)

# === Menu contextuel (clic droit) ===
def contextMenuEvent(self, event):
    menu = QMenu(self)
    
    copy_action = menu.addAction('Copier')
    paste_action = menu.addAction('Coller')
    menu.addSeparator()
    delete_action = menu.addAction('Supprimer')
    
    action = menu.exec_(self.mapToGlobal(event.pos()))
    
    if action == copy_action:
        print('Copier')
    elif action == paste_action:
        print('Coller')
    elif action == delete_action:
        print('Supprimer')


[OK] SIGNAUX ET SLOTS (ÉVÉNEMENTS)

# Les signaux/slots permettent la communication entre objets
# Signal = événement émis
# Slot = fonction appelée en réponse au signal

# === Connecter signal à slot ===
button.clicked.connect(self.on_button_clicked)

def on_button_clicked(self):
    print('Bouton cliqué')

# === Déconnecter ===
button.clicked.disconnect(self.on_button_clicked)

# === Lambda (fonction anonyme) ===
button.clicked.connect(lambda: print('Clic!'))
button.clicked.connect(lambda: self.process_data('argument'))

# === Plusieurs connexions ===
button.clicked.connect(self.function1)
button.clicked.connect(self.function2)
button.clicked.connect(self.function3)

# === Signaux personnalisés ===
from PyQt5.QtCore import pyqtSignal

class MyWidget(QWidget):
    # Définir signal personnalisé
    my_signal = pyqtSignal()           # Sans paramètre
    data_signal = pyqtSignal(str)      # Avec paramètre string
    multi_signal = pyqtSignal(int, str)  # Plusieurs paramètres
    
    def __init__(self):
        super().__init__()
        
        # Connecter signal
        self.my_signal.connect(self.handle_signal)
        self.data_signal.connect(self.handle_data)
    
    def emit_signal(self):
        # Émettre signal
        self.my_signal.emit()
        self.data_signal.emit('Hello')
        self.multi_signal.emit(42, 'World')
    
    def handle_signal(self):
        print('Signal reçu')
    
    def handle_data(self, data):
        print(f'Données: {data}')

# === Décorateur @pyqtSlot ===
from PyQt5.QtCore import pyqtSlot

@pyqtSlot()
def on_clicked(self):
    print('Slot décoré')

@pyqtSlot(str)
def on_text_changed(self, text):
    print(f'Texte: {text}')

# === Signaux courants ===

# QPushButton
button.clicked.connect(slot)
button.pressed.connect(slot)
button.released.connect(slot)

# QLineEdit
line_edit.textChanged.connect(slot)       # Texte modifié
line_edit.returnPressed.connect(slot)     # Touche Entrée
line_edit.editingFinished.connect(slot)   # Édition terminée

# QComboBox
combo.currentIndexChanged.connect(slot)
combo.currentTextChanged.connect(slot)

# QCheckBox / QRadioButton
checkbox.stateChanged.connect(slot)
checkbox.toggled.connect(slot)

# QSlider
slider.valueChanged.connect(slot)
slider.sliderMoved.connect(slot)
slider.sliderPressed.connect(slot)
slider.sliderReleased.connect(slot)

# QSpinBox
spin.valueChanged.connect(slot)

# === Bloquer signaux temporairement ===
widget.blockSignals(True)   # Bloquer
widget.setValue(50)         # Pas de signal émis
widget.blockSignals(False)  # Débloquer


[OK] STYLES ET APPARENCE

# === StyleSheet (CSS-like) ===
widget.setStyleSheet('''
    QWidget {
        background-color: #f0f0f0;
        font-family: Arial;
        font-size: 14px;
    }
    
    QPushButton {
        background-color: #4CAF50;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        font-weight: bold;
    }
    
    QPushButton:hover {
        background-color: #45a049;
    }
    
    QPushButton:pressed {
        background-color: #3d8b40;
    }
    
    QPushButton:disabled {
        background-color: #cccccc;
        color: #666666;
    }
    
    QLineEdit {
        border: 2px solid #ddd;
        border-radius: 4px;
        padding: 8px;
        background-color: white;
    }
    
    QLineEdit:focus {
        border: 2px solid #4CAF50;
    }
    
    QLabel {
        color: #333;
        font-size: 16px;
    }
    
    QLabel#title {
        font-size: 24px;
        font-weight: bold;
        color: #2196F3;
    }
''')

# Appliquer à toute l'application
app.setStyleSheet('...')

# === Object name (pour cibler en CSS) ===
label.setObjectName('title')
# Puis dans CSS: QLabel#title { ... }

# === Police (QFont) ===
from PyQt5.QtGui import QFont

font = QFont('Arial', 12)           # Famille, taille
font = QFont('Arial', 12, QFont.Bold)  # Avec poids

# Propriétés
font.setPointSize(14)
font.setPixelSize(18)
font.setBold(True)
font.setItalic(True)
font.setUnderline(True)
font.setStrikeOut(True)
font.setFamily('Courier New')

widget.setFont(font)

# === Couleurs (QColor) ===
from PyQt5.QtGui import QColor, QPalette

color = QColor(255, 0, 0)           # RGB
color = QColor(255, 0, 0, 128)      # RGBA (avec alpha)
color = QColor('#FF0000')           # Hex
color = QColor('red')               # Nom

# Palette
palette = widget.palette()
palette.setColor(QPalette.Window, QColor(240, 240, 240))
palette.setColor(QPalette.WindowText, QColor(0, 0, 0))
palette.setColor(QPalette.Button, QColor(76, 175, 80))
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
widget.setPalette(palette)

# Rôles palette courants:
# QPalette.Window - Fond fenêtre
# QPalette.WindowText - Texte fenêtre
# QPalette.Base - Fond widgets saisie
# QPalette.AlternateBase - Lignes alternées (listes)
# QPalette.Text - Texte widgets saisie
# QPalette.Button - Fond boutons
# QPalette.ButtonText - Texte boutons
# QPalette.Highlight - Sélection
# QPalette.HighlightedText - Texte sélectionné

# === Icônes (QIcon) ===
from PyQt5.QtGui import QIcon, QPixmap

icon = QIcon('icon.png')
icon = QIcon(QPixmap('icon.png'))

# Plusieurs tailles
icon.addFile('icon16.png', QSize(16, 16))
icon.addFile('icon32.png', QSize(32, 32))
icon.addFile('icon64.png', QSize(64, 64))

# Icônes système
icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation)
icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning)
icon = self.style().standardIcon(QStyle.SP_MessageBoxCritical)
icon = self.style().standardIcon(QStyle.SP_DialogOpenButton)
icon = self.style().standardIcon(QStyle.SP_DialogSaveButton)

button.setIcon(icon)

# === Thèmes Qt ===
# Fusion (moderne)
app.setStyle('Fusion')

# Windows
app.setStyle('Windows')

# macOS
app.setStyle('macOS')

# Thème sombre (Fusion)
app.setStyle('Fusion')
dark_palette = QPalette()
dark_palette.setColor(QPalette.Window, QColor(53, 53, 53))
dark_palette.setColor(QPalette.WindowText, Qt.white)
dark_palette.setColor(QPalette.Base, QColor(25, 25, 25))
dark_palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ToolTipBase, Qt.white)
dark_palette.setColor(QPalette.ToolTipText, Qt.white)
dark_palette.setColor(QPalette.Text, Qt.white)
dark_palette.setColor(QPalette.Button, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ButtonText, Qt.white)
dark_palette.setColor(QPalette.BrightText, Qt.red)
dark_palette.setColor(QPalette.Link, QColor(42, 130, 218))
dark_palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
dark_palette.setColor(QPalette.HighlightedText, Qt.black)
app.setPalette(dark_palette)


[OK] WIDGETS AVANCÉS

# === QTableWidget - Tableau ===
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem

table = QTableWidget()
table.setRowCount(5)
table.setColumnCount(3)

# En-têtes
table.setHorizontalHeaderLabels(['Nom', 'Âge', 'Ville'])
table.setVerticalHeaderLabels(['1', '2', '3', '4', '5'])

# Ajouter données
table.setItem(0, 0, QTableWidgetItem('Alice'))
table.setItem(0, 1, QTableWidgetItem('30'))
table.setItem(0, 2, QTableWidgetItem('Paris'))

# Item non éditable
item = QTableWidgetItem('Bob')
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
table.setItem(1, 0, item)

# Obtenir données
item = table.item(0, 0)
if item:
    text = item.text()

# Sélection
table.setSelectionBehavior(QTableWidget.SelectRows)     # Lignes entières
table.setSelectionBehavior(QTableWidget.SelectColumns)  # Colonnes entières
table.setSelectionMode(QTableWidget.SingleSelection)    # Simple
table.setSelectionMode(QTableWidget.MultiSelection)     # Multiple

# Obtenir sélection
selected = table.selectedItems()
for item in selected:
    print(item.text())

# Taille colonnes
table.setColumnWidth(0, 200)
table.horizontalHeader().setStretchLastSection(True)  # Dernière s'étend

# Alternance couleurs lignes
table.setAlternatingRowColors(True)

# Grille
table.setShowGrid(True)

# Ajouter/supprimer lignes
table.insertRow(0)              # Insérer au début
table.removeRow(2)              # Supprimer ligne 2
table.clearContents()           # Vider contenu
table.clear()                   # Vider tout (y compris en-têtes)

# Tri
table.setSortingEnabled(True)

# Signal
table.itemClicked.connect(self.on_item_clicked)
table.cellClicked.connect(self.on_cell_clicked)

def on_item_clicked(self, item):
    print(f'Clic sur: {item.text()}')

def on_cell_clicked(self, row, col):
    print(f'Cellule: ligne {row}, colonne {col}')

# === QListWidget - Liste ===
from PyQt5.QtWidgets import QListWidget, QListWidgetItem

list_widget = QListWidget()

# Ajouter items
list_widget.addItem('Item 1')
list_widget.addItem('Item 2')
list_widget.addItems(['Item 3', 'Item 4', 'Item 5'])

# Item avec icône
item = QListWidgetItem(QIcon('icon.png'), 'Item avec icône')
list_widget.addItem(item)

# Obtenir sélection
selected = list_widget.currentItem()
if selected:
    text = selected.text()

# Sélection multiple
list_widget.setSelectionMode(QListWidget.MultiSelection)
selected_items = list_widget.selectedItems()

# Supprimer
list_widget.takeItem(0)         # Supprimer par index
list_widget.clear()             # Tout supprimer

# Compter items
count = list_widget.count()

# Signal
list_widget.itemClicked.connect(self.on_list_clicked)
list_widget.currentItemChanged.connect(self.on_selection_changed)

# === QTreeWidget - Arbre hiérarchique ===
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem

tree = QTreeWidget()
tree.setHeaderLabels(['Nom', 'Taille', 'Type'])

# Racine
root = QTreeWidgetItem(tree, ['Dossier racine'])
tree.addTopLevelItem(root)

# Enfants
child1 = QTreeWidgetItem(root, ['Fichier 1', '1.2 MB', 'PDF'])
child2 = QTreeWidgetItem(root, ['Fichier 2', '850 KB', 'DOCX'])

# Sous-enfants
subchild = QTreeWidgetItem(child1, ['Annotation', '50 KB', 'TXT'])

# Développer/réduire
root.setExpanded(True)
tree.expandAll()
tree.collapseAll()

# Sélection
selected = tree.currentItem()
if selected:
    text = selected.text(0)  # Colonne 0

# Signal
tree.itemClicked.connect(self.on_tree_clicked)

def on_tree_clicked(self, item, column):
    print(f'Clic sur: {item.text(0)}')

# === QTextBrowser - Affichage texte riche (HTML) ===
from PyQt5.QtWidgets import QTextBrowser

browser = QTextBrowser()
browser.setHtml('''
    <h1>Titre</h1>
    <p>Paragraphe avec <b>gras</b> et <i>italique</i></p>
    <ul>
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
    <a href="https://example.com">Lien</a>
''')

# Ouvrir liens
browser.setOpenExternalLinks(True)  # Ouvrir dans navigateur
browser.setOpenLinks(False)         # Ne pas ouvrir

# Signal clic lien
browser.anchorClicked.connect(self.on_link_clicked)

def on_link_clicked(self, url):
    print(f'Lien cliqué: {url.toString()}')

# Charger depuis fichier
browser.setSource(QUrl.fromLocalFile('file.html'))

# === QCalendarWidget - Calendrier ===
from PyQt5.QtWidgets import QCalendarWidget
from PyQt5.QtCore import QDate

calendar = QCalendarWidget()

# Date actuelle
today = calendar.selectedDate()
print(f'{today.day()}/{today.month()}/{today.year()}')

# Définir date
calendar.setSelectedDate(QDate(2025, 12, 25))

# Plage de dates
calendar.setMinimumDate(QDate(2020, 1, 1))
calendar.setMaximumDate(QDate(2030, 12, 31))

# Format
calendar.setFirstDayOfWeek(Qt.Monday)
calendar.setGridVisible(True)

# Signal
calendar.selectionChanged.connect(self.on_date_changed)
calendar.clicked.connect(self.on_date_clicked)

def on_date_changed(self):
    date = self.calendar.selectedDate()
    print(f'Date sélectionnée: {date.toString("dd/MM/yyyy")}')

# === QDial - Bouton rotatif ===
from PyQt5.QtWidgets import QDial

dial = QDial()
dial.setMinimum(0)
dial.setMaximum(100)
dial.setValue(50)
dial.setNotchesVisible(True)

dial.valueChanged.connect(self.on_dial_changed)

# === QLCDNumber - Affichage LCD ===
from PyQt5.QtWidgets import QLCDNumber

lcd = QLCDNumber()
lcd.display(42)
lcd.setDigitCount(8)
lcd.setSegmentStyle(QLCDNumber.Filled)

# === QDateEdit, QTimeEdit, QDateTimeEdit ===
from PyQt5.QtWidgets import QDateEdit, QTimeEdit, QDateTimeEdit
from PyQt5.QtCore import QDate, QTime, QDateTime

# Date
date_edit = QDateEdit()
date_edit.setDate(QDate.currentDate())
date_edit.setCalendarPopup(True)  # Calendrier popup

# Temps
time_edit = QTimeEdit()
time_edit.setTime(QTime.currentTime())

# Date et temps
datetime_edit = QDateTimeEdit()
datetime_edit.setDateTime(QDateTime.currentDateTime())

# Format
date_edit.setDisplayFormat('dd/MM/yyyy')
time_edit.setDisplayFormat('HH:mm:ss')

# === QGroupBox - Boîte de groupe ===
from PyQt5.QtWidgets import QGroupBox

group = QGroupBox('Options')
group.setCheckable(True)  # Cochable pour activer/désactiver

# Layout dans groupe
layout = QVBoxLayout()
layout.addWidget(QCheckBox('Option 1'))
layout.addWidget(QCheckBox('Option 2'))
group.setLayout(layout)


[OK] DESSIN ET GRAPHIQUES

# === QPainter - Dessin personnalisé ===
from PyQt5.QtGui import QPainter, QPen, QBrush, QColor
from PyQt5.QtCore import Qt, QPoint, QRect

class DrawWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.setMinimumSize(400, 300)
    
    def paintEvent(self, event):
        painter = QPainter(self)
        
        # Anti-aliasing
        painter.setRenderHint(QPainter.Antialiasing)
        
        # Stylo (contour)
        pen = QPen(QColor(0, 0, 255), 2)  # Bleu, épaisseur 2
        painter.setPen(pen)
        
        # Brosse (remplissage)
        brush = QBrush(QColor(255, 255, 0))  # Jaune
        painter.setBrush(brush)
        
        # Rectangle
        painter.drawRect(50, 50, 100, 80)
        
        # Ligne
        painter.drawLine(10, 10, 100, 100)
        
        # Cercle/Ellipse
        painter.drawEllipse(200, 50, 80, 80)  # Cercle
        painter.drawEllipse(300, 50, 120, 60)  # Ellipse
        
        # Polygone
        points = [QPoint(150, 200), QPoint(200, 250), QPoint(100, 250)]
        painter.drawPolygon(points)
        
        # Arc
        painter.drawArc(50, 150, 100, 100, 0, 90 * 16)  # 0-90°
        
        # Texte
        painter.setPen(QColor(0, 0, 0))
        painter.drawText(250, 200, 'Hello PyQt5!')
        
        # Image
        pixmap = QPixmap('image.png')
        painter.drawPixmap(50, 250, pixmap)

# Styles de stylo
pen.setStyle(Qt.SolidLine)       # Ligne continue
pen.setStyle(Qt.DashLine)        # Tirets
pen.setStyle(Qt.DotLine)         # Points
pen.setStyle(Qt.DashDotLine)     # Tiret-point

# Styles de brosse
brush.setStyle(Qt.SolidPattern)       # Rempli
brush.setStyle(Qt.Dense1Pattern)      # Motif dense
brush.setStyle(Qt.HorPattern)         # Lignes horizontales
brush.setStyle(Qt.VerPattern)         # Lignes verticales
brush.setStyle(Qt.CrossPattern)       # Grille

# Gradient
from PyQt5.QtGui import QLinearGradient, QRadialGradient

gradient = QLinearGradient(0, 0, 200, 0)
gradient.setColorAt(0, QColor(255, 0, 0))    # Rouge au début
gradient.setColorAt(1, QColor(0, 0, 255))    # Bleu à la fin
brush = QBrush(gradient)
painter.setBrush(brush)


[OK] TIMERS ET THREADS

# === QTimer - Exécution répétée/différée ===
from PyQt5.QtCore import QTimer

# Timer simple
timer = QTimer()
timer.timeout.connect(self.update_display)
timer.start(1000)  # Toutes les 1000 ms (1 seconde)

def update_display(self):
    # Fonction appelée toutes les secondes
    print('Tick')

# Timer une seule fois
QTimer.singleShot(5000, self.delayed_function)  # Après 5 secondes

def delayed_function(self):
    print('Exécuté après 5 secondes')

# Arrêter timer
timer.stop()

# Vérifier si actif
is_active = timer.isActive()

# Changer intervalle
timer.setInterval(2000)  # 2 secondes

# === QThread - Threads ===
from PyQt5.QtCore import QThread, pyqtSignal

# Méthode 1: Hériter de QThread
class WorkerThread(QThread):
    # Signaux pour communication
    progress = pyqtSignal(int)
    finished = pyqtSignal(str)
    
    def __init__(self):
        super().__init__()
        self.running = True
    
    def run(self):
        # Code exécuté dans le thread
        for i in range(100):
            if not self.running:
                break
            
            # Travail lourd
            time.sleep(0.1)
            
            # Émettre progression
            self.progress.emit(i + 1)
        
        # Terminé
        self.finished.emit('Travail terminé!')
    
    def stop(self):
        self.running = False

# Utilisation
thread = WorkerThread()
thread.progress.connect(self.update_progress)
thread.finished.connect(self.on_finished)
thread.start()

def update_progress(self, value):
    self.progress_bar.setValue(value)

def on_finished(self, message):
    QMessageBox.information(self, 'Info', message)

# Attendre fin du thread
thread.wait()

# Méthode 2: QObject avec moveToThread (recommandé)
class Worker(QObject):
    progress = pyqtSignal(int)
    finished = pyqtSignal()
    
    def __init__(self):
        super().__init__()
        self._running = True
    
    @pyqtSlot()
    def do_work(self):
        for i in range(100):
            if not self._running:
                break
            time.sleep(0.1)
            self.progress.emit(i + 1)
        self.finished.emit()
    
    def stop(self):
        self._running = False

# Utilisation
thread = QThread()
worker = Worker()
worker.moveToThread(thread)

# Connexions
thread.started.connect(worker.do_work)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
worker.progress.connect(self.update_progress)

# Démarrer
thread.start()


[OK] GESTION DES FICHIERS ET RESSOURCES

# === Lire/écrire fichier ===
# Ouvrir fichier
filename, _ = QFileDialog.getOpenFileName(self, 'Ouvrir', '', 'Texte (*.txt)')
if filename:
    with open(filename, 'r', encoding='utf-8') as f:
        content = f.read()
        self.text_edit.setPlainText(content)

# Enregistrer fichier
filename, _ = QFileDialog.getSaveFileName(self, 'Enregistrer', '', 'Texte (*.txt)')
if filename:
    content = self.text_edit.toPlainText()
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(content)

# === Ressources Qt (.qrc) ===
# Fichier resources.qrc:
"""
<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource>
        <file>images/icon.png</file>
        <file>images/logo.png</file>
        <file>styles/style.qss</file>
    </qresource>
</RCC>
"""

# Compiler ressources
# pyrcc5 resources.qrc -o resources_rc.py

# Utiliser dans code
import resources_rc  # Importer le fichier généré

icon = QIcon(':/images/icon.png')
pixmap = QPixmap(':/images/logo.png')

# Charger stylesheet
with open(':/styles/style.qss', 'r') as f:
    app.setStyleSheet(f.read())

# === QSettings - Sauvegarder préférences ===
from PyQt5.QtCore import QSettings

# Créer settings
settings = QSettings('MonEntreprise', 'MonApp')

# Sauvegarder
settings.setValue('window/geometry', self.saveGeometry())
settings.setValue('window/state', self.saveState())
settings.setValue('user/name', 'Alice')
settings.setValue('user/age', 30)
settings.setValue('options/dark_mode', True)

# Charger
geometry = settings.value('window/geometry')
if geometry:
    self.restoreGeometry(geometry)

name = settings.value('user/name', 'Default')  # Avec valeur par défaut
age = settings.value('user/age', 0, type=int)  # Avec type
dark_mode = settings.value('options/dark_mode', False, type=bool)

# Supprimer
settings.remove('user/name')
settings.clear()  # Tout supprimer

# Groupes
settings.beginGroup('window')
settings.setValue('width', 800)
settings.setValue('height', 600)
settings.endGroup()

# Lire groupe
settings.beginGroup('window')
width = settings.value('width', 800, type=int)
height = settings.value('height', 600, type=int)
settings.endGroup()


[OK] DRAG AND DROP

# === Activer drag & drop ===
class DragDropWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)  # Accepter drops
    
    def dragEnterEvent(self, event):
        # Vérifier si on accepte le drag
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()
    
    def dropEvent(self, event):
        # Récupérer fichiers déposés
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        for file in files:
            print(f'Fichier déposé: {file}')

# === Drag depuis widget ===
class DragWidget(QLabel):
    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            mime.setText(self.text())
            drag.setMimeData(mime)
            drag.exec_(Qt.CopyAction)


[OK] QT DESIGNER - INTERFACE GRAPHIQUE VISUELLE

# Qt Designer permet de créer des interfaces graphiquement

# === Lancer Qt Designer ===
# Linux/Mac
designer

# Windows
# C:\Python311\Lib\site-packages\qt5_applications\Qt\bin\designer.exe

# Ou depuis Python
python -m PyQt5.uic.pyuic

# === Créer interface (.ui) ===
# 1. Ouvrir Qt Designer
# 2. Nouveau formulaire: Widget, Main Window, ou Dialog
# 3. Glisser-déposer widgets depuis la palette
# 4. Définir propriétés dans Property Editor
# 5. Définir layouts
# 6. Enregistrer fichier .ui

# === Convertir .ui en .py ===
# Ligne de commande
pyuic5 -x interface.ui -o interface_ui.py

# Options:
# -x : Ajouter code de test
# -o : Fichier de sortie

# === Utiliser fichier .ui dans code ===

# Méthode 1: Charger dynamiquement
from PyQt5 import uic

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        uic.loadUi('interface.ui', self)
        
        # Accéder aux widgets (par objectName défini dans Designer)
        self.pushButton.clicked.connect(self.on_button_clicked)
        self.lineEdit.textChanged.connect(self.on_text_changed)
    
    def on_button_clicked(self):
        text = self.lineEdit.text()
        self.label.setText(text)

# Méthode 2: Importer fichier .py converti
from interface_ui import Ui_MainWindow

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        
        # Connecter signaux
        self.pushButton.clicked.connect(self.on_button_clicked)

# === Signaux/Slots dans Designer ===
# Dans Qt Designer:
# 1. Mode "Edit Signals/Slots" (F4)
# 2. Glisser depuis widget source vers widget destination
# 3. Choisir signal et slot

# Définir slots personnalisés dans code:
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
    
    @pyqtSlot()
    def on_pushButton_clicked(self):  # Nommage: on_<objectName>_<signal>
        print('Bouton cliqué')


[OK] MODÈLES ET VUES (MODEL/VIEW)

# PyQt5 utilise le pattern Model/View pour séparer données et affichage

# === QTableView avec modèle ===
from PyQt5.QtCore import QAbstractTableModel, Qt

class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
    
    def rowCount(self, parent=None):
        return len(self._data)
    
    def columnCount(self, parent=None):
        return len(self._data[0]) if self._data else 0
    
    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return str(self._data[index.row()][index.column()])
        return None
    
    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return ['Nom', 'Âge', 'Ville'][section]
        return None

# Utilisation
data = [
    ['Alice', 30, 'Paris'],
    ['Bob', 25, 'Lyon'],
    ['Charlie', 35, 'Marseille']
]

model = TableModel(data)
table_view = QTableView()
table_view.setModel(model)

# === Modèle éditable ===
class EditableTableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
    
    def flags(self, index):
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
    
    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        return False
    
    # rowCount, columnCount, data comme avant...

# === QListView avec QStringListModel ===
from PyQt5.QtCore import QStringListModel

model = QStringListModel()
model.setStringList(['Item 1', 'Item 2', 'Item 3'])

list_view = QListView()
list_view.setModel(model)

# === QSortFilterProxyModel - Filtrage et tri ===
from PyQt5.QtCore import QSortFilterProxyModel

# Modèle proxy pour filtrer
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(model)

# Filtrer (case-insensitive)
proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
proxy_model.setFilterKeyColumn(0)  # Filtrer sur colonne 0
proxy_model.setFilterRegExp('alice')  # Regex

# Appliquer proxy
table_view.setModel(proxy_model)

# Activer tri
table_view.setSortingEnabled(True)

# Signal de recherche
search_box.textChanged.connect(proxy_model.setFilterRegExp)


[OK] BASE DE DONNÉES (QtSql)

# === Connection base de données ===
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel

# SQLite
db = QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName('database.db')

if not db.open():
    print('Erreur connexion DB')
    sys.exit(1)

# MySQL
db = QSqlDatabase.addDatabase('QMYSQL')
db.setHostName('localhost')
db.setDatabaseName('mydb')
db.setUserName('user')
db.setPassword('password')
db.open()

# PostgreSQL
db = QSqlDatabase.addDatabase('QPSQL')

# === Exécuter requêtes ===
query = QSqlQuery()

# Créer table
query.exec_('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE,
        age INTEGER
    )
''')

# Insérer données
query.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)')
query.addBindValue('Alice')
query.addBindValue('alice@example.com')
query.addBindValue(30)
query.exec_()

# Ou directement
query.exec_("INSERT INTO users VALUES (NULL, 'Bob', 'bob@example.com', 25)")

# Sélectionner
query.exec_('SELECT * FROM users')
while query.next():
    id = query.value(0)
    name = query.value(1)
    email = query.value(2)
    age = query.value(3)
    print(f'{id}: {name}, {email}, {age}')

# Mettre à jour
query.prepare('UPDATE users SET age = ? WHERE name = ?')
query.addBindValue(31)
query.addBindValue('Alice')
query.exec_()

# Supprimer
query.exec_("DELETE FROM users WHERE id = 2")

# === QSqlTableModel - Lier table à vue ===
model = QSqlTableModel()
model.setTable('users')
model.select()  # Charger données

# En-têtes
model.setHeaderData(0, Qt.Horizontal, 'ID')
model.setHeaderData(1, Qt.Horizontal, 'Nom')
model.setHeaderData(2, Qt.Horizontal, 'Email')
model.setHeaderData(3, Qt.Horizontal, 'Âge')

# Appliquer à table view
table_view = QTableView()
table_view.setModel(model)

# Cacher colonne ID
table_view.setColumnHidden(0, True)

# Éditer données
model.setData(model.index(0, 1), 'Nouveau nom')
model.submitAll()  # Sauvegarder changements

# Annuler changements
model.revertAll()

# Ajouter ligne
model.insertRow(model.rowCount())
model.setData(model.index(model.rowCount() - 1, 1), 'Nouvel utilisateur')
model.submitAll()

# Supprimer ligne
model.removeRow(0)
model.submitAll()

# Filtrer
model.setFilter('age > 25')
model.select()


[OK] RÉSEAU (QtNetwork)

# === Requête HTTP GET ===
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtCore import QUrl

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.manager = QNetworkAccessManager()
        self.manager.finished.connect(self.on_response)
    
    def fetch_data(self):
        url = QUrl('https://api.example.com/data')
        request = QNetworkRequest(url)
        self.manager.get(request)
    
    def on_response(self, reply):
        if reply.error() == QNetworkReply.NoError:
            data = reply.readAll()
            text = str(data, 'utf-8')
            print(text)
            
            # Parser JSON
            import json
            json_data = json.loads(text)
            print(json_data)
        else:
            print(f'Erreur: {reply.errorString()}')
        
        reply.deleteLater()

# === Requête POST ===
from PyQt5.QtCore import QByteArray

def post_data(self):
    url = QUrl('https://api.example.com/submit')
    request = QNetworkRequest(url)
    request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json')
    
    data = json.dumps({'name': 'Alice', 'age': 30})
    self.manager.post(request, QByteArray(data.encode()))

# === Télécharger fichier ===
def download_file(self, url, filename):
    request = QNetworkRequest(QUrl(url))
    reply = self.manager.get(request)
    reply.finished.connect(lambda: self.save_file(reply, filename))

def save_file(self, reply, filename):
    if reply.error() == QNetworkReply.NoError:
        with open(filename, 'wb') as f:
            f.write(reply.readAll())
        print(f'Fichier enregistré: {filename}')
    reply.deleteLater()

# === Progression téléchargement ===
def download_with_progress(self, url):
    request = QNetworkRequest(QUrl(url))
    reply = self.manager.get(request)
    reply.downloadProgress.connect(self.on_download_progress)

def on_download_progress(self, received, total):
    if total > 0:
        percent = int((received / total) * 100)
        self.progress_bar.setValue(percent)


[OK] MULTIMÉDIA

# === QMediaPlayer - Lecture audio/vidéo ===
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtCore import QUrl

class MediaWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # Player
        self.player = QMediaPlayer()
        
        # Widget vidéo
        self.video_widget = QVideoWidget()
        self.setCentralWidget(self.video_widget)
        self.player.setVideoOutput(self.video_widget)
        
        # Contrôles
        self.init_controls()
        
        # Signaux
        self.player.stateChanged.connect(self.on_state_changed)
        self.player.positionChanged.connect(self.on_position_changed)
        self.player.durationChanged.connect(self.on_duration_changed)
    
    def init_controls(self):
        # Boutons
        play_btn = QPushButton('Play')
        pause_btn = QPushButton('Pause')
        stop_btn = QPushButton('Stop')
        
        play_btn.clicked.connect(self.player.play)
        pause_btn.clicked.connect(self.player.pause)
        stop_btn.clicked.connect(self.player.stop)
        
        # Slider position
        self.position_slider = QSlider(Qt.Horizontal)
        self.position_slider.sliderMoved.connect(self.set_position)
        
        # Slider volume
        self.volume_slider = QSlider(Qt.Horizontal)
        self.volume_slider.setMaximum(100)
        self.volume_slider.setValue(50)
        self.volume_slider.valueChanged.connect(self.player.setVolume)
        
        # Layout
        controls = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(play_btn)
        layout.addWidget(pause_btn)
        layout.addWidget(stop_btn)
        layout.addWidget(self.position_slider)
        layout.addWidget(QLabel('Volume:'))
        layout.addWidget(self.volume_slider)
        controls.setLayout(layout)
        
        # Ajouter au bas
        main_layout = QVBoxLayout()
        main_layout.addWidget(self.video_widget)
        main_layout.addWidget(controls)
        
        container = QWidget()
        container.setLayout(main_layout)
        self.setCentralWidget(container)
    
    def load_media(self, filename):
        url = QUrl.fromLocalFile(filename)
        content = QMediaContent(url)
        self.player.setMedia(content)
    
    def set_position(self, position):
        self.player.setPosition(position)
    
    def on_state_changed(self, state):
        if state == QMediaPlayer.PlayingState:
            print('Lecture en cours')
        elif state == QMediaPlayer.PausedState:
            print('En pause')
        elif state == QMediaPlayer.StoppedState:
            print('Arrêté')
    
    def on_position_changed(self, position):
        self.position_slider.setValue(position)
    
    def on_duration_changed(self, duration):
        self.position_slider.setMaximum(duration)


[OK] NAVIGATEUR WEB INTÉGRÉ

# === QWebEngineView - Navigateur web ===
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtCore import QUrl

class BrowserWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # Navigateur
        self.browser = QWebEngineView()
        self.setCentralWidget(self.browser)
        
        # Charger URL
        self.browser.load(QUrl('https://www.example.com'))
        
        # Charger HTML
        html = '<h1>Hello World</h1><p>Contenu HTML</p>'
        self.browser.setHtml(html)
        
        # Contrôles
        self.init_navigation()
        
        # Signaux
        self.browser.loadStarted.connect(self.on_load_started)
        self.browser.loadProgress.connect(self.on_load_progress)
        self.browser.loadFinished.connect(self.on_load_finished)
        self.browser.titleChanged.connect(self.on_title_changed)
    
    def init_navigation(self):
        navbar = QToolBar('Navigation')
        self.addToolBar(navbar)
        
        # Boutons
        back_btn = QAction('<-', self)
        back_btn.triggered.connect(self.browser.back)
        navbar.addAction(back_btn)
        
        forward_btn = QAction('->', self)
        forward_btn.triggered.connect(self.browser.forward)
        navbar.addAction(forward_btn)
        
        reload_btn = QAction('⟳', self)
        reload_btn.triggered.connect(self.browser.reload)
        navbar.addAction(reload_btn)
        
        home_btn = QAction('Home', self)
        home_btn.triggered.connect(self.go_home)
        navbar.addAction(home_btn)
        
        # Barre d'adresse
        self.url_bar = QLineEdit()
        self.url_bar.returnPressed.connect(self.navigate_to_url)
        navbar.addWidget(self.url_bar)
        
        # Mettre à jour barre d'adresse
        self.browser.urlChanged.connect(
            lambda url: self.url_bar.setText(url.toString())
        )
    
    def navigate_to_url(self):
        url = self.url_bar.text()
        if not url.startswith('http'):
            url = 'http://' + url
        self.browser.load(QUrl(url))
    
    def go_home(self):
        self.browser.load(QUrl('https://www.google.com'))
    
    def on_load_started(self):
        print('Chargement démarré')
    
    def on_load_progress(self, progress):
        print(f'Progression: {progress}%')
    
    def on_load_finished(self, ok):
        if ok:
            print('Chargement terminé')
        else:
            print('Erreur chargement')
    
    def on_title_changed(self, title):
        self.setWindowTitle(title)

# === Exécuter JavaScript ===
def run_javascript(self):
    js_code = "document.title"
    self.browser.page().runJavaScript(js_code, self.on_js_result)

def on_js_result(self, result):
    print(f'Résultat JS: {result}')


[OK] GRAPHIQUES ET CHARTS

# === PyQtGraph (installation séparée) ===
# pip install pyqtgraph

import pyqtgraph as pg

# Créer fenêtre graphique
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('Graphique')

# Plot simple
plot = win.addPlot(title='Courbe')
x = [1, 2, 3, 4, 5]
y = [1, 4, 2, 5, 3]
plot.plot(x, y, pen='r')

# Plusieurs courbes
plot.plot(x, [2, 3, 4, 2, 4], pen='b')
plot.plot(x, [3, 2, 3, 4, 2], pen='g')

# Mise à jour temps réel
curve = plot.plot([], [], pen='r')

def update():
    global curve, data
    data.append(random.random())
    curve.setData(data)

timer = QTimer()
timer.timeout.connect(update)
timer.start(50)


[OK] ANIMATIONS

# === QPropertyAnimation ===
from PyQt5.QtCore import QPropertyAnimation, QEasingCurve

# Animer position
animation = QPropertyAnimation(widget, b'geometry')
animation.setDuration(1000)  # 1 seconde
animation.setStartValue(QRect(0, 0, 100, 100))
animation.setEndValue(QRect(200, 200, 100, 100))
animation.setEasingCurve(QEasingCurve.OutBounce)
animation.start()

# Animer opacité
from PyQt5.QtWidgets import QGraphicsOpacityEffect

effect = QGraphicsOpacityEffect()
widget.setGraphicsEffect(effect)

animation = QPropertyAnimation(effect, b'opacity')
animation.setDuration(500)
animation.setStartValue(1.0)
animation.setEndValue(0.0)
animation.start()

# === Animation séquence ===
from PyQt5.QtCore import QSequentialAnimationGroup, QParallelAnimationGroup

# Séquentielle (une après l'autre)
sequence = QSequentialAnimationGroup()
sequence.addAnimation(animation1)
sequence.addAnimation(animation2)
sequence.addAnimation(animation3)
sequence.start()

# Parallèle (en même temps)
parallel = QParallelAnimationGroup()
parallel.addAnimation(animation1)
parallel.addAnimation(animation2)
parallel.start()


[OK] VALIDATION ET MASQUES DE SAISIE

# === Input mask ===
line_edit.setInputMask('00/00/0000')  # Date
line_edit.setInputMask('(000) 000-0000')  # Téléphone
line_edit.setInputMask('>AAAA')  # 4 lettres majuscules

# Caractères spéciaux mask:
# A - Lettre requise (A-Z, a-z)
# a - Lettre optionnelle
# N - Lettre ou chiffre requis
# n - Lettre ou chiffre optionnel
# X - Tout caractère requis
# x - Tout caractère optionnel
# 9 - Chiffre requis
# 0 - Chiffre requis (accepte 0)
# D - Chiffre optionnel
# # - Chiffre ou signe plus/moins
# > - Tout ce qui suit en majuscules
# < - Tout ce qui suit en minuscules

# === Validation personnalisée ===
from PyQt5.QtGui import QValidator

class CustomValidator(QValidator):
    def validate(self, text, pos):
        # Retourner: Invalid, Intermediate, ou Acceptable
        if not text:
            return QValidator.Intermediate, text, pos
        
        if text.isdigit() and 0 <= int(text) <= 100:
            return QValidator.Acceptable, text, pos
        
        if text.isdigit():
            return QValidator.Intermediate, text, pos
        
        return QValidator.Invalid, text, pos

line_edit.setValidator(CustomValidator())


[OK] IMPRESSION

# === QPrinter - Imprimer document ===
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog

def print_document(self):
    printer = QPrinter()
    dialog = QPrintDialog(printer, self)
    
    if dialog.exec_() == QPrintDialog.Accepted:
        # Imprimer QTextEdit
        self.text_edit.print_(printer)

# === Aperçu avant impression ===
from PyQt5.QtPrintSupport import QPrintPreviewDialog

def print_preview(self):
    printer = QPrinter()
    preview = QPrintPreviewDialog(printer, self)
    preview.paintRequested.connect(self.handle_paint_request)
    preview.exec_()

def handle_paint_request(self, printer):
    self.text_edit.print_(printer)

# === Exporter PDF ===
def export_pdf(self):
    filename, _ = QFileDialog.getSaveFileName(
        self, 'Exporter PDF', '', 'PDF (*.pdf)'
    )
    if filename:
        printer = QPrinter(QPrinter.HighResolution)
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setOutputFileName(filename)
        self.text_edit.print_(printer)


[OK] SYSTRAY (ICÔNE DANS BARRE DES TÂCHES)

# === QSystemTrayIcon ===
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # Créer tray icon
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(QIcon('icon.png'))
        
        # Menu contextuel
        tray_menu = QMenu()
        show_action = tray_menu.addAction('Afficher')
        show_action.triggered.connect(self.show)
        
        hide_action = tray_menu.addAction('Masquer')
        hide_action.triggered.connect(self.hide)
        
        tray_menu.addSeparator()
        
        quit_action = tray_menu.addAction('Quitter')
        quit_action.triggered.connect(QApplication.quit)
        
        self.tray_icon.setContextMenu(tray_menu)
        
        # Signaux
        self.tray_icon.activated.connect(self.on_tray_activated)
        self.tray_icon.messageClicked.connect(self.on_message_clicked)
        
        # Afficher
        self.tray_icon.show()
    
    def on_tray_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            self.show()
    
    def on_message_clicked(self):
        self.show()
    
    # Notification
    def show_notification(self):
        self.tray_icon.showMessage(
            'Titre',
            'Message de notification',
            QSystemTrayIcon.Information,
            3000  # Durée en ms
        )
    
    # Fermeture minimise dans tray
    def closeEvent(self, event):
        event.ignore()
        self.hide()
        self.tray_icon.showMessage(
            'Application minimisée',
            'L\'application continue en arrière-plan',
            QSystemTrayIcon.Information,
            2000
        )


[OK] SPLASH SCREEN

# === QSplashScreen - Écran de démarrage ===
from PyQt5.QtWidgets import QSplashScreen
from PyQt5.QtCore import Qt

# Créer splash screen
pixmap = QPixmap('splash.png')
splash = QSplashScreen(pixmap, Qt.WindowStaysOnTopHint)

# Afficher
splash.show()
app.processEvents()  # Rafraîchir affichage

# Afficher messages pendant chargement
splash.showMessage(
    'Chargement des modules...',
    Qt.AlignBottom | Qt.AlignCenter,
    Qt.white
)
app.processEvents()

# Simuler chargement
time.sleep(2)

splash.showMessage('Initialisation interface...')
app.processEvents()

# Charger application
window = MainWindow()
window.show()

# Fermer splash
splash.finish(window)


[OK] RACCOURCIS CLAVIER GLOBAUX

# === QShortcut ===
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence

# Raccourci simple
shortcut = QShortcut(QKeySequence('Ctrl+S'), self)
shortcut.activated.connect(self.save_file)

# Raccourcis standards
save_shortcut = QShortcut(QKeySequence.Save, self)
open_shortcut = QShortcut(QKeySequence.Open, self)
copy_shortcut = QShortcut(QKeySequence.Copy, self)
paste_shortcut = QShortcut(QKeySequence.Paste, self)

# Raccourci avec touche seule
f5_shortcut = QShortcut(QKeySequence(Qt.Key_F5), self)
f5_shortcut.activated.connect(self.refresh)

# === Événement clavier ===
def keyPressEvent(self, event):
    if event.key() == Qt.Key_Escape:
        self.close()
    elif event.key() == Qt.Key_Return:
        self.submit()
    elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_Q:
        self.close()


[OK] GESTION DES ERREURS ET EXCEPTIONS

# === Capturer exceptions dans slots ===
import sys
import traceback

def exception_hook(exctype, value, tb):
    # Afficher dans terminal
    traceback.print_exception(exctype, value, tb)
    
    # Afficher dialogue
    msg = QMessageBox()
    msg.setIcon(QMessageBox.Critical)
    msg.setWindowTitle('Erreur')
    msg.setText('Une erreur s\'est produite')
    msg.setInformativeText(str(value))
    msg.setDetailedText(''.join(traceback.format_tb(tb)))
    msg.exec_()

# Installer hook
sys.excepthook = exception_hook

# === Try-except dans slots ===
@pyqtSlot()
def on_button_clicked(self):
    try:
        # Code qui peut lever exception
        result = self.process_data()
    except ValueError as e:
        QMessageBox.warning(self, 'Erreur', f'Valeur invalide: {e}')
    except FileNotFoundError as e:
        QMessageBox.critical(self, 'Erreur', f'Fichier introuvable: {e}')
    except Exception as e:
        QMessageBox.critical(self, 'Erreur', f'Erreur inattendue: {e}')


[OK] BONNES PRATIQUES

# 1. Structure du code
# ==================
# - Séparer logique métier de l'interface
# - Utiliser des classes pour les fenêtres
# - Un fichier par fenêtre/dialogue complexe
# - Utiliser Qt Designer pour interfaces complexes

# Structure recommandée:
"""
myapp/
├── main.py                 # Point d'entrée
├── ui/
│   ├── __init__.py
│   ├── main_window.py      # Fenêtre principale
│   ├── settings_dialog.py  # Dialogues
│   └── forms/
│       └── main.ui         # Fichiers Qt Designer
├── core/
│   ├── __init__.py
│   ├── database.py         # Logique base de données
│   └── business.py         # Logique métier
├── resources/
│   ├── icons/
│   ├── images/
│   └── resources.qrc
├── requirements.txt
└── README.md
"""

# 2. Signaux et slots
# ===================
# - Préférer signaux/slots aux appels directs
# - Utiliser @pyqtSlot pour documentation
# - Déconnecter signaux si nécessaire (éviter fuites mémoire)

# 3. Gestion mémoire
# ==================
# - Toujours définir parent pour widgets
widget = QWidget(parent=self)  # [OK] Bon
widget = QWidget()             # [X] Peut fuiter

# - Utiliser deleteLater() pour threads
thread.finished.connect(thread.deleteLater)

# - Nettoyer ressources
def closeEvent(self, event):
    self.timer.stop()
    self.thread.quit()
    self.thread.wait()
    event.accept()

# 4. Performance
# ==============
# - Bloquer signaux pendant mises à jour multiples
widget.blockSignals(True)
# ... modifications ...
widget.blockSignals(False)

# - Utiliser QThread pour opérations longues
# - Éviter trop de widgets (>1000 peut ralentir)
# - Utiliser modèles (Model/View) pour grandes listes

# 5. Multithreading
# =================
# - Jamais modifier interface depuis thread
# - Utiliser signaux pour communication thread -> GUI
# - QObject.moveToThread() pour objets worker

# 6. Debug
# ========
# - Utiliser print() temporairement
# - Logger avec module logging
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

# - Débugger PyCharm/VSCode avec breakpoints

# 7. Tests
# ========
# - pytest-qt pour tester interfaces
pip install pytest-qt

def test_button_click(qtbot):
    widget = MyWidget()
    qtbot.addWidget(widget)
    qtbot.mouseClick(widget.button, Qt.LeftButton)
    assert widget.label.text() == 'Clicked'

# 8. Internationalisation (i18n)
# ===============================
from PyQt5.QtCore import QTranslator, QLocale

translator = QTranslator()
translator.load('translations/app_fr.qm')
app.installTranslator(translator)

# Dans code, utiliser tr()
label = QLabel(self.tr('Hello'))

# Générer fichiers traduction
pylupdate5 *.py -ts translations/app_fr.ts
lrelease translations/app_fr.ts

# 9. Packaging
# ============
# PyInstaller - Créer exécutable
pip install pyinstaller
pyinstaller --windowed --onefile --icon=icon.ico main.py

# Options utiles:
# --windowed : Pas de console
# --onefile : Un seul fichier
# --icon : Icône application
# --add-data : Ajouter ressources

# 10. Configuration
# =================
# - Utiliser QSettings pour préférences
# - Fichier config.ini ou config.json pour paramètres complexes
# - Variables d'environnement pour secrets


[OK] EXEMPLES COMPLETS

# === Application complète: Éditeur de texte ===
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

class TextEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.filename = None
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('Éditeur de Texte')
        self.setGeometry(100, 100, 800, 600)
        
        # Zone de texte
        self.text_edit = QTextEdit()
        self.setCentralWidget(self.text_edit)
        
        # Créer actions
        self.create_actions()
        
        # Créer menu
        self.create_menu()
        
        # Créer toolbar
        self.create_toolbar()
        
        # Status bar
        self.statusBar().showMessage('Prêt')
        
        # Signaux
        self.text_edit.textChanged.connect(self.on_text_changed)
    
    def create_actions(self):
        # Nouveau
        self.new_action = QAction(QIcon.fromTheme('document-new'), '&Nouveau', self)
        self.new_action.setShortcut('Ctrl+N')
        self.new_action.setStatusTip('Créer nouveau fichier')
        self.new_action.triggered.connect(self.new_file)
        
        # Ouvrir
        self.open_action = QAction(QIcon.fromTheme('document-open'), '&Ouvrir', self)
        self.open_action.setShortcut('Ctrl+O')
        self.open_action.triggered.connect(self.open_file)
        
        # Enregistrer
        self.save_action = QAction(QIcon.fromTheme('document-save'), '&Enregistrer', self)
        self.save_action.setShortcut('Ctrl+S')
        self.save_action.triggered.connect(self.save_file)
        
        # Enregistrer sous
        self.save_as_action = QAction('Enregistrer &sous...', self)
        self.save_as_action.setShortcut('Ctrl+Shift+S')
        self.save_as_action.triggered.connect(self.save_file_as)
        
        # Quitter
        self.exit_action = QAction('&Quitter', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.triggered.connect(self.close)
        
        # Copier, Couper, Coller
        self.copy_action = QAction(QIcon.fromTheme('edit-copy'), 'Copier', self)
        self.copy_action.setShortcut('Ctrl+C')
        self.copy_action.triggered.connect(self.text_edit.copy)
        
        self.cut_action = QAction(QIcon.fromTheme('edit-cut'), 'Couper', self)
        self.cut_action.setShortcut('Ctrl+X')
        self.cut_action.triggered.connect(self.text_edit.cut)
        
        self.paste_action = QAction(QIcon.fromTheme('edit-paste'), 'Coller', self)
        self.paste_action.setShortcut('Ctrl+V')
        self.paste_action.triggered.connect(self.text_edit.paste)
        
        # Gras, Italique
        self.bold_action = QAction(QIcon.fromTheme('format-text-bold'), 'Gras', self)
        self.bold_action.setShortcut('Ctrl+B')
        self.bold_action.setCheckable(True)
        self.bold_action.triggered.connect(self.toggle_bold)
        
        self.italic_action = QAction(QIcon.fromTheme('format-text-italic'), 'Italique', self)
        self.italic_action.setShortcut('Ctrl+I')
        self.italic_action.setCheckable(True)
        self.italic_action.triggered.connect(self.toggle_italic)
    
    def create_menu(self):
        menubar = self.menuBar()
        
        # Menu Fichier
        file_menu = menubar.addMenu('&Fichier')
        file_menu.addAction(self.new_action)
        file_menu.addAction(self.open_action)
        file_menu.addSeparator()
        file_menu.addAction(self.save_action)
        file_menu.addAction(self.save_as_action)
        file_menu.addSeparator()
        file_menu.addAction(self.exit_action)
        
        # Menu Édition
        edit_menu = menubar.addMenu('&Édition')
        edit_menu.addAction(self.copy_action)
        edit_menu.addAction(self.cut_action)
        edit_menu.addAction(self.paste_action)
        
        # Menu Format
        format_menu = menubar.addMenu('F&ormat')
        format_menu.addAction(self.bold_action)
        format_menu.addAction(self.italic_action)
    
    def create_toolbar(self):
        toolbar = self.addToolBar('Principale')
        toolbar.addAction(self.new_action)
        toolbar.addAction(self.open_action)
        toolbar.addAction(self.save_action)
        toolbar.addSeparator()
        toolbar.addAction(self.copy_action)
        toolbar.addAction(self.cut_action)
        toolbar.addAction(self.paste_action)
        toolbar.addSeparator()
        toolbar.addAction(self.bold_action)
        toolbar.addAction(self.italic_action)
    
    def new_file(self):
        if self.maybe_save():
            self.text_edit.clear()
            self.filename = None
            self.setWindowTitle('Éditeur de Texte - Nouveau')
    
    def open_file(self):
        if self.maybe_save():
            filename, _ = QFileDialog.getOpenFileName(
                self, 'Ouvrir', '', 'Texte (*.txt);;Tous (*)'
            )
            if filename:
                with open(filename, 'r', encoding='utf-8') as f:
                    self.text_edit.setPlainText(f.read())
                self.filename = filename
                self.setWindowTitle(f'Éditeur de Texte - {filename}')
                self.statusBar().showMessage(f'Ouvert: {filename}')
    
    def save_file(self):
        if self.filename:
            self.write_file(self.filename)
        else:
            self.save_file_as()
    
    def save_file_as(self):
        filename, _ = QFileDialog.getSaveFileName(
            self, 'Enregistrer sous', '', 'Texte (*.txt);;Tous (*)'
        )
        if filename:
            self.write_file(filename)
            self.filename = filename
            self.setWindowTitle(f'Éditeur de Texte - {filename}')
    
    def write_file(self, filename):
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(self.text_edit.toPlainText())
        self.statusBar().showMessage(f'Enregistré: {filename}')
        self.text_edit.document().setModified(False)
    
    def maybe_save(self):
        if self.text_edit.document().isModified():
            reply = QMessageBox.question(
                self, 'Enregistrer?',
                'Le document a été modifié. Enregistrer les changements?',
                QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
            )
            if reply == QMessageBox.Save:
                self.save_file()
                return True
            elif reply == QMessageBox.Cancel:
                return False
        return True
    
    def toggle_bold(self):
        font = self.text_edit.currentFont()
        font.setBold(self.bold_action.isChecked())
        self.text_edit.setCurrentFont(font)
    
    def toggle_italic(self):
        font = self.text_edit.currentFont()
        font.setItalic(self.italic_action.isChecked())
        self.text_edit.setCurrentFont(font)
    
    def on_text_changed(self):
        modified = self.text_edit.document().isModified()
        title = self.windowTitle()
        if modified and not title.startswith('*'):
            self.setWindowTitle('*' + title)
    
    def closeEvent(self, event):
        if self.maybe_save():
            event.accept()
        else:
            event.ignore()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    editor = TextEditor()
    editor.show()
    sys.exit(app.exec_())


# === Application: To-Do List ===
class TodoApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('To-Do List')
        self.setGeometry(100, 100, 400, 500)
        
        # Layout principal
        layout = QVBoxLayout()
        
        # Champ ajout tâche
        add_layout = QHBoxLayout()
        self.task_input = QLineEdit()
        self.task_input.setPlaceholderText('Nouvelle tâche...')
        self.task_input.returnPressed.connect(self.add_task)
        add_layout.addWidget(self.task_input)
        
        add_button = QPushButton('Ajouter')
        add_button.clicked.connect(self.add_task)
        add_layout.addWidget(add_button)
        
        layout.addLayout(add_layout)
        
        # Liste tâches
        self.task_list = QListWidget()
        self.task_list.itemDoubleClicked.connect(self.toggle_task)
        layout.addWidget(self.task_list)
        
        # Boutons
        button_layout = QHBoxLayout()
        
        delete_button = QPushButton('Supprimer')
        delete_button.clicked.connect(self.delete_task)
        button_layout.addWidget(delete_button)
        
        clear_button = QPushButton('Tout effacer')
        clear_button.clicked.connect(self.clear_all)
        button_layout.addWidget(clear_button)
        
        layout.addLayout(button_layout)
        
        self.setLayout(layout)
        
        # Charger tâches
        self.load_tasks()
    
    def add_task(self):
        text = self.task_input.text().strip()
        if text:
            item = QListWidgetItem(text)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Unchecked)
            self.task_list.addItem(item)
            self.task_input.clear()
            self.save_tasks()
    
    def toggle_task(self, item):
        if item.checkState() == Qt.Checked:
            item.setCheckState(Qt.Unchecked)
        else:
            item.setCheckState(Qt.Checked)
        self.save_tasks()
    
    def delete_task(self):
        current = self.task_list.currentRow()
        if current >= 0:
            self.task_list.takeItem(current)
            self.save_tasks()
    
    def clear_all(self):
        reply = QMessageBox.question(
            self, 'Confirmation',
            'Supprimer toutes les tâches?',
            QMessageBox.Yes | QMessageBox.No
        )
        if reply == QMessageBox.Yes:
            self.task_list.clear()
            self.save_tasks()
    
    def save_tasks(self):
        settings = QSettings('TodoApp', 'Tasks')
        tasks = []
        for i in range(self.task_list.count()):
            item = self.task_list.item(i)
            tasks.append({
                'text': item.text(),
                'checked': item.checkState() == Qt.Checked
            })
        settings.setValue('tasks', tasks)
    
    def load_tasks(self):
        settings = QSettings('TodoApp', 'Tasks')
        tasks = settings.value('tasks', [])
        for task in tasks:
            item = QListWidgetItem(task['text'])
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked if task['checked'] else Qt.Unchecked)
            self.task_list.addItem(item)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    todo = TodoApp()
    todo.show()
    sys.exit(app.exec_())


[OK] DÉPANNAGE ERREURS COURANTES

# === Erreur: no module named 'PyQt5' ===
# Solution: Installer PyQt5
pip install PyQt5

# === Erreur: cannot import name 'QApplication' ===
# Solution: Import correct
from PyQt5.QtWidgets import QApplication  # [OK]
from PyQt5 import QApplication  # [X]

# === Erreur: QObject::connect: No such slot ===
# Solution: Vérifier nom de la méthode ou utiliser lambda
button.clicked.connect(self.on_clicked)  # Méthode doit exister
button.clicked.connect(lambda: print('Click'))  # Ou lambda

# === Erreur: QWidget: Must construct a QApplication before ===
# Solution: Créer QApplication en premier
app = QApplication(sys.argv)  # Toujours en premier
window = MainWindow()

# === Erreur: Process finished with exit code -1073740791 (Windows) ===
# Solution: Problème de DLL, réinstaller
pip uninstall PyQt5
pip install PyQt5

# === Application ne se ferme pas ===
# Solution: Utiliser sys.exit(app.exec_())
sys.exit(app.exec_())  # [OK]
app.exec_()  # [X] (peut ne pas quitter proprement)

# === Widgets ne s'affichent pas ===
# Solution: Appeler show() et exec_()
window.show()  # Afficher fenêtre
sys.exit(app.exec_())  # Boucle événements

# === Layout ne fonctionne pas ===
# Solution: Vérifier setLayout()
widget.setLayout(layout)  # Obligatoire!

# === Signal ne se déclenche pas ===
# Solution: Vérifier connexion
# - Méthode existe?
# - Bonne signature?
# - Object toujours vivant?

# === ImportError: DLL load failed (Windows) ===
# Solution: Installer Visual C++ Redistributable
# https://www.microsoft.com/en-us/download/details.aspx?id=48145

# === Freeze / Blocage interface ===
# Solution: Utiliser QThread pour opérations longues
# Ne JAMAIS bloquer GUI thread avec time.sleep() ou boucles longues

# === Mémoire qui augmente ===
# Solution:
# - Définir parents pour widgets
# - Déconnecter signaux inutiles
# - Utiliser deleteLater()
# - Nettoyer dans closeEvent()


[OK] RESSOURCES ET DOCUMENTATION

# Documentation officielle:
# https://doc.qt.io/qt-5/
# https://www.riverbankcomputing.com/static/Docs/PyQt5/

# Tutoriels:
# https://realpython.com/python-pyqt-gui-calculator/
# https://www.pythonguis.com/

# Exemples Qt:
# https://github.com/baoboa/pyqt5/tree/master/examples

# Qt Designer:
# Inclus avec PyQt5-tools

# Alternatives:
# - PySide6 (binding officiel Qt, LGPL)
# - PyQt6 (nouvelle version, API modernisée)

# Outils:
# - Qt Creator: IDE pour Qt
# - Qt Designer: Éditeur visuel interfaces
# - PyCharm: IDE Python avec support PyQt

# Livres recommandés:
# - "Rapid GUI Programming with Python and Qt" - Mark Summerfield
# - "Create GUI Applications with Python & Qt5" - Martin Fitzpatrick

# Forums:
# - Stack Overflow: tag [pyqt5]
# - Qt Forum: https://forum.qt.io/
# - Reddit: r/QtFramework

# Style guides et ressources design:
# - Material Design
# - Qt Style Sheets examples
# - QSS (Qt Style Sheets) documentation


[OK] CHECKLIST PROJET PYQT5

# Avant de commencer:
# [WHITE_SQUARE] Python 3.7+ installé
# [WHITE_SQUARE] PyQt5 installé (pip install PyQt5)
# [WHITE_SQUARE] PyQt5-tools installé si besoin Designer (pip install PyQt5-tools)
# [WHITE_SQUARE] IDE configuré (PyCharm/VSCode)

# Structure projet:
# [WHITE_SQUARE] Créer structure dossiers (ui/, core/, resources/)
# [WHITE_SQUARE] Initialiser git
# [WHITE_SQUARE] Créer requirements.txt
# [WHITE_SQUARE] Créer .gitignore pour Python

# Développement:
# [WHITE_SQUARE] Définir fenêtre principale (QMainWindow ou QWidget)
# [WHITE_SQUARE] Créer layouts pour organisation widgets
# [WHITE_SQUARE] Implémenter logique métier séparée de l'UI
# [WHITE_SQUARE] Connecter signaux/slots
# [WHITE_SQUARE] Gérer erreurs (try-except)
# [WHITE_SQUARE] Implémenter sauvegarde préférences (QSettings)
# [WHITE_SQUARE] Ajouter icônes et ressources
# [WHITE_SQUARE] Tester sur toutes les plateformes cibles

# Performance:
# [WHITE_SQUARE] Utiliser QThread pour opérations longues
# [WHITE_SQUARE] Éviter modifications GUI depuis threads
# [WHITE_SQUARE] Optimiser nombre de widgets si >1000
# [WHITE_SQUARE] Utiliser Model/View pour grandes données

# Finition:
# [WHITE_SQUARE] Ajouter raccourcis clavier courants
# [WHITE_SQUARE] Implémenter menu Aide/À propos
# [WHITE_SQUARE] Définir icône application
# [WHITE_SQUARE] Gérer fermeture propre (closeEvent)
# [WHITE_SQUARE] Sauvegarder position/taille fenêtre
# [WHITE_SQUARE] Tests utilisateurs

# Packaging:
# [WHITE_SQUARE] Tester avec PyInstaller
# [WHITE_SQUARE] Créer fichier .spec personnalisé
# [WHITE_SQUARE] Inclure toutes les ressources
# [WHITE_SQUARE] Tester exécutable sur machine propre
# [WHITE_SQUARE] Créer installeur (NSIS, Inno Setup, etc.)

# Distribution:
# [WHITE_SQUARE] README complet
# [WHITE_SQUARE] Licence claire
# [WHITE_SQUARE] Changelog
# [WHITE_SQUARE] Screenshots
# [WHITE_SQUARE] Documentation utilisateur


[OK] DÉPLOIEMENT ET DISTRIBUTION - GUIDE COMPLET

# Le déploiement consiste à créer un exécutable autonome de votre application PyQt5
# pour que les utilisateurs puissent l'utiliser sans installer Python

# === OUTILS DE DÉPLOIEMENT ===

# PyInstaller - Le plus utilisé et recommandé
pip install pyinstaller

# cx_Freeze - Alternative cross-platform
pip install cx_Freeze

# py2exe - Windows uniquement
pip install py2exe

# py2app - macOS uniquement
pip install py2app

# Nuitka - Compile en C++ (plus rapide)
pip install nuitka


[OK] PYINSTALLER - GUIDE DÉTAILLÉ

# === Installation ===
pip install pyinstaller

# Vérifier installation
pyinstaller --version

# === Commandes de base ===

# Créer exécutable simple (mode console)
pyinstaller main.py

# Créer exécutable sans console (GUI)
pyinstaller --windowed main.py
pyinstaller -w main.py  # Équivalent court

# Créer un seul fichier exécutable
pyinstaller --onefile main.py
pyinstaller -F main.py  # Équivalent court

# Combinaison recommandée pour GUI
pyinstaller --onefile --windowed main.py
pyinstaller -F -w main.py

# Avec icône personnalisée
pyinstaller --onefile --windowed --icon=icon.ico main.py
pyinstaller -F -w -i icon.ico main.py

# === Options PyInstaller détaillées ===

# --name ou -n : Nom de l'exécutable
pyinstaller -F -w --name="MonApp" main.py

# --add-data : Inclure fichiers supplémentaires
# Format Windows: source;destination
pyinstaller -F -w --add-data "images;images" --add-data "config.ini;." main.py

# Format Linux/Mac: source:destination
pyinstaller -F -w --add-data "images:images" --add-data "config.ini:." main.py

# --add-binary : Inclure bibliothèques binaires
pyinstaller -F -w --add-binary "lib/custom.dll;lib" main.py

# --hidden-import : Importer modules non détectés
pyinstaller -F -w --hidden-import=PyQt5.QtPrintSupport main.py

# --exclude-module : Exclure modules inutiles
pyinstaller -F -w --exclude-module=tkinter main.py

# --noconsole : Pas de console (équivalent à --windowed)
pyinstaller -F --noconsole main.py

# --debug : Mode debug (voir erreurs)
pyinstaller -F -w --debug=all main.py

# --clean : Nettoyer avant build
pyinstaller -F -w --clean main.py

# --noconfirm : Écraser sans demander
pyinstaller -F -w --noconfirm main.py

# === Structure après build ===
# PyInstaller crée 3 dossiers:

"""
mon_projet/
├── main.py
├── build/              # Fichiers temporaires de build
│   └── main/
├── dist/               # * Exécutable final ici
│   └── main.exe       # Ou MonApp.exe
└── main.spec          # Fichier de configuration
"""

# === Fichier .spec (configuration avancée) ===

# Générer fichier .spec sans build
pyi-makespec --onefile --windowed main.py

# Exemple de fichier main.spec:
"""
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('images', 'images'),
        ('config.ini', '.'),
        ('resources/*', 'resources'),
    ],
    hiddenimports=[
        'PyQt5.QtPrintSupport',
        'PyQt5.QtMultimedia',
    ],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=['tkinter'],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='MonApplication',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='icon.ico'
)
"""

# Build avec fichier .spec
pyinstaller main.spec

# === Optimiser taille de l'exécutable ===

# 1. Exclure modules inutiles
pyinstaller -F -w \
    --exclude-module=tkinter \
    --exclude-module=matplotlib \
    --exclude-module=numpy \
    main.py

# 2. Utiliser UPX (compresseur)
# Télécharger UPX: https://upx.github.io/
# Placer upx.exe dans PATH ou dossier PyInstaller

pyinstaller -F -w --upx-dir=/path/to/upx main.py

# 3. Mode onefile (un seul fichier, mais plus lent au démarrage)
pyinstaller -F -w main.py

# 4. Mode onedir (plus rapide, mais plusieurs fichiers)
pyinstaller -w main.py  # Crée dossier avec DLL séparées

# === Gérer les ressources (images, fichiers) ===

# Dans votre code, utiliser chemin relatif à l'exécutable
import sys
import os

def resource_path(relative_path):
    """Obtenir chemin absolu vers ressource (fonctionne pour dev et PyInstaller)"""
    try:
        # PyInstaller crée dossier temporaire _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    
    return os.path.join(base_path, relative_path)

# Utilisation:
icon_path = resource_path('images/icon.png')
pixmap = QPixmap(icon_path)

config_path = resource_path('config.ini')
with open(config_path, 'r') as f:
    config = f.read()

# === Inclure ressources Qt (.qrc) ===

# Si vous utilisez ressources Qt compilées
# 1. Compiler ressources
pyrcc5 resources.qrc -o resources_rc.py

# 2. Importer dans main.py
import resources_rc

# 3. PyInstaller détectera automatiquement
pyinstaller -F -w main.py

# === Problèmes courants et solutions ===

# Problème: Module manquant
# Solution: Ajouter --hidden-import
pyinstaller -F -w --hidden-import=module_name main.py

# Problème: DLL manquante
# Solution: Copier DLL et ajouter avec --add-binary
pyinstaller -F -w --add-binary "path/to/file.dll;." main.py

# Problème: Exécutable trop gros
# Solutions:
# - Utiliser environnement virtuel propre (sans packages inutiles)
# - Exclure modules inutiles
# - Utiliser UPX
# - Mode onedir au lieu de onefile

# Problème: Antivirus bloque l'exécutable
# Solution:
# - Signer numériquement l'exécutable (certificat code signing)
# - Uploader sur VirusTotal pour vérification
# - Contacter éditeur antivirus pour whitelist

# Problème: Erreur au lancement
# Solution: Build avec --debug pour voir logs
pyinstaller -F -w --debug=all main.py

# === Script de build complet ===

# build.py - Script pour automatiser le build
"""
import os
import shutil
import subprocess

# Nettoyer anciens builds
if os.path.exists('build'):
    shutil.rmtree('build')
if os.path.exists('dist'):
    shutil.rmtree('dist')

# Options PyInstaller
options = [
    '--onefile',
    '--windowed',
    '--name=MonApplication',
    '--icon=resources/icon.ico',
    '--add-data=images;images',
    '--add-data=config.ini;.',
    '--hidden-import=PyQt5.QtPrintSupport',
    '--exclude-module=tkinter',
    '--clean',
    '--noconfirm',
]

# Lancer build
cmd = ['pyinstaller'] + options + ['main.py']
subprocess.run(cmd)

print("Build terminé! Exécutable dans dist/")
"""

# Exécuter script de build
python build.py

# === Build pour différentes plateformes ===

# IMPORTANT: PyInstaller n'est PAS cross-platform!
# Pour créer .exe Windows, vous devez builder sur Windows
# Pour créer .app macOS, vous devez builder sur macOS
# Pour créer binaire Linux, vous devez builder sur Linux

# Windows (.exe)
pyinstaller -F -w --icon=icon.ico main.py

# Linux (binaire)
pyinstaller -F -w --icon=icon.png main.py

# macOS (.app)
pyinstaller -F -w --icon=icon.icns main.py


[OK] CX_FREEZE - ALTERNATIVE À PYINSTALLER

# === Installation ===
pip install cx_Freeze

# === Créer setup.py ===
"""
# setup.py
import sys
from cx_Freeze import setup, Executable

# Dépendances
build_exe_options = {
    "packages": ["os", "PyQt5"],
    "excludes": ["tkinter"],
    "include_files": [
        ("images", "images"),
        ("config.ini", "config.ini"),
    ]
}

# Configuration base (GUI sans console)
base = None
if sys.platform == "win32":
    base = "Win32GUI"

setup(
    name="MonApplication",
    version="1.0",
    description="Description de mon application",
    options={"build_exe": build_exe_options},
    executables=[
        Executable(
            "main.py",
            base=base,
            icon="icon.ico",
            target_name="MonApplication.exe"
        )
    ]
)
"""

# === Build ===
python setup.py build

# Résultat dans: build/exe.win-amd64-3.11/ (ou similaire)

# === Créer installeur MSI (Windows) ===
python setup.py bdist_msi

# Résultat dans: dist/MonApplication-1.0-amd64.msi


[OK] CRÉER INSTALLEUR PROFESSIONNEL

# === Inno Setup (Windows) - Recommandé ===

# 1. Télécharger Inno Setup
# https://jrsoftware.org/isinfo.php

# 2. Créer script .iss
"""
; installer.iss

[Setup]
AppName=Mon Application
AppVersion=1.0
DefaultDirName={pf}\MonApplication
DefaultGroupName=Mon Application
OutputDir=installer
OutputBaseFilename=MonApplication-Setup
Compression=lzma2
SolidCompression=yes
SetupIconFile=icon.ico
UninstallDisplayIcon={app}\MonApplication.exe

[Files]
Source: "dist\MonApplication.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "images\*"; DestDir: "{app}\images"; Flags: ignoreversion recursesubdirs
Source: "config.ini"; DestDir: "{app}"; Flags: ignoreversion
Source: "README.txt"; DestDir: "{app}"; Flags: isreadme

[Icons]
Name: "{group}\Mon Application"; Filename: "{app}\MonApplication.exe"
Name: "{group}\Désinstaller"; Filename: "{uninstallexe}"
Name: "{commondesktop}\Mon Application"; Filename: "{app}\MonApplication.exe"

[Run]
Filename: "{app}\MonApplication.exe"; Description: "Lancer Mon Application"; Flags: nowait postinstall skipifsilent

[Registry]
Root: HKCU; Subkey: "Software\MonApplication"; Flags: uninsdeletekeyifempty
Root: HKCU; Subkey: "Software\MonApplication\Settings"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"
"""

# 3. Compiler avec Inno Setup Compiler
# File > Open > installer.iss > Build > Compile

# Résultat: installer/MonApplication-Setup.exe

# === NSIS (Windows) - Alternative ===

# 1. Télécharger NSIS
# https://nsis.sourceforge.io/

# 2. Créer script .nsi
"""
; installer.nsi

!define APP_NAME "Mon Application"
!define APP_VERSION "1.0"
!define APP_PUBLISHER "Votre Nom"
!define APP_EXE "MonApplication.exe"

Name "${APP_NAME}"
OutFile "MonApplication-Setup.exe"
InstallDir "$PROGRAMFILES\${APP_NAME}"

Page directory
Page instfiles

Section "Install"
  SetOutPath "$INSTDIR"
  File "dist\${APP_EXE}"
  File /r "images"
  File "config.ini"
  
  CreateDirectory "$SMPROGRAMS\${APP_NAME}"
  CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}"
  CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}"
  
  WriteUninstaller "$INSTDIR\Uninstall.exe"
SectionEnd

Section "Uninstall"
  Delete "$INSTDIR\${APP_EXE}"
  Delete "$INSTDIR\Uninstall.exe"
  RMDir /r "$INSTDIR"
  Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk"
  Delete "$DESKTOP\${APP_NAME}.lnk"
  RMDir "$SMPROGRAMS\${APP_NAME}"
SectionEnd
"""

# 3. Compiler
makensis installer.nsi

# === AppImage (Linux) ===

# 1. Structure dossier
"""
MonApplication.AppDir/
├── AppRun                  # Script de lancement
├── mon-application.desktop # Fichier .desktop
├── icon.png               # Icône
└── usr/
    ├── bin/
    │   └── MonApplication # Exécutable
    └── lib/               # Bibliothèques
"""

# 2. Créer AppRun
"""
#!/bin/bash
APPDIR=$(dirname $(readlink -f $0))
exec "$APPDIR/usr/bin/MonApplication" "$@"
"""

# 3. Créer .desktop
"""
[Desktop Entry]
Name=Mon Application
Exec=MonApplication
Icon=icon
Type=Application
Categories=Utility;
"""

# 4. Télécharger appimagetool
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage

# 5. Créer AppImage
./appimagetool-x86_64.AppImage MonApplication.AppDir

# === DMG (macOS) ===

# 1. Créer application avec PyInstaller
pyinstaller -F -w --icon=icon.icns main.py

# 2. Créer DMG
hdiutil create -volname "Mon Application" -srcfolder dist/MonApplication.app -ov -format UDZO MonApplication.dmg


[OK] SIGNATURE NUMÉRIQUE ET CERTIFICATS

# === Windows Code Signing ===

# 1. Obtenir certificat code signing
# Autorités: DigiCert, Sectigo, GlobalSign
# Prix: ~100-400€/an

# 2. Signer avec signtool (Windows SDK)
signtool sign /f certificat.pfx /p motdepasse /t http://timestamp.digicert.com dist\MonApplication.exe

# 3. Vérifier signature
signtool verify /pa dist\MonApplication.exe

# Avantages:
# - Moins d'avertissements Windows Defender
# - Confiance utilisateurs
# - SmartScreen moins strict

# === macOS Code Signing ===

# 1. Obtenir certificat Apple Developer
# https://developer.apple.com/
# Prix: 99$/an

# 2. Signer application
codesign --deep --force --verify --verbose --sign "Developer ID Application: Votre Nom" dist/MonApplication.app

# 3. Notarization (requis pour macOS 10.15+)
xcrun notarytool submit MonApplication.dmg --keychain-profile "notary" --wait

# === Linux ===
# Pas de signature standard, mais GPG possible
gpg --armor --detach-sign MonApplication


[OK] AUTO-UPDATE (MISE À JOUR AUTOMATIQUE)

# === Implémenter vérification version ===

# version.py
VERSION = "1.0.0"

# main.py
import requests
from PyQt5.QtWidgets import QMessageBox
import version

def check_updates():
    """Vérifier si nouvelle version disponible"""
    try:
        # Fichier version.json sur serveur
        response = requests.get('https://monsite.com/version.json', timeout=5)
        data = response.json()
        latest_version = data['version']
        download_url = data['download_url']
        
        if latest_version > version.VERSION:
            reply = QMessageBox.question(
                None,
                'Mise à jour disponible',
                f'Version {latest_version} disponible.\nVoulez-vous télécharger?',
                QMessageBox.Yes | QMessageBox.No
            )
            
            if reply == QMessageBox.Yes:
                import webbrowser
                webbrowser.open(download_url)
                return True
    except Exception as e:
        print(f'Erreur vérification mise à jour: {e}')
    
    return False

# Vérifier au démarrage
if __name__ == '__main__':
    app = QApplication(sys.argv)
    
    # Vérifier updates
    check_updates()
    
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

# === version.json (sur serveur) ===
"""
{
  "version": "1.1.0",
  "download_url": "https://monsite.com/MonApplication-Setup.exe",
  "release_notes": "- Correction bugs\n- Nouvelles fonctionnalités",
  "required": false
}
"""

# === PyUpdater (solution complète) ===
pip install PyUpdater

# Configuration et utilisation avancée sur:
# https://www.pyupdater.org/


[OK] DISTRIBUTION SUR DIFFÉRENTES PLATEFORMES

# === Windows ===

# Option 1: Fichier .exe seul (PyInstaller)
pyinstaller -F -w --icon=icon.ico main.py
# Distribuer: dist/MonApplication.exe

# Option 2: Installeur (Inno Setup)
# Distribuer: MonApplication-Setup.exe
# Avantages:
# - Installation professionnelle
# - Désinstalleur
# - Raccourcis bureau/menu
# - Entrée Programmes et fonctionnalités

# Option 3: Microsoft Store
# Créer package MSIX avec Windows Application Packaging Project

# Option 4: Portable (ZIP)
# Créer archive avec exécutable + ressources
# Pas d'installation requise

# === macOS ===

# Option 1: Application .app
pyinstaller -F -w --icon=icon.icns main.py
# Distribuer: MonApplication.app dans DMG

# Option 2: Mac App Store
# Nécessite:
# - Apple Developer Program (99$/an)
# - Code signing
# - Sandboxing
# - Notarization

# === Linux ===

# Option 1: AppImage (recommandé)
# Un seul fichier, fonctionne sur toutes distributions

# Option 2: Snap
snapcraft
# Distribuer sur Snap Store

# Option 3: Flatpak
flatpak-builder
# Distribuer sur Flathub

# Option 4: Package .deb (Debian/Ubuntu)
dpkg-deb --build mon-application

# Option 5: Package .rpm (Fedora/RedHat)
rpmbuild -ba mon-application.spec

# Option 6: Tarball
tar -czf MonApplication-1.0.tar.gz MonApplication/


[OK] HÉBERGEMENT ET DISTRIBUTION

# === GitHub Releases ===

# 1. Créer release sur GitHub
git tag v1.0.0
git push origin v1.0.0

# 2. Aller sur GitHub > Releases > Create new release

# 3. Upload assets:
# - MonApplication-Windows.exe
# - MonApplication-Linux.AppImage
# - MonApplication-macOS.dmg

# 4. Téléchargement direct via:
# https://github.com/user/repo/releases/download/v1.0.0/MonApplication-Windows.exe

# === Site web personnel ===

# Structure recommandée:
"""
monsite.com/
├── index.html              # Page principale
├── download.html           # Page téléchargements
├── downloads/
│   ├── windows/
│   │   └── MonApplication-Setup.exe
│   ├── linux/
│   │   └── MonApplication.AppImage
│   └── macos/
│       └── MonApplication.dmg
└── version.json           # Pour auto-update
"""

# === Services d'hébergement gratuits ===

# GitHub Releases (gratuit, recommandé)
# - 2 GB par fichier
# - Bande passante illimitée

# SourceForge (gratuit)
# - Hébergement fichiers
# - Statistiques téléchargements

# Google Drive / Dropbox
# - Simple mais pas professionnel
# - Limitation bande passante

# AWS S3 / Azure Blob Storage
# - Payant mais fiable
# - CDN disponible


[OK] MARKETING ET LANCEMENT

# === Préparer le lancement ===

# 1. Site web/Landing page
# - Description claire de l'application
# - Screenshots/vidéos
# - Boutons téléchargement
# - Documentation
# - FAQ
# - Contact/Support

# 2. README complet
"""
# Mon Application

## Description
Courte description de l'application.

## Fonctionnalités
- Fonctionnalité 1
- Fonctionnalité 2
- Fonctionnalité 3

## Installation

### Windows
1. Télécharger `MonApplication-Setup.exe`
2. Double-cliquer pour installer
3. Lancer depuis menu Démarrer

### macOS
1. Télécharger `MonApplication.dmg`
2. Ouvrir le DMG
3. Glisser l'application dans Applications

### Linux
1. Télécharger `MonApplication.AppImage`
2. Rendre exécutable: `chmod +x MonApplication.AppImage`
3. Double-cliquer pour lancer

## Utilisation
Guide rapide d'utilisation...

## Configuration requise
- Windows 10+, macOS 10.15+, ou Linux
- 4 GB RAM minimum
- 100 MB espace disque

## Support
- Email: support@monapp.com
- Issues: https://github.com/user/repo/issues

## Licence
MIT License

## Changelog
Voir [CHANGELOG.md](CHANGELOG.md)
"""

# 3. Screenshots de qualité
# - Résolution minimum 1920x1080
# - Fenêtre claire et bien éclairée
# - Fonctionnalités principales visibles
# - Format PNG avec fond transparent si possible

# 4. Vidéo de démonstration
# - 1-3 minutes
# - Montrer cas d'usage réels
# - Voix-off ou sous-titres
# - Upload sur YouTube/Vimeo

# === Où promouvoir ===

# Reddit
# - r/Python
# - r/learnpython
# - r/opensource
# - Subreddits spécifiques à votre niche

# Hacker News
# - Show HN: Mon Application
# - Attention à la qualité du post

# Product Hunt
# - Lancement produits
# - Communauté active

# Twitter/X
# - Thread détaillé
# - Hashtags #Python #PyQt5 #OpenSource

# Dev.to / Medium
# - Article technique sur le développement
# - Partager expérience

# PyPI (si bibliothèque)
pip install mon-application

# === Statistiques et analytics ===

# 1. Téléchargements
# - GitHub Releases: Traffic insights
# - Google Analytics sur site web

# 2. Télémétrie (optionnelle, avec consentement)
"""
import requests

def send_analytics(event):
    if user_consent:
        try:
            requests.post('https://analytics.monapp.com', {
                'event': event,
                'version': VERSION,
                'os': platform.system()
            })
        except:
            pass
"""

# 3. Crash reports (optionnelle)
# - Sentry.io
# - Rollbar
# - Bugsnag


[OK] WORKFLOW COMPLET DE DÉPLOIEMENT

# === Checklist avant release ===

# [WHITE_SQUARE] Tests complets sur toutes plateformes
# [WHITE_SQUARE] Version incrémentée
# [WHITE_SQUARE] CHANGELOG mis à jour
# [WHITE_SQUARE] README à jour
# [WHITE_SQUARE] Documentation complète
# [WHITE_SQUARE] Screenshots récents
# [WHITE_SQUARE] Tous les bugs critiques corrigés
# [WHITE_SQUARE] Build test sur machines propres
# [WHITE_SQUARE] Taille exécutable raisonnable (<100 MB idéalement)
# [WHITE_SQUARE] Temps démarrage acceptable (<5 secondes)
# [WHITE_SQUARE] Pas de dépendances manquantes
# [WHITE_SQUARE] Ressources incluses correctement
# [WHITE_SQUARE] Icône de qualité
# [WHITE_SQUARE] About/À propos avec version

# === Script de release automatique ===

# release.py
"""
#!/usr/bin/env python3
import os
import subprocess
import shutil
from datetime import datetime

VERSION = "1.0.0"
APP_NAME = "MonApplication"

def clean():
    print("Nettoyage...")
    for dir in ['build', 'dist']:
        if os.path.exists(dir):
            shutil.rmtree(dir)

def build_windows():
    print("Build Windows...")
    subprocess.run([
        'pyinstaller',
        '--onefile',
        '--windowed',
        '--name', APP_NAME,
        '--icon', 'resources/icon.ico',
        '--add-data', 'images;images',
        'main.py'
    ])
    
    # Renommer avec version
    os.rename(
        f'dist/{APP_NAME}.exe',
        f'dist/{APP_NAME}-{VERSION}-Windows.exe'
    )

def create_installer():
    print("Création installeur...")
    subprocess.run(['iscc', 'installer.iss'])

def create_zip():
    print("Création archive...")
    shutil.make_archive(
        f'dist/{APP_NAME}-{VERSION}-Portable',
        'zip',
        'dist',
        APP_NAME
    )

def git_tag():
    print("Git tag...")
    subprocess.run(['git', 'tag', f'v{VERSION}'])
    subprocess.run(['git', 'push', 'origin', f'v{VERSION}'])

def main():
    print(f"=== Release {APP_NAME} v{VERSION} ===")
    print(f"Date: {datetime.now()}")
    
    clean()
    build_windows()
    create_installer()
    create_zip()
    git_tag()
    
    print("\n[OK] Release terminée!")
    print(f"Fichiers dans dist/:")
    for file in os.listdir('dist'):
        print(f"  - {file}")

if __name__ == '__main__':
    main()
"""

# Exécuter release
python release.py

# === CI/CD avec GitHub Actions ===

# .github/workflows/build.yml
"""
name: Build and Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.11
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pyinstaller
      
      - name: Build executable
        run: |
          pyinstaller --onefile --windowed --icon=icon.ico main.py
      
      - name: Upload artifact
        uses: actions/upload-artifact@v2
        with:
          name: windows-executable
          path: dist/*.exe
      
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: dist/*.exe
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  build-linux:
    runs-on: ubuntu-latest
    steps:
      # Similar pour Linux
      
  build-macos:
    runs-on: macos-latest
    steps:
      # Similar pour macOS
"""


[OK] SUPPORT POST-LANCEMENT

# === Canaux de support ===

# 1. GitHub Issues
# - Bugs
# - Feature requests
# - Questions

# 2. Email support
support@monapp.com

# 3. Discord/Slack community

# 4. Documentation
# - Wiki GitHub
# - ReadTheDocs
# - Site web

# === Gestion des retours ===

# Template issue bug:
"""
**Description**
Description claire du bug.

**Reproduction**
1. Aller à '...'
2. Cliquer sur '...'
3. Erreur apparaît

**Comportement attendu**
Ce qui devrait se passer.

**Screenshots**
Si applicable.

**Environnement:**
 - OS: [Windows 10, macOS 13, Ubuntu 22.04]
 - Version: [1.0.0]
"""

# === Planification mises à jour ===

# Versioning sémantique (SemVer):
# MAJOR.MINOR.PATCH
# 1.0.0 -> 1.0.1 (patch: bug fix)
# 1.0.1 -> 1.1.0 (minor: nouvelle fonctionnalité)
# 1.1.0 -> 2.0.0 (major: changement incompatible)

# CHANGELOG.md
"""
# Changelog

## [1.1.0] - 2024-03-15
### Added
- Nouvelle fonctionnalité X
- Support thème sombre

### Changed
- Amélioration performance
- Interface redesignée

### Fixed
- Correction bug #42
- Correction crash au démarrage

### Deprecated
- Ancienne API sera supprimée en 2.0

### Security
- Correction faille CVE-2024-XXXX

## [1.0.1] - 2024-02-01
### Fixed
- Correction bugs mineurs
"""


[OK] MONÉTISATION (OPTIONNEL)

# === Modèles de distribution ===

# 1. Gratuit et Open Source
# - Licence: MIT, GPL, Apache 2.0
# - Hébergement: GitHub
# - Revenus: Donations, sponsoring

# 2. Freemium
# - Version gratuite avec fonctionnalités limitées
# - Version Pro payante
# - Vérification licence dans code

# 3. Shareware / Trial
# - Essai gratuit 30 jours
# - Achat pour utilisation complète

# 4. Payant uniquement
# - Vente directe
# - Plateformes: Gumroad, Paddle, FastSpring

# 5. Abonnement
# - Paiement mensuel/annuel
# - Vérification serveur

# === Implémenter système de licence ===

# license_manager.py
"""
import hashlib
import json
from datetime import datetime, timedelta
from PyQt5.QtWidgets import QMessageBox

class LicenseManager:
    def __init__(self):
        self.license_file = 'license.key'
        self.secret_key = 'YOUR_SECRET_KEY_HERE'
    
    def generate_key(self, email, expiry_days=365):
        '''Générer clé de licence (côté serveur)'''
        expiry = datetime.now() + timedelta(days=expiry_days)
        data = f"{email}|{expiry.isoformat()}|{self.secret_key}"
        key = hashlib.sha256(data.encode()).hexdigest()
        
        license_data = {
            'email': email,
            'key': key,
            'expiry': expiry.isoformat()
        }
        return license_data
    
    def verify_license(self):
        '''Vérifier licence valide'''
        try:
            with open(self.license_file, 'r') as f:
                license_data = json.load(f)
            
            email = license_data['email']
            key = license_data['key']
            expiry = datetime.fromisoformat(license_data['expiry'])
            
            # Vérifier signature
            data = f"{email}|{license_data['expiry']}|{self.secret_key}"
            expected_key = hashlib.sha256(data.encode()).hexdigest()
            
            if key != expected_key:
                return False, "Licence invalide"
            
            # Vérifier expiration
            if datetime.now() > expiry:
                return False, "Licence expirée"
            
            return True, "Licence valide"
            
        except FileNotFoundError:
            return False, "Aucune licence trouvée"
        except Exception as e:
            return False, f"Erreur: {str(e)}"
    
    def save_license(self, license_data):
        '''Sauvegarder licence'''
        with open(self.license_file, 'w') as f:
            json.dump(license_data, f)

# Utilisation dans application
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # Vérifier licence au démarrage
        license_mgr = LicenseManager()
        valid, message = license_mgr.verify_license()
        
        if not valid:
            self.show_license_dialog()
        else:
            self.initUI()
    
    def show_license_dialog(self):
        from license_dialog import LicenseDialog
        dialog = LicenseDialog(self)
        if dialog.exec_() != QDialog.Accepted:
            sys.exit(0)  # Quitter si pas de licence
"""

# license_dialog.py
"""
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt

class LicenseDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle('Activation')
        self.setModal(True)
        self.initUI()
    
    def initUI(self):
        layout = QVBoxLayout()
        
        # Message
        label = QLabel('Entrez votre clé de licence:')
        layout.addWidget(label)
        
        # Champ licence
        self.license_input = QLineEdit()
        self.license_input.setPlaceholderText('XXXX-XXXX-XXXX-XXXX')
        layout.addWidget(self.license_input)
        
        # Boutons
        button_layout = QHBoxLayout()
        
        activate_btn = QPushButton('Activer')
        activate_btn.clicked.connect(self.activate)
        button_layout.addWidget(activate_btn)
        
        trial_btn = QPushButton('Essai gratuit (30 jours)')
        trial_btn.clicked.connect(self.start_trial)
        button_layout.addWidget(trial_btn)
        
        buy_btn = QPushButton('Acheter')
        buy_btn.clicked.connect(self.open_purchase_page)
        button_layout.addWidget(buy_btn)
        
        layout.addLayout(button_layout)
        self.setLayout(layout)
    
    def activate(self):
        key = self.license_input.text().strip()
        
        # Vérifier clé avec serveur
        if self.verify_with_server(key):
            QMessageBox.information(self, 'Succès', 'Licence activée!')
            self.accept()
        else:
            QMessageBox.warning(self, 'Erreur', 'Clé invalide')
    
    def verify_with_server(self, key):
        # Envoyer requête à serveur de licence
        try:
            import requests
            response = requests.post('https://api.monapp.com/verify', {
                'key': key
            })
            if response.status_code == 200:
                license_data = response.json()
                # Sauvegarder licence localement
                license_mgr = LicenseManager()
                license_mgr.save_license(license_data)
                return True
        except:
            pass
        return False
    
    def start_trial(self):
        # Créer licence d'essai 30 jours
        license_mgr = LicenseManager()
        trial_license = license_mgr.generate_key('trial@local', expiry_days=30)
        license_mgr.save_license(trial_license)
        QMessageBox.information(self, 'Essai', 'Essai de 30 jours activé!')
        self.accept()
    
    def open_purchase_page(self):
        import webbrowser
        webbrowser.open('https://monapp.com/acheter')
"""

# === Intégration paiement ===

# Stripe
"""
import stripe
stripe.api_key = 'sk_test_...'

def create_checkout_session():
    session = stripe.checkout.Session.create(
        payment_method_types=['card'],
        line_items=[{
            'price_data': {
                'currency': 'eur',
                'product_data': {
                    'name': 'Mon Application - Licence',
                },
                'unit_amount': 4999,  # 49.99 EUR
            },
            'quantity': 1,
        }],
        mode='payment',
        success_url='https://monapp.com/success',
        cancel_url='https://monapp.com/cancel',
    )
    return session.url
"""

# PayPal
# Utiliser API PayPal pour générer boutons de paiement

# Paddle
# Service merchant of record (gère TVA, comptabilité)

# Gumroad
# Simple, commission ~10%


[OK] ANALYTICS ET TÉLÉMÉTRIE

# === Google Analytics pour desktop app ===

# analytics.py
"""
import requests
import uuid
from PyQt5.QtCore import QSettings

class Analytics:
    def __init__(self):
        self.tracking_id = 'UA-XXXXXXXXX-X'
        self.base_url = 'https://www.google-analytics.com/collect'
        
        # ID utilisateur unique
        settings = QSettings('MonApp', 'Analytics')
        if not settings.contains('client_id'):
            settings.setValue('client_id', str(uuid.uuid4()))
        self.client_id = settings.value('client_id')
    
    def track_event(self, category, action, label=None, value=None):
        '''Envoyer événement à Google Analytics'''
        data = {
            'v': '1',  # Version API
            'tid': self.tracking_id,
            'cid': self.client_id,
            't': 'event',
            'ec': category,
            'ea': action,
        }
        
        if label:
            data['el'] = label
        if value:
            data['ev'] = value
        
        try:
            requests.post(self.base_url, data=data, timeout=2)
        except:
            pass  # Échec silencieux
    
    def track_screen(self, screen_name):
        '''Tracker affichage écran'''
        data = {
            'v': '1',
            'tid': self.tracking_id,
            'cid': self.client_id,
            't': 'screenview',
            'an': 'MonApplication',
            'av': '1.0.0',
            'cd': screen_name,
        }
        
        try:
            requests.post(self.base_url, data=data, timeout=2)
        except:
            pass

# Utilisation
analytics = Analytics()

# Tracker ouverture app
analytics.track_event('App', 'Launch', version='1.0.0')

# Tracker fonctionnalité utilisée
analytics.track_event('Feature', 'ExportPDF')

# Tracker écran
analytics.track_screen('MainWindow')
"""

# === Collecte erreurs avec Sentry ===

# Installation
pip install sentry-sdk

# Configuration
"""
import sentry_sdk

sentry_sdk.init(
    dsn="https://xxx@sentry.io/xxx",
    traces_sample_rate=1.0,
    release="mon-application@1.0.0",
    environment="production"
)

# Les erreurs non catchées sont automatiquement envoyées

# Capturer erreur manuellement
try:
    risky_operation()
except Exception as e:
    sentry_sdk.capture_exception(e)

# Ajouter contexte
sentry_sdk.set_user({"email": "user@example.com"})
sentry_sdk.set_tag("feature", "export")
"""

# === Respect vie privée (RGPD) ===

# 1. Demander consentement
"""
class PrivacyDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle('Confidentialité')
        
        layout = QVBoxLayout()
        
        text = QLabel('''
        <p>Cette application collecte des données anonymes d'utilisation 
        pour améliorer l'expérience utilisateur.</p>
        
        <p><b>Données collectées:</b></p>
        <ul>
        <li>Événements d'utilisation (fonctionnalités utilisées)</li>
        <li>Rapports de crash</li>
        <li>Système d'exploitation et version</li>
        </ul>
        
        <p><b>Données NON collectées:</b></p>
        <ul>
        <li>Informations personnelles</li>
        <li>Contenu de vos fichiers</li>
        <li>Données sensibles</li>
        </ul>
        
        <p>Acceptez-vous le partage de ces données?</p>
        ''')
        text.setWordWrap(True)
        layout.addWidget(text)
        
        button_layout = QHBoxLayout()
        
        accept_btn = QPushButton('Accepter')
        accept_btn.clicked.connect(self.accept)
        button_layout.addWidget(accept_btn)
        
        decline_btn = QPushButton('Refuser')
        decline_btn.clicked.connect(self.reject)
        button_layout.addWidget(decline_btn)
        
        layout.addLayout(button_layout)
        self.setLayout(layout)

# Demander au premier lancement
settings = QSettings('MonApp', 'Settings')
if not settings.contains('analytics_consent'):
    dialog = PrivacyDialog()
    consent = dialog.exec_() == QDialog.Accepted
    settings.setValue('analytics_consent', consent)

# Vérifier avant d'envoyer
if settings.value('analytics_consent', type=bool):
    analytics.track_event(...)
"""

# 2. Option pour désactiver
"""
# Dans menu préférences
analytics_checkbox = QCheckBox('Partager données anonymes d\'utilisation')
analytics_checkbox.setChecked(settings.value('analytics_consent', type=bool))
analytics_checkbox.stateChanged.connect(
    lambda state: settings.setValue('analytics_consent', state == Qt.Checked)
)
"""


[OK] TESTS AVANT DÉPLOIEMENT

# === Tests manuels essentiels ===

# Checklist de tests:
"""
INSTALLATION
[WHITE_SQUARE] Installation propre sur système neuf
[WHITE_SQUARE] Installation avec version précédente déjà installée (mise à jour)
[WHITE_SQUARE] Désinstallation complète (pas de fichiers restants)
[WHITE_SQUARE] Installation dossier personnalisé
[WHITE_SQUARE] Installation sans droits admin (si possible)

LANCEMENT
[WHITE_SQUARE] Première ouverture après installation
[WHITE_SQUARE] Double-clic sur fichier associé (si applicable)
[WHITE_SQUARE] Lancement depuis raccourci bureau
[WHITE_SQUARE] Lancement depuis menu démarrer
[WHITE_SQUARE] Plusieurs instances simultanées (comportement?)
[WHITE_SQUARE] Lancement sans connexion internet

FONCTIONNALITÉS
[WHITE_SQUARE] Tester chaque menu
[WHITE_SQUARE] Tester chaque bouton
[WHITE_SQUARE] Tester raccourcis clavier
[WHITE_SQUARE] Ouvrir/Enregistrer fichiers
[WHITE_SQUARE] Import/Export données
[WHITE_SQUARE] Préférences/Paramètres
[WHITE_SQUARE] Recherche (si applicable)
[WHITE_SQUARE] Drag & drop
[WHITE_SQUARE] Copier/Coller

INTERFACE
[WHITE_SQUARE] Toutes les fenêtres s'affichent correctement
[WHITE_SQUARE] Redimensionnement fenêtre
[WHITE_SQUARE] Maximiser/Minimiser
[WHITE_SQUARE] Thème clair/sombre (si applicable)
[WHITE_SQUARE] Tous les textes lisibles
[WHITE_SQUARE] Icônes affichées
[WHITE_SQUARE] Pas de textes coupés

PERFORMANCE
[WHITE_SQUARE] Temps de démarrage <5 secondes
[WHITE_SQUARE] Interface réactive
[WHITE_SQUARE] Pas de freeze
[WHITE_SQUARE] Utilisation RAM raisonnable (<500 MB idle)
[WHITE_SQUARE] Utilisation CPU au repos <5%
[WHITE_SQUARE] Fermeture propre

DONNÉES
[WHITE_SQUARE] Sauvegarde données
[WHITE_SQUARE] Chargement données
[WHITE_SQUARE] Export formats divers
[WHITE_SQUARE] Gestion fichiers volumineux
[WHITE_SQUARE] Gestion caractères spéciaux/unicode
[WHITE_SQUARE] Sauvegarde préférences

ERREURS
[WHITE_SQUARE] Comportement si fichier manquant
[WHITE_SQUARE] Comportement si permission refusée
[WHITE_SQUARE] Comportement si espace disque insuffisant
[WHITE_SQUARE] Messages d'erreur clairs
[WHITE_SQUARE] Pas de crash sur erreur
[WHITE_SQUARE] Logs d'erreurs créés

COMPATIBILITÉ
[WHITE_SQUARE] Windows 10
[WHITE_SQUARE] Windows 11
[WHITE_SQUARE] macOS versions supportées
[WHITE_SQUARE] Ubuntu/Debian récent
[WHITE_SQUARE] Résolutions écran: 1920x1080, 1366x768, 4K
[WHITE_SQUARE] Écrans haute DPI
[WHITE_SQUARE] Multi-écrans

SÉCURITÉ
[WHITE_SQUARE] Pas d'avertissement antivirus
[WHITE_SQUARE] Pas d'avertissement SmartScreen (Windows)
[WHITE_SQUARE] Pas d'avertissement Gatekeeper (macOS)
[WHITE_SQUARE] Certificat valide (si signé)
[WHITE_SQUARE] Connexions HTTPS uniquement
"""

# === Tests automatisés avec pytest-qt ===

pip install pytest pytest-qt

# test_main.py
"""
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from main import MainWindow

@pytest.fixture
def app(qtbot):
    test_app = QApplication.instance()
    if test_app is None:
        test_app = QApplication([])
    return test_app

@pytest.fixture
def window(qtbot, app):
    window = MainWindow()
    qtbot.addWidget(window)
    return window

def test_window_title(window):
    assert window.windowTitle() == 'Mon Application'

def test_button_click(qtbot, window):
    # Simuler clic sur bouton
    qtbot.mouseClick(window.my_button, Qt.LeftButton)
    
    # Vérifier résultat
    assert window.label.text() == 'Bouton cliqué'

def test_line_edit_input(qtbot, window):
    # Saisir texte
    qtbot.keyClicks(window.line_edit, 'Hello World')
    
    # Vérifier
    assert window.line_edit.text() == 'Hello World'

def test_dialog_appears(qtbot, window):
    # Simuler action qui ouvre dialogue
    with qtbot.waitSignal(window.dialog_opened, timeout=1000):
        qtbot.mouseClick(window.open_dialog_btn, Qt.LeftButton)

def test_file_open(qtbot, window, tmp_path):
    # Créer fichier temporaire
    test_file = tmp_path / "test.txt"
    test_file.write_text("Test content")
    
    # Simuler ouverture
    window.open_file(str(test_file))
    
    # Vérifier contenu chargé
    assert window.text_edit.toPlainText() == "Test content"

# Exécuter tests
# pytest test_main.py -v
"""

# === Test sur machines virtuelles ===

# VirtualBox - Gratuit
"""
1. Créer VM Windows 10/11 propre
2. Installer application
3. Tester toutes fonctionnalités
4. Prendre snapshots pour réutilisation
"""

# Windows Sandbox (Windows Pro uniquement)
"""
1. Activer Windows Sandbox dans fonctionnalités Windows
2. Lancer sandbox (environnement jetable)
3. Copier installeur dans sandbox
4. Tester installation et fonctionnement
5. Fermer sandbox (tout est effacé automatiquement)
"""


[OK] DOCUMENTATION UTILISATEUR

# === Guide de démarrage rapide ===

# quick_start.md
"""
# Guide de démarrage rapide - Mon Application

## Installation

### Windows
1. Téléchargez `MonApplication-Setup.exe`
2. Double-cliquez pour lancer l'installation
3. Suivez les instructions à l'écran
4. L'application se lancera automatiquement après installation

### macOS
1. Téléchargez `MonApplication.dmg`
2. Ouvrez le fichier DMG
3. Glissez l'icône de l'application dans le dossier Applications
4. Lancez depuis Applications ou Launchpad

### Linux
1. Téléchargez `MonApplication.AppImage`
2. Rendez le fichier exécutable:
   ```bash
   chmod +x MonApplication.AppImage
   ```
3. Double-cliquez sur le fichier pour lancer

## Premier lancement

Au premier lancement, l'application vous demandera:

1. **Préférences de base**: Langue, thème, etc.
2. **Emplacement des données**: Où sauvegarder vos fichiers
3. **Autorisations**: Analytics (optionnel)

## Interface principale

```
┌─────────────────────────────────────────┐
│ Fichier  Édition  Affichage  Aide      │
├─────────────────────────────────────────┤
│ [[FOLDER]] [[SAUVEGARDE]] [[LISTE]] [[CONFIG]]                     │
├──────────────┬──────────────────────────┤
│              │                          │
│  Panneau     │    Zone de travail      │
│  latéral     │    principale           │
│              │                          │
├──────────────┴──────────────────────────┤
│ Barre de statut: Prêt                   │
└─────────────────────────────────────────┘
```

## Opérations de base

### Créer nouveau fichier
- Menu: `Fichier > Nouveau`
- Raccourci: `Ctrl+N` (Windows/Linux) ou `[PLACE_OF_INTEREST_SIGN]+N` (macOS)
- Bouton: Icône [FOLDER] dans la barre d'outils

### Ouvrir fichier
- Menu: `Fichier > Ouvrir...`
- Raccourci: `Ctrl+O` (Windows/Linux) ou `[PLACE_OF_INTEREST_SIGN]+O` (macOS)
- Formats supportés: .txt, .pdf, .docx

### Enregistrer
- Menu: `Fichier > Enregistrer`
- Raccourci: `Ctrl+S` (Windows/Linux) ou `[PLACE_OF_INTEREST_SIGN]+S` (macOS)

## Fonctionnalités principales

### 1. Fonctionnalité A
Description et utilisation...

### 2. Fonctionnalité B
Description et utilisation...

## Raccourcis clavier

| Action | Windows/Linux | macOS |
|--------|---------------|-------|
| Nouveau | Ctrl+N | [PLACE_OF_INTEREST_SIGN]+N |
| Ouvrir | Ctrl+O | [PLACE_OF_INTEREST_SIGN]+O |
| Enregistrer | Ctrl+S | [PLACE_OF_INTEREST_SIGN]+S |
| Copier | Ctrl+C | [PLACE_OF_INTEREST_SIGN]+C |
| Coller | Ctrl+V | [PLACE_OF_INTEREST_SIGN]+V |
| Annuler | Ctrl+Z | [PLACE_OF_INTEREST_SIGN]+Z |
| Refaire | Ctrl+Y | [PLACE_OF_INTEREST_SIGN]+Shift+Z |

## FAQ

**Q: L'application ne démarre pas, que faire?**
R: Vérifiez que vous avez les droits d'exécution. Sur Windows, 
   essayez en tant qu'administrateur.

**Q: Mes données sont-elles sauvegardées automatiquement?**
R: Oui, l'application sauvegarde automatiquement toutes les 5 minutes.

**Q: Comment mettre à jour l'application?**
R: Menu `Aide > Vérifier les mises à jour`

## Support

- Email: support@monapp.com
- Documentation: https://docs.monapp.com
- Signaler un bug: https://github.com/user/repo/issues
"""

# === Documentation complète (avec MkDocs) ===

pip install mkdocs mkdocs-material

# mkdocs.yml
"""
site_name: Mon Application - Documentation
theme:
  name: material
  palette:
    primary: blue
  features:
    - navigation.sections
    - toc.integrate

nav:
  - Accueil: index.md
  - Installation:
    - Windows: install/windows.md
    - macOS: install/macos.md
    - Linux: install/linux.md
  - Guide utilisateur:
    - Démarrage rapide: guide/quickstart.md
    - Interface: guide/interface.md
    - Fonctionnalités: guide/features.md
  - Tutoriels:
    - Tutoriel 1: tutorials/tuto1.md
    - Tutoriel 2: tutorials/tuto2.md
  - FAQ: faq.md
  - Changelog: changelog.md

markdown_extensions:
  - admonition
  - codehilite
  - toc:
      permalink: true
"""

# Générer documentation
mkdocs build

# Serveur local de développement
mkdocs serve  # http://localhost:8000

# Déployer sur GitHub Pages
mkdocs gh-deploy

# === Vidéo tutoriel ===

"""
Outils recommandés:
- OBS Studio (gratuit) - Enregistrement écran
- DaVinci Resolve (gratuit) - Montage
- Audacity (gratuit) - Audio

Structure vidéo:
1. Introduction (10s)
   - Nom application
   - Ce qu'elle fait
   
2. Installation (30s)
   - Téléchargement
   - Installation rapide
   
3. Démonstration (2min)
   - Cas d'usage réel
   - Fonctionnalités principales
   
4. Appel à l'action (10s)
   - Lien téléchargement
   - Site web

Conseils:
- Résolution 1920x1080 minimum
- Audio clair (micro correct)
- Rythme soutenu (pas de lenteurs)
- Musique de fond discrète
- Sous-titres si possible
"""


[OK] MAINTENANCE À LONG TERME

# === Plan de maintenance ===

"""
Hebdomadaire:
[WHITE_SQUARE] Vérifier GitHub Issues nouvelles
[WHITE_SQUARE] Répondre questions support
[WHITE_SQUARE] Surveiller crash reports (Sentry)

Mensuel:
[WHITE_SQUARE] Analyser analytics (fonctionnalités utilisées/ignorées)
[WHITE_SQUARE] Planifier correctifs bugs critiques
[WHITE_SQUARE] Mettre à jour dépendances (pip list --outdated)
[WHITE_SQUARE] Tester compatibilité nouvelles versions OS
[WHITE_SQUARE] Backup code et données

Trimestriel:
[WHITE_SQUARE] Release correctifs bugs
[WHITE_SQUARE] Évaluer feedback utilisateurs
[WHITE_SQUARE] Roadmap prochaines fonctionnalités
[WHITE_SQUARE] Audit sécurité
[WHITE_SQUARE] Mise à jour documentation

Annuel:
[WHITE_SQUARE] Renouveler certificat code signing
[WHITE_SQUARE] Révision licence/pricing
[WHITE_SQUARE] Analyse compétition
[WHITE_SQUARE] Refactoring code legacy
[WHITE_SQUARE] Mise à jour majeure
"""

# === Gestion des dépendances ===

# requirements.txt avec versions fixées
"""
PyQt5==5.15.10
requests==2.31.0
Pillow==10.1.0
"""

# Mettre à jour prudemment
pip list --outdated
pip install --upgrade package_name

# Tester après chaque mise à jour
pytest

# === Deprecation et fin de support ===

"""
Annoncer 6 mois à l'avance minimum:

**Fin de support Windows 7**

Nous annonçons la fin du support pour Windows 7 
à partir de la version 3.0 (sortie prévue: Juin 2025).

Raisons:
- Windows 7 n'est plus supporté par Microsoft
- Limitation techniques pour nouvelles fonctionnalités
- <1% utilisateurs sur Windows 7

Alternatives:
- Mise à jour vers Windows 10/11 (recommandé)
- Version 2.x continuera de fonctionner
- Support limité jusqu'à Décembre 2025
"""


[OK] RESSOURCES DÉPLOIEMENT

# === Outils essentiels ===

# PyInstaller - Créer exécutables
https://pyinstaller.org/

# Inno Setup - Installeur Windows
https://jrsoftware.org/isinfo.php

# NSIS - Alternative installeur Windows
https://nsis.sourceforge.io/

# AppImage - Distribution Linux
https://appimage.org/

# MkDocs - Documentation
https://www.mkdocs.org/

# Sentry - Monitoring erreurs
https://sentry.io/

# === Services utiles ===

# GitHub Actions - CI/CD gratuit
https://github.com/features/actions

# ReadTheDocs - Hébergement docs gratuit
https://readthedocs.org/

# Gumroad - Vente simple
https://gumroad.com/

# Paddle - Merchant of record
https://paddle.com/

# Mailchimp - Newsletter
https://mailchimp.com/

# === Communautés ===

# Reddit
- r/Python
- r/learnpython
- r/PyQt

# Discord
- Python Discord
- PyQt/Qt Discord servers

# Stack Overflow
- Tag: [pyqt5]
- Tag: [pyinstaller]

# === Lectures recommandées ===

"""
Livres:
- "Deploying Python Applications" - Sander Teunissen
- "Python Packaging Authority Guide"

Articles:
- "Distributing Python Applications" (Real Python)
- "PyQt5 Tutorial" (zetcode.com)

Vidéos:
- "Packaging Python Applications" (PyCon talks)
- "PyInstaller Tutorial" (YouTube)
"""


[OK] EXEMPLE COMPLET: PROJET DÉPLOYABLE

# Structure projet complet prêt au déploiement
"""
MonApplication/
│
├── src/
│   ├── main.py                 # Point d'entrée
│   ├── __init__.py
│   ├── version.py              # VERSION = "1.0.0"
│   │
│   ├── ui/                     # Interface utilisateur
│   │   ├── __init__.py
│   │   ├── main_window.py
│   │   ├── dialogs.py
│   │   └── widgets.py
│   │
│   ├── core/                   # Logique métier
│   │   ├── __init__.py
│   │   ├── business.py
│   │   └── utils.py
│   │
│   └── resources/              # Ressources
│       ├── __init__.py
│       ├── resources.qrc
│       └── resources_rc.py
│
├── resources/                  # Assets sources
│   ├── icons/
│   │   ├── icon.ico           # Windows
│   │   ├── icon.icns          # macOS
│   │   └── icon.png           # Linux
│   ├── images/
│   └── styles/
│       └── style.qss
│
├── tests/                      # Tests
│   ├── __init__.py
│   ├── test_main.py
│   └── test_core.py
│
├── docs/                       # Documentation
│   ├── index.md
│   ├── quickstart.md
│   └── mkdocs.yml
│
├── installers/                 # Scripts installeurs
│   ├── windows/
│   │   └── installer.iss      # Inno Setup
│   ├── linux/
│   │   └── build_appimage.sh
│   └── macos/
│       └── create_dmg.sh
│
├── scripts/                    # Scripts utilitaires
│   ├── build.py               # Script de build
│   ├── release.py             # Script de release
│   └── test_all.py            # Lancer tous les tests
│
├── .github/                    # GitHub Actions
│   └── workflows/
│       ├── build.yml
│       └── release.yml
│
├── requirements.txt            # Dépendances production
├── requirements-dev.txt        # Dépendances développement
├── main.spec                   # Configuration PyInstaller
├── setup.py                    # Configuration setup
├── README.md                   # Documentation principale
├── CHANGELOG.md                # Historique versions
├── LICENSE                     # Licence (MIT, GPL, etc.)
├── .gitignore                  # Fichiers ignorés par git
└── .pylintrc                   # Configuration linter
"""

# main.py - Point d'entrée complet
"""
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import logging
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt

# Configurer chemins ressources pour PyInstaller
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# Configurer logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

def exception_hook(exctype, value, traceback):
    '''Hook pour capturer exceptions non gérées'''
    logger.error("Uncaught exception", exc_info=(exctype, value, traceback))
    sys.__excepthook__(exctype, value, traceback)

# Installer hook d'exceptions
sys.excepthook = exception_hook

def main():
    '''Point d'entrée principal'''
    try:
        # Attributs pour High DPI
        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
        
        # Créer application
        app = QApplication(sys.argv)
        app.setApplicationName("Mon Application")
        app.setApplicationVersion("1.0.0")
        app.setOrganizationName("Mon Entreprise")
        
        # Charger stylesheet
        stylesheet_path = resource_path('resources/styles/style.qss')
        if os.path.exists(stylesheet_path):
            with open(stylesheet_path, 'r') as f:
                app.setStyleSheet(f.read())
        
        # Importer fenêtre principale
        from ui.main_window import MainWindow
        
        # Créer et afficher fenêtre
        window = MainWindow()
        window.show()
        
        logger.info("Application démarrée")
        
        # Lancer boucle événements
        sys.exit(app.exec_())
        
    except Exception as e:
        logger.critical(f"Erreur critique au démarrage: {e}", exc_info=True)
        sys.exit(1)

if __name__ == '__main__':
    main()
"""

# version.py
"""
VERSION = "1.0.0"
BUILD = "20240315"
"""

# build.py - Script de build automatisé
"""
#!/usr/bin/env python3
import os
import sys
import shutil
import subprocess
import platform
from datetime import datetime

class Builder:
    def __init__(self):
        self.app_name = "MonApplication"
        self.version = "1.0.0"
        self.platform = platform.system()
        
    def clean(self):
        '''Nettoyer anciens builds'''
        print("[NETTOYAGE] Nettoyage...")
        for dir in ['build', 'dist']:
            if os.path.exists(dir):
                shutil.rmtree(dir)
                print(f"  [OK] Supprimé {dir}/")
    
    def run_tests(self):
        '''Exécuter tests'''
        print("\n[TEST] Exécution des tests...")
        result = subprocess.run(['pytest', 'tests/', '-v'])
        if result.returncode != 0:
            print("  [X] Tests échoués!")
            return False
        print("  [OK] Tests réussis")
        return True
    
    def build_executable(self):
        '''Construire exécutable avec PyInstaller'''
        print(f"\n[OUTIL] Construction exécutable {self.platform}...")
        
        if self.platform == "Windows":
            icon = "resources/icons/icon.ico"
        elif self.platform == "Darwin":  # macOS
            icon = "resources/icons/icon.icns"
        else:
            icon = "resources/icons/icon.png"
        
        cmd = [
            'pyinstaller',
            '--onefile',
            '--windowed',
            f'--name={self.app_name}',
            f'--icon={icon}',
            '--add-data', f'resources{os.pathsep}resources',
            '--clean',
            '--noconfirm',
            'src/main.py'
        ]
        
        result = subprocess.run(cmd)
        if result.returncode != 0:
            print("  [X] Build échoué!")
            return False
        
        print("  [OK] Build réussi")
        return True
    
    def create_installer(self):
        '''Créer installeur selon plateforme'''
        print(f"\n[PACKAGE] Création installeur...")
        
        if self.platform == "Windows":
            return self._create_windows_installer()
        elif self.platform == "Darwin":
            return self._create_macos_dmg()
        elif self.platform == "Linux":
            return self._create_appimage()
    
    def _create_windows_installer(self):
        '''Créer installeur Windows avec Inno Setup'''
        iscc = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
        
        if not os.path.exists(iscc):
            print("  [ATTENTION] Inno Setup non trouvé, skip installeur")
            return True
        
        result = subprocess.run([iscc, 'installers/windows/installer.iss'])
        if result.returncode != 0:
            print("  [X] Création installeur échouée")
            return False
        
        print("  [OK] Installeur créé")
        return True
    
    def _create_macos_dmg(self):
        '''Créer DMG pour macOS'''
        result = subprocess.run([
            'hdiutil', 'create',
            '-volname', self.app_name,
            '-srcfolder', f'dist/{self.app_name}.app',
            '-ov',
            '-format', 'UDZO',
            f'dist/{self.app_name}-{self.version}.dmg'
        ])
        
        if result.returncode != 0:
            print("  [X] Création DMG échouée")
            return False
        
        print("  [OK] DMG créé")
        return True
    
    def _create_appimage(self):
        '''Créer AppImage pour Linux'''
        script = 'installers/linux/build_appimage.sh'
        
        if not os.path.exists(script):
            print("  [ATTENTION] Script AppImage non trouvé, skip")
            return True
        
        result = subprocess.run(['bash', script])
        if result.returncode != 0:
            print("  [X] Création AppImage échouée")
            return False
        
        print("  [OK] AppImage créé")
        return True
    
    def show_summary(self):
        '''Afficher résumé'''
        print("\n" + "="*50)
        print("[OK] BUILD TERMINÉ")
        print("="*50)
        print(f"Application: {self.app_name}")
        print(f"Version: {self.version}")
        print(f"Plateforme: {self.platform}")
        print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print("\nFichiers générés:")
        
        if os.path.exists('dist'):
            for file in os.listdir('dist'):
                size = os.path.getsize(f'dist/{file}') / (1024*1024)
                print(f"  [FICHIER] {file} ({size:.1f} MB)")
    
    def build_all(self):
        '''Lancer build complet'''
        print(f"\n{'='*50}")
        print(f"[RAPIDE] BUILD {self.app_name} v{self.version}")
        print(f"{'='*50}\n")
        
        # Étapes
        steps = [
            ("Nettoyage", self.clean),
            ("Tests", self.run_tests),
            ("Build exécutable", self.build_executable),
            ("Création installeur", self.create_installer)
        ]
        
        for step_name, step_func in steps:
            success = step_func()
            if success is False:
                print(f"\n[X] Échec à l'étape: {step_name}")
                return False
        
        self.show_summary()
        return True

if __name__ == '__main__':
    builder = Builder()
    success = builder.build_all()
    sys.exit(0 if success else 1)
"""

# release.py - Script pour créer release GitHub
"""
#!/usr/bin/env python3
import os
import sys
import subprocess
import requests
from version import VERSION

GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
REPO = 'username/mon-application'

def create_git_tag():
    '''Créer tag git'''
    tag = f'v{VERSION}'
    
    print(f"Création tag {tag}...")
    subprocess.run(['git', 'tag', '-a', tag, '-m', f'Release {VERSION}'])
    subprocess.run(['git', 'push', 'origin', tag])
    print("[OK] Tag créé")

def create_github_release():
    '''Créer release GitHub'''
    if not GITHUB_TOKEN:
        print("[ATTENTION] GITHUB_TOKEN non défini")
        return
    
    print("Création release GitHub...")
    
    # Lire changelog
    with open('CHANGELOG.md', 'r') as f:
        changelog = f.read()
    
    url = f'https://api.github.com/repos/{REPO}/releases'
    headers = {
        'Authorization': f'token {GITHUB_TOKEN}',
        'Accept': 'application/vnd.github.v3+json'
    }
    
    data = {
        'tag_name': f'v{VERSION}',
        'name': f'Version {VERSION}',
        'body': changelog,
        'draft': False,
        'prerelease': False
    }
    
    response = requests.post(url, json=data, headers=headers)
    
    if response.status_code == 201:
        print("[OK] Release créée")
        return response.json()['id']
    else:
        print(f"[X] Erreur: {response.status_code}")
        print(response.text)
        return None

def upload_assets(release_id):
    '''Upload fichiers vers release'''
    if not release_id:
        return
    
    print("Upload assets...")
    
    files = []
    if os.path.exists('dist'):
        for file in os.listdir('dist'):
            if file.endswith(('.exe', '.dmg', '.AppImage', '.zip')):
                files.append(f'dist/{file}')
    
    for file in files:
        print(f"  Upload {file}...")
        
        url = f'https://uploads.github.com/repos/{REPO}/releases/{release_id}/assets'
        headers = {
            'Authorization': f'token {GITHUB_TOKEN}',
            'Content-Type': 'application/octet-stream'
        }
        params = {'name': os.path.basename(file)}
        
        with open(file, 'rb') as f:
            response = requests.post(
                url,
                headers=headers,
                params=params,
                data=f.read()
            )
        
        if response.status_code == 201:
            print(f"    [OK] {file}")
        else:
            print(f"    [X] Erreur: {response.status_code}")

def main():
    print(f"\n{'='*50}")
    print(f"[PACKAGE] RELEASE v{VERSION}")
    print(f"{'='*50}\n")
    
    # Vérifier que dist/ existe
    if not os.path.exists('dist'):
        print("[X] Dossier dist/ introuvable. Lancez build.py d'abord.")
        sys.exit(1)
    
    # Créer tag
    create_git_tag()
    
    # Créer release GitHub
    release_id = create_github_release()
    
    # Upload assets
    upload_assets(release_id)
    
    print("\n[OK] Release terminée!")
    print(f"URL: https://github.com/{REPO}/releases/tag/v{VERSION}")

if __name__ == '__main__':
    main()
"""

# .github/workflows/build.yml - CI/CD GitHub Actions
"""
name: Build and Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-windows:
    runs-on: windows-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pyinstaller
    
    - name: Build executable
      run: |
        pyinstaller --onefile --windowed --icon=resources/icons/icon.ico --name=MonApplication src/main.py
    
    - name: Create installer
      run: |
        # Installer Inno Setup
        choco install innosetup -y
        & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installers\windows\installer.iss
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: windows-builds
        path: |
          dist/*.exe
          installers/windows/Output/*.exe
  
  build-macos:
    runs-on: macos-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pyinstaller
    
    - name: Build executable
      run: |
        pyinstaller --onefile --windowed --icon=resources/icons/icon.icns --name=MonApplication src/main.py
    
    - name: Create DMG
      run: |
        hdiutil create -volname "MonApplication" -srcfolder dist/MonApplication.app -ov -format UDZO dist/MonApplication.dmg
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: macos-builds
        path: dist/*.dmg
  
  build-linux:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pyinstaller
    
    - name: Build executable
      run: |
        pyinstaller --onefile --windowed --icon=resources/icons/icon.png --name=MonApplication src/main.py
    
    - name: Create AppImage
      run: |
        chmod +x installers/linux/build_appimage.sh
        ./installers/linux/build_appimage.sh
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: linux-builds
        path: dist/*.AppImage
  
  release:
    needs: [build-windows, build-macos, build-linux]
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Download all artifacts
      uses: actions/download-artifact@v3
    
    - name: Create Release
      uses: softprops/action-gh-release@v1
      with:
        files: |
          windows-builds/*
          macos-builds/*
          linux-builds/*
        body_path: CHANGELOG.md
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
"""

# README.md complet
"""
# Mon Application

![Logo](resources/icons/icon.png)

[![Build Status](https://github.com/username/mon-application/workflows/Build/badge.svg)](https://github.com/username/mon-application/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/github/v/release/username/mon-application)](https://github.com/username/mon-application/releases)

## [LISTE] Description

Mon Application est un [description courte de votre application].

### Fonctionnalités principales

- * Fonctionnalité 1
- [RAPIDE] Fonctionnalité 2
- [DESIGN] Interface moderne et intuitive
- [VERROUILLE] Sécurisé et respectueux de la vie privée
- [MONDE] Multi-plateforme (Windows, macOS, Linux)

## [PACKAGE] Installation

### Windows

1. Téléchargez le dernier installeur depuis [Releases](https://github.com/username/mon-application/releases/latest)
2. Lancez `MonApplication-Setup.exe`
3. Suivez l'assistant d'installation
4. Lancez depuis le menu Démarrer

### macOS

1. Téléchargez le fichier `.dmg` depuis [Releases](https://github.com/username/mon-application/releases/latest)
2. Ouvrez le fichier DMG
3. Glissez l'icône dans le dossier Applications
4. Lancez depuis Applications

> **Note**: Sur macOS 10.15+, vous devrez peut-être autoriser l'application dans Préférences Système > Sécurité

### Linux

#### AppImage (Recommandé)

```bash
# Télécharger
wget https://github.com/username/mon-application/releases/latest/download/MonApplication.AppImage

# Rendre exécutable
chmod +x MonApplication.AppImage

# Lancer
./MonApplication.AppImage
```

#### Ou depuis le code source

```bash
git clone https://github.com/username/mon-application.git
cd mon-application
pip install -r requirements.txt
python src/main.py
```

## [RAPIDE] Utilisation

### Démarrage rapide

1. Lancez l'application
2. [Étape 2]
3. [Étape 3]

Pour plus de détails, consultez la [documentation complète](https://docs.monapp.com).

### Exemples

```python
# Exemple d'utilisation si applicable
```

## [OUTILS] Développement

### Prérequis

- Python 3.9+
- PyQt5
- [Autres dépendances]

### Installation développement

```bash
# Cloner le dépôt
git clone https://github.com/username/mon-application.git
cd mon-application

# Créer environnement virtuel
python -m venv venv
source venv/bin/activate  # Linux/Mac
venv\Scripts\activate     # Windows

# Installer dépendances
pip install -r requirements.txt
pip install -r requirements-dev.txt

# Lancer en mode développement
python src/main.py
```

### Tests

```bash
# Lancer tous les tests
pytest

# Avec couverture
pytest --cov=src tests/

# Tests spécifiques
pytest tests/test_main.py -v
```

### Build

```bash
# Build pour votre plateforme
python scripts/build.py

# Les fichiers seront dans dist/
```

### Structure du projet

```
mon-application/
├── src/              # Code source
├── tests/            # Tests
├── resources/        # Assets
├── docs/             # Documentation
└── scripts/          # Scripts build/release
```

## [GUIDE] Documentation

- [Guide de démarrage rapide](docs/quickstart.md)
- [Documentation complète](https://docs.monapp.com)
- [FAQ](docs/faq.md)
- [Changelog](CHANGELOG.md)

## [ACCORD] Contribution

Les contributions sont les bienvenues! Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour les guidelines.

### Rapporter un bug

[Créer une issue](https://github.com/username/mon-application/issues/new?template=bug_report.md)

### Proposer une fonctionnalité

[Créer une issue](https://github.com/username/mon-application/issues/new?template=feature_request.md)

## [FICHIER] Licence

Ce projet est sous licence MIT - voir [LICENSE](LICENSE) pour plus de détails.

## [UTILISATEURS] Auteurs

- **Votre Nom** - *Travail initial* - [@username](https://github.com/username)

Voir aussi la liste des [contributeurs](https://github.com/username/mon-application/contributors).

## [MERCI] Remerciements

- [Librairies/Ressources utilisées]
- [Personnes à remercier]

## [TEL] Support

- Email: support@monapp.com
- Discord: [Lien serveur]
- Twitter: [@monapp](https://twitter.com/monapp)

## * Star History

[![Star History Chart](https://api.star-history.com/svg?repos=username/mon-application&type=Date)](https://star-history.com/#username/mon-application&Date)

---

Made with [HEAVY_BLACK_HEART] and Python
"""

# CHANGELOG.md
"""
# Changelog

Tous les changements notables de ce projet seront documentés dans ce fichier.

Le format est basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/),
et ce projet adhère au [Semantic Versioning](https://semver.org/lang/fr/).

## [Non publié]

### À venir
- Fonctionnalité X
- Amélioration Y

## [1.0.0] - 2024-03-15

### Ajouté
- Première version publique
- Interface utilisateur complète
- Support Windows, macOS, Linux
- Documentation utilisateur
- Tests unitaires

### Changé
- N/A (première version)

### Déprécié
- N/A

### Supprimé
- N/A

### Corrigé
- N/A

### Sécurité
- N/A

---

## [0.9.0] - 2024-03-01

### Ajouté
- Version bêta pour tests
- Fonctionnalités de base

### Corrigé
- Bugs critiques identifiés

---

[Non publié]: https://github.com/username/mon-application/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/username/mon-application/releases/tag/v1.0.0
[0.9.0]: https://github.com/username/mon-application/releases/tag/v0.9.0
"""

# CONTRIBUTING.md
"""
# Guide de Contribution

Merci de l'intérêt que vous portez à ce projet! 

## Comment contribuer

### Rapporter un bug

1. Vérifiez que le bug n'a pas déjà été rapporté dans [Issues](https://github.com/username/mon-application/issues)
2. Créez une nouvelle issue en utilisant le template de bug report
3. Incluez le maximum de détails:
   - Version de l'application
   - Système d'exploitation
   - Étapes pour reproduire
   - Comportement attendu vs observé
   - Screenshots si applicable

### Proposer une fonctionnalité

1. Vérifiez que la fonctionnalité n'a pas déjà été proposée
2. Créez une issue de type "Feature Request"
3. Décrivez clairement:
   - Le problème que ça résout
   - Comment ça devrait fonctionner
   - Alternatives envisagées

### Soumettre du code

1. **Fork** le projet
2. Créez une **branche** pour votre fonctionnalité (`git checkout -b feature/AmazingFeature`)
3. **Committez** vos changements (`git commit -m 'Add AmazingFeature'`)
4. **Push** vers la branche (`git push origin feature/AmazingFeature`)
5. Ouvrez une **Pull Request**

#### Standards de code

- Suivez PEP 8 pour le style Python
- Ajoutez des tests pour les nouvelles fonctionnalités
- Documentez le code (docstrings)
- Mettez à jour la documentation si nécessaire

#### Avant de soumettre

```bash
# Vérifier style
flake8 src/

# Lancer tests
pytest

# Vérifier types (optionnel)
mypy src/
```

### Processus de revue

1. Un mainteneur reviendra votre PR
2. Des changements peuvent être demandés
3. Une fois approuvée, votre PR sera mergée

## Code de Conduite

### Notre engagement

Nous nous engageons à faire de la participation à ce projet une expérience sans harcèlement pour tous.

### Standards

Exemples de comportements acceptables:
- Utiliser un langage accueillant et inclusif
- Respecter les points de vue différents
- Accepter la critique constructive

Exemples de comportements inacceptables:
- Langage ou images à caractère sexuel
- Trolling, insultes
- Harcèlement public ou privé

### Application

Les mainteneurs peuvent supprimer, éditer ou rejeter les contributions qui ne respectent pas ce Code de Conduite.

## Questions

Pour toute question, contactez support@monapp.com
"""


[OK] CHECKLIST FINALE DÉPLOIEMENT

"""
AVANT LE DÉPLOIEMENT
====================

Code
[WHITE_SQUARE] Code review complet
[WHITE_SQUARE] Tous les tests passent
[WHITE_SQUARE] Pas de warnings du linter
[WHITE_SQUARE] Pas de TODO ou FIXME critiques
[WHITE_SQUARE] Version incrémentée correctement
[WHITE_SQUARE] CHANGELOG mis à jour

Tests
[WHITE_SQUARE] Tests unitaires (pytest)
[WHITE_SQUARE] Tests d'intégration
[WHITE_SQUARE] Tests sur Windows 10
[WHITE_SQUARE] Tests sur Windows 11
[WHITE_SQUARE] Tests sur macOS (si applicable)
[WHITE_SQUARE] Tests sur Ubuntu (si applicable)
[WHITE_SQUARE] Tests sur écran haute DPI
[WHITE_SQUARE] Tests avec peu de RAM
[WHITE_SQUARE] Tests avec peu d'espace disque
[WHITE_SQUARE] Tests sans connexion internet
[WHITE_SQUARE] Tests de performance
[WHITE_SQUARE] Tests de sécurité

Documentation
[WHITE_SQUARE] README à jour
[WHITE_SQUARE] Documentation utilisateur complète
[WHITE_SQUARE] API documentée (si applicable)
[WHITE_SQUARE] Commentaires code clairs
[WHITE_SQUARE] CHANGELOG détaillé
[WHITE_SQUARE] LICENSE présente
[WHITE_SQUARE] CONTRIBUTING.md à jour

Build
[WHITE_SQUARE] Build Windows réussi
[WHITE_SQUARE] Build macOS réussi (si applicable)
[WHITE_SQUARE] Build Linux réussi (si applicable)
[WHITE_SQUARE] Installeur Windows créé
[WHITE_SQUARE] DMG macOS créé (si applicable)
[WHITE_SQUARE] AppImage Linux créé (si applicable)
[WHITE_SQUARE] Taille exécutable raisonnable
[WHITE_SQUARE] Toutes ressources incluses
[WHITE_SQUARE] Pas d'erreurs au lancement
[WHITE_SQUARE] Temps démarrage acceptable

Sécurité
[WHITE_SQUARE] Certificat code signing (Windows/macOS)
[WHITE_SQUARE] Scan antivirus passé
[WHITE_SQUARE] Pas de secrets dans le code
[WHITE_SQUARE] Connexions HTTPS uniquement
[WHITE_SQUARE] Dépendances à jour
[WHITE_SQUARE] Vulnérabilités corrigées

Légal
[WHITE_SQUARE] Licence claire
[WHITE_SQUARE] Copyright à jour
[WHITE_SQUARE] Attributions tierces parties
[WHITE_SQUARE] RGPD compliant (si applicable)
[WHITE_SQUARE] Conditions d'utilisation
[WHITE_SQUARE] Politique de confidentialité

Marketing
[WHITE_SQUARE] Screenshots de qualité
[WHITE_SQUARE] Vidéo de démonstration
[WHITE_SQUARE] Site web/landing page
[WHITE_SQUARE] Logo professionnel
[WHITE_SQUARE] Description marketing
[WHITE_SQUARE] Mots-clés SEO

Distribution
[WHITE_SQUARE] GitHub Release créée
[WHITE_SQUARE] Tag git créé
[WHITE_SQUARE] Assets uploadés
[WHITE_SQUARE] Liens de téléchargement fonctionnels
[WHITE_SQUARE] Auto-update configuré (si applicable)
[WHITE_SQUARE] Analytics configuré (si applicable)

APRÈS LE DÉPLOIEMENT
====================

Communication
[WHITE_SQUARE] Annonce sur réseaux sociaux
[WHITE_SQUARE] Email aux beta testeurs
[WHITE_SQUARE] Post sur Reddit/forums
[WHITE_SQUARE] Article de blog
[WHITE_SQUARE] Mise à jour site web

Monitoring
[WHITE_SQUARE] Analytics fonctionnel
[WHITE_SQUARE] Crash reports surveillés
[WHITE_SQUARE] Issues GitHub surveillées
[WHITE_SQUARE] Emails support surveillés
[WHITE_SQUARE] Métriques téléchargement suivies

Support
[WHITE_SQUARE] Répondre aux questions (GitHub/email)
[WHITE_SQUARE] Corriger bugs critiques rapidement
[WHITE_SQUARE] Planifier prochaine version
[WHITE_SQUARE] Collecter feedback utilisateurs
[WHITE_SQUARE] Mettre à jour FAQ
"""


[OK] RESSOURCES FINALES

# Checklist interactive (HTML)
"""
<!DOCTYPE html>
<html>
<head>
    <title>Checklist Déploiement PyQt5</title>
    <style>
        body { font-family: Arial; padding: 20px; max-width: 800px; margin: auto; }
        h2 { color: #2196F3; }
        .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
        input[type="checkbox"] { margin-right: 10px; }
        label { cursor: pointer; }
        label:hover { background: #f0f0f0; }
        .completed { text-decoration: line-through; color: #888; }
        .progress { background: #e0e0e0; height: 30px; border-radius: 15px; overflow: hidden; }
        .progress-bar { background: #4CAF50; height: 100%; transition: width 0.3s; }
        .stats { text-align: center; font-size: 20px; margin: 20px 0; }
    </style>
</head>
<body>
    <h1>[RAPIDE] Checklist Déploiement PyQt5</h1>
    
    <div class="stats">
        <div class="progress">
            <div class="progress-bar" id="progressBar"></div>
        </div>
        <p><span id="completed">0</span> / <span id="total">0</span> complétées</p>
    </div>
    
    <div class="section">
        <h2>[NOTE] Code</h2>
        <label><input type="checkbox" class="task"> Code review complet</label><br>
        <label><input type="checkbox" class="task"> Tous les tests passent</label><br>
        <label><input type="checkbox" class="task"> Version incrémentée</label><br>
        <label><input type="checkbox" class="task"> CHANGELOG mis à jour</label><br>
    </div>
    
    <div class="section">
        <h2>[OUTIL] Build</h2>
        <label><input type="checkbox" class="task"> Build Windows réussi</label><br>
        <label><input type="checkbox" class="task"> Installeur créé</label><br>
        <label><input type="checkbox" class="task"> Tests sur machine propre</label><br>
    </div>
    
    <div class="section">
        <h2>[PACKAGE] Distribution</h2>
        <label><input type="checkbox" class="task"> GitHub Release créée</label><br>
        <label><input type="checkbox" class="task"> Assets uploadés</label><br>
        <label><input type="checkbox" class="task"> Documentation à jour</label><br>
    </div>
    
    <script>
        const tasks = document.querySelectorAll('.task');
        const progressBar = document.getElementById('progressBar');
        const completed = document.getElementById('completed');
        const total = document.getElementById('total');
        
        total.textContent = tasks.length;
        
        tasks.forEach(task => {
            // Charger état depuis localStorage
            const key = task.parentElement.textContent.trim();
            if (localStorage.getItem(key) === 'true') {
                task.checked = true;
                task.parentElement.classList.add('completed');
            }
            
            task.addEventListener('change', function() {
                // Sauvegarder état
                localStorage.setItem(key, this.checked);
                
                // Style
                if (this.checked) {
                    this.parentElement.classList.add('completed');
                } else {
                    this.parentElement.classList.remove('completed');
                }
                
                updateProgress();
            });
        });
        
        function updateProgress() {
            const completedCount = document.querySelectorAll('.task:checked').length;
            const percentage = (completedCount / tasks.length) * 100;
            
            completed.textContent = completedCount;
            progressBar.style.width = percentage + '%';
            
            if (completedCount === tasks.length) {
                alert('[BRAVO] Toutes les tâches sont complétées! Prêt à déployer!');
            }
        }
        
        updateProgress();
    </script>
</body>
</html>
"""


[OK] DÉPANNAGE DÉPLOIEMENT

# === Problèmes courants et solutions ===

# PROBLÈME: "ModuleNotFoundError" dans l'exécutable
"""
Cause: PyInstaller n'a pas détecté une dépendance

Solutions:
1. Ajouter --hidden-import
   pyinstaller --hidden-import=module_name main.py

2. Modifier .spec pour inclure le module
   hiddenimports=['module_name', 'other_module']

3. Vérifier imports dynamiques
   # [X] Mauvais (PyInstaller ne détecte pas)
   module_name = 'PyQt5.QtPrintSupport'
   module = __import__(module_name)
   
   # [OK] Bon
   from PyQt5 import QtPrintSupport
"""

# PROBLÈME: "FileNotFoundError" pour ressources
"""
Cause: Chemins ressources incorrects après packaging

Solution: Utiliser resource_path()
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# Utilisation
icon_path = resource_path('images/icon.png')
pixmap = QPixmap(icon_path)
"""

# PROBLÈME: Exécutable trop volumineux (>200 MB)
"""
Solutions:
1. Utiliser environnement virtuel propre
   python -m venv venv_clean
   source venv_clean/bin/activate
   pip install -r requirements.txt  # Seulement ce qui est nécessaire
   pyinstaller main.py

2. Exclure modules inutiles
   pyinstaller --exclude-module=tkinter --exclude-module=matplotlib main.py

3. Utiliser UPX pour compresser
   pyinstaller --upx-dir=/path/to/upx main.py

4. Mode --onedir au lieu de --onefile
   pyinstaller --onedir main.py
   # Plus rapide au démarrage, mais multiple fichiers
"""

# PROBLÈME: Antivirus bloque l'exécutable
"""
Causes:
- PyInstaller génère code qui ressemble à malware
- Pas de signature numérique
- Comportement suspect (accès réseau, fichiers)

Solutions:
1. Signer numériquement (recommandé)
   - Obtenir certificat code signing
   - Signer avec signtool (Windows)

2. Soumettre à VirusTotal pour analyse
   - Upload sur virustotal.com
   - Partager rapport avec utilisateurs

3. Whitelisting
   - Contacter éditeurs antivirus
   - Demander ajout à whitelist

4. Alternatives à PyInstaller
   - Nuitka (compile en C++)
   - cx_Freeze
"""

# PROBLÈME: Crash au démarrage sans message
"""
Solutions:
1. Build avec mode debug
   pyinstaller --debug=all main.py
   # Voir logs dans console

2. Ajouter logging
   import logging
   logging.basicConfig(filename='app.log', level=logging.DEBUG)
   logger.info('Application starting...')

3. Try-catch autour du code principal
   try:
       app = QApplication(sys.argv)
       window = MainWindow()
       window.show()
       sys.exit(app.exec_())
   except Exception as e:
       with open('crash.log', 'w') as f:
           f.write(str(e))
           import traceback
           traceback.print_exc(file=f)
       raise

4. Tester sur machine propre
   - VM ou Windows Sandbox
   - Installer Visual C++ Redistributable
"""

# PROBLÈME: Interface graphique corrompue
"""
Causes:
- DPI scaling
- Qt plugins manquants
- Styles non inclus

Solutions:
1. Forcer DPI awareness
   QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
   QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

2. Inclure plugins Qt
   pyinstaller --collect-all PyQt5 main.py

3. Copier plugins manuellement
   datas=[
       ('venv/Lib/site-packages/PyQt5/Qt5/plugins', 'PyQt5/Qt5/plugins')
   ]
"""

# PROBLÈME: Lancement très lent (>30 secondes)
"""
Causes:
- Mode --onefile décompresse à chaque lancement
- Trop de dépendances
- Antivirus scanne au lancement

Solutions:
1. Utiliser --onedir
   pyinstaller --onedir main.py
   # Démarrage plus rapide

2. Optimiser imports
   # [X] Importer tout
   from PyQt5.QtWidgets import *
   
   # [OK] Importer uniquement nécessaire
   from PyQt5.QtWidgets import QApplication, QMainWindow

3. Lazy loading
   # Importer modules lourds seulement quand nécessaire
   def export_pdf():
       from reportlab import pdfgen  # Import tardif
       # ...

4. Splash screen
   # Montrer progression pendant chargement
   splash = QSplashScreen(pixmap)
   splash.show()
"""


[OK] OPTIMISATIONS AVANCÉES

# === Réduire taille exécutable ===

# 1. Analyser dépendances
"""
# Voir ce que PyInstaller inclut
pyinstaller --log-level=DEBUG main.py

# Analyser .spec
# Vérifier section 'datas' et 'binaries'
"""

# 2. Strip binaries (Linux/macOS)
"""
# Dans .spec
exe = EXE(
    ...,
    strip=True,  # Enlever symboles debug
    upx=True,    # Compresser avec UPX
)
"""

# 3. Exclure Qt translations non nécessaires
"""
# Dans .spec
a = Analysis(
    ...,
    excludes=['PyQt5.Qt.translations'],  # Si pas besoin traductions
)
"""

# 4. Utiliser Qt minimal
"""
# Si vous n'utilisez pas tout Qt
excludes=[
    'PyQt5.QtWebEngine',      # Navigateur web
    'PyQt5.QtWebEngineWidgets',
    'PyQt5.QtMultimedia',     # Audio/vidéo
    'PyQt5.QtMultimediaWidgets',
    'PyQt5.QtPrintSupport',   # Impression
    'PyQt5.QtSql',            # Base de données
]
"""

# === Améliorer temps de démarrage ===

# 1. Lazy imports
"""
# Au lieu de tout importer au début
# from PIL import Image
# from reportlab.pdfgen import canvas
# import numpy as np

# Importer seulement quand nécessaire
class MainWindow(QMainWindow):
    def export_pdf(self):
        from reportlab.pdfgen import canvas  # Import tardif
        # ...
    
    def process_image(self):
        from PIL import Image  # Import tardif
        # ...
"""

# 2. Threads pour tâches lourdes au démarrage
"""
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()  # Interface légère d'abord
        
        # Charger données lourdes en arrière-plan
        self.worker = LoadWorker()
        self.worker.finished.connect(self.on_data_loaded)
        self.worker.start()
    
    def on_data_loaded(self):
        # Données chargées, activer fonctionnalités
        pass
"""

# 3. Cache et préchargement
"""
# Cacher données fréquemment utilisées
from functools import lru_cache

@lru_cache(maxsize=128)
def get_config(key):
    # Lecture fichier config (lente)
    return config[key]
"""

# === Optimiser performance runtime ===

# 1. Profiling
"""
import cProfile
import pstats

# Profiler fonction
cProfile.run('my_function()', 'output.prof')

# Analyser résultats
stats = pstats.Stats('output.prof')
stats.sort_stats('cumulative')
stats.print_stats(10)  # Top 10 fonctions lentes
"""

# 2. Éviter recréation widgets
"""
# [X] Mauvais: Recrée à chaque fois
def update_display(self):
    self.label = QLabel('New text')
    layout.addWidget(self.label)

# [OK] Bon: Réutilise widget existant
def update_display(self):
    self.label.setText('New text')
"""

# 3. Batch updates
"""
# [X] Mauvais: Met à jour un par un
for item in items:
    list_widget.addItem(item)

# [OK] Bon: Batch
list_widget.addItems(items)
"""

# 4. Désactiver signaux pendant updates
"""
widget.blockSignals(True)
# ... multiples modifications ...
widget.blockSignals(False)
widget.update()  # Un seul update
"""


[OK] SCALABILITÉ ET CROISSANCE

# === Planifier pour le futur ===

# 1. Architecture modulaire
"""
# Séparer fonctionnalités en modules indépendants
mon_app/
├── core/           # Logique métier
├── ui/             # Interface
├── plugins/        # Extensions
└── api/            # API externe (si nécessaire)

# Permet:
- Maintenance facile
- Tests isolés
- Ajout fonctionnalités sans casser existant
"""

# 2. Système de plugins
"""
# plugin_manager.py
import importlib
import os

class PluginManager:
    def __init__(self):
        self.plugins = {}
    
    def load_plugins(self, plugin_dir='plugins'):
        for filename in os.listdir(plugin_dir):
            if filename.endswith('.py') and not filename.startswith('_'):
                module_name = filename[:-3]
                module = importlib.import_module(f'plugins.{module_name}')
                
                if hasattr(module, 'Plugin'):
                    plugin = module.Plugin()
                    self.plugins[module_name] = plugin
    
    def execute_plugin(self, name, *args, **kwargs):
        if name in self.plugins:
            return self.plugins[name].execute(*args, **kwargs)

# plugins/example_plugin.py
class Plugin:
    def __init__(self):
        self.name = 'Example Plugin'
    
    def execute(self, data):
        # Traitement
        return result
"""

# 3. API pour intégrations
"""
# Exposer API REST si l'application doit communiquer avec autres services
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/data', methods=['GET'])
def get_data():
    return jsonify({'data': 'value'})

# Lancer dans thread séparé
def run_api():
    app.run(port=5000)

api_thread = threading.Thread(target=run_api)
api_thread.daemon = True
api_thread.start()
"""

# 4. Internationalisation (i18n)
"""
# Préparer pour multiple langues dès le début
from PyQt5.QtCore import QTranslator, QLocale

# Dans chaque string UI
label = QLabel(self.tr('Hello'))  # Au lieu de QLabel('Hello')

# Générer fichiers traduction
pylupdate5 *.py -ts translations/app_fr.ts

# Éditer avec Qt Linguist
# linguist translations/app_fr.ts

# Compiler
lrelease translations/app_fr.ts  # Génère app_fr.qm

# Charger traduction
translator = QTranslator()
translator.load('translations/app_fr.qm')
app.installTranslator(translator)
"""

# 5. Versioning de données
"""
# Si vous sauvegardez données utilisateur
import json

class DataManager:
    VERSION = 2
    
    def save(self, data, filename):
        output = {
            'version': self.VERSION,
            'data': data
        }
        with open(filename, 'w') as f:
            json.dump(output, f)
    
    def load(self, filename):
        with open(filename, 'r') as f:
            content = json.load(f)
        
        version = content.get('version', 1)
        data = content['data']
        
        # Migration si ancienne version
        if version < self.VERSION:
            data = self.migrate(data, version)
        
        return data
    
    def migrate(self, data, from_version):
        # Migration v1 -> v2
        if from_version == 1:
            data['new_field'] = 'default'
        
        return data
"""


[OK] EXEMPLES DE PROJETS DÉPLOYÉS

# === Exemples réels (open source) ===

"""
1. Calibre (Gestion e-books)
   - https://github.com/kovidgoyal/calibre
   - Multi-plateforme PyQt5
   - Déploiement professionnel
   - Millions d'utilisateurs

2. Spyder (IDE Python)
   - https://github.com/spyder-ide/spyder
   - PyQt5 scientifique
   - Installeurs Windows/Mac/Linux

3. Anki (Flashcards)
   - https://github.com/ankitects/anki
   - PyQt5/6
   - Mobile + Desktop
   - Auto-update

4. QDirStat (Analyse disque)
   - https://github.com/shundhammer/qdirstat
   - Qt/C++ mais applicable à PyQt5
   - AppImage Linux

5. Orange (Data mining)
   - https://github.com/biolab/orange3
   - PyQt5 scientifique
   - Workflows visuels
"""


[OK] MÉTRIQUES DE SUCCÈS

# === KPIs à suivre ===

"""
Téléchargements
[WHITE_SQUARE] Nombre de téléchargements total
[WHITE_SQUARE] Téléchargements par plateforme
[WHITE_SQUARE] Taux de croissance mensuel
[WHITE_SQUARE] Sources de téléchargement (GitHub, site, etc.)

Utilisation
[WHITE_SQUARE] Utilisateurs actifs quotidiens (DAU)
[WHITE_SQUARE] Utilisateurs actifs mensuels (MAU)
[WHITE_SQUARE] Durée moyenne de session
[WHITE_SQUARE] Fonctionnalités les plus utilisées
[WHITE_SQUARE] Taux de rétention (jour 1, 7, 30)

Qualité
[WHITE_SQUARE] Taux de crash
[WHITE_SQUARE] Nombre de bugs reportés
[WHITE_SQUARE] Temps de résolution bugs
[WHITE_SQUARE] Score satisfaction utilisateurs
[WHITE_SQUARE] Notes/Reviews

Performance
[WHITE_SQUARE] Temps de démarrage moyen
[WHITE_SQUARE] Utilisation mémoire
[WHITE_SQUARE] Utilisation CPU
[WHITE_SQUARE] Taille téléchargement

Engagement
[WHITE_SQUARE] Issues GitHub (ouvertes/fermées)
[WHITE_SQUARE] Pull Requests
[WHITE_SQUARE] Stars GitHub
[WHITE_SQUARE] Contributeurs
[WHITE_SQUARE] Mentions réseaux sociaux
"""

# Tableau de bord simple
"""
# dashboard.py - Générer rapport métriques

import requests
from datetime import datetime

def get_github_stats(repo):
    url = f'https://api.github.com/repos/{repo}'
    response = requests.get(url)
    data = response.json()
    
    return {
        'stars': data['stargazers_count'],
        'forks': data['forks_count'],
        'watchers': data['subscribers_count'],
        'open_issues': data['open_issues_count']
    }

def get_download_stats(repo):
    url = f'https://api.github.com/repos/{repo}/releases'
    response = requests.get(url)
    releases = response.json()
    
    total = 0
    for release in releases:
        for asset in release['assets']:
            total += asset['download_count']
    
    return total

def generate_report():
    repo = 'username/mon-application'
    
    stats = get_github_stats(repo)
    downloads = get_download_stats(repo)
    
    report = f'''
    [GRAPHIQUE] RAPPORT MÉTRIQUES - {datetime.now().strftime('%Y-%m-%d')}
    {'='*50}
    
    GitHub
    ------
    * Stars: {stats['stars']}
    [FORK_AND_KNIFE] Forks: {stats['forks']}
    [EYE] Watchers: {stats['watchers']}
    [BUG] Issues ouvertes: {stats['open_issues']}
    
    Téléchargements
    ---------------
    [ENTREE] Total: {downloads}
    
    {'='*50}
    '''
    
    print(report)
    
    # Sauvegarder
    with open('reports/metrics.txt', 'w') as f:
        f.write(report)

if __name__ == '__main__':
    generate_report()
"""


[OK] CONCLUSION ET PROCHAINES ÉTAPES

# === Roadmap post-lancement ===

"""
MOIS 1: Stabilisation
[WHITE_SQUARE] Corriger bugs critiques rapidement
[WHITE_SQUARE] Répondre support utilisateurs
[WHITE_SQUARE] Collecter feedback
[WHITE_SQUARE] Améliorer documentation basée sur questions fréquentes
[WHITE_SQUARE] Patch release (v1.0.1, v1.0.2)

MOIS 2-3: Amélioration
[WHITE_SQUARE] Implémenter fonctionnalités demandées
[WHITE_SQUARE] Optimisations performance
[WHITE_SQUARE] Tests A/B (si applicable)
[WHITE_SQUARE] Améliorer UX basé sur analytics
[WHITE_SQUARE] Minor release (v1.1.0)

MOIS 4-6: Expansion
[WHITE_SQUARE] Nouvelles fonctionnalités majeures
[WHITE_SQUARE] Améliorer intégrations
[WHITE_SQUARE] Étendre compatibilité (anciennes versions OS)
[WHITE_SQUARE] Marketing accru
[WHITE_SQUARE] Builds pour autres plateformes
[WHITE_SQUARE] Major release (v2.0.0)

MOIS 6+: Maturité
[WHITE_SQUARE] Communauté active
[WHITE_SQUARE] Contributions externes
[WHITE_SQUARE] Plugins/Extensions
[WHITE_SQUARE] API publique
[WHITE_SQUARE] Version Enterprise (si applicable)
"""

# === Checklist post-déploiement ===

"""
JOUR 1
[WHITE_SQUARE] Surveiller crash reports
[WHITE_SQUARE] Vérifier liens téléchargement
[WHITE_SQUARE] Monitorer feedback initial
[WHITE_SQUARE] Être disponible pour support

SEMAINE 1
[WHITE_SQUARE] Analyser premiers analytics
[WHITE_SQUARE] Identifier bugs communs
[WHITE_SQUARE] Planifier hotfix si nécessaire
[WHITE_SQUARE] Répondre questions FAQ
[WHITE_SQUARE] Remercier early adopters

MOIS 1
[WHITE_SQUARE] Release patch si bugs critiques
[WHITE_SQUARE] Analyser métriques détaillées
[WHITE_SQUARE] Planifier v1.1 basé sur feedback
[WHITE_SQUARE] Améliorer marketing
[WHITE_SQUARE] Construire communauté
"""


[OK] RESSOURCES ESSENTIELLES DÉPLOIEMENT

# === Liens utiles ===

"""
Documentation officielle:
- PyInstaller: https://pyinstaller.org/en/stable/
- cx_Freeze: https://cx-freeze.readthedocs.io/
- Inno Setup: https://jrsoftware.org/isinfo.php

Tutoriels vidéo:
- "PyInstaller Full Tutorial" (YouTube)
- "Distributing Python Applications" (Real Python)

Communautés:
- r/learnpython (Reddit)
- Python Discord
- Stack Overflow [pyinstaller]

Outils:
- VirusTotal: https://virustotal.com/
- GitHub Actions: https://github.com/features/actions
- Sentry: https://sentry.io/

Inspiration:
- GitHub Explore: Projets PyQt5 populaires
- Awesome PyQt: https://github.com/topics/pyqt5

Services:
- Certificats code signing: DigiCert, Sectigo
- Analytics: Google Analytics, Mixpanel
- Crash reporting: Sentry, Rollbar
- Distribution: GitHub Releases, Gumroad
"""


[OK] TEMPLATE EMAIL DE LANCEMENT

"""
Sujet: [RAPIDE] Lancement de Mon Application v1.0

Bonjour,

Je suis ravi d'annoncer le lancement officiel de Mon Application v1.0!

[MOBILE] QU'EST-CE QUE C'EST?
Mon Application est [description courte]. Elle permet de [problème résolu].

* FONCTIONNALITÉS PRINCIPALES
• Fonctionnalité 1
• Fonctionnalité 2
• Fonctionnalité 3

[CODE] DISPONIBLE SUR
• Windows 10/11
• macOS 10.15+
• Linux (Ubuntu, Fedora, etc.)

[ENTREE] TÉLÉCHARGER
Téléchargement gratuit: https://github.com/user/app/releases

[GUIDE] EN SAVOIR PLUS
Site web: https://monapp.com
Documentation: https://docs.monapp.com

[ACCORD] CONTRIBUER
Le projet est open source! Contributions bienvenues:
https://github.com/user/app

Merci pour votre soutien!

[Votre nom]
Créateur de Mon Application

---
[LOVE_LETTER] Support: support@monapp.com
[BIRD] Twitter: @monapp
"""


[OK] FIN - RÉCAPITULATIF COMPLET

"""
[BRAVO] FÉLICITATIONS!

Vous avez maintenant toutes les connaissances pour:

[OK] Développer une application PyQt5 complète
[OK] Structurer votre projet professionnellement
[OK] Implémenter toutes les fonctionnalités courantes
[OK] Créer des exécutables pour Windows/Mac/Linux
[OK] Créer des installeurs professionnels
[OK] Déployer sur GitHub et autres plateformes
[OK] Gérer mises à jour et support utilisateurs
[OK] Monitorer performance et analytics
[OK] Faire croître votre application

PROCHAINES ÉTAPES:
1. Commencer un projet
2. Implémenter MVP (Minimum Viable Product)
3. Tester avec beta testeurs
4. Déployer première version
5. Itérer basé sur feedback

RAPPEL: Rome ne s'est pas construite en un jour!
- Commencez simple
- Itérez fréquemment
- Écoutez utilisateurs
- Améliorez continuellement

Bonne chance avec votre projet PyQt5! [RAPIDE]
"""


# ============================================================================
# FIN DU GUIDE COMPLET PYQT5
# ============================================================================