Contexte
Depuis QGIS 3.34 (et surtout 3.36+), QGIS a basculé de Qt5 vers Qt6.
Problème : les plugins Python générés avec Qt Designer / pyuic5 utilisent encore les constantes de Qt5.
Résultat : au lancement du plugin, on se retrouve avec des erreurs du type :
AttributeError: type object 'QFrame' has no attribute 'Sunken'
AttributeError: type object 'QDialogButtonBox' has no attribute 'Cancel'
Ce script autopatch.py
corrige automatiquement tous les fichiers Python d’un plugin pour remplacer les vieilles constantes Qt5 par leurs équivalents Qt6.
Le code complet
import os
# Dossier où se trouve ton plugin
PLUGIN_DIR = r"C:\fuzzy\FuzzyAttributes4"
# Dictionnaire des correspondances Qt5 → Qt6
qt_constants = {
# QFrame
"QtWidgets.QFrame.HLine": "QtWidgets.QFrame.Shape.HLine",
"QtWidgets.QFrame.VLine": "QtWidgets.QFrame.Shape.VLine",
"QtWidgets.QFrame.StyledPanel": "QtWidgets.QFrame.Shape.StyledPanel",
"QtWidgets.QFrame.Panel": "QtWidgets.QFrame.Shape.Panel",
"QtWidgets.QFrame.Box": "QtWidgets.QFrame.Shape.Box",
"QtWidgets.QFrame.NoFrame": "QtWidgets.QFrame.Shape.NoFrame",
"QtWidgets.QFrame.WinPanel": "QtWidgets.QFrame.Shape.WinPanel",
"QtWidgets.QFrame.Sunken": "QtWidgets.QFrame.Shadow.Sunken",
"QtWidgets.QFrame.Raised": "QtWidgets.QFrame.Shadow.Raised",
# Alignements
"Qt.AlignLeft": "Qt.AlignmentFlag.AlignLeft",
"Qt.AlignRight": "Qt.AlignmentFlag.AlignRight",
"Qt.AlignHCenter": "Qt.AlignmentFlag.AlignHCenter",
"Qt.AlignJustify": "Qt.AlignmentFlag.AlignJustify",
"Qt.AlignTop": "Qt.AlignmentFlag.AlignTop",
"Qt.AlignBottom": "Qt.AlignmentFlag.AlignBottom",
"Qt.AlignVCenter": "Qt.AlignmentFlag.AlignVCenter",
"Qt.AlignCenter": "Qt.AlignmentFlag.AlignCenter",
# États des widgets
"Qt.Checked": "Qt.CheckState.Checked",
"Qt.Unchecked": "Qt.CheckState.Unchecked",
"Qt.PartiallyChecked": "Qt.CheckState.PartiallyChecked",
# Orientations
"Qt.Horizontal": "Qt.Orientation.Horizontal",
"Qt.Vertical": "Qt.Orientation.Vertical",
# QDialogButtonBox
"QtWidgets.QDialogButtonBox.Ok": "QtWidgets.QDialogButtonBox.StandardButton.Ok",
"QtWidgets.QDialogButtonBox.Cancel": "QtWidgets.QDialogButtonBox.StandardButton.Cancel",
"QtWidgets.QDialogButtonBox.Yes": "QtWidgets.QDialogButtonBox.StandardButton.Yes",
"QtWidgets.QDialogButtonBox.No": "QtWidgets.QDialogButtonBox.StandardButton.No",
"QtWidgets.QDialogButtonBox.Apply": "QtWidgets.QDialogButtonBox.StandardButton.Apply",
"QtWidgets.QDialogButtonBox.Close": "QtWidgets.QDialogButtonBox.StandardButton.Close",
"QtWidgets.QDialogButtonBox.Help": "QtWidgets.QDialogButtonBox.StandardButton.Help",
"QtWidgets.QDialogButtonBox.Reset": "QtWidgets.QDialogButtonBox.StandardButton.Reset",
"QtWidgets.QDialogButtonBox.RestoreDefaults": "QtWidgets.QDialogButtonBox.StandardButton.RestoreDefaults",
# Shadows (sécurité pour d’autres cas)
"QFrame.Sunken": "QFrame.Shadow.Sunken",
"QFrame.Raised": "QFrame.Shadow.Raised",
"QFrame.Plain": "QFrame.Shadow.Plain",
"QFrame.Shadow": "QFrame.Shadow"
}
def patch_file(file_path):
"""Corrige un fichier .py en remplaçant les constantes Qt5 par Qt6"""
changed = False
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
for line in lines:
for old, new in qt_constants.items():
if old in line:
line = line.replace(old, new)
changed = True
new_lines.append(line)
if changed:
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(new_lines)
print(f"[✔] Corrigé : {file_path}")
else:
print(f"[ ] Aucun changement : {file_path}")
def patch_all_py_files():
"""Parcourt tout le dossier du plugin et corrige les .py"""
for root, dirs, files in os.walk(PLUGIN_DIR):
for file in files:
if file.endswith(".py"): # tous les fichiers Python
patch_file(os.path.join(root, file))
if __name__ == "__main__":
patch_all_py_files()
Comment ça marche ?
- Dictionnaire de correspondances
Tout en haut, on définit une table de conversionqt_constants
qui fait le lien entre les anciennes constantes Qt5 et les nouvelles Qt6.
Exemple :QtWidgets.QFrame.Sunken
devientQtWidgets.QFrame.Shadow.Sunken
QtWidgets.QDialogButtonBox.Cancel
devientQtWidgets.QDialogButtonBox.StandardButton.Cancel
- Analyse ligne par ligne
Le script lit chaque fichier.py
du plugin, parcourt les lignes et applique les remplacements. - Écriture des fichiers corrigés
Si un remplacement a eu lieu, le fichier est réécrit directement (⚠️ il vaut mieux faire une sauvegarde du plugin avant). - Rapport en console
Le script affiche :[✔] Corrigé : ...
si le fichier contenait des constantes Qt5 remplacées,[ ] Aucun changement : ...
si le fichier ne nécessitait pas de correction.
Mode d’emploi
- Copiez
autopatch.py
à côté de votre plugin QGIS (ou ailleurs, mais pas à l’intérieur). - Éditez la ligne suivante pour mettre le bon chemin de votre plugin :
PLUGIN_DIR = r"C:\Users\...\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\NomDuPlugin"
- Faites une sauvegarde de votre plugin (par précaution).
- Lancez le script dans une console (cmd ou PowerShell) :
python autopatch.py
- Ouvrez QGIS → votre plugin devrait fonctionner sans erreur avec Qt6.
1. Pourquoi ça coince ?
- QGIS est construit sur PyQt (Python + Qt).
- Jusqu’à QGIS 3.28 inclus, la base était Qt5.
- Depuis QGIS 3.34 (et encore plus avec la 3.40 à venir), QGIS migre progressivement vers Qt6.
Or, PyQt6 a changé plein de noms de classes, constantes et énumérations :
- Ce qui s’écrivait avant
Qt.AlignLeft
devientQt.AlignmentFlag.AlignLeft
. - Les boutons de
QDialogButtonBox
ont été déplacés (Ok
→StandardButton.Ok
). - Les ombres de cadres (
QFrame.Sunken
, etc.) sont devenues des sous-énums (QFrame.Shadow.Sunken
).
Résultat :
- Votre plugin, écrit pour Qt5, fonctionne parfaitement sous QGIS basé sur Qt5 (3.28, LTR actuelle).
- Mais sous Qt6, Python crashe dès qu’il rencontre un symbole Qt5 qui n’existe plus → d’où les erreurs
AttributeError: type object 'QFrame' has no attribute 'Sunken'
.
2. Quelles conséquences pour votre plugin ?
Cas 1 : QGIS basé sur Qt5 (ex. 3.28 LTR)
- Votre plugin fonctionne normalement.
- Si vous appliquez les changements Qt6 dedans, il risque de ne plus fonctionner sous Qt5 car les anciens noms ne sont plus reconnus.
Cas 2 : QGIS basé sur Qt6 (ex. 3.34, 3.40+)
- Votre plugin plante immédiatement si vous ne faites rien.
- Une fois corrigé (Qt5 → Qt6), il fonctionne comme avant, le comportement de votre plugin ne change pas : ce ne sont que des renommages, pas des modifications de logique.
Donc :
- Le cœur fonctionnel (calculs, algorithmes, interface utilisateur) reste identique.
- Seuls les noms des constantes Qt doivent être adaptés.
- Votre plugin n’aura aucune différence visible dans QGIS : les fenêtres, boutons, cases à cocher, aperçus graphiques s’affichent pareil.
3. Compatibilité double Qt5 / Qt6
Comme beaucoup de développeurs veulent un seul plugin qui marche à la fois sous 3.28 (Qt5) et sous 3.34+ (Qt6), il y a trois stratégies possibles :
- Deux branches du plugin
- Une version Qt5 pour QGIS ≤ 3.28.
- Une version Qt6 pour QGIS ≥ 3.34.
- Avantage : simple et propre.
- Inconvénient : vous devez maintenir deux versions.
- Un patch automatique (comme
autopatch.py
)- Vous gardez voytre code en Qt5.
- Quand vous voulez publier pour Qt6, vous lancez le script → il réécrit les lignes incompatibles.
- Vous publiez le résultat comme version Qt6.
- Détection dynamique dans le code
- Au lieu de patcher, vous testez au lancement :
from qgis.PyQt import QtCore, QtWidgets
try:
AlignLeft = QtCore.Qt.AlignmentFlag.AlignLeft # Qt6
except AttributeError:
AlignLeft = QtCore.Qt.AlignLeft # Qt5
- Plus complexe mais ça permet un seul plugin universel.
- Au lieu de patcher, vous testez au lancement :
4. En résumé
- Un plugin Qt5 ne fonctionne pas directement sous Qt6, car beaucoup de noms ont changé.
- Le fonctionnement fonctionnel du plugin ne change pas → c’est uniquement du « renommage technique ».
- Le script
autopatch.py
sert à traduire automatiquement Qt5 → Qt6 dans tous les.py
du plugin. - Vous pouvez soit :
- publier deux versions séparées (Qt5 et Qt6),
- ou gérer une version patchée dynamiquement.
5. Exemple concret
Actuellement votre code (après autopatch) ressemble sûrement à ça :
frame.setFrameShape(QtWidgets.QFrame.Shape.HLine) # Qt6 only
label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) # Qt6 only
Sous QGIS Qt5 → ça crashe, car Shape
et AlignmentFlag
n’existent pas.
Voici une fonction utilitaire “qt_compat.py” que vous pouvez ajouter dans votre plugin et importer partout, pour gérer automatiquement Qt5/Qt6 sans vous casser la tête
6. Fonction qt-compat.py
from qgis.PyQt import QtWidgets, QtCore
# ---------
# QFrame
# ---------
def frame_shape(shape_name: str):
"""
Retourne la valeur correcte d'une constante QFrame (Shape).
Exemple : frame_shape("HLine"), frame_shape("VLine")
"""
if hasattr(QtWidgets.QFrame, "Shape"): # Qt6
return getattr(QtWidgets.QFrame.Shape, shape_name)
else: # Qt5
return getattr(QtWidgets.QFrame, shape_name)
def frame_shadow(shadow_name: str):
"""
Retourne la valeur correcte d'une constante QFrame (Shadow).
Exemple : frame_shadow("Sunken"), frame_shadow("Raised")
"""
if hasattr(QtWidgets.QFrame, "Shadow"): # Qt6
return getattr(QtWidgets.QFrame.Shadow, shadow_name)
else:
return getattr(QtWidgets.QFrame, shadow_name)
# ---------
# QSizePolicy
# ---------
def size_policy(policy_name: str):
"""
Retourne la valeur correcte d'une constante QSizePolicy.
Exemple : size_policy("Expanding"), size_policy("Fixed")
"""
if hasattr(QtWidgets.QSizePolicy, "Policy"): # Qt6
return getattr(QtWidgets.QSizePolicy.Policy, policy_name)
else:
return getattr(QtWidgets.QSizePolicy, policy_name)
# ---------
# QDialogButtonBox
# ---------
def dialog_button(button_name: str):
"""
Retourne la valeur correcte pour un bouton standard de QDialogButtonBox.
Exemple : dialog_button("Ok"), dialog_button("Cancel"), dialog_button("Apply")
"""
if hasattr(QtWidgets.QDialogButtonBox, "StandardButton"): # Qt6
return getattr(QtWidgets.QDialogButtonBox.StandardButton, button_name)
else:
return getattr(QtWidgets.QDialogButtonBox, button_name)
# ---------
# Qt Alignment
# ---------
def alignment(align_name: str):
"""
Retourne la valeur correcte pour les alignements Qt.
Exemple : alignment("AlignLeft"), alignment("AlignCenter"), alignment("AlignRight")
"""
if hasattr(QtCore, "AlignmentFlag"): # Qt6
return getattr(QtCore.AlignmentFlag, align_name)
else:
return getattr(QtCore, align_name)
# ---------
# QFont
# ---------
def font_weight(weight_name: str):
"""
Retourne la valeur correcte d'un poids de police.
Exemple : font_weight("Bold"), font_weight("Normal")
"""
if hasattr(QtGui.QFont, "Weight"): # Qt6
return getattr(QtGui.QFont.Weight, weight_name)
else:
return getattr(QtGui.QFont, weight_name)
Ce que vous devrez faire avec votre module
Au lieu de coder en dur, vous appellez les helpers :
from . import qt_compat # votre fichier de compatibilité
frame.setFrameShape(qt_compat.frame_shape("HLine"))
label.setAlignment(qt_compat.alignment("AlignCenter"))
buttonBox.addButton(qt_compat.dialog_button("Ok"))
Résultat
- Sous Qt5 →
qt_compat
renverraQtWidgets.QFrame.HLine
ouQt.AlignCenter
. - Sous Qt6 → il renverra
QtWidgets.QFrame.Shape.HLine
ouQt.AlignmentFlag.AlignCenter
. - Même code, plugin universel Qt5 + Qt6.
Donc, il faut remplacer partout les usages directs de Qt.*
et QtWidgets.QFrame.*
par les fonctions.