diff --git a/extension-graphique/index.html b/extension-graphique/index.html index a5edd29..380cbcc 100644 --- a/extension-graphique/index.html +++ b/extension-graphique/index.html @@ -2189,7 +2189,7 @@

Ajouter un bouton pour lancer 2 3
self.btn_traitement_1.setToolTip("Permet de lancer l'algorithme des tampons sur la couche ci-dessus avec un buffer de 2km")
 self.btn_traitement_1.setIcon(QIcon(":/images/themes/default/algorithms/mAlgorithmBuffer.svg"))
-self.btn_traitement_1.clicked.connect(self.traitement_1_clicked)
+# self.btn_traitement_1.clicked.connect(self.traitement_1_clicked)
 

Lancer le dialogue de Processing#

Pour les imports :

diff --git a/search/search_index.json b/search/search_index.json index 3e8c503..7b353fb 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["fr"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Formation PyQGIS","text":""},{"location":"#pre-requis","title":"Pr\u00e9-requis","text":"

Cette formation concerne des utilisateurs de QGIS, g\u00e9omaticiens, qui souhaitent apprendre l'API Python de QGIS :

Pour suivre la formation, il faut :

Si n\u00e9cessaire, il peut \u00eatre utile d'avoir en plus :

"},{"location":"#plan","title":"Plan","text":""},{"location":"action/","title":"Les actions","text":"

Info

La couche HYDROGRAPHIE/COURS_D_EAU.shp est de type multilinestring. Nous allons donc prendre en compte ce cas par d\u00e9faut dans la suite de ce tutoriel.

# Notation pour ajouter des attributs en cr\u00e9ant une couche m\u00e9moire\n# https://docs.qgis.org/latest/fr/docs/pyqgis_developer_cookbook/vector.html#from-an-instance-of-qgsvectorlayer\nriver = QgsVectorLayer('MultiLineString?crs=epsg:2154&field=id:integer&field=name:string(20)&index=yes', 'Rivers', 'memory')\n\nQgsProject.instance().addMapLayer(river)\n\nwith edit(river):\n    # Cette fonction permet de faire des v\u00e9rifications sur les contraintes si n\u00e9cessaires contrairement \u00e0 QgsFeature(fields)\n    feature = QgsVectorLayerUtils.createFeature(river)\n    feature.setAttribute('id', 0)\n    feature.setAttribute('name', 'Une rivi\u00e8re')\n    geom = QgsGeometry.fromMultiPolylineXY(\n    [\n        [QgsPointXY(1, 1), QgsPointXY(2, 2), QgsPointXY(3, 2), QgsPointXY(4, 1)]\n    ])\n    feature.setGeometry(geom)\n    river.addFeature(feature)\n\nextent = river.extent()\ncanvas = iface.mapCanvas()\ncanvas.setExtent(extent)\ncanvas.refresh()\n
"},{"location":"action/#les-actions-par-defaut","title":"Les actions par d\u00e9faut","text":"

Info

Selon le champ d'application de l'action, il y a plus ou moins de variables. Il faut regarder les infobulles.

"},{"location":"action/#notre-propre-action","title":"Notre propre action","text":"
def reverse_geom(layer: QgsVectorLayer, ids: int):\n    \"\"\" Inverser le sens des diff\u00e9rentes entit\u00e9s dans la couche layer.\n\n    ids est l'identifiant d'une entit\u00e9 qu'il faut inverser.\n    \"\"\"\n    pass\n

Le mot-cl\u00e9 pass est juste une instruction Python qui ne fait strictement rien, mais qui permet de rendre une ligne de code valide en respectant l'indentation. Vous pouvez la supprimer d\u00e8s qu'il y a du code.

Il faut :

On peut appeler notre nouvelle fonction \u00e0 l'aide du code suivant :

layer = iface.activeLayer()\n# Une action ne s'effectuant que sur une seule entit\u00e9, on peut utiliser [0]\nids = layer.selectedFeatureIds()[0]\n\nreverse_geom(layer, ids)\n
Afficher la solution
def reverse_geom(layer, ids):\n    \"\"\" Inverser le sens d'une entit\u00e9 dans la couche layer.\n\n    ids est une liste comportant les IDs des entit\u00e9s \u00e0 inverser.\n    \"\"\"\n    feature = layer.getFeature(ids)\n    geom = feature.geometry()\n    lines = geom.asMultiPolyline()\n    for line in lines:\n        line.reverse()\n    new_geom = QgsGeometry.fromMultiPolylineXY(lines)\n    with edit(layer):\n        layer.changeGeometry(feature.id(), new_geom)\n\nlayer = iface.activeLayer()\nids = layer.selectedFeatureIds()[0]\n\nreverse_geom(layer, ids)\n

Incorporons ce code dans une action et adaptons-le l\u00e9g\u00e8rement :

def reverse_geom(layer, ids):\n    \"\"\" Inverser le sens d'une entit\u00e9 dans la couche layer.\n\n    ids est une liste comportant les IDs des entit\u00e9s \u00e0 inverser.\n    \"\"\"\n    feature = layer.getFeature(ids)\n    geom = feature.geometry()\n    lines = geom.asMultiPolyline()\n    for line in lines:\n        line.reverse()\n    new_geom = QgsGeometry.fromMultiPolylineXY(lines)\n    with edit(layer):\n        layer.changeGeometry(feature.id(), new_geom)\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nreverse_geom(layer, [% @id %])\n

On peut d\u00e9sormais cliquer sur une ligne pour automatiquement inverser une ligne.

Le code de l'action est enregistr\u00e9 dans le style QML de la couche vecteur. Il peut donc \u00eatre partag\u00e9 avec d'autres utilisateurs qui ne connaissent pas Python.

"},{"location":"action/#informer-lutilisateur","title":"Informer l'utilisateur","text":"

Si on souhaite informer l'utilisateur que cela s'est bien pass\u00e9, on peut utiliser la \"message bar\" :

from qgis.utils import iface\n\niface.messageBar().pushMessage('Inversion', 'La rivi\u00e8re est invers\u00e9e', Qgis.Success)\n

Note, contrairement \u00e0 la console o\u00f9 QGIS importait pour nous directement la variable iface, dans ce contexte, il faut le faire manuellement.

"},{"location":"action/#astuce-pour-stocker-le-code-dune-action-dans-une-extension-qgis","title":"Astuce pour stocker le code d'une action dans une extension QGIS","text":"

Tip

Pour suivre cette partie, il faut la plupart du temps une extension par exemple, voir l'autre chapitre, afin de stocker le code Python.

Pour \u00e9viter d'avoir du code les propri\u00e9t\u00e9s de la couche QGIS, on peut r\u00e9duire le code Python au minimum en faisant dans le c\u0153ur de l'action uniquement l'import d'une fonction et de lancer son ex\u00e9cution.

Exemple du code d'une action dans l'extension QuickOSM lors de l'ex\u00e9cution d'une requ\u00eate rapide :

from QuickOSM.core.actions import Actions\nActions.run(\"josm\",\"[% \"full_id\" %]\")\n

Ou alors l'extension RAEPA :

from qgis.utils import plugins\nplugins['raepa'].run_action(\"nom_de_laction\", params)\n
"},{"location":"action/#avec-processing","title":"Avec Processing","text":"

Dans le chapitre Processing, nous verrons comment int\u00e9grer un algorithme Processing dans une action.

"},{"location":"console/","title":"Introduction \u00e0 la console Python","text":""},{"location":"console/#donnees","title":"Donn\u00e9es","text":"

Nous allons utiliser un d\u00e9partement de la BDTopo.

Tip

Les DROM-COM ou le Territoire de Belfort (90) sont assez l\u00e9gers.

  1. Renommer le dossier BDT_3-3_SHP_LAMB93_D0ZZ-EDYYYY-MM-DD en BD_TOPO afin de simplifier les corrections.
"},{"location":"console/#configurer-le-projet","title":"Configurer le projet","text":"
.\n\u251c\u2500\u2500 BD_TOPO\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 ADMINISTRATIF\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 ADRESSES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 BATI\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 HYDROGRAPHIE\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 LIEUX_NOMMES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 OCCUPATION_DU_SOL\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 SERVICES_ET_ACTIVITES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 TRANSPORT\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 ZONES_REGLEMENTEES\n\u2514\u2500\u2500 formation.qgs\n
"},{"location":"console/#manipulation-dans-la-console","title":"Manipulation dans la console","text":""},{"location":"console/#rappel-sur-la-poo","title":"Rappel sur la POO","text":"

La Programmation Orient\u00e9e Objet, POO :

Imaginons le cas d'une voiture.

Une voiture est un objet, on peut en cr\u00e9er une instance. Sur cette instance, on a des \"propri\u00e9t\u00e9s\" comme :

Sur cette instance, on a des \"m\u00e9thodes\" :

classDiagram class Voiture{ +Color Couleur +Int NbPuissance +Int NbPortes +String Marque +Personne Proprietaire +avancer() bool +reculer(amount) int +tourner(direction) bool }

On peut continuer en \u00e9crivant une classe qui va contenir une Personne :

classDiagram class Personne{ +String Nom +String Prenom +Date DateNaissance +Date DatePermisB }"},{"location":"console/#pratique","title":"Pratique","text":""},{"location":"console/#documentation","title":"Documentation","text":"

Warning

Il est important de bien pouvoir lire la signature des m\u00e9thodes :

Afficher la solution
color = QColor(\"#00A2FF\")\nQgsProject.instance().setBackgroundColor(color)\n
"},{"location":"console/#manipulation-en-console-pour-ajouter-une-couche-shapefile","title":"Manipulation en console pour ajouter une couche Shapefile","text":"

Note

L'utilisation de l'objet Path est tr\u00e8s habituelle, mais on rencontre aussi l'utilisation des fonctions du module os.path comme os.path.join... On rencontre son utilisation dans plusieurs tutoriels/solutions/forums.

"},{"location":"console/#solution-etape-par-etape","title":"Solution \u00e9tape par \u00e9tape","text":"

Pour r\u00e9cup\u00e9rer le projet en cours

project = QgsProject.instance()\n

Pour passer du chemin str de QGIS \u00e0 un objet Path et directement appeler la propri\u00e9t\u00e9 parent pour obtenir le dossier :

racine = Path(project.absoluteFilePath()).parent\n

On peut joindre notre BDTopo, en donnant plusieurs param\u00e8tres \u00e0 joinpath :

chemin = racine.joinpath('BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = chemin.joinpath('COMMUNE.shp')\n

Il ne faut pas h\u00e9siter \u00e0 v\u00e9rifier au fur et \u00e0 mesure avec des print :

print(racine)\nprint(racine.is_dir())\nprint(racine.is_file())\nprint(fichier_shape.exists())\nprint(fichier_shape.is_file())\n

Tip

Tant que l'on est en console, on n'a pas besoin de faire print, la console le fait pour nous automatiquement. On peut se contenter de fichier_shape.exists().

Si tout est bon pour le chemin, charger la couche vecteur \u00e0 l'aide de iface qui est une instance de QgisInterface CPP/PyQGIS (et non pas QgsInterface), en utilisant la m\u00e9thode addVectorLayer.

Attention, QGIS, \u00e9tant \u00e9crit en C++, ne connait pas l'usage de Path, il faut repasser par une cha\u00eene de caract\u00e8re avec l'aide de str :

communes = iface.addVectorLayer(str(fichier_shape), 'communes', 'ogr')\nprint(communes)\n

Charger la couche autrement \u00e0 l'aide du constructeur QgsVectorLayer (conseill\u00e9)

communes = QgsVectorLayer(str(fichier_shape), 'communes', 'ogr')\n# communes.isValid()\nQgsProject.instance().addMapLayer(communes)\n
Afficher la solution compl\u00e8te avec pathlib
from pathlib import Path\nproject = QgsProject.instance()\nracine = Path(project.absoluteFilePath()).parent\nchemin = racine.joinpath('BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = chemin.joinpath('COMMUNE.shp')\n# fichier_shape.is_file()\ncommunes = QgsVectorLayer(str(fichier_shape), 'communes', 'ogr')\n# communes.isValid()\nQgsProject.instance().addMapLayer(communes)\n
Afficher \"l'ancienne\" solution compl\u00e8te avec os.path
from os.path import join, isfile, isdir\n\nproject = QgsProject.instance()\n\nracine = project.homePath()\nchemin = join(racine, 'BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = join(chemin, 'COMMUNE.shp')\ncommunes = QgsVectorLayer(fichier_shape, 'communes', 'ogr')\ncommunes.isValid()\nQgsProject.instance().addMapLayer(communes)\n

Success

Bien jou\u00e9 si vous avez votre couche des communes !

"},{"location":"console/#decouverte-des-methodes-sur-notre-objet-communes","title":"D\u00e9couverte des m\u00e9thodes sur notre objet communes","text":"

Notre variable communes est une instance de QgsVectorLayer.

API QgsVectorLayer C++, API QgsVectorLayer Python

\u00c0 l'aide de la documentation, recherchons :

Info

L'API est en train de changer depuis QGIS 3.30 environ, concernant l'usage et l'affichage des \u00e9num\u00e9rations. Par exemple pour geometryType.

communes.geometryType() == QgsWkbTypes.PolygonGeometry\ncommunes.geometryType() == QgsWkbTypes.PointGeometry\n

Pour la g\u00e9om\u00e9trie, toujours utiliser l'\u00e9num\u00e9ration et non pas le chiffre, ce n'est pas compr\u00e9hensible (QGIS < 3.30)

On ne les trouve pas dans la page QgsVectorLayer ! Pour cela, il faut faire r\u00e9f\u00e9rence \u00e0 la notion d'h\u00e9ritage en Programmation Orient\u00e9e Objet.

"},{"location":"console/#heritage","title":"H\u00e9ritage","text":"

Il faut bien regarder les diagrammes en haut de la documentation :

API QgsVectorLayer C++, API QgsVectorLayer Python

classDiagram class QgsMapLayer{ +name() str +id() str +crs() QgsCoordinateReferenceSystem +maximumScale() double +minimumScale() double +autreFonctionsPourToutesLesCouches() } class QgsVectorLayer{ +featureCount() int +startEditing() bool +commitChanges() bool +autreFonctionsPourUneCoucheVecteur() } class QgsRasterLayer{ +bandCount() int +largeur int +hauteur int +autreFonctionsPourUneCoucheRaster() } QgsMapLayer <-- QgsVectorLayer QgsMapLayer <-- QgsRasterLayer

L'objet QgsVectorLayer h\u00e9rite de QgsMapLayer qui est une classe commune avec QgsRasterLayer.

API QgsMapLayer C++, API QgsMapLayer Python

Tip

On peut d\u00e9sormais regarder la documentation CPP de QGIS et Qt pour voir l'ensemble des membres, y compris les membres h\u00e9rit\u00e9s. QgsVectorLayer CPP ou QComboBox

Regardons la fonction isinstance qui permet de tester si un objet est une instance d'une classe :

isinstance(communes, QgsVectorLayer)\nTrue\nisinstance(communes, QgsRasterLayer)\nFalse\nisinstance(communes, QgsMapLayer)\nTrue\n
communes.setMinimumScale(2000000)\ncommunes.setMaximumScale(500000)\ncommunes.setScaleBasedVisibility(True)\n# communes.triggerRepaint()\n

Important

Un raccourci \u00e0 savoir, dans la console :

iface.activeLayer()\n

Cela retourne la couche QgsMapLayer active dans la l\u00e9gende !

"},{"location":"console/#code","title":"Code","text":"

Petit r\u00e9capitulatif \u00e0 tester pour voir si cela fonctionne correctement !

from pathlib import Path\ndossier = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\nproject = QgsProject.instance()\nracine = Path(project.absoluteFilePath()).parent\nchemin = racine.joinpath(dossier, thematique)\nfichier_shape = chemin.joinpath(f'{couche}.shp')\n\nprint(layer.featureCount())\nprint(layer.crs().authid())\nprint('Est en m\u00e8tre : {}'.format(layer.crs().mapUnits() ==  QgsUnitTypes.DistanceMeters))\nprint(layer.name())\nlayer.setScaleBasedVisibility(True)\nlayer.setMaximumScale(500000)\nlayer.setMinimumScale(2000000)\nlayer.triggerRepaint()\n
"},{"location":"console/#transition-vers-le-script-avec-le-parcourir-des-entites","title":"Transition vers le script, avec le parcourir des entit\u00e9s","text":"

Ajouter \u00e9galement la couche ARRONDISSEMENT et s\u00e9lectionner l\u00e0.

On souhaite d\u00e9sormais it\u00e9rer sur les polygones et les faire clignoter depuis la console. Nous allons donc avoir besoin de la m\u00e9thode getFeatures() qui fait partie de QgsVectorLayer.

layer = iface.activeLayer()\nfeatures = layer.getFeatures()\nfeatures\nfeature = QgsFeature()\nfeatures.nextFeature(feature)\niface.mapCanvas().flashFeatureIds(layer, [feature.id()])\n

Note, nous pouvons concat\u00e9ner les deux derni\u00e8res lignes \u00e0 l'aide du caract\u00e8re ; pour que cela soit plus pratique.

Ce code est plus pour la partie \"amusante\" pour montrer les limites de la console. Nous allons d\u00e9sormais utiliser un script Python dans le prochain chapitre.

Petite chose suppl\u00e9mentaire avant de passer aux scripts, on souhaite d\u00e9sormais afficher le nom des arrondissements \u00e0 l'aide d'une boucle for.

layer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    # On peut traiter l'entit\u00e9 courante gr\u00e2ce \u00e0 la variable \"feature\".\n    # Pour acc\u00e9der \u00e0 un attribut en particulier, on peut y acc\u00e9der avec des crochets.\n    pass\n

Noter l'apparition de ... au lieu de >>> apr\u00e8s avoir \u00e9crit la premi\u00e8re ligne du for. Il faut faire une indentation obligatoire !

Pour afficher un attribut, on peut faire print(feature['NOM_ARR']) pour afficher le contenu de l'attribut NOM_ARR.

"},{"location":"documentation/","title":"Documentation et liens utiles","text":"

Tip

QGIS est en train de migrer vers la librairie Qt version 6. QGIS 3.42 va certainement avoir un support pour Qt6 et pouvoir faire des premiers tests PyQGIS. Lire le chapitre sur les migrations majeures de PyQGIS.

Voici une liste de liens pour la documentation, tous en anglais, sauf le cookbook :

Voici une liste non exhaustive de blog-post utiles pour manipuler PyQGIS, tous en anglais :

Autre lien pour l'apprentissage de Python (sans QGIS) en fran\u00e7ais :

Tip

QGIS 3.42 va int\u00e9grer un outil pour avoir l'aide d'une classe directement depuis une variable. Voir la d\u00e9mo de QGIS 3.42.

"},{"location":"ecriture-classe-poo/","title":"\u00c9criture de notre classe en POO","text":""},{"location":"ecriture-classe-poo/#notion-sur-la-poo-en-python","title":"Notion sur la POO en Python","text":"

Le framework Processing utilise le concept de la Programmation Orient\u00e9e Objet. Il existe un tutoriel sur le site d'OpenClassRooms sur le sujet.

Mais depuis le d\u00e9but de la formation, nous l'utilisons sans trop le savoir. Les objets Qgs*, comme QgsVectorLayer utilisent le principe de la POO.

On a pu cr\u00e9er des objets QgsVectorLayer en appelant son constructeur :

from qgis.core import QgsVectorLayer\n\nlayer = QgsVectorLayer(\"C:/chemin/vers/un/fichier.gpkg|layername=communes\", \"communes\", \"ogr\")\n

et ensuite, on a pu appeler des m\u00e9thodes sur cet objet, comme :

layer.setName(\"Communes\")\nlayer.name()  # Retourne \"Communes\"\n

Tip

Vous pouvez relire le passage sur la POO en d\u00e9but de formation.

"},{"location":"ecriture-classe-poo/#exemple","title":"Exemple","text":"

Nous allons faire un \"tr\u00e8s\" petit exemple rapide. \u00c9crivons notre premier jeu vid\u00e9o en console ! \ud83c\udfae

from time import sleep\n\nMAX_ENERGIE = 20\n\n\nclass Personnage:\n\n    \"\"\" Classe repr\u00e9sentant un personnage du jeu vid\u00e9o. \"\"\"\n\n    def __init__(self, un_nom, energie=MAX_ENERGIE):\n        \"\"\" Constructeur. \"\"\"\n        self.nom = un_nom\n        self.energie = energie\n\n    def marcher(self):\n        \"\"\" Permet au personnage de marcher.\n\n        Cela d\u00e9pense de l'\u00e9nergie.\n        \"\"\"\n        cout = 5\n        if self.energie >= cout:\n            print(f\"{self.nom} marche.\")\n            self.energie -= cout\n        else:\n            print(f\"{self.nom} ne peut pas marcher car il n'a pas assez d'\u00e9nergie.\")\n\n    def courir(self):\n        \"\"\" Permet au personnage de courir.\n\n        Cela d\u00e9pense de l'\u00e9nergie.\n        \"\"\"\n        cout = 10\n        if self.energie >= cout:\n            print(f\"{self.nom} court.\")\n            self.energie -= cout\n        else:\n            print(f\"{self.nom} ne peut pas courir car il n\\'a pas assez d\\'\u00e9nergie.\")\n\n    def dormir(self):\n        \"\"\" Permet au personnage de dormir et restaurer le niveau maximum d'\u00e9nergie.\"\"\"\n        print(f\"{self.nom} dort et fait le plein d'\u00e9nergie.\")\n        for i in range(2):\n            print('...')\n            sleep(1)\n        self.energie = MAX_ENERGIE\n\n    def manger(self):\n        \"\"\" Permet au personnage de manger et d'augmenter de 10 points le niveau d'\u00e9nergie.\"\"\"\n        energie = 10\n        print(f\"{self.nom} mange et r\u00e9cup\u00e8re {energie} points d'\u00e9nergie.\")\n        if self.energie <= MAX_ENERGIE - energie:\n            self.energie += energie\n        else:\n            self.energie = MAX_ENERGIE\n\n    def __repr__(self):\n        return f\"<Personnage: '{self.nom}' avec {self.energie} points d'\u00e9nergie>\"\n
"},{"location":"ecriture-classe-poo/#utilisation-de-notre-classe","title":"Utilisation de notre classe","text":"
a = Personnage('Dark Vador')\ndir(a)\nhelp(a)\n

Que remarquons-nous ?

Solution
a = Personnage('Dark Vador')\na.courir()\na.dormir()\na.manger()\nprint(a)\n

Afficher le nom du personnage (et juste son nom, pas la phrase de pr\u00e9sentation)

"},{"location":"ecriture-classe-poo/#ajouter-dautres-methodes","title":"Ajouter d'autres m\u00e9thodes","text":"

Ajoutons une m\u00e9thode dialoguer pour discuter avec un autre personnage.

Tip

def dialoguer(self, autre_personnage):\n    \"\"\" Permet de dialoguer avec un autre personnage. \"\"\"\n    pass\n
  1. \u00c9crire le code la fonction \u00e0 l'aide d'un print pour commencer disant que X dialogue avec Y.
  2. V\u00e9rifier le niveau d'\u00e9nergie avant de dialoguer ! Difficile de discuter si on n'a plus d'\u00e9nergie \ud83d\ude09
  3. Garder son code \u00e0 gauche, on peut utiliser une instruction return

Nous pouvons d\u00e9sormais utiliser le constructeur afin de cr\u00e9er deux instances de notre classe.

b = Personnage('Luke')\nb.dialoguer(a)\n
Solution pour la m\u00e9thode dialoguer()
def dialoguer(self, autre_personnage):\n    if self.energie <= 0:\n        print(f\"{self.nom} ne peut pas dialoguer car il n'a pas assez d'\u00e9nergie.\")\n        return\n\n    print(f\"{self.nom} dialogue avec {autre_personnage.nom} et ils \u00e9changent des informations secr\u00e8tes\")\n

Continuons notre classe pour la gestion de son inventaire. Admettons que notre personnage puisse ramasser des objets afin de les mettre dans son sac \u00e0 dos.

  1. Il va falloir ajouter une nouvelle propri\u00e9t\u00e9 \u00e0 notre classe de type list que l'on peut nommer inventaire. Par d\u00e9faut, son inventaire sera vide.
  2. Ajoutons 3 m\u00e9thodes : ramasser, deposer et utiliser. Pour le moment, pour faciliter l'exercice, utilisons une cha\u00eene de caract\u00e8re pour d\u00e9signer l'objet. Ces m\u00e9thodes vont interagir avec notre inventaire \u00e0 l'aide des m\u00e9thodes remove(), append() que l'on trouve sur une liste.
  3. Pour les m\u00e9thodes deposer et utiliser, nous pouvons avoir \u00e0 cr\u00e9er une autre m\u00e9thode priv\u00e9e afin de v\u00e9rifier l'existence de l'objet dans l'inventaire. Par convention, nous pr\u00e9fixons la m\u00e9thode par _ comme _est_dans_inventaire afin de signaler que c'est une m\u00e9thode dite priv\u00e9e. L'utilisation de cette m\u00e9thode priv\u00e9e est uniquement \u00e0 titre p\u00e9dagogique, on peut vouloir exposer la m\u00e9thode est_dans_inventaire. Cette m\u00e9thode doit renvoyer un bool\u00e9en.
  4. Ajoutons des commentaires et/ou des docstrings, CF m\u00e9mo Python. On peut utiliser la m\u00e9thode help.
  5. Pensons aussi annotations Python

  6. Refaire la commande help(a) pour voir le r\u00e9sultat final \ud83d\ude09

Info

Il est important de comprendre que la POO permet de construire une sorte de bo\u00eete opaque du point de vue de l'utilisateur de la classe. Un peu comme une voiture, elles ont toutes un capot et une p\u00e9dale d'acc\u00e9l\u00e9ration. L'appui sur l'acc\u00e9l\u00e9rateur d\u00e9clenche plusieurs m\u00e9canismes \u00e0 l'int\u00e9rieur de la voiture, mais du point de vue utilisateur, c'est plut\u00f4t simple.

Tip

On peut vite imaginer d'autres classes, comme Arme, car ramasser un bout de bois ou un sabre laser n'a pas le m\u00eame impact lors de son utilisation dans un combat. Le d\u00e9g\u00e2t qu'inflige une arme sur le niveau d'\u00e9nergie de l'autre personnage est une propri\u00e9t\u00e9 de l'arme en question et du niveau du personnage.

"},{"location":"ecriture-classe-poo/#des-idees-pour-continuer-plus-loin","title":"Des id\u00e9es pour continuer plus loin","text":"

Des jeux en Python dans QGIS :

Ou pour le fun avec des expressions :

"},{"location":"ecriture-classe-poo/#solution","title":"Solution","text":"

Sur la classe Personnage ci-dessus :

def _est_dans_inventaire(self, un_objet: str) -> bool:\n    \"\"\" Fonction \"interne\" pour tester si un objet est dans l'inventaire. \"\"\"\n    # On ne souhaite pas qu'un autre personnage puis v\u00e9rifier le contenu d'un inventaire d'un autre.\n    # Il faut plut\u00f4t lui demander :)\n    # Note cette m\u00e9thode n'est pas dans le help()\n    return un_objet in self.inventaire\n\ndef ramasser(self, un_objet: str) -> bool:\n    \"\"\" Ramasser un objet et le mettre dans l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    print(f\"{self.nom} ramasse {un_objet} et le met dans son inventaire.\")\n    self.inventaire.append(un_objet)\n    return True\n\ndef utiliser(self, un_objet: str) -> bool:\n    \"\"\" Utiliser un objet s'il est disponible avant dans l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if self._est_dans_inventaire(un_objet):\n        print(f\"{self.nom} utilise {un_objet}\")\n        return True\n\n    print(f\"{self.nom} ne poss\u00e8de pas {un_objet}\")\n    return False\n\ndef deposer(self, un_objet: str) -> bool:\n    \"\"\" Retirer un objet de l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if not self._est_dans_inventaire(un_objet):\n        return False\n\n    print(f\"{self.nom} d\u00e9pose {un_objet}\")\n    self.inventaire.remove(un_objet)\n    return True\n\ndef donner(self, autre_personnage, un_objet: str) -> bool:\n    \"\"\" Donner un objet \u00e0 un autre personnage.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if not self._est_dans_inventaire(un_objet):\n        return False\n    self.inventaire.remove(un_objet)\n    autre_personnage.inventaire.append(un_objet)\n    print(f\"{autre_personnage.nom} re\u00e7oit {un_objet} de la part de {self.nom} et le remercie \ud83d\udc4d\")\n    return True\n
"},{"location":"expression/","title":"Expression","text":"

On peut d\u00e9finir sa propre expression QGIS \u00e0 l'aide de Python. Il existe un chapitre dans le Python cookbook

Dans la fen\u00eatre des expressions QGIS, on peut observer la fonction d\u00e9j\u00e0 existante.

feature, parent et context sont des param\u00e8tres particuliers dans la signature de la fonction. Si QGIS trouve le mot-cl\u00e9, il assigne l'objet correspondant :

"},{"location":"expression/#exemple","title":"Exemple","text":"

On souhaite utiliser l'API de Wikip\u00e9dia afin de r\u00e9cup\u00e9rer la description d'un terme.

Par exemple, si on cherche le terme Montpellier avec l'API Wikip\u00e9dia :

https://fr.wikipedia.org/w/api.php?action=query&titles=Montpellier&prop=description&format=json

Il existe plusieurs moyens de faire des requ\u00eates HTTP en Python et/ou PyQGIS. Utilisons la technique Processing avec l'algorithme \"T\u00e9l\u00e9chargeur de fichier\" (graphiquement, il n'est disponible que dans le modeleur) :

search = \"montpellier\"\nresults = processing.run(\n    \"native:filedownloader\",\n    {\n        \"URL\": f\"https://fr.wikipedia.org/w/api.php?action=query&titles={search}&prop=description&format=json\",\n        \"OUTPUT\": \"TEMPORARY_OUTPUT\"\n    }\n)\n

Tip

On peut afficher le panneau de d\u00e9bogage et d\u00e9veloppement de QGIS afin de voir les requ\u00eates HTTP. Il se trouve dans le menu Vue \u25b6 Panneau \u25b6 D\u00e9bogage et d\u00e9veloppement

On va d\u00e9sormais parser le fichier JSON que l'on obtient avec la libraire json afin de r\u00e9cup\u00e9rer la description.

"},{"location":"expression/#memo","title":"M\u00e9mo","text":"

Pour lire un fichier \u00e0 l'aide d'un \"contexte Python\" qui va ouvrir et fermer le fichier :

import json\n\nwith open(\"/mon/fichier.json\") as f:\n    data = json.load(f)\nprint(data)\n

Une m\u00e9thode pour r\u00e9cup\u00e9rer la bonne cl\u00e9, dynamiquement :

pages = data['query']['pages']\nkey = list(pages.keys())[0]\ndescription = pages[key]['description']\nprint(description)\n

Peut-\u00eatre surement plus simple \u00e0 comprendre, avec l'usage d'une boucle for

description = \"\"\nfor page in pages.values():\n    description = page.get('description')\n\nprint(description)\n
Une solution compl\u00e8te pour l'expression QGIS
import json\nimport processing\n\n@qgsfunction(args='auto', group='Formation PyQGIS')\ndef wiki_description(search, feature, parent):\n    \"\"\"Permet de r\u00e9cup\u00e9rer la description Wikipedia\n\n    wiki_description('Paris') \u27a1 'capitale de la France'\n    \"\"\"\n    results = processing.run(\n        \"native:filedownloader\",\n        {\n            \"URL\": f\"https://fr.wikipedia.org/w/api.php?action=query&titles={search}&prop=description&format=json\",\n            \"OUTPUT\": \"TEMPORARY_OUTPUT\"\n        }\n    )\n\n    with open(results['OUTPUT']) as f:\n        data = json.load(f)\n\n    pages = data['query']['pages']\n    # Only the first page will be used\n    for page_id, page in pages.items():\n        description = page.get('description')\n        if page_id == \"-1\":\n            error = page.get('invalidReason')\n            if description:\n                return f'Pas de page, {description}'\n            if not error:\n                return 'Pas de page'\n            else:\n                return f\"Pas de page, erreur {error}\"\n        return description\n
"},{"location":"expression/#fournir-une-expression-depuis-une-extension","title":"Fournir une expression depuis une extension","text":"

Pour le moment, cette expression est dans le dossier de l'utilisateur, dans python \u2192 expressions.

Mais une fois que nous avons une extension g\u00e9n\u00e9rique, nous pouvons l'int\u00e9grer dans un fichier Python de l'extension.

Exemple sur StackExchange

"},{"location":"extension-deploiement/","title":"Comment d\u00e9ployer son extension","text":"

Comme vu dans le chapitre concernant la cr\u00e9ation d'une extension g\u00e9n\u00e9rique, une extension QGIS est un dossier comportant :

Ce dossier doit \u00eatre zipp\u00e9.

Pour du d\u00e9ploiement, nous recommandons l'usage de QGIS-Plugin-CI qui peut faire du packaging, la g\u00e9n\u00e9ration du plugins.xml, envoyer sur plugins.qgis.org etc.

"},{"location":"extension-deploiement/#en-interne","title":"En interne","text":"

Si on souhaite publier en interne, on peut d\u00e9poser son dossier zip sur un serveur et on recommande l'utilisation du fichier plugins.xml qui permet de renseigner \u00e0 QGIS la disponibilit\u00e9 d'une extension.

Exemple avec l'installation de PgMetadata et son fichier plugins.xml

Il est possible de prot\u00e9ger son d\u00e9p\u00f4t avec un login/mot de passe.

"},{"location":"extension-deploiement/#tutoriel-pour-installer-un-depot","title":"Tutoriel pour installer un d\u00e9p\u00f4t","text":"

Notre tutoriel pour l'installation d'un d\u00e9p\u00f4t, avec ou sans mot de passe.

"},{"location":"extension-deploiement/#pluginsqgisorg","title":"plugins.qgis.org","text":"

Plus simple pour le d\u00e9ploiement, car le d\u00e9p\u00f4t plugins.qgis.org est par d\u00e9faut dans les installations de QGIS. Il faut cependant que le code source soit disponible sur internet.

Lire les recommandations pour la publication sur ce d\u00e9p\u00f4t :

"},{"location":"extension-generique/","title":"La base pour cr\u00e9er une extension","text":""},{"location":"extension-generique/#une-extension-nest-quun-zip-pour-fournir-du-code-python","title":"Une extension n'est qu'un ZIP pour fournir du code Python","text":"

Comme vu dans Le Python dans QGIS, une extension peut \u00eatre sous diff\u00e9rente forme :

"},{"location":"extension-generique/#modele-de-base","title":"Mod\u00e8le de base","text":"

Pour cr\u00e9er une extension dans QGIS, il existe deux fa\u00e7ons de d\u00e9marrer :

  1. T\u00e9l\u00e9charger le ZIP
  2. Installer le depuis le gestionnaire des extensions, \u00e0 l'aide du ZIP

Tip

Pour trouver le profil courant, dans QGIS, Pr\u00e9f\u00e9rences -> Profils Utilisateurs -> Ouvrir le dossier du profil actif.

"},{"location":"extension-generique/#le-fichier-metadatatxt","title":"Le fichier metadata.txt","text":"

Liste des valeurs possibles dans un fichier metadata.txt

"},{"location":"extension-generique/#exemple-dune-extension-processing-et-graphique","title":"Exemple d'une extension Processing et graphique","text":"

Exemple d'utilisation d'un panneau qui pr\u00e9sentent les algorithmes \"Processing\" :

"},{"location":"extension-generique/#extensions-utiles","title":"Extensions utiles","text":""},{"location":"extension-generique/#plugin-reloader","title":"Plugin reloader","text":"

Indispensable

Le \"Plugin Reloader\" est une extension indispensable pour d\u00e9velopper une extension pour recharger son extension. Elle est disponible dans le gestionnaire des extensions.

"},{"location":"extension-generique/#pyqgis-resource-browser","title":"PyQGIS Resource Browser","text":"

Utile pour l'ergonomie

Permet d'aller chercher des ic\u00f4nes d\u00e9j\u00e0 existantes dans la libraire QGIS et Qt

"},{"location":"extension-generique/#first-aid","title":"First aid","text":"

Utile pour aller plus loin

Extension pour d\u00e9bugger en cas d'une erreur Python

"},{"location":"extension-generique/#apprendre-dune-autre-extension","title":"Apprendre d'une autre extension","text":"

Comme les extensions sur qgis.org sont disponibles sur internet, on peut regarder le code source pour comprendre.

Pensez \u00e0 ouvrir le dossier de votre profil QGIS en suivant l'astuce ci-dessus puis dans python/plugins.

"},{"location":"extension-graphique/","title":"Cr\u00e9er une extension QGIS avec une interface graphique","text":"

Pour faire ce chapitre, il faut d'abord avoir une extension de base, \u00e0 l'aide du chapitre pr\u00e9c\u00e9dent.

"},{"location":"extension-graphique/#qtdesigner","title":"QtDesigner","text":""},{"location":"extension-graphique/#premier-dialogue","title":"Premier dialogue","text":"

Cr\u00e9ons un fichier QtDesigner comme-ceci :

"},{"location":"extension-graphique/#decouverte-de-linterface-qtdesigner","title":"D\u00e9couverte de l'interface QtDesigner","text":"

Tip

Privil\u00e9gier aussi les \"Item Widgets (Item Based)\" plut\u00f4t que les \"Item Views (Model Based) pour d\u00e9buter.

"},{"location":"extension-graphique/#ajoutons-les-widgets","title":"Ajoutons les \"widgets\"","text":"

Important

Ne tenez pas compte de l'alignement des widgets pour le moment. On fait juste un placement \"rapide\" vertical des widgets.

Dans l'ordre vertical, ce sont ces classes :

Classe QLabel QLineEdit QgsMapLayerComboBox QPlainTextEdit Vertical spacer QDialogButtonBox

Une fois que l'ensemble des \"widgets\" sont pr\u00e9sents, on peut faire un clic droit \u00e0 droite sur notre QDialog, puis Mise en page et enfin Verticalement \ud83d\ude80

"},{"location":"extension-graphique/#ajout-dun-bouton-dans-notre-buttonbox-en-bas","title":"Ajout d'un bouton dans notre \"ButtonBox\" en bas","text":"

Ajoutons le bouton d'aide, dans les propri\u00e9t\u00e9s de notre widget QDialogButtonBox.

"},{"location":"extension-graphique/#nommage-de-nos-widgets","title":"Nommage de nos \"widgets\"","text":"

Pour chacun de nos widgets, changeons le nom par d\u00e9faut de l'objet, propri\u00e9t\u00e9 objectName tout en haut :

Classe Nom par d\u00e9faut de objectName Nouveau nom pour objectName QLineEdit lineEdit input_text QgsMapLayerComboBox mMapLayerComboBox couche QPlainTextEdit plainTextEdit metadata QDialogButtonBox buttonBox button_box

Cette propri\u00e9t\u00e9 objectName est tr\u00e8s importante, car elle d\u00e9termine l'appellation de notre propri\u00e9t\u00e9 dans l'objet self pour la suite du TP.

"},{"location":"extension-graphique/#astuces","title":"Astuces","text":"

Pourquoi supprimer les signaux de QtDesigner ?

Un fichier QtDesigner est \"gros\" fichier XML. Il est difficile, dans le temps, de suivre ces modifications, changement\u2026

Il est, \u00e0 mon avis, plus simple de garder le fichier XML le plus l\u00e9ger possible, et de garder la logique dans le code Python. Un signal en XML, c'est plusieurs lignes dans le fichier UI, alors que en Python, c'est une seule ligne.

On peut t\u00e9l\u00e9charger la solution si besoin.

"},{"location":"extension-graphique/#la-classe-qui-accompagne","title":"La classe qui accompagne","text":"

Cr\u00e9ons un fichier dialog.py avec le contenu suivant :

# Les imports\nfrom qgis.core import Qgis\nfrom qgis.utils import iface\nfrom qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox\nfrom qgis.PyQt import uic\nfrom pathlib import Path\n\n# Permettre d'aller chercher le fichier UI correspondant\nfolder = Path(__file__).resolve().parent\nui_file = folder.joinpath('dialog.ui')\nui_class, _ = uic.loadUiType(ui_file)\n\n\nclass MonDialog(ui_class, QDialog):\n\n    \"\"\" Classe qui repr\u00e9sente le dialogue de l'extension. \"\"\"\n\n    def __init__(self, parent: QDialog):\n        \"\"\" Constructeur. \"\"\"\n        super().__init__(parent)  # Appel du constructeur parent\n        self.parent = parent  # Stockage du parent dans self, on va l'utiliser plus tard, si besoin\n        self.setupUi(self)  # Fichier de QtDesigner\n\n        # Quelques propri\u00e9t\u00e9s facultatives\n        self.setWindowTitle(\"Notre super machine \u00e0 caf\u00e9\")\n        # self.setModal(False)  # Utile plus bas si on souhaite ouvrir une autre fen\u00eatre par-dessus\n

Modifions la m\u00e9thode run du fichier __init__.py en

    def run(self):\n        \"\"\" Lors du clic sur le bouton pour lancer la fen\u00eatre de l'extension. \"\"\"\n        from .dialog import MonDialog\n        dialog = MonDialog(self.iface.mainWindow())\n        dialog.show()\n

Relan\u00e7ons l'extension \u00e0 l'aide du \"plugin reloader\" et cliquons sur le bouton.

"},{"location":"extension-graphique/#les-signaux-et-les-slots","title":"Les signaux et les slots","text":""},{"location":"extension-graphique/#signaux-des-boutons-de-la-fenetre","title":"Signaux des boutons de la fen\u00eatre","text":"

Tip

N'h\u00e9sitez pas \u00e0 relire le chapitre sur les signaux.

Connectons le signal clicked du bouton \"Annuler\" dans le constructeur __init__ :

self.button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close)\n

On dit que clicked est un signal, auquel on connecte le slot close.

Connectons-le signal clicked du bouton \"Accepter\" \u00e0 notre propre slot (qui est une fonction) :

self.button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.click_ok)\n

Ensuite, ajoutons notre propre fonction click_ok pour quitter la fen\u00eatre et en affichant la saisie de l'utilisateur dans la QgsMessageBar de QGIS.

Le widget de saisie est un QLineEdit : documentation Qt

def click_ok(self):\n    \"\"\" Clic sur le bouton OK afin de fermer la fen\u00eatre. \"\"\"\n    message = self.input_text.text()\n    iface.messageBar().pushMessage('Notre extension', message, Qgis.Success)\n    self.accept()\n

Faire le test dans QGIS avec une saisie de l'utilisateur et fermer la fen\u00eatre.

"},{"location":"extension-graphique/#clic-sur-le-bouton-daide","title":"Clic sur le bouton d'aide","text":"
# Dans le constructeur :\nself.button_box.button(QDialogButtonBox.StandardButton.Help).clicked.connect(self.open_help)\n\n# Puis la fonction :\ndef open_help(self):\n    \"\"\" Open the online help. \"\"\"\n    from qgis.PyQt.QtGui import QDesktopServices\n    from qgis.PyQt.QtCore import QUrl\n    QDesktopServices.openUrl(QUrl('https://www.youtube.com/watch?v=AdQ3JDLlmPI'))\n
"},{"location":"extension-graphique/#signaux-et-proprietes-du-formulaire-de-saisie","title":"Signaux et propri\u00e9t\u00e9s du formulaire de saisie","text":"

Continuons en rendant en lecture seule le gros bloc de texte et affichons \u00e0 l'int\u00e9rieur la description de la couche qui est s\u00e9lectionn\u00e9e dans le menu d\u00e9roulant.

Documentation :

Dans la fonction __init__ du fichier dialog.py :

self.metadata.setReadOnly(True)\nself.couche.layerChanged.connect(self.layer_changed)\n

Et la nouvelle fonction qui va se charger de mettre \u00e0 jour le texte :

def layer_changed(self):\n    \"\"\" Permet de mettre \u00e0 jour l'UI selon la couche dans le menu d\u00e9roulant. \"\"\"\n    self.metadata.clear()\n    layer = self.couche.currentLayer()\n    self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\n

Danger

Que remarquez-vous en terme d'ergonomie, \u00e0 l'ouverture de la fen\u00eatre ?

La solution plus compl\u00e8te
layer = self.couche.currentLayer()\nif layer:\n    self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\nelse:\n    self.metadata.appendPlainText(\"Pas de couche\")\n

On peut donc d\u00e9sormais cumuler l'ensemble des chapitres pr\u00e9c\u00e9dents pour lancer des algorithmes, manipuler les donn\u00e9es, etc.

Bonus

Le texte actuel concernant les m\u00e9tadonn\u00e9es est \"limit\u00e9\". N'h\u00e9sitez pas \u00e0 compl\u00e9ter, un peu comme lors de l'exercice avec le fichier CSV, pour rappel :

"},{"location":"extension-graphique/#solution","title":"Solution","text":"Afficher
from qgis.core import Qgis\nfrom qgis.utils import iface\nfrom qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox\nfrom qgis.PyQt import uic\nfrom pathlib import Path\n\nfolder = Path(__file__).resolve().parent\nui_file = folder.joinpath('dialog.ui')\nui_class, _ = uic.loadUiType(ui_file)\n\n\nclass MonDialog(ui_class, QDialog):\n\n    \"\"\" Classe qui repr\u00e9sente le dialogue de l'extension. \"\"\"\n\n    def __init__(self, parent=None):\n        \"\"\" Constructeur. \"\"\"\n        _ = parent\n        # TODO CORRECTION\n        super().__init__()\n        self.setupUi(self)  # Fichier de QtDesigner\n\n        # Connectons les signaux\n        self.button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.click_ok)\n        self.button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close)\n\n        self.metadata.setReadOnly(True)\n        self.couche.layerChanged.connect(self.layer_changed)\n        self.layer_changed()\n\n    def click_ok(self):\n        \"\"\" Clic sur le bouton OK afin de fermer la fen\u00eatre. \"\"\"\n        self.close()\n        message = self.input_text.text()\n        iface.messageBar().pushMessage('Notre extension', message, Qgis.Success)\n\n    def layer_changed(self):\n        \"\"\" Permet de mettre \u00e0 jour l'UI selon la couche dans le menu d\u00e9roulant. \"\"\"\n        self.metadata.clear()\n        layer = self.couche.currentLayer()\n        if layer:\n            self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\n        else:\n            self.metadata.appendPlainText(\"Pas de couche\")\n
"},{"location":"extension-graphique/#organisation-du-code","title":"Organisation du code","text":"

Il ne faut pas h\u00e9siter \u00e0 cr\u00e9er des fichiers Python afin de s\u00e9parer le code.

On peut aussi cr\u00e9er des dossiers afin d'y mettre plusieurs fichiers Python. Un dossier en Python se nomme un module. Pour faire un module compatible, pour Python, il faut toujours avoir un fichier __init__.py m\u00eame s\u2019il n'y a rien dedans.

Warning

Il ne faut vraiment pas oublier le fichier __init__.py. Cela peut emp\u00eacher Python de fonctionner correctement. Un bon IDE peut signaler ce genre d'erreur.

Dans l'exemple ci-dessus, on peut diviser le code du fichier __init__.py :

def classFactory(iface):\n    # from minimal_plugin.plugin import MinimalPlugin  # Import absolue\n    from .plugin import MinimalPlugin  # Import relatif\n    return MinimalPlugin(iface)\n

En faisant un couper/coller, enlever la classe MinimalPlugin du fichier __init__.py.

Tip

On essaie souvent d'avoir une classe par fichier en Python.

Cr\u00e9er un fichier plugin.py et ajouter le contenu en collant. Il est bien de v\u00e9rifier les imports dans les deux fichiers.

"},{"location":"extension-graphique/#un-dossier-resources","title":"Un dossier \"resources\"","text":"

On peut cr\u00e9er un fichier qgis_plugin_tools.py \u00e0 la racine de notre extension afin d'y ajouter des outils :

\"\"\"Tools to work with resources files.\"\"\"\n\nfrom pathlib import Path\n\n\ndef plugin_path(*args) -> Path:\n    \"\"\"Return the path to the plugin root folder.\"\"\"\n    path = Path(__file__).resolve().parent\n    for item in args:\n        path = path.joinpath(item)\n\n    return path\n\n\ndef resources_path(*args) -> Path:\n    \"\"\"Return the path to the plugin resources folder.\"\"\"\n    return plugin_path(\"resources\", *args)\n\n# On peut ajouter ici une m\u00e9thode qui charge un fichier UI qui se trouve dans le dossier \"UI\"\n# et retourne la classe directement.\n

On peut ensuite cr\u00e9er un dossier resources puis icons afin d'y d\u00e9placer un fichier PNG, JPG, SVG.

Warning

Attention \u00e0 la taille de vos fichiers pour une petite ic\u00f4ne \ud83d\ude09

Pour les experts, solution pour faire une fonction qui charge un fichier UI
def load_ui(*args):\n    \"\"\"Get compiled UI file.\n\n    :param args List of path elements e.g. ['img', 'logos', 'image.png']\n    \"\"\"\n    ui_class, _ = uic.loadUiType(str(resources_path(\"ui\", *args)))\n    return ui_class\n
"},{"location":"extension-graphique/#dans-une-extension-graphique-pour-les-icones","title":"Dans une extension graphique pour les ic\u00f4nes","text":"

Lien documentation QAction

# En haut du fichier, on ajoute les imports n\u00e9cessaires\nfrom qgis.PyQt.QtGui import QIcon\nfrom .qgis_plugin_tools import resources_path\n\n# Plus bas dans le code\n# Quand n\u00e9cessaire, \u00e0 remplacer la QAction existante. Il s'agit du premier param\u00e8tre avec QIcon\nself.action = QAction(\n    QIcon(str(resources_path('icons', 'icon.svg'))),\n    'Go!',\n    self.iface.mainWindow())\n

Tip

Ce qu'il faut retenir, c'est l'usage de QIcon(str(resources_path('icons', 'icon.svg'))) si l'on souhaite utiliser une ic\u00f4ne dans autre endroit de l'extension.

"},{"location":"extension-graphique/#dans-une-extension-processing","title":"Dans une extension \"Processing\"","text":"

Dans le provider et les algorithmes :

# En haut du fichier\nfrom ..qgis_plugin_tools import resources_path\n\n# Dans la classe, on ajoute/modifie la m\u00e9thode 'icon'\ndef icon(self) -> QIcon:\n    return QIcon(str(resources_path(\"icons\", \"icon.png\")))\n
"},{"location":"extension-graphique/#utilisation-dune-icone-provenant-de-qgis","title":"Utilisation d'une ic\u00f4ne provenant de QGIS","text":"

\u00c0 l'aide de l'extension \"PyQGIS Resource Browser\", rechercher une ic\u00f4ne concordant avec bouton :

On peut ensuite faire un clic-droit, puis coller son chemin.

Ensuite, quand on souhaite utiliser l'ic\u00f4ne :

# En haut\nfrom qgis.PyQt.QtGui import QIcon\n\n# Ensuite\nicon = QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\")\n\n# Par exemple sur un bouton QPushButton\nself.un_bouton.setIcon(QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\"))\n
"},{"location":"extension-graphique/#ajouter-un-bouton-pour-lancer-processing","title":"Ajouter un bouton pour lancer Processing","text":"

Nous souhaitons ajouter 2 boutons :

Classe objectName QPushButton btn_traitement_1 QPushButton btn_traitement_2

On peut faire la mise en page vertical dans le QGroupBox.

Ajoutons les ic\u00f4nes et infobulles si n\u00e9cessaires, dans le constructeur :

self.btn_traitement_1.setToolTip(\"Permet de lancer l'algorithme des tampons sur la couche ci-dessus avec un buffer de 2km\")\nself.btn_traitement_1.setIcon(QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\"))\nself.btn_traitement_1.clicked.connect(self.traitement_1_clicked)\n
"},{"location":"extension-graphique/#lancer-le-dialogue-de-processing","title":"Lancer le dialogue de Processing","text":"

Pour les imports :

from qgis.core import QgsVectorLayer\nfrom qgis import processing\n

Pour le code dans la fonction :

def traitement_1_clicked(self):\n    \"\"\" Lancement de la fen\u00eatre de QGIS Processing. \"\"\"\n    layer = self.couche.currentLayer()\n\n    # Les pr\u00e9-requis pour continuer\n    # On sort de la fonction si on ne peut pas continuer\n    if not isinstance(layer, QgsVectorLayer):\n        return\n\n    if not layer.isSpatial():\n        return\n\n    dialog = processing.createAlgorithmDialog(\n        \"native:buffer\",\n        {\n            'INPUT': layer,\n            'DISTANCE': 2000,\n            'OUTPUT': 'TEMPORARY_OUTPUT'\n        }\n    )\n    dialog.show()\n

Pour rappel, nous ne sommes pas oblig\u00e9 d'ouvrir la fen\u00eatre de Processing, on peut directement faire processing.run, lire le chapitre pr\u00e9c\u00e9dent. Il ne faut pas oublier de donner la variable layer \u00e0 notre INPUT si vous copiez/coller le code de processing.run du chapitre pr\u00e9c\u00e9dent.

"},{"location":"extension-processing/","title":"Cr\u00e9er une extension QGIS pour Processing","text":"

Pour faire ce chapitre, il faut :

  1. Avoir une extension de base, \u00e0 l'aide du chapitre pr\u00e9c\u00e9dent
  2. Faire la mise \u00e0 jour en extension Processing \u00e0 l'aide de la documentation QGIS
"},{"location":"fonctions-scripts/","title":"Organisation du code dans un script avec des fonctions","text":""},{"location":"fonctions-scripts/#communication-avec-lutilisateur-des-erreurs-et-des-logs","title":"Communication avec l'utilisateur des erreurs et des logs","text":"

Avant de commencer \u00e0 vraiment \u00e9crire un script avec des fonctions, regardons comment communiquer des informations \u00e0 l'utilisateur.

Cookbook

Lien vers le Python cookbook qui pr\u00e9sente cette partie plus pr\u00e9cis\u00e9ment.

"},{"location":"fonctions-scripts/#la-barre-de-message","title":"La barre de message","text":"

On peut envoyer des messages vers l'utilisateur avec l'utilisation de la messageBar de la classe QgisInterface CPP/PyQGIS :

iface.messageBar().pushMessage('Erreur','On peut afficher une erreur', Qgis.Critical)\niface.messageBar().pushMessage('Avertissement','ou un avertissement', Qgis.Warning)\niface.messageBar().pushMessage('Information','ou une information', Qgis.Info)\niface.messageBar().pushMessage('Succ\u00e8s','ou un succ\u00e8s', Qgis.Success)\n

Cette fonction prend 3 param\u00e8tres :

  1. un titre
  2. un message
  3. un niveau d'alerte

On peut voir dans la classe de QgsMessageBar qu'il existe aussi pushSuccess qui est une alternative par exemple.

"},{"location":"fonctions-scripts/#journal-des-logs","title":"Journal des logs","text":"

On peut aussi \u00e9crire des logs comme ceci (plus discret, mais plus verbeux) :

QgsMessageLog.logMessage('Une erreur est survenue','Notre outil', Qgis.Critical)\nQgsMessageLog.logMessage('Un avertissement','Notre outil', Qgis.Warning)\nQgsMessageLog.logMessage('Une information','Notre outil', Qgis.Info)\nQgsMessageLog.logMessage('Un succ\u00e8s','Notre outil', Qgis.Success)\n

Cette fonction prend 3 param\u00e8tres :

"},{"location":"fonctions-scripts/#des-fonctions-pour-simplifier-le-code","title":"Des fonctions pour simplifier le code","text":""},{"location":"fonctions-scripts/#une-fonction-pour-charger-une-couche","title":"Une fonction pour charger UNE couche","text":"

La console, c'est bien, mais c'est tr\u00e8s limitant. Passons \u00e0 l'\u00e9criture d'un script qui va nous faciliter l'organisation du code.

  1. Red\u00e9marrer QGIS (afin de vider l'ensemble des variables que l'on a dans notre console)
  2. N'ouvrez pas le projet pr\u00e9c\u00e9dent !
  3. Ouvrer la console, puis cliquer sur Afficher l'\u00e9diteur
  4. Copier/coller le script ci-dessous
  5. Ex\u00e9cuter le
# En haut du script, ce sont souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\nprojet_qgis = QgsProject.instance().absoluteFilePath()\nif not projet_qgis:\n    iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\nelse:\n    racine = Path(projet_qgis).parent\n    fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n    if not fichier_shape.exists():\n        iface.messageBar().pushMessage('Erreur de chargement', f'Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n    else:\n        layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n        if not layer.isValid():\n            iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n        else:\n            QgsProject.instance().addMapLayer(layer)\n            iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n    print('Fin du script si on a un projet')\n

Tip

Pour d\u00e9sindenter le code, MAJ + TAB.

# Avec annotations Python\ndef charger_couche(bd_topo: str, thematique: str, couche: str):\n    ...\n\n# Sans annotations Python\ndef charger_couche(bd_topo, thematique, couche):\n    ...\n

Tip

Le mot-cl\u00e9 pass (ou encore ... qui est synonyme) ne sert \u00e0 rien. C'est un mot-cl\u00e9 Python pour rendre un bloc valide mais ne faisant rien. On peut le supprimer le bloc n'est pas vide.

On peut ajouter une docstring \u00e0 notre fonction, juste en dessous du def, avec des indentations :

\"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n

Afficher la solution interm\u00e9diaire
# En haut du script, ce souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\ndef charger_couche(bd_topo, thematique, couche):\n    \"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n    projet_qgis = QgsProject.instance().absoluteFilePath()\n    if not projet_qgis:\n        iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\n    else:\n        racine = Path(projet_qgis).parent\n        fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n        if not fichier_shape.exists():\n            iface.messageBar().pushMessage('Erreur de chargement', f'Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n        else:\n            layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n            if not layer.isValid():\n                iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n            else:\n                QgsProject.instance().addMapLayer(layer)\n                iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n        print('Fin du script si on a un projet')\n\n# Appel de notre fonction\ncharger_couche(bd_topo, thematique, couche)\n

Am\u00e9liorons encore cette solution interm\u00e9diaire avec la gestion des erreurs avec l'instruction return

On peut garder le code le plus \u00e0 gauche possible gr\u00e2ce \u00e0 return qui ordonne la sortie de la fonction.

Afficher une des solutions finales
# En haut du script, ce souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\ndef charger_couche(bd_topo, thematique, couche):\n    \"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n    projet_qgis = QgsProject.instance().absoluteFilePath()\n    if not projet_qgis:\n        iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\n        return False\n\n    racine = Path(projet_qgis).parent\n    fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n    if not fichier_shape.exists():\n        iface.messageBar().pushMessage('Erreur de chargement','Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n        return False\n\n    layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n    if not layer.isValid():\n        iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n        return False\n\n    QgsProject.instance().addMapLayer(layer)\n    iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n    # return True\n    return layer\n\n# Appel de notre fonction\ncharger_couche(bd_topo, 'ADMINISTRATIF', 'COMMUNE')\ncharger_couche(bd_topo, 'ADMINISTRATIF', 'ARRONDISSEMENT')\n
"},{"location":"fonctions-scripts/#une-fonction-pour-lister-les-couches-dune-thematique","title":"Une fonction pour lister LES couches d'UNE th\u00e9matique","text":"

Essayons de faire une fonction qui liste les shapefiles d'une certaine th\u00e9matique \ud83d\ude80

Plus pr\u00e9cis\u00e9ment, on souhaite une liste de cha\u00eenes de caract\u00e8res : ['COMMUNE', 'EPCI'].

Dans l'objet Path, il existe une m\u00e9thode iterdir(). Par exemple, pour it\u00e9rer sur le dossier courant de l'utilisateur :

from pathlib import Path\n\ndossier = Path.home()\nfor fichier in dossier.iterdir():\n    print(fichier)\n

Tip

Il faut se r\u00e9f\u00e9rer \u00e0 la documentation du module pathlib pour comprendre le fonctionnement de cette classe.

Voici la signature de la fonction que l'on souhaite :

def liste_shapefiles(bd_topo: str, thematique: str):\n    \"\"\" Lister les shapefiles d'une th\u00e9matique dans la BDTopo. \"\"\"\n    ...\n

Petit m\u00e9mo pour cet exercice :

Correction
def liste_shapefiles(bd_topo: str, thematique: str):\n    \"\"\" Lister les shapefiles d'une th\u00e9matique dans la BDTopo. \"\"\"\n    racine = Path(QgsProject.instance().absoluteFilePath()).parent\n    dossier = racine.joinpath(bd_topo, thematique)\n    shapes = []\n    for file in dossier.iterdir():\n        if file.suffix.lower() == '.shp':\n            shapes.append(file.stem)\n    return shapes\n\nshapes = liste_shapefiles(bd_topo, 'ADMINISTRATIF')\nprint(shapes)\n

On a d\u00e9sormais deux fonctions : liste_shapefiles et charger_couche.

Il est d\u00e9sormais simple de charger toutes une th\u00e9matique de notre BDTopo :

thematique = 'ADMINISTRATIF'\nshapes = liste_shapesfiles(bd_topo, thematique)\nfor shape in shapes:\n    charger_couche(bd_topo, thematique, shape)\n

Success

On a termin\u00e9 avec ces deux fonctions, c'\u00e9tait pour manipuler les fonctions \ud83d\ude0e

"},{"location":"fonctions-scripts/#pour-les-curieux","title":"Pour les curieux \ud83e\udd2d","text":"

Zoomer sur l'emprise d'une couche, sans la charger dans la l\u00e9gende

Example
  1. Modifions la signature de la fonction, en ajoutant un bool\u00e9en si on souhaite la couche dans la l\u00e9gende :
    def charger_couche(bd_topo, thematique, couche, ajouter_dans_legende = True):\n
    Puis dans cette m\u00eame fonction, utilisons cette variable :
    if ajouter_dans_legende:\n    QgsProject.instance().addMapLayer(layer)\n    iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n# return True\nreturn layer\n

Puis on peut ordonner au QgsMapCanvas de zoomer sur une emprise :

hydro = charger_couche(bd_topo, 'ZONES_REGLEMENTEES', 'PARC_OU_RESERVE', False)\niface.mapCanvas().setExtent(hydro.extent())\n

Ne pas oublier de tenir compte d'une projection diff\u00e9rente entre le canevas et la couche.

TODO, \u00e0 adapter, mais le code est la pour faire une reprojection entre 2 CRS

extent = iface.activeLayer().extent()\ncrs_layer = iface.activeLayer().crs()\ncrs = iface.mapCanvas().mapSettings().destinationCrs()\ntransformer = QgsCoordinateTransform(crs_layer, crs, QgsProject.instance())\nnew_extent = transformer.transform(extent)\niface.mapCanvas().setExtent(new_extent)\n

"},{"location":"fonctions-scripts/#extraction-des-informations-sous-forme-dun-fichier-csv","title":"Extraction des informations sous forme d'un fichier CSV.","text":""},{"location":"fonctions-scripts/#introduction","title":"Introduction","text":"

On souhaite d\u00e9sormais r\u00e9aliser une fonction d'export des m\u00e9tadonn\u00e9es de nos couches au format CSV, avec des tabulations comme s\u00e9parateur et son CSVT.

Il existe d\u00e9j\u00e0 un module CSV dans Python pour nous aider \u00e0 \u00e9crire un fichier de type CSV, mais nous n'allons pas l'utiliser.

Nous allons plut\u00f4t utiliser l'API QGIS pour :

  1. Cr\u00e9er une nouvelle couche en m\u00e9moire comportant les diff\u00e9rentes informations que l'on souhaite exporter
  2. Puis, nous allons utiliser l'API pour exporter cette couche m\u00e9moire au format CSV (l'\u00e9quivalent dans QGIS de l'action Exporter la couche).

Les diff\u00e9rents champs qui devront \u00eatre export\u00e9s sont :

"},{"location":"fonctions-scripts/#exemple-de-sortie","title":"Exemple de sortie","text":"nom type projection nombre_entite encodage source seuil_de_visibilite couche_1 Line EPSG:4326 5 UTF-8 /tmp/...geojson False couche_2 Tab No geometry 0 /tmp/...shp True"},{"location":"fonctions-scripts/#petit-memo-avec-des-exemples","title":"Petit m\u00e9mo avec des exemples","text":"

Pour cr\u00e9er une couche tabulaire en m\u00e9moire, code qui vient du cookbook :

layer_info = QgsVectorLayer('None', 'info', 'memory')\n

La liste des couches :

layers = QgsProject.instance().mapLayers()\n

Cr\u00e9er une entit\u00e9 ayant d\u00e9j\u00e0 les champs pr\u00e9configur\u00e9s d'une couche vecteur, et y affecter des valeurs :

feature = QgsFeature(objet_qgsvectorlayer.fields())\nfeature['nom'] = \"NOM\"\n

Obtenir le dossier du projet actuel :

projet_qgis = Path(QgsProject.instance().fileName())\ndossier_qgis = projet_qgis.parent\n

Afficher la g\u00e9om\u00e9trie, sous sa forme \"humaine\", en cha\u00eene de caract\u00e8re, avec l'aide de QgsWkbTypes :

QgsWkbTypes.geometryDisplayString(vector_layer.geometryType())\n

Pour utiliser une session d'\u00e9dition, on peut faire :

layer.startEditing()  # D\u00e9but de la session\nlayer.commitChanges()  # Fin de la session en enregistrant\nlayer.rollback()  # Fin de la session en annulant les modifications\n

"},{"location":"fonctions-scripts/#les-contextes-python","title":"Les contextes Python","text":"

On peut \u00e9galement faire une session d'\u00e9dition avec un \"contexte Python\" :

from qgis.core import edit\n\nwith edit(layer):\n    # Faire une \u00e9dition sur la couche\n    pass\n\n# \u00c0 la fin du bloc d'indentation, la session d'\u00e9dition est automatiquement close, m\u00eame en cas d'erreur Python\n
Exemple de l'utilisation d'un contexte Python avec la session d'\u00e9dition

Sans contexte, la couche reste en mode \u00e9dition en cas d'erreur fatale Python

layer = iface.activeLayer()\n\nlayer.startEditing()\nprint(\"D\u00e9but de la session\")\n# Code inutile, mais qui va volontairement faire une exception Python\na = 10 / 0\n\nprint(\"Fin de la session\")\nlayer.commitChanges()\nprint(\"Fin du script\")\n

Mais utilisons d\u00e9sormais un contexte Python \u00e0 l'aide dewith, sur une couche qui n'est pas en \u00e9dition :

layer = iface.activeLayer()\n\nwith edit(layer):\n    print(\"D\u00e9but de la session\")\n    # Code inutile, mais qui va volontairement faire une exception Python\n    a = 10 / 0\n\nprint(\"Fin du script\")\n

On peut lire le code comme En \u00e9ditant la couche \"layer\", faire :.

"},{"location":"fonctions-scripts/#petit-memo-des-classes","title":"Petit m\u00e9mo des classes","text":"

Nous allons avoir besoin de plusieurs classes dans l'API QGIS :

Pour le type de champ, on va avoir besoin de l'API Qt \u00e9galement :

Note

Note perso, je pense qu'avec la migration vers Qt6, cela va pouvoir se simplifier un peu pour les QVariant...

"},{"location":"fonctions-scripts/#etapes","title":"\u00c9tapes","text":"

Il va y avoir plusieurs \u00e9tapes dans ce script :

  1. Cr\u00e9er une couche en m\u00e9moire
  2. Ajouter des champs \u00e0 cette couche en utilisant une session d'\u00e9dition
  3. R\u00e9cup\u00e9rer la liste des couches pr\u00e9sentes dans la l\u00e9gende
  4. It\u00e9rer sur les couches pour ajouter ligne par ligne les m\u00e9tadonn\u00e9es dans une session d'\u00e9dition
  5. Enregistrer en CSV la couche m\u00e9moire

Tip

Pour d\u00e9boguer, on peut afficher la couche m\u00e9moire en question avec QgsProject.instance().addMapLayer(layer_info)

"},{"location":"fonctions-scripts/#solution-possible","title":"Solution possible","text":"
from qgis.core import edit\n\n# Cr\u00e9ation de la couche m\u00e9moire\nlayer_info = QgsVectorLayer('None', 'info', 'memory')\n# QgsProject.instance().addMapLayer(layer_info)\n\n# Ajout des champs\nwith edit(layer_info):\n    layer_info.addAttribute(QgsField('nom', QVariant.String))\n    layer_info.addAttribute(QgsField('type', QVariant.String))\n    layer_info.addAttribute(QgsField('projection', QVariant.String))\n    layer_info.addAttribute(QgsField('nombre_entit\u00e9', QVariant.Int))\n    layer_info.addAttribute(QgsField('encodage', QVariant.String))\n    layer_info.addAttribute(QgsField('seuil', QVariant.Bool))\n    layer_info.addAttribute(QgsField('source', QVariant.String))\n\nlayers = QgsProject.instance().mapLayers()\nif not layers:\n    iface.messageBar().pushMessage('Pas de couche', \"Attention, il n'a pas de couche\", Qgis.Warning)\n\n# It\u00e9ration sur l'ensemble des couches du projet\nfor layer in layers.values():\n    feature = QgsFeature(layer_info.fields())\n    feature['nom'] = layer.name()\n    feature['type'] = QgsWkbTypes.geometryDisplayString(layer.geometryType())\n    feature['nombre_entit\u00e9'] = layer.featureCount()\n    feature['encodage'] = layer.dataProvider().encoding()\n    feature['projection'] = layer.crs().authid()\n    feature['seuil'] = layer.hasScaleBasedVisibility()\n    feature['source'] = layer.publicSource()\n\n    with edit(layer_info):\n        layer_info.addFeature(feature)\n\n# Export de la couche m\u00e9moire au format CSV\noptions = QgsVectorFileWriter.SaveVectorOptions()\noptions.driverName = 'CSV'\noptions.fileEncoding = 'UTF-8'\noptions.layerOptions = ['CREATE_CSVT=YES', 'SEPARATOR=TAB']\n\nbase_name = QgsProject.instance().baseName()\nracine = Path(QgsProject.instance().absoluteFilePath()).parent\noutput_file = racine.joinpath(f'{base_name}.csv')\n\nQgsVectorFileWriter.writeAsVectorFormatV3(\n    layer_info,\n    str(output_file),\n    QgsProject.instance().transformContext(),\n    options,\n)\n

Warning

Ajouter une couche raster et retester le script ... surprise \ud83c\udf81

Pour les experts, ajouter un alias ou un commentaire sur un champ

field = QgsField(\n    'seuil_visibilite',\n    QVariant.Bool,\n    comment=\"Champ contenant le seuil de visibilit\u00e9\")\nfield.setAlias(\"Seuil de visibilit\u00e9\")\nlayer_info.addAttribute(field)\n
Ceci dit, cela d\u00e9pend dans quel format on exporte la couche, dans l'exercice, on fait du CSV, donc on perd ces informations.

Tip

Pour obtenir en Python la liste des fournisseurs GDAL/OGR :

from osgeo import ogr\n[ogr.GetDriver(i).GetDescription() for i in range(ogr.GetDriverCount())]    \n
ou dans le menu Pr\u00e9f\u00e9rences \u27a1 Options \u27a1 GDAL \u27a1 Pilotes vecteurs

"},{"location":"fonctions-scripts/#finalisation","title":"Finalisation","text":"

Id\u00e9alement, il faut v\u00e9rifier le r\u00e9sultat de l'enregistrement du fichier. Les diff\u00e9rentes m\u00e9thodes writeAsVectorFormat retournent syst\u00e9matiquement un tuple avec un code d'erreur et un message si n\u00e9cessaire, voir la documentation.

Pour s'en rendre compte, on peut ajouter une variable result = QgsVectorFileWriter.writeAsVectorFormatV3(...). Puis de faire un print(result) pour s'en rendre compte. On peut tenir compte donc ce tuple :

De plus, en cas de succ\u00e8s, il est pratique d'avertir l'utilisateur. On peut aussi fournir un lien pour ouvrir l'explorateur de fichier :

# Affichage d'un message \u00e0 l'utilisateur\niface.messageBar().pushSuccess(\n    \"Export OK des couches \ud83d\udc4d\",\n    (\n        \"Le fichier CSV a \u00e9t\u00e9 enregistr\u00e9 dans \"\n        \"<a href=\\\"{}\\\">{}</a>\"\n    ).format(output_file.parent, output_file)\n)\n
Pour ajouter le support du message d'erreur
if result[0] != QgsVectorFileWriter.WriterError.NoError:\n    print(f\"Erreur : {result[1]}\")\nelse:\n    # Affichage d'un message \u00e0 l'utilisateur\n    iface.messageBar().pushSuccess(\n        \"Export OK des couches \ud83d\udc4d\",\n        (\n            \"Le fichier CSV a \u00e9t\u00e9 enregistr\u00e9 dans \"\n            \"<a href=\\\"{}\\\">{}</a>\"\n        ).format(output_file.parent, output_file)\n    )\n
"},{"location":"formulaire/","title":"Formulaire","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

On peut personnaliser un formulaire avec :

Tip

Le blog de Nathan est une bonne ressource concernant les formulaires et QtDesigner pour cette partie la, mais cela commence \u00e0 \u00eatre vieux.

Sur la couche Geopackage, dans les propri\u00e9t\u00e9s de la couche \u27a1 Formulaire d'attributs, cliquer sur le petit logo Python en haut bleu et jaune. Choisir l'option Fournir le code dans cette bo\u00eete de dialogue.

Dans le nom de la fonction, mettre my_form_open qui correspond \u00e0 l'exemple du code en dessous.

La fonction my_form_open sera donc ex\u00e9cut\u00e9 par d\u00e9faut lors de l'ouverture du formulaire. On remarque qu'il y a trois param\u00e8tres qui sont donn\u00e9s :

Dans l'objet dialog :

Pour information :

Afficher
from qgis.PyQt.QtCore import QUrl\nfrom qgis.PyQt.QtWidgets import QWidget, QDialogButtonBox\nfrom qgis.PyQt.QtGui import QDesktopServices\n\ndef my_form_open(dialog, layer, feature):\n    button_box = dialog.findChild(QDialogButtonBox)\n    button_box.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Help|QDialogButtonBox.Ok)\n\n    button_box.button(QDialogButtonBox.Help).clicked.connect(open_help)\n\ndef open_help():\n    QDesktopServices.openUrl(QUrl('https://docs.3liz.org/'))\n

type_field = dialog.findChild(QLineEdit, \"type\")\ntype_field.setStyleSheet(\"background-color: rgba(255, 107, 107, 150);\")\n
* On souhaite rendre le champ en rouge seulement s'il y a une condition :

type_field = dialog.findChild(QLineEdit, \"type\")\ntype_field.textChanged.connect(type_field_changed)\n\ndef type_field_changed():\n    type_field = dialog.findChild(QLineEdit, \"type\")\n    if type_field.text() not in ('studio', 'appartement', 'maison'):\n        type_field.setStyleSheet(\"background-color: rgba(255, 107, 107, 150);\")\n        type_field.setToolTip(\"La valeur doit \u00eatre 'studio', 'appartement' ou 'maison'.\")\n    else:\n        type_field.setStyleSheet(\"\")\n        type_field.setToolTip()\n

Info

Notons que ce ne sont que des exemples des fonctionnalit\u00e9s Python. On peut faire ces masques de saisie \u00e0 l'aide des expressions QGIS ou simplement en changeant le type de widget pour un champ en particulier.

"},{"location":"ide-git/","title":"Python avanc\u00e9","text":""},{"location":"ide-git/#utilisation-dun-ide","title":"Utilisation d'un IDE","text":"

Pour \u00e9crire du code Python, on peut utiliser n'importe quel \u00e9diteur de texte brut quelque soit l'OS. Cependant, l'utilisation d'un \u00e9diteur de texte qui \"comprend\" le code Python est vivement recommand\u00e9, car il peut vous signaler quelques erreurs facilement d\u00e9tectables, telles que les imports manquants. Comme \u00e9diteur de texte, il en existe plusieurs.

Si vous souhaitez faire plus de programmation, nous vous recommandons l'utilisation d'un IDE. Il embarque l'\u00e9diteur de texte ci-dessus, mais poss\u00e8de aussi des outils de debugs et d'assistance dans l'\u00e9criture du code comme l'autocompl\u00e9tion.

En IDE gratuit, il existe :

Un IDE est outil tr\u00e8s complet pour d\u00e9veloppement. Il est possible de coder en Python avec un \u00e9diteur de texte, mais si possible qui sait quand m\u00eame faire de la coloration syntaxique du code Python est vraiment un plus (NotePad++\u2026).

"},{"location":"ide-git/#lancer-un-script-python-dans-la-console","title":"Lancer un script Python dans la console","text":"

Si vous utilisez un IDE pour \u00e9crire du code Python, vous pouvez lancer le code Python dans la console Python \u00e0 l'aide de cette astuce.

"},{"location":"ide-git/#utilisation-de-git","title":"Utilisation de GIT","text":"

Il est vivement recommand\u00e9 d'utiliser GIT :

La documentation : https://git-scm.com/docs/

Les commandes les plus utiles :

Liens vers OpenClassRooms :

"},{"location":"legende/","title":"Objectif","text":"

\u00c0 travers ce TP, on va traiter plusieurs points :

METTRE PHOTO l\u00e9gende

"},{"location":"legende/#donnees","title":"Donn\u00e9es","text":"

On peut placer les deux fichiers l'un \u00e0 c\u00f4t\u00e9 de l'autre. Ouvrir dans QGIS le fichier GPKG seulement.

"},{"location":"legende/#objectif_1","title":"Objectif","text":"
from pathlib import Path\n\nlayer = iface.activeLayer()\n\nparent_folder = Path(layer.source()).parent\n\nprint(parent_folder)\n\nfichier_ods = parent_folder / \"base_cc_comparateur.ods\"\nprint(fichier_ods.is_file())\n\ntableur = QgsVectorLayer(str(fichier_ods), \"tableur\", \"ogr\")\nprint(tableur.isValid())\n
"},{"location":"memo-python/","title":"Introduction au language Python","text":""},{"location":"memo-python/#quest-ce-que-python","title":"Qu'est-ce que Python ?","text":"

Exemple d'un code qui d\u00e9clare une variable et compare si sa valeur est sup\u00e9rieur \u00e0 5 afin d'afficher un message :

# D\u00e9claration d'une variable de type entier\nx = 10\n\n# D\u00e9claration d'une variable cha\u00eene de caract\u00e8re\ninfo = 'X est sup\u00e9rieur \u00e0 5'\n\nif x > 5:\n    print(info)\n
"},{"location":"memo-python/#versions","title":"Versions","text":""},{"location":"memo-python/#rappel-de-base-sur-python","title":"Rappel de base sur Python","text":""},{"location":"memo-python/#la-console","title":"La console","text":"

Pour la suite de la formation, nous allons utiliser la console Python de QGIS.

Dans le menu Extensions \u27a1 Console Python.

Tip

Souvent, avec Windows, il y a un conflit avec un raccourci clavier pour taper le caract\u00e8re { ou } dans la console.

Ces caract\u00e8res sont utilis\u00e9s en Python. Il est donc conseill\u00e9 de supprimer ce raccourci clavier. Il s'agit du \"zoom + secondaire\" dans QGIS \u2192 menu Pr\u00e9f\u00e9rences \u27a1 Raccourcis clavier.

"},{"location":"memo-python/#les-types-de-donnees","title":"Les types de donn\u00e9es","text":"

Une variable peut contenir un entier, un bool\u00e9en (True ou False), cha\u00eene de caract\u00e8res, nombre d\u00e9cimal, un objet... Il y a un faible typage des variables, c'est-\u00e0-dire qu'une variable peut changer de type au cours de l'ex\u00e9cution du programme.

# Pour cr\u00e9er une variable, on d\u00e9clare juste le nom de la variable ainsi que sa valeur :\ncompteur = 0\n

Nous allons par la suite utiliser type(variable) pour v\u00e9rifier le type de la variable.

mon_compteur = 0\ntype(mon_compteur)\n<class 'int'>\n\nest_valide = False\ntype(est_valide)\n<class 'bool'>\n\nnom_couche = 'communes'\ntype(nom_couche)\n<class 'str'>\n\nnom_couche = \"communes\"\ntype(nom_couche)\n<class 'str'>\n\ntexte = 'Bonjour je m\\'appelle \"Olivier\"'\ntype(texte)\n<class 'str'>\n\ndensite = 3.5\ntype(densite)\n<class 'float'>\n\nnom_couche = None\ntype(nom_couche)\n<class 'NoneType'>\n
"},{"location":"memo-python/#les-structures-de-donnees","title":"Les structures de donn\u00e9es","text":"

Il existe quatre types de structure de donn\u00e9es :

Tip

Documentation Python sur les listes

# Cr\u00e9er une liste vide\nnombres = []\ntype(nombres)\n<class 'list'>\n\n# Cr\u00e9er une liste avec des \u00e9l\u00e9ments \u00e0 l'int\u00e9rieur\nmois = ['janvier', 'f\u00e9vrier', 'mars']\n\n# Ajouter un \u00e9l\u00e9ment\nmois.append('avril')\n# Ajouter une autre liste\nmois.extend(['mai', 'juin'])\n\n# Nombre de mois\nlen(mois)\n\n# Supprimer un \u00e9l\u00e9ment\ndel mois[1]\n\n# On peut acc\u00e9der \u00e0 un \u00e9l\u00e9ment avec un \"index\" \u00e0 l'aide de []\nmois[2]\n\n# Attention \u00e0 l'index maximum\nmois[12]\nTraceback (most recent call last):\n  File \"/usr/lib/python3.12/code.py\", line 90, in runcode\n    exec(code, self.locals)\n  File \"<input>\", line 1, in <module>\nIndexError: tuple index out of range\n

Tip

Documentation Python sur les tuples

liste = ('route double sens', 'route sens unique')\ntype(liste)\n<class 'tuple'>\nlen(liste)\n2\nliste[0]\n\nliste[5]\nTraceback (most recent call last):\n  File \"/usr/lib/python3.12/code.py\", line 90, in runcode\n    exec(code, self.locals)\n  File \"<input>\", line 1, in <module>\nIndexError: tuple index out of range\n

Attention, les dictionnaires ne sont pas ordonn\u00e9s, de fa\u00e7on native, m\u00eame depuis Python 3.9. Si vraiment, il y a besoin, il existe une classe OrderedDict, mais ce n'est pas une structure de donn\u00e9es native dans Python. C'est un objet qu'il faut importer.

commune = {}\ntype(commune)\n# <class 'dict'>\ncommune['nom'] = 'Besan\u00e7on'\ncommune['code_insee'] = 25056\ncommune['est_prefecture'] = True\n\n# Ou directement lors de la cr\u00e9ation de la variable :\ncommune = {'nom': 'Besan\u00e7on', 'code_insee': 25056, 'est_prefecture': True}\n\n# Lire le contenu d'une cl\u00e9 :\nprint(commune['nom'])\nprint(commune['population'])  # Leve une erreur IndexError\nprint(commune.get('population'))  # Imprime None\n
"},{"location":"memo-python/#les-commentaires","title":"Les commentaires","text":"

Pour commenter le code dans un script, pas dans la console :

# Ceci est un commentaire sur une ligne\n\n\"\"\" Ces lignes sont r\u00e9serv\u00e9s pour la documentation de l'API et ne doivent pas \u00eatre des lignes de commentaires. \"\"\"\n
"},{"location":"memo-python/#arithmetique","title":"Arithm\u00e9tique","text":"
a = 10\n# Op\u00e9rateurs de base\nb = a + 1\nc = a - 1\nd = a * 2\ne = a / 2\n\n# Les espaces ne sont pas importants\n# 1+2 ou 1 + 2 sont \u00e9quivalents\n\n# Il est souvent utile de faire de l'incr\u00e9mentation ou d\u00e9cr\u00e9mentation d'une variable :\na = a + 1\n# mais on \u00e9crit plus souvent\na += 1\n# On peut changer le pas d'incr\u00e9mentation ou alors faire de la d\u00e9cr\u00e9mentation\na -= 1 # Diminuer de 1\na += 5 # Incr\u00e9menter de 5\n\n\na = 10\nf = a % 3  # Fonction \"modulo\", r\u00e9sultat 1\ng = a ** 2  # Fonction puissance, r\u00e9sultat 100\n
"},{"location":"memo-python/#concatener-des-chaines-et-des-variables","title":"Concat\u00e9ner des cha\u00eenes et des variables","text":"

Concat\u00e9ner, c'est assembler des cha\u00eenes de caract\u00e8res dans une seule et m\u00eame sortie. On peut concat\u00e9ner des variables entre elles ou du texte.

Il existe plein de mani\u00e8res de faire, mais certaines sont plus pratiques que d'autres

# Non recommand\u00e9\na = 'bon'\nb = 'jour'\na + b  # 'bonjour'\nc = 1\na + c  # Erreur\na + str(c)  # Marche\n

\u00c0 l'ancienne avec %

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = 'Bonjour %s !' % prenom\nbienvenue = 'Bonjour %s, nous sommes le %s novembre' % (prenom, numero_jour)\n

Nouveau avec {} et format

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = 'Bonjour {} !'.format(prenom)\nbienvenue = 'Bonjour {}, nous sommes le {} novembre'.format(prenom, numero_jour)\nbienvenue = 'Bonjour {prenom}, nous sommes le {jour} novembre'.format(prenom=prenom, jour=numero_jour)\n

Warning

Attention \u00e0 la port\u00e9e des variables.

Encore plus moderne avec fstring

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = f'Bonjour {prenom} !'\nbienvenue = f'Bonjour {prenom}, nous sommes le {numero_jour} novembre'\n

"},{"location":"memo-python/#operateurs-logiques","title":"Op\u00e9rateurs logiques","text":"
a > b\na >= b\na < b\na <= b\na == b\na != b\n\n# Dans plusieurs langages, pour v\u00e9rifier si \"a\" est entre deux bornes :\n0 < a and a < 10\n# En Python, on peut faire\n0 < a < 10\n\n# Pour les objets\na is b\na is not b\na in b\n
"},{"location":"memo-python/#condition","title":"Condition","text":"

Important, Python oblige l'indentation sinon il y a une erreur. Par convention, il s'agit de 4 espaces.

note = 13\nif note >= 16:\n    if note == 20:\n        print('Toutes mes f\u00e9licitations')\n    else:\n        print('F\u00e9licitations')\nelif 14 <= note < 16:\n    print('Tr\u00e8s bien')\nelif 12 <= note < 14:\n    print('Bien')\nelse:\n    print('Peu mieux faire')\n
"},{"location":"memo-python/#boucle-for","title":"Boucle for","text":"

Utile lors que l'on connait le nombre de r\u00e9p\u00e9titions avant l'ex\u00e9cution de la boucle.

countries = ['Allemagne', 'Espagne', 'France']\nfor country in countries:\n    print(f'Pays : {country}')\n\nfor x in range(10):\n    print(x)\n\nregions = {\n    'Auvergne-Rh\u00f4ne-Alpes': 'Lyon',\n    'Bourgogne-Franche-Comt\u00e9': 'Dijon',\n    'Bretagne': 'Rennes',\n    'Centre-Val de Loire': 'Orl\u00e9ans',\n}\n\nfor region in regions:\n    print(region)\n\nfor region in regions.keys():\n    print(region)\n\nfor city in regions.values():\n    print(city)\n\nfor region, city in regions.items():\n    print(f'R\u00e9gion {region} dont le chef lieu est {city}')\n\n# Non recommand\u00e9, mais on peut le rencontrer\nfor region in regions.keys():\n  print(f\"R\u00e9gion {region} dont le chef lieu est {regions[region]}\")\n
"},{"location":"memo-python/#recherche-dun-element","title":"Recherche d'un \u00e9l\u00e9ment","text":"
countries = ['Allemagne', 'Espagne', 'France']\n\n# Solution simple\nif 'Allemagne' in countries:\n    print('Pr\u00e9sent')\nelse:\n    print('Non pr\u00e9sent')\n\n# Plus complexe, avec une fonction pour les minuscules\npresent = False\nfor country in countries:\n    if country.lower() == 'allemagne':\n        present = True\nif present:\n    print('Pr\u00e9sent')\nelse:\n    print('Non pr\u00e9sent')\n\n\n# Le plus pythonique\nfor country in countries:\n    if country.lower() == 'allemagne':\n        print('Pr\u00e9sent')\n        break\nelse:\n    print('Non pr\u00e9sent')\n\n# Encore plus pythonique avec une list-comprehension, voir plus bas\n
"},{"location":"memo-python/#boucle-while","title":"Boucle while","text":"

Contrairement \u00e0 la boucle for, on ne connait pas forc\u00e9ment le nombre d'ex\u00e9cution de la boucle en lisant uniquement la ligne while.

x = 0\nwhile x < 10:\n    print(x)\n    x += 1\n

Error

Attention \u00e0 ne pas faire une boucle infinie !

executer_une_fonction()\nwhile not conditon_echec:\n    executer_une_fonction()\n
"},{"location":"memo-python/#switch","title":"Switch","text":"

Python 3.10 minimum

numero_jour = 2\n\nmatch numero_jour:\n  case 1:\n    print('Lundi')\n  case 2:\n    print('Mardi')\n  case 3:\n    print('Mercredi')\n  case 4:\n    print('Jeudi')\n  case 5:\n    print('Vendredi')\n  case 6:\n    print('Samedi')\n  case 7:\n    print('Dimanche')\n  case _:\n    print('Pas un jour de la semaine')\n
Avant Python 3.10 avec un if elif
numero_jour = 2\n\nif numero_jour == 1:\n    print('Lundi')\nelif numero_jour == 2:\n    print('Mardi')\nelif numero_jour == 3:\n    print('Mercredi')\nelif numero_jour == 4:\n    print('Jeudi')\nelif numero_jour == 5:\n    print('Vendredi')\nelif numero_jour == 6:\n    print('Samedi')\nelif numero_jour == 7:\n    print('Dimanche')\nelse:\n    print('Pas un jour de la semaine')\n
"},{"location":"memo-python/#list-comprehensions","title":"List Comprehensions","text":"

C'est une fa\u00e7on tr\u00e8s pythonique et tr\u00e8s utilis\u00e9e de cr\u00e9er des listes.

"},{"location":"memo-python/#pour-transformer-une-liste-existante-en-la-remplacant","title":"Pour transformer une liste existante, en la rempla\u00e7ant :","text":"
countries = ['Allemagne', 'Espagne', 'France']\ncountries = [c.upper() for c in countries]\n

Par exemple, cr\u00e9er une liste des nombres impairs entre 1 et 9 :

# Non pythonique\nimpair = []\nfor x in range(10):\n    if x % 2:\n        impair.append(x)\n\n# Pythonique\nimpair = [x for x in range(10) if x % 2]\n
"},{"location":"memo-python/#manipulation-sur-les-chaines-de-caracteres","title":"Manipulation sur les cha\u00eenes de caract\u00e8res","text":"

Slicing sur les mois de l'ann\u00e9e :

mois = ['Janvier', 'F\u00e9vrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Octobre', 'Novembre', 'D\u00e9cembre']\n\nmois[0:2]\n['Janvier', 'F\u00e9vrier']\n\nmois[2:]\n['Mars', 'Avril', 'Mai', 'Juin', 'Octobre', 'Novembre', 'D\u00e9cembre']\n\nmois[:-2]\n['Janvier', 'F\u00e9vrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Octobre']\n\nmois[-2:]\n['Novembre', 'D\u00e9cembre']\n
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nlen(alphabet)\n','.join(alphabet)\nalphabet.lower()\nalphabet.upper()\nalphabet[1]  # B\nalphabet[1:3]  # BC\nalphabet[-1]  # Z\nalphabet[-3:]  # XYZ\nalphabet[:6]  # ABCDEF\n
"},{"location":"memo-python/#fonctions","title":"Fonctions","text":"

Une fonction permet de factoriser son code. Elle peut :

def ajouter(x, y):\n    \"\"\"Ajouter deux nombres.\"\"\"\n    return x + y\n\ndef crier(phrase='bonjour'):\n    print(phrase.upper())\n\ndef discuter(texte, personnage='Charles'):\n    \"\"\"Un personnage discute.\"\"\"\n    print('{}: \"{}\"'.format(personnage, texte))\n\ndef discuter(texte, personnage='Charles'):\n    \"\"\"Un personnage discute.\"\"\"\n    return f'{personnage}: \"{texte}\"'\n
def decomposer(entier, diviser_par):\n    \"\"\"Retourne la partie enti\u00e8re et le reste d'une division.\"\"\"\n    partie_entiere = entier / diviser_par\n    reste = entier % diviser_par\n    return int(partie_entiere), reste\n
def une_fonction(*args, **kwargs):\n    print('Les arguments')\n    for arg in args:\n        print(arg)\n    print('Les arguments non nomm\u00e9s')\n    for key, value in kwargs.items():\n        print('{} -> {}'.format(key, value))\n\nune_fonction(1,2,3, text='Ma phrase')\n
"},{"location":"memo-python/#poo-programmation-orientee-objet","title":"POO : Programmation Orient\u00e9e Objet","text":"

Pour l'explication th\u00e9orique, lire l'introduction dans le chapitre de la console.

On peut introduire l'utilisation de la POO \u00e0 l'aide de l'objet Path. La documentation de cette classe se trouve en ligne.

La librairie Path est install\u00e9 de base avec Python.

La programmation orient\u00e9e objet permet de cr\u00e9er un objet (on parle plus pr\u00e9cis\u00e9ment d'instancier) puis on peut appeler des m\u00e9thodes sur cet objet.

Dans une console QGIS :

from pathlib import Path\n# Appel du \"constructeur\"\nchemin = Path('.')\n# La notation . est une cha\u00eene de caract\u00e8re particuli\u00e8re pour un OS demandant le dossier courant de l'ex\u00e9cution.\n# On peut utiliser .. pour faire r\u00e9f\u00e9rence au dossier parent\nprint(chemin.absolute())\nprint(chemin.is_dir())\nun_fichier = chemin / 'mon_projet.qgs'\nprint(un_fichier.exists())\nprint(un_fichier.name)\nprint(un_fichier.name)\nprint(chemin.joinpath('mon_projet.qgs').exists())\n

Tip

Quand l'instruction se termine par des (), on dit que c'est une m\u00e9thode de cet objet. Il s'agit d'une fonction, qui peut prendre ou non des param\u00e8tres et qui peut renvoyer ou non des r\u00e9sultats en sortie. Quand l'instruction ne se termine pas par (), on acc\u00e8de \u00e0 une propri\u00e9t\u00e9 de l'objet.

Pour une application avec des objets QGIS, il faut lire le chapitre suivant sur la console ou encore la partie sur l'\u00e9criture d'un script Processing.

"},{"location":"memo-python/#exceptions","title":"Exceptions","text":"

Lire le chapitre sur le parcours des entit\u00e9s.

"},{"location":"memo-python/#truc-et-astuces","title":"Truc et astuces","text":""},{"location":"memo-python/#passage-par-reference","title":"Passage par r\u00e9f\u00e9rence","text":"

Warning

Attention au passage par r\u00e9f\u00e9rence :

ma_liste_1 = [1, 2, 3]\nma_liste_2 = ma_liste_1\nma_liste_2.append(4)\nprint(ma_liste_2)\nprint(ma_liste_1)\n

"},{"location":"memo-python/#enumerate","title":"Enumerate","text":"

Avoir un compteur lors de l'it\u00e9ration d'une liste :

users = ['Tom', 'James', 'John']\nfor i, user in enumerate(users):\n    print('{} -> {}'.format(i + 1, user))\n

"},{"location":"memo-python/#annotations","title":"Annotations","text":"

Dans la suite de la formation, il est possible de voir des annotations Python. Cela permet de sp\u00e9cifier le type des variables dans les param\u00e8tres des fonctions et/ou de d\u00e9finir le type de retour.

from typing import Tuple\ndef decomposer(entier: int, diviser_par: int) -> Tuple[int, int]:\n    \"\"\"Retourne la partie enti\u00e8re et le reste d'une division.\"\"\"\n    partie_entiere = entier / diviser_par\n    reste = entier % diviser_par\n    return int(partie_entiere), reste\n

Il faut lire la documentation des annotations pour voir les diff\u00e9rentes possibilit\u00e9s.

"},{"location":"memo-python/#signal-et-slot","title":"\"Signal\" et \"slot\"","text":"

Lire le passage dans Signaux et slots.

"},{"location":"memo-python/#terminologie","title":"Terminologie","text":""},{"location":"migration-majeure/","title":"Migration majeure au sein de PyQGIS","text":"

Lire la page recensant les \"breaking changes\" de QGIS de toutes les versions.

"},{"location":"migration-majeure/#qgis-2-qgis-3","title":"QGIS 2 \u2192 QGIS 3","text":"

Petit guide sur une migration vers QGIS 3.

"},{"location":"migration-majeure/#qt-5-qt-6","title":"Qt 5 \u2192 Qt 6","text":"

QGIS tente de passer de la version Qt 5 vers Qt 6, sans passer par la case QGIS 4.0 (et donc non \"cassage\" de l'API QGIS 3.X).

Le travail de migration a commenc\u00e9 depuis QGIS 3.34. Mais \u00e0 l'heure actuelle, fin 2024, il n'existe qu'un binaire de QGIS pour tester Qt 6. C'est pour le moment, \"sous le capot\" de QGIS que cela se passe.

Contrairement au passage Qt 4 vers Qt 5, il est possible de rendre une extension compatible pour les deux versions \u00e0 l'aide d'un script que l'on peut trouver dans l'autre petit guide pour une migration vers Qt 6.

Par exemple, l'extension Lizmap avec le commit qui ajoute la compatibilit\u00e9 Qt 6.

"},{"location":"postgis/","title":"PostGIS","text":""},{"location":"postgis/#psycopg","title":"Psycopg","text":"

En Python, il existe un package d\u00e9di\u00e9 \u00e0 PostgreSQL, il s'agit de Psycopg. Il s'agit d'un package totalement ind\u00e9pendant de QGIS.

Exemple pour r\u00e9cup\u00e9rer les tables pr\u00e9sentes dans une base de donn\u00e9es \u00e0 l'aide de SQL

import psycopg\n\ninspect_schema = \"mon_schema\"\n\nconnection = psycopg.connect(\n    user=\"docker\", password=\"docker\", host=\"db\", port=\"5432\", database=\"gis\"\n)\ncursor = connection.cursor()\ncursor.execute(\n    f\"SELECT table_name FROM information_schema.tables WHERE table_schema = '{inspect_schema}'\"\n)\nrecords = cursor.fetchall()\nprint(records)\n
"},{"location":"postgis/#pyqgis","title":"PyQGIS","text":"

Depuis QGIS 3.16, il existe de plus en plus de m\u00e9thodes dans la classe QgsAbstractDatabaseProviderConnection pour interagir avec une base de donn\u00e9es PostGIS.

from qgis.core import QgsProviderRegistry\n\nmetadata = QgsProviderRegistry.instance().providerMetadata('postgres')\nconnection = metadata.findConnection(\"nom de la connexion PG dans votre panneau\")\n\n# Faire une requ\u00eate SQL (ou plusieurs)\n# Besoin d'\u00e9chapper en utilisant \"\" si votre sch\u00e9ma ou table comporte des majuscules\nresults = connection.executeSql(\"SELECT * FROM \\\"schema\\\".\\\"table\\\";\")\nprint(results)\n\n# Cr\u00e9er un sch\u00e9ma\nconnection.createSchema(\"mon_nouveau_schema\")\n\n# Lister les tables\nprint(connection.tables(\"un_schema\"))\n\n# Afficher une table dans QGIS, cela retourne une cha\u00eene de caract\u00e8re\n# permettant de faire une source de donn\u00e9es pour une QgsVectorLayer\nprint(connection.tableUri(\"schema\", \"table\"))\n

Afficher une table sans g\u00e9om\u00e9trie :

layer = QgsVectorLayer(connection.tableUri(\"schema\", \"table\"), \"Ma table\", \"postgres\")\nlayer.loadDefaultStyle()  # Si un style par d\u00e9faut existe dans votre base PostgreSQL, avec la table layer_styles\nQgsProject.instance().addMapLayer(layer)\n

Afficher une table avec g\u00e9om\u00e9trie en partant de QgsDataSourceUri :

uri = QgsDataSourceUri(connection.uri())\nuri.setSchema('schema')\nuri.setTable('table')\nuri.setKeyColumn('uid')\n\n# Avec une geom si besoin\nuri.setGeometryColumn('geom')\n\nlayer = QgsVectorLayer(uri.uri(), 'Ma table', 'postgres')\nQgsProject.instance().addMapLayer(layer)\n

Afficher le r\u00e9sultat d'un SELECT :

# Notons l'usage des parenth\u00e8ses autour du SELECT\nuri = QgsDataSourceUri(connection.uri())\nuri.setTable('(SELECT * FROM schema.table)')\nuri.setKeyColumn('uid')\n\n# Avec une geom si besoin\nuri.setGeometryColumn('geom')\n\nlayer = QgsVectorLayer(uri.uri(), 'Requ\u00eate SELECT', 'postgres')\nQgsProject.instance().addMapLayer(layer)\n

Exemple d'extension

Si besoin, l'extension PgMetadata utilise exclusivement l'API \"Base de donn\u00e9es PG\" de QGIS.

"},{"location":"python-qgis/","title":"Le python dans QGIS","text":"

QGIS permet d'utiliser du Python dans divers emplacement que nous allons voir ci-dessous. Python poss\u00e8de de tr\u00e8s nombreux packages/modules disponibles sur internet qui fournissent des fonctions d\u00e9j\u00e0 \u00e9crites.

"},{"location":"python-qgis/#console","title":"Console","text":"

La console est accessible par le menu Extension -> Console Python. Elle permet l'\u00e9criture de commande simple, une par une. On ne peut pas enregistrer les commandes dans un fichier.

"},{"location":"python-qgis/#script-python","title":"Script Python","text":"

L'\u00e9diteur de script Python est accessible depuis l'ic\u00f4ne d\u00e9di\u00e9e dans la console Python. Il permet un prototypage rapide d'un script. On peut y \u00e9crire du code plus complexe en faisant intervenir des librairies ou des classes.

"},{"location":"python-qgis/#script-processing","title":"Script Processing","text":"

Le menu Traitement dans QGIS donne acc\u00e8s a plusieurs algorithmes d'analyse. Ces algorithms proviennent soient de QGIS, GDAL ou encore de plugins. La bo\u00eete \u00e0 outils de traitements ainsi que le modeleur graphique utilisent le \"framework\" Processing propre \u00e0 QGIS. Ce framework permet de d\u00e9finir les entr\u00e9es et les sorties d'un algorithme. Les algorithms sont donc normalis\u00e9s en suivant tous le m\u00eame mod\u00e8le. Processing impose la fa\u00e7on d'\u00e9crire les scripts.

\u00c9crire un script compatible QGIS Processing permet l'int\u00e9gration dans ce menu, permet \u00e9galement l'utilisation de ce-dernier dans un mod\u00e8le ou encore l'utilisation en mode traitement par lot. Le framework peut aussi g\u00e9n\u00e9rer automatiquement l'interface graphique de l'algorithme et le code est optimis\u00e9.

Il existe un mod\u00e8le par d\u00e9faut que l'on peut utiliser pour d\u00e9marrer l'\u00e9criture d'un script Processing. Depuis la barre d'outils traitements, Cr\u00e9er un nouveau script depuis un mod\u00e8le. Ce mod\u00e8le utilise la syntaxe Programmation Orient\u00e9e Objet. Depuis QGIS 3.6, on peut \u00e9galement utiliser la syntaxe par d\u00e9corateur @alg.

Voir la documentation https://docs.qgis.org/latest/fr/docs/user_manual/processing/scripts.html#the-alg-decorator

"},{"location":"python-qgis/#un-modele-processing-en-python","title":"Un mod\u00e8le Processing en Python","text":"

Depuis QGIS 3.6, on peut d\u00e9sormais exporter un mod\u00e8le de traitement Processing en Python. Il faut faire un clic droit sur un mod\u00e8le dans la bo\u00eete \u00e0 outils puis choisir \"Exporter le mod\u00e8le comme un algorithme Python\". On peut donc modifier ensuite ce fichier Python afin de rajouter de la logique suppl\u00e9mentaire.

"},{"location":"python-qgis/#extension-plugin","title":"Extension (plugin)","text":""},{"location":"python-qgis/#fournisseur-processing-dans-une-extension-processing-provider","title":"Fournisseur Processing dans une extension (Processing Provider)","text":"

Similaire au script Processing, une extension QGIS peut aussi avoir son propre fournisseur d'algorithme.

On peut remarquer les plugins DataPlotly, QuickOSM etc.

Ajout de Processing \u00e0 un plugin QGIS :

Il se peut que certaines extensions ne soient que des fournisseurs Processing.

"},{"location":"python-qgis/#expressions","title":"Expressions","text":"

Les expressions sont souvent pr\u00e9sentes dans QGIS. On peut les utiliser dans nombreux endroits, pour faire des s\u00e9lections, des conditions, etc. On peut \u00e9galement les utiliser \u00e0 chaque fois que vous pouvez voir ce symbole :

Un plugin, ou m\u00eame simplement un utilisateur, peut enregistrer ses propres expressions. Ci-dessous, le plugin InaSAFE:

"},{"location":"python-qgis/#macros","title":"Macros","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

Accessible depuis les propri\u00e9t\u00e9s du projet, dans l'onglet Macros. On peut lancer du code Python automatiquement soit :

"},{"location":"python-qgis/#formulaire","title":"Formulaire","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

On peut personnaliser un formulaire par l'ajout de logique Python. Cependant, dans QGIS 3, l'utilisation de Python n'est plus forc\u00e9ment n\u00e9cessaire, on peut d\u00e9sormais utiliser des expressions (recommand\u00e9).

"},{"location":"python-qgis/#actions","title":"Actions","text":"

Les actions sont des petits traitements que l'on peut lancer soit depuis la table attributaire ou depuis le canevas. Par exemple, on peut ouvrir un lien WEB ou un PDF en fonction d'un attribut d'une entit\u00e9. Il est possible d'\u00e9crire les actions en Python.

Pour la cr\u00e9ation :

Pour l'utilisation c\u00f4t\u00e9 utilisateur :

"},{"location":"python-qgis/#applicationscript-independant","title":"Application/script ind\u00e9pendant","text":"

Sans lancer QGIS graphiquement, on peut utiliser la librairie QGIS dans nos scripts Python. On peut donc cr\u00e9er notre propre application graphique ou notre propre ex\u00e9cutable et ainsi utiliser les fonctions de QGIS. On peut donc faire un programme en ligne de commande qui effectue une certaine op\u00e9ration dans un r\u00e9pertoire donn\u00e9.

Depuis QGIS 3.16, nous pouvons lancer un mod\u00e8le ou un script Processing depuis la ligne de commande depuis l'outil qgis_process.

"},{"location":"python-qgis/#le-fichier-startuppy","title":"Le fichier \"startup.py\"","text":"

Si l'on place un fichier nomm\u00e9 startup.py dans le dossier Python du profil de l'utilisateur, QGIS va le lancer automatiquement \u00e0 chaque ouverture de QGIS.

"},{"location":"script-processing/","title":"Processing","text":"

Processing est un framework pour faire des algorithmes dans QGIS.

Toute la boite \u00e0 outils Traitement dans QGIS sont des bas\u00e9s sur \"Processing\".

Note, depuis QGIS 3.6, il existe d\u00e9sormais une autre syntaxe pour \u00e9crire script Processing \u00e0 l'aide des d\u00e9corateurs Python. Lire sur Docteur Python pour les d\u00e9corateurs

"},{"location":"script-processing/#documentation","title":"Documentation","text":"

Pour l'\u00e9criture d'un script Processing, tant en utilisant la POO ou la version avec les d\u00e9corateurs, il y a une page sur la documentation.

"},{"location":"script-processing/#utiliser-processing-en-python-avec-un-algorithme-existant","title":"Utiliser Processing en Python avec un algorithme existant","text":"

On peut appeler un traitement en ligne de commande Python :

result = processing.run(\n    \"native:buffer\", \n    {\n        'INPUT': '/chemin/vers/HYDROGRAPHIE/CANALISATION_EAU.shp',\n        'DISTANCE': 10,\n        'SEGMENTS': 5,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 2,\n        'DISSOLVE': False,\n        'OUTPUT': 'TEMPORARY_OUTPUT',\n    }\n)\n# print(result)\n

Tip

Pour obtenir l'identifiant de l'algorithme, laissez la souris sur le nom de l'algorithme pour avoir son info-bulle dans le panneau traitement.

Idem pour les identifiants des param\u00e8tres, dans la fen\u00eatre de l'algorithme.

Lien vers la documentation de Processing en console

QgsProject.instance().addMapLayer(result['OUTPUT'])\n

Pour obtenir la description d'un algorithme :

processing.algorithmHelp(\"native:buffer\")\n

Exercice, faire une 3 tampons sur la m\u00eame couche vecteur, distance 10, 20 et 30 m\u00e8tres, avec une fonction.

def tampon(distance):\n    result = processing.run(\n        \"native:buffer\", \n        {\n            'INPUT':'/chemin/vers/HYDROGRAPHIE/BARRAGE.shp',\n            'DISTANCE':distance,\n            'SEGMENTS':5,\n            'END_CAP_STYLE':0,\n            'JOIN_STYLE':0,\n            'MITER_LIMIT':2,\n            'DISSOLVE':False,\n            'OUTPUT':'TEMPORARY_OUTPUT'\n        }\n    )\n    QgsProject.instance().addMapLayer(result['OUTPUT'])\n\nfor x in [10, 20, 30]:\n    tampon(x)\n

Warning

Attention si utilisation de iface.activeLayer() qui va \u00eatre modifi\u00e9 si utilisation de QgsProject.instance().addMapLayer(). Il peut \u00eatre n\u00e9cessaire d'extraire la s\u00e9lection de la couche hors de la boucle.

Tip

Il existe aussi processing.runandLoadResults qui permet de charger directement les r\u00e9sultats, comme QGIS en mode graphique.

"},{"location":"script-processing/#lancer-linterface-graphique-de-notre-algorithme","title":"Lancer l'interface graphique de notre algorithme","text":"

Au lieu de processing.run, on peut cr\u00e9er uniquement le dialogue. Il faut alors l'afficher manuellement.

dialog = processing.createAlgorithmDialog(\n    \"native:buffer\",\n    {\n        'INPUT': '/data/lines.shp',\n        'DISTANCE': 100.0,\n        'SEGMENTS': 10,\n        'DISSOLVE': True,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 10,\n        'OUTPUT': '/data/buffers.shp'\n    }\n)\ndialog.show()\n

Ou alors directement lancer ex\u00e9cution du dialogue :

processing.execAlgorithmDialog(\n    \"native:buffer\",\n    {\n        'INPUT': '/data/lines.shp',\n        'DISTANCE': 100.0,\n        'SEGMENTS': 10,\n        'DISSOLVE': True,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 10,\n        'OUTPUT': '/data/buffers.shp'\n    }\n)\n
"},{"location":"script-processing/#convertir-un-modele-processing-en-python","title":"Convertir un mod\u00e8le Processing en python","text":"

Il est possible de convertir un mod\u00e8le Processing en script Python. On peut alors le modifier avec plus de finesse.

On ne peut pas reconvertir un script Python en mod\u00e8le.

"},{"location":"script-processing/#manipulation","title":"Manipulation","text":"
  1. Ouvrir en mod\u00e8le, par exemple ce fichier model3
  2. Depuis le mod\u00e8le, cliquer sur le bouton \"Convertir en script Processing\".
  3. Ouvrir en parall\u00e8le le script Processing Python de QGIS (Bo\u00eete \u00e0 outil \u2192 Python \u2192 Cr\u00e9er un nouveau script depuis un mod\u00e8le)

Avec QGIS < 3.40.2

Le mod\u00e8le par d\u00e9faut dans QGIS est un plus compr\u00e9hensible dans QGIS >= 3.40.2. On peut le r\u00e9cup\u00e9rer manuellement.

"},{"location":"script-processing/#utiliser-un-script-processing-dans-une-action","title":"Utiliser un script Processing dans une action","text":"

On peut utiliser processing.run() dans le code d'une action, pour faire une zone tampon sur un point en particulier par exemple.

On peut lancer, graphiquement depuis la bo\u00eete \u00e0 outil Processing, une zone tampon, avec une s\u00e9lection. Regardons ensuite dans l'historique Processing pour voir comment QGIS a pu sp\u00e9cifier la s\u00e9lection dans son appel PyQGIS.

On note l'usage d'une nouvelle classe QgsProcessingFeatureSourceDefinition.

On souhaite donc pouvoir faire une zone tampon personnalis\u00e9e en cliquant sur un point \u00e0 l'aide d'une action.

Il faut donc revoir le code dans le chapitre actions pour voir comment cr\u00e9er une action. Pour utiliser la s\u00e9lection, nous allons faire dans l'action :

from qgis.core import QgsProject, QgsVectorLayer\n\nlayer: QgsVectorLayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nlayer.selectByIds([int('[% $id %]')])\n# Ajouter ici le code processing.run avec une s\u00e9lection\nlayer.removeSelection()\n

On peut compl\u00e9ter l'action avec un processing.run en utilisant uniquement l'entit\u00e9 en s\u00e9lection.

Solution
import processing\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nlayer.selectByIds([int('[% $id %]')])\n\nresult = processing.run(\n    \"native:buffer\",\n    {\n        'INPUT':QgsProcessingFeatureSourceDefinition(layer.source(), selectedFeaturesOnly=True),\n        'DISTANCE':1000,\n        'SEGMENTS':5,\n        'END_CAP_STYLE':0,\n        'JOIN_STYLE':0,\n        'MITER_LIMIT':2,\n        'DISSOLVE':False,\n        'OUTPUT':'TEMPORARY_OUTPUT'\n    }\n)\nQgsProject.instance().addMapLayer(result['OUTPUT'])\n\nlayer.removeSelection()\n
"},{"location":"script-processing/#introduction-aux-decorateurs","title":"Introduction aux d\u00e9corateurs","text":"

Comme mentionn\u00e9 au d\u00e9but de ce chapitre, il est possible de ne pas utiliser la POO pour \u00e9crire un \"Script Processing\" mais plut\u00f4t l'\u00e9criture \u00e0 l'aide des d\u00e9corateurs.

C'est \"cens\u00e9\" \u00eatre plus simple, pour \u00e9viter de voir l'aspect de la programmation orient\u00e9 objet avec la complexit\u00e9 des classes.

Info

Cependant, cette syntaxe n'est pas compatible avec une extension Processing.

Dans la documentation QGIS, on trouve :

Le code suivant utilise le d\u00e9corateur @alg :

from qgis import processing\nfrom qgis.processing import alg\n\n\n@alg(name='bufferrasteralg', label='Buffer and export to raster (alg)',\n     group='examplescripts', group_label='Example scripts')\n@alg.input(type=alg.SOURCE, name='INPUT', label='Input vector layer')\n@alg.input(type=alg.RASTER_LAYER_DEST, name='OUTPUT',\n           label='Raster output')\n@alg.input(type=alg.VECTOR_LAYER_DEST, name='BUFFER_OUTPUT',\n           label='Buffer output')\n@alg.input(type=alg.DISTANCE, name='BUFFERDIST', label='BUFFER DISTANCE',\n           default=1.0)\n@alg.input(type=alg.DISTANCE, name='CELLSIZE', label='RASTER CELL SIZE',\n           default=10.0)\n@alg.output(type=alg.NUMBER, name='NUMBEROFFEATURES',\n            label='Number of features processed')\ndef bufferrasteralg(instance, parameters, context, feedback, inputs):\n   \"\"\"\n   Description of the algorithm.\n   (If there is no comment here, you will get an error)\n   \"\"\"\n   input_featuresource = instance.parameterAsSource(parameters,\n                                                    'INPUT', context)\n   numfeatures = input_featuresource.featureCount()\n   bufferdist = instance.parameterAsDouble(parameters, 'BUFFERDIST',\n                                           context)\n   rastercellsize = instance.parameterAsDouble(parameters, 'CELLSIZE',\n                                               context)\n\n   if feedback.isCanceled():\n      return {}\n\n   params = {\n      'INPUT': parameters['INPUT'],\n      'OUTPUT': parameters['BUFFER_OUTPUT'],\n      'DISTANCE': bufferdist,\n      'SEGMENTS': 10,\n      'DISSOLVE': True,\n      'END_CAP_STYLE': 0,\n      'JOIN_STYLE': 0,\n      'MITER_LIMIT': 10\n   }\n   buffer_result = processing.run(\n      'native:buffer',\n      params,\n      is_child_algorithm=True,\n      context=context,\n      feedback=feedback)\n\n   if feedback.isCanceled():\n      return {}\n\n   params = {\n      'LAYER': buffer_result['OUTPUT'],\n      'EXTENT': buffer_result['OUTPUT'],\n      'MAP_UNITS_PER_PIXEL': rastercellsize,\n      'OUTPUT': parameters['OUTPUT']\n   }\n   rasterized_result = processing.run(\n      'qgis:rasterize',\n      params,\n      is_child_algorithm=True, context=context,\n      feedback=feedback)\n\n   if feedback.isCanceled():\n      return {}\n\n   results = {\n      'OUTPUT': rasterized_result['OUTPUT'],\n      'BUFFER_OUTPUT': buffer_result['OUTPUT'],\n      'NUMBEROFFEATURES': numfeatures,\n   }\n   return results\n
"},{"location":"selection-parcours-entites/","title":"Fonctions sur une couche vecteur","text":""},{"location":"selection-parcours-entites/#utilisation-des-expressions-qgis","title":"Utilisation des expressions QGIS","text":"

Tip

Adapter le num\u00e9ro des codes INSEE ou des d\u00e9partements selon votre BDTOPO \ud83d\ude09

"},{"location":"selection-parcours-entites/#selection-dentite","title":"S\u00e9lection d'entit\u00e9","text":"

Nous souhaitons s\u00e9lectionner les entit\u00e9s dont le code INSEE commence par 77. Commen\u00e7ons par faire cela graphiquement dans QGIS Bureautique. \u00c0 l'aide d'une expression QGIS, s\u00e9lectionner les codes INSEE qui commencent par 77 (\u00e0 choisir un code INSEE propre au jeu de donn\u00e9es).

Solution en mode graphique :

\"INSEE_COM\" LIKE '77%'\n

Nous allons faire la m\u00eame chose, mais en utilisant Python. Pensez \u00e0 d\u00e9s\u00e9lectionner les entit\u00e9s.

Il va falloir \"\u00e9chapper\" un caract\u00e8re \u00e0 l'aide de \\. Voir la page Wikip\u00e9dia sur l'\u00e9chappement ou ce meme pour les devs \ud83e\udee2

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nlayer.removeSelection()\nlayer.selectByExpression(f\"\\\"INSEE_COM\\\" LIKE '77%'\")\n# layer.selectByExpression(f'\"INSEE_COM\" LIKE \\'77%\\'')  # R\u00e9sultat identique\nlayer.invertSelection()\nlayer.removeSelection()\n

Le raccourci iface.activeLayer() est tr\u00e8s pratique, mais de temps en temps, on a besoin de plusieurs couches qui sont d\u00e9j\u00e0 dans la l\u00e9gende. Il existe dans QgsProject plusieurs m\u00e9thodes pour r\u00e9cup\u00e9rer des couches dans la l\u00e9gende :

from qgis.core import QgsProject\n\nprojet = QgsProject.instance()\ncommunes = projet.mapLayersByName('communes')[0]\ninsee = projet.mapLayersByName('tableau INSEE')\n

Notons le s dans mapLayersByName. Il peut y avoir plusieurs couches avec ce m\u00eame nom de couche. La fonction retourne donc une liste de couches. Il convient alors de regarder si la liste est vide ou si elle contient plusieurs couches avec len(communes) par exemple.

Warning

mapLayersByName fait uniquement une recherche stricte, sensible \u00e0 la casse. Il faut passer par du code Python \"pure\" en it\u00e9rant sur l'ensemble des couches, ind\u00e9pendamment de leur nom si l'on souhaite faire une recherche plus fine. Si vraiment, on a besoin, on peut utiliser le module re (lien du Docteur Python).

from qgis.core import QgsProject\n\nprojet = QgsProject.instance()\ncommunes = projet.mapLayersByName('communes')\n\nif len(communes) == 0:\n    print(\"Pas de couches dans la l\u00e9gende qui se nomme 'communes'\")\n    layer = None\nelif len(communes) >= 1:\n    # TODO FIX ME, pas forc\u00e9ment la bonne couche 'communes'\n    layer = communes[0]\n
"},{"location":"selection-parcours-entites/#exemple-dune-selection-avec-un-export","title":"Exemple d'une s\u00e9lection avec un export","text":"

On souhaite pouvoir exporter les communes par d\u00e9partement. On peut cr\u00e9er une variable depts = ('34', '30') puis boucler dessus pour exporter les entit\u00e9s s\u00e9lectionn\u00e9es dans un nouveau fichier.

from pathlib import Path\nfrom qgis.core import QgsProject, QgsVectorFileWriter\nfrom qgis.utils import iface\n\nlayer = iface.activeLayer()\n\noptions = QgsVectorFileWriter.SaveVectorOptions()\noptions.driverName = 'ESRI Shapefile'\noptions.fileEncoding = 'UTF-8'\noptions.onlySelectedFeatures = True  # Nouvelle option pour la s\u00e9lection\n\ndepts = ('34', '30')\nfor dept in depts:\n    print(f\"Dept {dept}\")\n    layer.selectByExpression(f\"\\\"INSEE_DEP\\\"  =  '{dept}'\")\n    result = QgsVectorFileWriter.writeAsVectorFormatV3(\n        layer,\n        str(Path(QgsProject.instance().homePath()).joinpath(f'{dept}.shp')),\n        QgsProject.instance().transformContext(),\n        options\n    )\n    print(result)\n    if result[0] == QgsVectorFileWriter.WriterError.NoError:\n        print(\" \u2192 OK\")\n

Bonus

Si l'on souhaite parcourir automatiquement les d\u00e9partements existants, on peut r\u00e9cup\u00e9rer les valeurs uniques. Pour cela, il faut modifier deux lignes :

index = layer.fields().indexFromName(\"INSEE_DEP\")\nfor dept in layer.uniqueValues(index):\n
"},{"location":"selection-parcours-entites/#boucler-sur-les-entites-dune-couche-sans-expression","title":"Boucler sur les entit\u00e9s d'une couche sans expression","text":"

Si besoin, pour que la suite de l'exercice soit plus rapide, on peut utiliser une couche ARRONDISSEMENT par exemple.

On peut parcourir les entit\u00e9s d'une couche QgsVectorLayer \u00e0 l'aide de getFeatures().

Info

Avec PyQGIS, on peut acc\u00e9der aux attributs d'une QgsFeature simplement avec l'op\u00e9rateur [] sur l'objet courant comme s'il s'agissait d'un dictionnaire Python :

# Pour acc\u00e9der au champ \"NOM\" de l'entit\u00e9 \"feature\" :\nprint(feature['NOM'])\n

On peut le voir dans les exemples attribute de QgsFeature : https://qgis.org/pyqgis/3.34/core/QgsFeature.html#qgis.core.QgsFeature.attribute

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    print(feature)\n    print(feature['NOM'])\n    print(feature.attribute('NOM'))\n
"},{"location":"selection-parcours-entites/#boucler-sur-les-entites-a-laide-dune-expression","title":"Boucler sur les entit\u00e9s \u00e0 l'aide d'une expression","text":"

L'objectif est d'afficher dans la console le nom des communes dont la code d\u00e9partement INSEE_DEP correspond uniquement \u00e0 un seul d\u00e9partement arbitraire.

L'exemple \u00e0 ne pas faire, m\u00eame si cela fonctionne (car on peut l'optimiser tr\u00e8s facilement) :

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    if feature['INSEE_DEP'] == '84':\n        print(f'{feature['NOM']} : d\u00e9partement {feature['INSEE_DEP']}')\n
  1. Imaginons qu'il s'agisse d'une couche PostgreSQL, sur un serveur distant
  2. On demande \u00e0 QGIS de r\u00e9cup\u00e9rer l'ensemble de la table distante, \u00e9quivalent \u00e0 SELECT * FROM ma_table
  3. Puis, on filtre dans QGIS (toute la donn\u00e9e est pr\u00e9sente dans QGIS Bureautique d\u00e9sormais)

Tip

Ce qui prend du temps lors de l'ex\u00e9cution, c'est surtout le print en lui-m\u00eame. Si vous n'utilisez pas print, mais un autre traitement, cela sera plus rapide. Un simple print ralenti l'ex\u00e9cution d'un script.

"},{"location":"selection-parcours-entites/#optimisation-de-la-requete","title":"Optimisation de la requ\u00eate","text":"

Dans la documentation, observez bien la signature de la fonction getFeatures. Que remarquez-vous ? Utilisons donc une expression pour limiter les r\u00e9sultats.

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest\n\nlayer = iface.activeLayer()\n\nrequest = QgsFeatureRequest()\n# \u00c9quivalent \u00e0 SELECT * FROM ma_table WHERE \"INSEE_DEP\" = '84'\nrequest.setFilterExpression('\"INSEE_DEP\" = \\'84\\'')\n\nfor feature in layer.getFeatures(request):\n    print(f'{feature['NOM']} : d\u00e9partement {feature['INSEE_DEP']}')\n

Nous pouvons accessoirement ordonner les r\u00e9sultats et surtout encore optimiser la requ\u00eate en :

La solution pour les experts
request = QgsFeatureRequest()\nrequest.setFilterExpression('\"INSEE_DEP\" = \\'84\\'')\nrequest.addOrderBy('NOM')\nrequest.setFlags(QgsFeatureRequest.NoGeometry)\n# request.setSubsetOfAttributes([1, 4]) autre mani\u00e8re moins pratique, historique\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\n# # \u00c9quivalent \u00e0 SELECT NOM, POPULATION FROM ma_table WHERE \"INSEE_DEP\" = '84' ORDER BY NOM\nfor feature in layer.getFeatures(request):\n    print('{commune} : {nombre} habitants'.format(commune=feature['NOM'], nombre=feature['POPULATION']))\n
"},{"location":"selection-parcours-entites/#enregistrement-dune-requete-dans-une-couche-en-memoire","title":"Enregistrement d'une requ\u00eate dans une couche en m\u00e9moire","text":"

Si l'on souhaite \"enregistrer\" le r\u00e9sultat de cette expression QGIS, on peut la mat\u00e9rialiser dans une nouvelle couche :

memory_layer = layer.materialize(request)\nQgsProject.instance().addMapLayer(memory_layer)\n

Warning

Attention \u00e0 la ligne iface.activeLayer() qui peut changer lors de l'ajout d'une nouvelle couche dans la l\u00e9gende.

Regardons le r\u00e9sultat et corrigeons ce probl\u00e8me d'export afin d'obtenir les g\u00e9om\u00e9tries et les attributs, il faut supprimer la ligne NoGeometry si vous l'avez.

"},{"location":"selection-parcours-entites/#valeur-null","title":"Valeur NULL","text":"

En PyQGIS, il existe la valeur NULL qui peut \u00eatre pr\u00e9sente dans la table attributaire d'une couche vecteur.

from qgis.PyQt.QtCore import NULL\n\nif feature['nom_attribut'] == NULL:\n    # Traiter la valeur NULL\n    pass\nelse:\n    # Continuer\n    pass\n
"},{"location":"selection-parcours-entites/#calculer-un-champ-densite","title":"Calculer un champ \"densite\"","text":"

Nous souhaitons avoir une colonne densite dans notre table attributaire, avec la densit\u00e9 de population.

Mais regardons avant la gestion des erreurs lors d'un traitement. En effet, nous allons vouloir \"caster\" (transformer le type) de la variable population en entier, mais attention, il y a des valeurs NC dans les valeurs.

_Note, il n'y a d\u00e9sormais plus de valeur NC dans le champ POPULATION dans la donn\u00e9e, mais imaginons. Il peut s'agir d'une autre couche dont on ne connait pas la provenance et le contenu.

"},{"location":"selection-parcours-entites/#les-exceptions-en-python","title":"Les exceptions en Python","text":"

Avant de traiter cet exercice, nous devons voir ce qu'est une exception en Python.

\u00c0 plusieurs reprises depuis le d\u00e9but de la formation, il est fort \u00e0 parier que nous ayons des messages en rouges dans la console de temps en temps. Ce sont des exceptions. C'est une notion de programmation qui existe dans beaucoup de languages.

Dans le langage informatique, une exception peut-\u00eatre :

Essayons dans la console de faire une op\u00e9ration 10 / 2 :

10 / 2\n

Essayons cette fois-ci 10 / 0, ce qui est math\u00e9matiquement impossible :

10 / 0\n

Passons cette fois-ci dans un script pour que cela soit plus simple, et voir que le script s'arr\u00eate brutalement \ud83d\ude09

print('D\u00e9but')\nprint(10 / 2)\nprint('Fin')\n

On peut \"attraper\" cette erreur Python \u00e0 l'aide d'un try ... except... :

print('D\u00e9but')\ntry:\n    print(10 / 2)\nexcept ZeroDivisionError:\n    print('Ceci est une division par z\u00e9ro !')\nprint('Fin')\n

Le try permet d'essayer le code qui suit. Le except permet d'attraper en filtrant s'il y a des exceptions et de traiter l'erreur si besoin.

Tip

On peut avoir une ou plusieurs lignes de code dans chacun de ces blocs. On peut appeler des fonctions, etc.

"},{"location":"selection-parcours-entites/#une-exception-remonte-le-fil-dexecution-du-programme","title":"Une exception remonte le fil d'ex\u00e9cution du programme","text":"

Important, une exception remonte tant qu'elle n'est pas attrap\u00e9e :

def function_3():\n    print(\"D\u00e9but fonction 3\")\n    a = 10\n    b = 2\n    print(f\"\u2192 {a} / {b} = {a/b}\")\n    print(\"Fin fonction 3\")\n\ndef function_2():\n    print(\"D\u00e9but fonction 2\")\n    function_3()\n    print(\"Fin fonction 2\")\n\ndef function_1():\n    print(\"D\u00e9but fonction 1\")\n    function_2()\n    print(\"Fin fonction 1\")\n\nfunction_1()\n

Testons d\u00e9sormais d'attraper l'erreur dans la fonction 1 :

try:\n    function_2()\nexcept ZeroDivisionError:\n    print(\"Fin de l'exception\")\n

On voit que Python, quand il peut, nous indique la \"stacktrace\" ou encore \"traceback\", c'est-\u00e0-dire une sorte de fil d'ariane.

"},{"location":"selection-parcours-entites/#heritage-des-exceptions","title":"H\u00e9ritage des exceptions","text":"

Toutes les exceptions h\u00e9ritent de Exception donc le code ci-dessous fonctionne, mais n'est pas recommand\u00e9, car il masque d'autres erreurs :

try:\n    a = 10 / 5\n\n    mois = ['janvier', 'f\u00e9vrier']\n    b = mois[0]\n\nexcept Exception:\n    print('Erreur inconnue')\n

On peut par contre \"encha\u00eener\" les exceptions, afin de filtrer progressivement les exceptions.

try:\n    a = 10 / 5\n    # a = 10 / 0\n    # a = 10 / int(\"NC\")\n    # a = 10 / \"NC\"\nexcept ZeroDivisionError:\n    print('Erreur, division par 0')\nexcept ValueError:\n    print(\"Erreur, il n'y a avait pas que des chiffres.\")\nexcept Exception:\n    print('Erreur inconnue')\n

Il existe d'autres mots-cl\u00e9s en Python pour les exceptions comme finally: et else:. Voir un autre tutoriel.

\u00c9videment, on peut v\u00e9rifier la valeur de b en amont si c'est \u00e9gal \u00e0 0. Mais ceci est pour pr\u00e9senter le concept des exceptions en Python.

"},{"location":"selection-parcours-entites/#retour-a-lexercice","title":"Retour \u00e0 l'exercice","text":"

On souhaite donc savoir si un nombre est transformable en entier, dans le cas de la population (s'il y a NC par exemple) :

int('10')\nint('NC')\n

Correction possible de l'exercice :

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest\n\nlayer = iface.activeLayer()\nrequest = QgsFeatureRequest()\n# request.setLimit(5)  # Pour aller plus vite si-besoin\nrequest.addOrderBy('NOM')\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\nfor feature in layer.getFeatures(request):\n    area = feature.geometry().area() / 1000000\n    try:\n        population = int(feature['POPULATION'])\n        # Par exemple, pour nettoyer une cha\u00eene de caract\u00e8re en Python des espaces avant/apr\u00e8s : \"  Bonjour  \".strip()\n        # \"rstrip\"/\"lstrip\" existent \u00e9galement\n    except ValueError:\n        population = 0\n\n    densite = population/area\n\n    print(f\"{feature['NOM']} : {densite} habitants/km\u00b2\")\n

Nous souhaitons enregistrer ces informations dans une vraie table avec un nouveau champ densite_population.

Solution possible :

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest, QgsField, edit\n\nfrom qgis.PyQt.QtCore import QVariant\n\nlayer = iface.activeLayer()\n\nif 'densite' not in layer.fields().names():\n    with edit(layer):\n        field = QgsField('densite', QVariant.Double, prec=2, len=2)\n        layer.addAttribute(field)\n\nindex = layer.fields().indexFromName('densite')\nlayer.startEditing()\nrequest = QgsFeatureRequest()\n# request.setLimit(5)  # Pour aller plus vite si-besoin\nrequest.addOrderBy('NOM')\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\n# SELECT NOM, POPULATION FROM communes\nfor feature in layer.getFeatures(request):\n    area = feature.geometry().area() / 1000000\n    try:\n        population = int(feature['POPULATION'])\n    except ValueError:\n        population = 0\n\n    densite = population/area\n\n    # Cette ligne n'aura aucun effet, contrairement \u00e0 l'exercice d'export au format CSV pr\u00e9c\u00e9dent.\n    # https://docs.3liz.org/formation-pyqgis/fonctions-scripts/#solution-possible\n    # La variable \"feature\" est une copie, comme un peu le r\u00e9sultat du SELECT * FROM ma_table LIMIT 5\n    # Un SELECT est en lecture seule. Ce n'est pas comme \u00e7a que cela se passe, c'est pour imager.\n    feature['densite'] = densite\n\n    # Uniquement l'appel \u00e0 \"changeAttributeValue\" fonctionne\n    # Pour information, il existe \"changeGeometry\" pour la m\u00eame raison\n    # Un peu comme la commande SQL UPDATE, sur une entit\u00e9 existante, bien qu'il ne faille pas oublier la session d'\u00e9dition.\n    layer.changeAttributeValue(feature.id(), index, densite)\n    # print('{commune} : {densite} habitants/km\u00b2'.format(commune=feature['NOM'], densite=round(population/area,2)))\n\nlayer.commitChanges()\n
"},{"location":"selection-parcours-entites/#calculer-deux-champs-en-utilisant-la-geometrie-et-une-reprojection-a-la-volee","title":"Calculer deux champs en utilisant la g\u00e9om\u00e9trie et une reprojection \u00e0 la vol\u00e9e","text":"

Manipulons d\u00e9sormais la g\u00e9om\u00e9trie en ajoutant le centro\u00efde de la commune dans une colonne latitude et longitude en degr\u00e9es.

Warning

TODO, en cours de correction, suppression de la variable petite_communes

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest, QgsField, edit\n\nlayer = iface.activeLayer()\n\nrequest = QgsFeatureRequest()\nrequest.setFilterExpression('to_int( \"POPULATION\" ) < 1000')\npetites_communes = layer.materialize(request)\n\nwith edit(petites_communes):\n    petites_communes.addAttribute(QgsField('densite_population', QVariant.Double))\n\n    # /!\\ Ajouter les 2 lignes ci-dessous\n    petites_communes.addAttribute(QgsField('longitude', QVariant.Double))\n    petites_communes.addAttribute(QgsField('latitude', QVariant.Double))\n\nrequest = QgsFeatureRequest()\nrequest.setSubsetOfAttributes([4])\n\n# /!\\ Ajouter les 2 lignes ci-dessous \u00e0 propos de la transformation\ntransform = QgsCoordinateTransform(\n    QgsCoordinateReferenceSystem(\"EPSG:2154\"), QgsCoordinateReferenceSystem(\"EPSG:4326\"), QgsProject.instance())\n\nwith edit(petites_communes):\n    for feature in petites_communes.getFeatures(request):\n        area = feature.geometry().area() / 1000000\n        population = int(feature['POPULATION'])\n        densite=population/area\n        petites_communes.changeAttributeValue(feature.id(), 5, densite)\n\n        # /!\\ Ajouter les lignes ci-dessous\n        geom = feature.geometry()\n        # La transformation affecte directement l'objet Python en cours, mais pas l'entit\u00e9 dans la couche\n        geom.transform(transform)\n        centroid = geom.centroid().asPoint()\n        petites_communes.changeAttributeValue(feature.id(), 6, centroid.x())\n        petites_communes.changeAttributeValue(feature.id(), 7, centroid.y())\n\nQgsProject.instance().addMapLayer(petites_communes)\n
"},{"location":"signal-slot/","title":"Les signaux et les slots","text":""},{"location":"signal-slot/#definition","title":"D\u00e9finition","text":"

Nous avons pu voir dans la documentation des librairies Qt et QGIS, il y a une section Signals.

Chaque objet \u00e9met tr\u00e8s souvent un signal d\u00e8s qu'une action est faite sur l'objet. Cela sert \u00e0 d\u00e9clencher du code Python lorsqu'un signal pr\u00e9cis est \u00e9mis.

Par exemple sur la documentation de QgsMapLayer, on peut chercher le tableau signals.

Info

Comme on peut le voir dans la documentation CPP, c'est d\u00e9sormais dans la classe QgsMapLayer et non QgsVectorLayer depuis QGIS 3.22. \ud83e\uddd0

La plupart des signaux sont aux pass\u00e9s, par exemple crsChanged, nameChanged. Cependant, certaines sont dans un \"futur\" proche comme willBeDeleted.

"},{"location":"signal-slot/#syntaxe","title":"Syntaxe","text":"

On dit que l'on souhaite connecter un signal \u00e0 une fonction/slot :

variable_de_lobjet.nom_du_signal.connect(nom_de_la_fonction)\n

Danger

Il ne faut pas \u00e9crire nom_de_la_fonction() car on ne souhaite pas appeler la fonction, juste connecter.

Cela sera Python, plus tard, quand le signal sera \u00e9mis, que la fonction sera r\u00e9ellement appel\u00e9e.

"},{"location":"signal-slot/#exemple","title":"Exemple","text":"

Par exemple, dans la classe QgsMapLayer, cherchons un signal qui est \u00e9mis apr\u00e8s (before) que la session d'\u00e9dition commence. Il s'agit de editingStarted.

Affichons un message \u00e0 l'utilisateur lors du d\u00e9but et de la fin d'une session d'\u00e9dition.

Tip

On profite de cet exemple pour voir comment \u00e9valuer une expression QGIS \u00e0 l'aide des diff\u00e9rents contextes.

def user_from_qgis() -> str:\n    \"\"\" \u00c0 l'aide d'une expression QGIS, r\u00e9cup\u00e9ration du nom d'utilisateur.\"\"\"\n    context = QgsExpressionContext()\n    context.appendScope(QgsExpressionContextUtils.globalScope())\n    # On peut th\u00e9oriquement s'arr\u00eater \u00e0 ce niveau la concernant l'ajout des \"scopes\" avec le GlobalScope\n    # Mais on peut ajouter d'autre \"scope\", comme ajouter celui du projet :\n    # context.appendScope(QgsExpressionContextUtils.projectScope(project))\n    # context.appendScope(QgsExpressionContextUtils.layerScope(layer))\n\n    # On peut ensuite \u00e9valuer l'expression QGIS\n    # \"@user_account_name\" ou encore \"@user_full_name\"\n    expression = QgsExpression(\"@user_full_name\")\n    return expression.evaluate(context)\n\ndef user_from_os() -> str:\n    \"\"\" \u00c0 l'aide de l'OS, retourne le nom d'utilisateur.\"\"\"\n    import os\n    return os.getlogin()\n\ndef we_are_watching_you():\n    \"\"\" Just warn the user about the editing session.\"\"\"\n    current_user = user_from_qgis()\n    # Attention aux effets si on lance le code plusieurs fois !\n    # print(\"Hello \ud83d\ude09\")\n    iface.messageBar().pushMessage('Hey',f'Be careful <strong>{current_user}</strong> while you are editing \ud83e\uddd0', Qgis.Warning)\n\ndef thanks():\n    iface.messageBar().pushMessage('Hey', \"Thanks \ud83d\ude09\", Qgis.Success)\n\n\nlayer = iface.activeLayer()\n\nlayer.beforeEditingStarted.connect(we_are_watching_you)\nlayer.editingStopped.connect(thanks)\n
"},{"location":"standalone/","title":"Librairie QGIS","text":""},{"location":"standalone/#qgis-process","title":"QGIS Process","text":"

Depuis QGIS 3.16, il existe un outil qgis_process qui permet de lancer QGIS Processing en ligne de commande.

Quelques rappels pour utiliser la ligne de commande sous Windows :

Dans le shell OSGEO, taper :

cd C:/Program Files/QGIS 3.14/bin/\n# Il peut s'agit du chemin ci-dessous\ncd C:\\OSGeo4W\\apps\\qgis-ltr\\bin\\\n

On doit avoir d\u00e9sormais un ex\u00e9cutable qgis_process-qgis-ltr.bat

qgis_process-qgis-ltr.bat\nqgis_process-qgis-ltr.bat --help\nqgis_process-qgis-ltr.bat list\n

On peut lancer les algorithmes, les mod\u00e8les, les scripts Python qui sont dans la version graphique de QGIS Processing.

On peut donc lancer en ligne de commande, ou alors avec notre propre ic\u00f4ne sur son bureau un ex\u00e9cutable.

qgis_process help qgis:buffer\nqgis_process run qgis:buffer -- INPUT=/home/etienne/source.shp DISTANCE=2 OUTPUT=/tmp/sortie.gpkg\n

L'id\u00e9e de QGIS Process est soit de faire un petit ex\u00e9cutable ou alors de lancer le programme \u00e0 intervalle de temps r\u00e9gulier.

"},{"location":"standalone/#standalone-application","title":"Standalone application","text":"

Il est possible de faire un programme qui ne se lance pas dans QGIS Bureautique mais qui utilise la librairie QGIS qui se trouve sur l'ordinateur.

Warning

Gr\u00e2ce \u00e0 qgis_process, c'est exemple d'application standalone perd la plupart de son int\u00e9r\u00eat. Pour lancer QGIS en ligne de commande pour faire des traitements, il est d\u00e9sormais fortement conseill\u00e9 d'utiliser qgis_process.

On peut donc cr\u00e9er son propre programme, en ligne de commande ou avec une interface graphique qui utilise le moteur de QGIS en arri\u00e8re-plan pour utiliser ce que sait d\u00e9j\u00e0 faire QGIS.

Exemple sur le gist de Thomas Gratier

# Code borrowed from https://subscription.packtpub.com/book/application_development/9781783984985/1/ch01lvl1sec18/creating-a-standalone-application\n# and upgraded for QGIS 3.0\nimport os\nimport sys\nimport shutil\nimport tempfile\nimport urllib.request\nfrom zipfile import ZipFile\nfrom glob import glob\n\nfrom qgis.core import (QgsApplication, QgsCoordinateReferenceSystem, QgsFeature,\n                   QgsGeometry, QgsProject, QgsRasterLayer, QgsVectorLayer)\nfrom qgis.gui import QgsLayerTreeMapCanvasBridge, QgsMapCanvas\nfrom qgis.PyQt.QtCore import Qt\n# Unused so commented\n# from qgis.PyQt.QtGui import *\n\napp = QgsApplication([], True)\n# On Windows : https://gis.stackexchange.com/questions/334172/creating-standalone-application-in-qgis\n# On Linux, didn't need to set it so commented\n# app.setPrefixPath(\"C:/Program Files/QGIS Brighton/apps/qgis\", True)\napp.initQgis()\ncanvas = QgsMapCanvas()\ncanvas.setWindowTitle(\"PyQGIS Standalone Application Example\")\ncanvas.setCanvasColor(Qt.white)\ncrs = QgsCoordinateReferenceSystem('EPSG:3857')\nproject = QgsProject.instance()\ncanvas.setDestinationCrs(crs)\n\nurlWithParams = 'type=xyz&url=https://a.tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=19&zmin=0&crs=EPSG3857'\nrlayer2 = QgsRasterLayer(urlWithParams, 'OpenStreetMap', 'wms')\n\nif rlayer2.isValid():\n    project.addMapLayer(rlayer2)\nelse:\n    print('invalid layer')\n\n# Download shp ne_10m_admin_0_countries.shp and associated files in the same directory\nurl = \"https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip\"\nif not glob(\"ne_10m_admin_0_countries.*\"):\n    with urllib.request.urlopen(url) as response:\n        with tempfile.NamedTemporaryFile(delete=False) as tmp_file:\n            shutil.copyfileobj(response, tmp_file)\n        with ZipFile(tmp_file.name, 'r') as zipObj:\n            # Extract all the contents of zip file in current directory\n            zipObj.extractall()\n\nlayer_shp = QgsVectorLayer(os.path.join(os.path.dirname(__file__), \"ne_10m_admin_0_countries.shp\"), \"Natural Earth\", \"ogr\")\nif not layer_shp.isValid():\n  print(\"Layer failed to load!\")\n\nproject.addMapLayer(layer_shp)\n\nprint(layer_shp.crs().authid())\nprint(rlayer2.crs().authid())\ncanvas.setExtent(layer_shp.extent())\ncanvas.setLayers([rlayer2, layer_shp])\ncanvas.zoomToFullExtent()\n# canvas.freeze(True)\ncanvas.show()\ncanvas.refresh()\n# canvas.freeze(False)\ncanvas.repaint()\nbridge = QgsLayerTreeMapCanvasBridge(\n    project.layerTreeRoot(),\n    canvas\n)\n\ndef run_when_project_saved():\n    print('Saved')\n\nproject.projectSaved.connect(run_when_project_saved)\n\nproject.write('my_new_qgis_project.qgz')\n\ndef run_when_application_state_changed(state):\n    print('State changed', state)\n\napp.applicationStateChanged.connect(run_when_application_state_changed)\n\nexitcode = app.exec()\nQgsApplication.exitQgis()\nsys.exit(True)\n
"},{"location":"symbologie/","title":"Symbologie","text":"

\u00c9tant donn\u00e9 que la symbologie pouvant \u00eatre complexe dans QGIS Bureautique, avec les diff\u00e9rents types de symbologie, les diff\u00e9rents niveaux de symbole, les ensembles de r\u00e8gles avec des filtres etc, il n'est pas forc\u00e9ment simple de s'y retrouver dans l'API PyQGIS \u00e9galement.

"},{"location":"symbologie/#utilisation-dun-qml-au-lieu-de-pyqgis","title":"Utilisation d'un QML au lieu de PyQGIS","text":"

On peut se passer de PyQGIS pour fournir une symbologie \u00e0 l'aide d'un fichier QML, si on ne souhaite pas faire \u00e7a en Python enti\u00e8rement.

Regarder les m\u00e9thodes loadNamedStyle de la classe QgsMapLayer.

from pathlib import Path\n\nlayer = iface.activeLayer()\n\nqml = Path(\"path_to_qml\")\nif qml.exists():\n    layer.loadNamedStyle(str(qml))\n    iface.legendInterface().refreshLayerSymbology(layer)\n
"},{"location":"symbologie/#classes-utiles-en-pyqgis","title":"Classes utiles en PyQGIS","text":"

Voir les graphiques d'h\u00e9ritage sur :

"},{"location":"symbologie/#afficher-les-infos-de-la-symbologie","title":"Afficher les infos de la symbologie","text":"
layer = iface.activeLayer()\nrenderer = layer.renderer()\nprint(renderer.dump())\n

SINGLE: FILL SYMBOL (1 layers) color 125,139,143,255

layer = iface.activeLayer()\nrenderer = layer.renderer()\nprint(renderer.symbol().symbolLayers()[0].properties())\n

{'border_width_map_unit_scale': '3x:0,0,0,0,0,0', 'color': '125,139,143,255', 'joinstyle': 'bevel', 'offset': '0,0', 'offset_map_unit_scale': '3x:0,0,0,0,0,0', 'offset_unit': 'MM', 'outline_color': '35,35,35,255', 'outline_style': 'solid', 'outline_width': '0.26', 'outline_width_unit': 'MM', 'style': 'solid'}

"},{"location":"symbologie/#affecter-une-symbologie-a-une-couche","title":"Affecter une symbologie \u00e0 une couche","text":"

Il peut \u00eatre tr\u00e8s pratique de partir d'une symbologie existante, faite via l'interface graphique, puis de l'exporter pour voir les propri\u00e9t\u00e9s.

"},{"location":"symbologie/#un-symbole-ponctuel-unique-simple","title":"Un symbole ponctuel unique simple","text":"
from qgis.core import QgsMarkerSymbol, QgsSingleSymbolRenderer\n\nsymbol = QgsMarkerSymbol.createSimple(\n    {\n        \"name\": \"circle\",\n        \"color\": \"yellow\",\n        \"size\": 3,\n    }\n)\nrenderer = QgsSingleSymbolRenderer(symbol)\nlayer = iface.activeLayer()\nlayer.setRenderer(renderer)\n# layer.triggerRepaint()  # If necessary\n
"},{"location":"symbologie/#un-symbole-lineaire-unique-sous-forme-de-fleche","title":"Un symbole lin\u00e9aire unique sous forme de fl\u00e8che","text":"
from qgis.core import QgsApplication, QgsSymbol, Qgis, QgsSingleSymbolRenderer\nfrom qgis.PyQt.QtGui import QColor\n\n# Quelques propri\u00e9t\u00e9s d'une fl\u00e8che si besoin de surcharger. Utiliser le code PyQGIS pour r\u00e9cup\u00e9rer la liste des propri\u00e9t\u00e9s.\nARROW = {\n    'arrow_start_width': '1',\n    'arrow_start_width_unit': 'MM',\n    'arrow_start_width_unit_scale': '3x:0,0,0,0,0,0',\n    'arrow_type': '0',\n}\n\nregistry = QgsApplication.symbolLayerRegistry()\nline_metadata = registry.symbolLayerMetadata('ArrowLine')\nline_layer = line_metadata.createSymbolLayer(ARROW)\nline_layer.setColor(QColor('#33a02c'))\n\nsymbol = QgsSymbol.defaultSymbol(Qgis.GeometryType.LineGeometry)\nsymbol.deleteSymbolLayer(0)\nsymbol.appendSymbolLayer(line_layer)\n\n\nrenderer = QgsSingleSymbolRenderer(symbol)\nlayer = iface.activeLayer()\nlayer.setRenderer(renderer)\n
"}]} \ No newline at end of file +{"config":{"lang":["fr"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Formation PyQGIS","text":""},{"location":"#pre-requis","title":"Pr\u00e9-requis","text":"

Cette formation concerne des utilisateurs de QGIS, g\u00e9omaticiens, qui souhaitent apprendre l'API Python de QGIS :

Pour suivre la formation, il faut :

Si n\u00e9cessaire, il peut \u00eatre utile d'avoir en plus :

"},{"location":"#plan","title":"Plan","text":""},{"location":"action/","title":"Les actions","text":"

Info

La couche HYDROGRAPHIE/COURS_D_EAU.shp est de type multilinestring. Nous allons donc prendre en compte ce cas par d\u00e9faut dans la suite de ce tutoriel.

# Notation pour ajouter des attributs en cr\u00e9ant une couche m\u00e9moire\n# https://docs.qgis.org/latest/fr/docs/pyqgis_developer_cookbook/vector.html#from-an-instance-of-qgsvectorlayer\nriver = QgsVectorLayer('MultiLineString?crs=epsg:2154&field=id:integer&field=name:string(20)&index=yes', 'Rivers', 'memory')\n\nQgsProject.instance().addMapLayer(river)\n\nwith edit(river):\n    # Cette fonction permet de faire des v\u00e9rifications sur les contraintes si n\u00e9cessaires contrairement \u00e0 QgsFeature(fields)\n    feature = QgsVectorLayerUtils.createFeature(river)\n    feature.setAttribute('id', 0)\n    feature.setAttribute('name', 'Une rivi\u00e8re')\n    geom = QgsGeometry.fromMultiPolylineXY(\n    [\n        [QgsPointXY(1, 1), QgsPointXY(2, 2), QgsPointXY(3, 2), QgsPointXY(4, 1)]\n    ])\n    feature.setGeometry(geom)\n    river.addFeature(feature)\n\nextent = river.extent()\ncanvas = iface.mapCanvas()\ncanvas.setExtent(extent)\ncanvas.refresh()\n
"},{"location":"action/#les-actions-par-defaut","title":"Les actions par d\u00e9faut","text":"

Info

Selon le champ d'application de l'action, il y a plus ou moins de variables. Il faut regarder les infobulles.

"},{"location":"action/#notre-propre-action","title":"Notre propre action","text":"
def reverse_geom(layer: QgsVectorLayer, ids: int):\n    \"\"\" Inverser le sens des diff\u00e9rentes entit\u00e9s dans la couche layer.\n\n    ids est l'identifiant d'une entit\u00e9 qu'il faut inverser.\n    \"\"\"\n    pass\n

Le mot-cl\u00e9 pass est juste une instruction Python qui ne fait strictement rien, mais qui permet de rendre une ligne de code valide en respectant l'indentation. Vous pouvez la supprimer d\u00e8s qu'il y a du code.

Il faut :

On peut appeler notre nouvelle fonction \u00e0 l'aide du code suivant :

layer = iface.activeLayer()\n# Une action ne s'effectuant que sur une seule entit\u00e9, on peut utiliser [0]\nids = layer.selectedFeatureIds()[0]\n\nreverse_geom(layer, ids)\n
Afficher la solution
def reverse_geom(layer, ids):\n    \"\"\" Inverser le sens d'une entit\u00e9 dans la couche layer.\n\n    ids est une liste comportant les IDs des entit\u00e9s \u00e0 inverser.\n    \"\"\"\n    feature = layer.getFeature(ids)\n    geom = feature.geometry()\n    lines = geom.asMultiPolyline()\n    for line in lines:\n        line.reverse()\n    new_geom = QgsGeometry.fromMultiPolylineXY(lines)\n    with edit(layer):\n        layer.changeGeometry(feature.id(), new_geom)\n\nlayer = iface.activeLayer()\nids = layer.selectedFeatureIds()[0]\n\nreverse_geom(layer, ids)\n

Incorporons ce code dans une action et adaptons-le l\u00e9g\u00e8rement :

def reverse_geom(layer, ids):\n    \"\"\" Inverser le sens d'une entit\u00e9 dans la couche layer.\n\n    ids est une liste comportant les IDs des entit\u00e9s \u00e0 inverser.\n    \"\"\"\n    feature = layer.getFeature(ids)\n    geom = feature.geometry()\n    lines = geom.asMultiPolyline()\n    for line in lines:\n        line.reverse()\n    new_geom = QgsGeometry.fromMultiPolylineXY(lines)\n    with edit(layer):\n        layer.changeGeometry(feature.id(), new_geom)\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nreverse_geom(layer, [% @id %])\n

On peut d\u00e9sormais cliquer sur une ligne pour automatiquement inverser une ligne.

Le code de l'action est enregistr\u00e9 dans le style QML de la couche vecteur. Il peut donc \u00eatre partag\u00e9 avec d'autres utilisateurs qui ne connaissent pas Python.

"},{"location":"action/#informer-lutilisateur","title":"Informer l'utilisateur","text":"

Si on souhaite informer l'utilisateur que cela s'est bien pass\u00e9, on peut utiliser la \"message bar\" :

from qgis.utils import iface\n\niface.messageBar().pushMessage('Inversion', 'La rivi\u00e8re est invers\u00e9e', Qgis.Success)\n

Note, contrairement \u00e0 la console o\u00f9 QGIS importait pour nous directement la variable iface, dans ce contexte, il faut le faire manuellement.

"},{"location":"action/#astuce-pour-stocker-le-code-dune-action-dans-une-extension-qgis","title":"Astuce pour stocker le code d'une action dans une extension QGIS","text":"

Tip

Pour suivre cette partie, il faut la plupart du temps une extension par exemple, voir l'autre chapitre, afin de stocker le code Python.

Pour \u00e9viter d'avoir du code les propri\u00e9t\u00e9s de la couche QGIS, on peut r\u00e9duire le code Python au minimum en faisant dans le c\u0153ur de l'action uniquement l'import d'une fonction et de lancer son ex\u00e9cution.

Exemple du code d'une action dans l'extension QuickOSM lors de l'ex\u00e9cution d'une requ\u00eate rapide :

from QuickOSM.core.actions import Actions\nActions.run(\"josm\",\"[% \"full_id\" %]\")\n

Ou alors l'extension RAEPA :

from qgis.utils import plugins\nplugins['raepa'].run_action(\"nom_de_laction\", params)\n
"},{"location":"action/#avec-processing","title":"Avec Processing","text":"

Dans le chapitre Processing, nous verrons comment int\u00e9grer un algorithme Processing dans une action.

"},{"location":"console/","title":"Introduction \u00e0 la console Python","text":""},{"location":"console/#donnees","title":"Donn\u00e9es","text":"

Nous allons utiliser un d\u00e9partement de la BDTopo.

Tip

Les DROM-COM ou le Territoire de Belfort (90) sont assez l\u00e9gers.

  1. Renommer le dossier BDT_3-3_SHP_LAMB93_D0ZZ-EDYYYY-MM-DD en BD_TOPO afin de simplifier les corrections.
"},{"location":"console/#configurer-le-projet","title":"Configurer le projet","text":"
.\n\u251c\u2500\u2500 BD_TOPO\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 ADMINISTRATIF\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 ADRESSES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 BATI\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 HYDROGRAPHIE\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 LIEUX_NOMMES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 OCCUPATION_DU_SOL\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 SERVICES_ET_ACTIVITES\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 TRANSPORT\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 ZONES_REGLEMENTEES\n\u2514\u2500\u2500 formation.qgs\n
"},{"location":"console/#manipulation-dans-la-console","title":"Manipulation dans la console","text":""},{"location":"console/#rappel-sur-la-poo","title":"Rappel sur la POO","text":"

La Programmation Orient\u00e9e Objet, POO :

Imaginons le cas d'une voiture.

Une voiture est un objet, on peut en cr\u00e9er une instance. Sur cette instance, on a des \"propri\u00e9t\u00e9s\" comme :

Sur cette instance, on a des \"m\u00e9thodes\" :

classDiagram class Voiture{ +Color Couleur +Int NbPuissance +Int NbPortes +String Marque +Personne Proprietaire +avancer() bool +reculer(amount) int +tourner(direction) bool }

On peut continuer en \u00e9crivant une classe qui va contenir une Personne :

classDiagram class Personne{ +String Nom +String Prenom +Date DateNaissance +Date DatePermisB }"},{"location":"console/#pratique","title":"Pratique","text":""},{"location":"console/#documentation","title":"Documentation","text":"

Warning

Il est important de bien pouvoir lire la signature des m\u00e9thodes :

Afficher la solution
color = QColor(\"#00A2FF\")\nQgsProject.instance().setBackgroundColor(color)\n
"},{"location":"console/#manipulation-en-console-pour-ajouter-une-couche-shapefile","title":"Manipulation en console pour ajouter une couche Shapefile","text":"

Note

L'utilisation de l'objet Path est tr\u00e8s habituelle, mais on rencontre aussi l'utilisation des fonctions du module os.path comme os.path.join... On rencontre son utilisation dans plusieurs tutoriels/solutions/forums.

"},{"location":"console/#solution-etape-par-etape","title":"Solution \u00e9tape par \u00e9tape","text":"

Pour r\u00e9cup\u00e9rer le projet en cours

project = QgsProject.instance()\n

Pour passer du chemin str de QGIS \u00e0 un objet Path et directement appeler la propri\u00e9t\u00e9 parent pour obtenir le dossier :

racine = Path(project.absoluteFilePath()).parent\n

On peut joindre notre BDTopo, en donnant plusieurs param\u00e8tres \u00e0 joinpath :

chemin = racine.joinpath('BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = chemin.joinpath('COMMUNE.shp')\n

Il ne faut pas h\u00e9siter \u00e0 v\u00e9rifier au fur et \u00e0 mesure avec des print :

print(racine)\nprint(racine.is_dir())\nprint(racine.is_file())\nprint(fichier_shape.exists())\nprint(fichier_shape.is_file())\n

Tip

Tant que l'on est en console, on n'a pas besoin de faire print, la console le fait pour nous automatiquement. On peut se contenter de fichier_shape.exists().

Si tout est bon pour le chemin, charger la couche vecteur \u00e0 l'aide de iface qui est une instance de QgisInterface CPP/PyQGIS (et non pas QgsInterface), en utilisant la m\u00e9thode addVectorLayer.

Attention, QGIS, \u00e9tant \u00e9crit en C++, ne connait pas l'usage de Path, il faut repasser par une cha\u00eene de caract\u00e8re avec l'aide de str :

communes = iface.addVectorLayer(str(fichier_shape), 'communes', 'ogr')\nprint(communes)\n

Charger la couche autrement \u00e0 l'aide du constructeur QgsVectorLayer (conseill\u00e9)

communes = QgsVectorLayer(str(fichier_shape), 'communes', 'ogr')\n# communes.isValid()\nQgsProject.instance().addMapLayer(communes)\n
Afficher la solution compl\u00e8te avec pathlib
from pathlib import Path\nproject = QgsProject.instance()\nracine = Path(project.absoluteFilePath()).parent\nchemin = racine.joinpath('BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = chemin.joinpath('COMMUNE.shp')\n# fichier_shape.is_file()\ncommunes = QgsVectorLayer(str(fichier_shape), 'communes', 'ogr')\n# communes.isValid()\nQgsProject.instance().addMapLayer(communes)\n
Afficher \"l'ancienne\" solution compl\u00e8te avec os.path
from os.path import join, isfile, isdir\n\nproject = QgsProject.instance()\n\nracine = project.homePath()\nchemin = join(racine, 'BD_TOPO', 'ADMINISTRATIF')\nfichier_shape = join(chemin, 'COMMUNE.shp')\ncommunes = QgsVectorLayer(fichier_shape, 'communes', 'ogr')\ncommunes.isValid()\nQgsProject.instance().addMapLayer(communes)\n

Success

Bien jou\u00e9 si vous avez votre couche des communes !

"},{"location":"console/#decouverte-des-methodes-sur-notre-objet-communes","title":"D\u00e9couverte des m\u00e9thodes sur notre objet communes","text":"

Notre variable communes est une instance de QgsVectorLayer.

API QgsVectorLayer C++, API QgsVectorLayer Python

\u00c0 l'aide de la documentation, recherchons :

Info

L'API est en train de changer depuis QGIS 3.30 environ, concernant l'usage et l'affichage des \u00e9num\u00e9rations. Par exemple pour geometryType.

communes.geometryType() == QgsWkbTypes.PolygonGeometry\ncommunes.geometryType() == QgsWkbTypes.PointGeometry\n

Pour la g\u00e9om\u00e9trie, toujours utiliser l'\u00e9num\u00e9ration et non pas le chiffre, ce n'est pas compr\u00e9hensible (QGIS < 3.30)

On ne les trouve pas dans la page QgsVectorLayer ! Pour cela, il faut faire r\u00e9f\u00e9rence \u00e0 la notion d'h\u00e9ritage en Programmation Orient\u00e9e Objet.

"},{"location":"console/#heritage","title":"H\u00e9ritage","text":"

Il faut bien regarder les diagrammes en haut de la documentation :

API QgsVectorLayer C++, API QgsVectorLayer Python

classDiagram class QgsMapLayer{ +name() str +id() str +crs() QgsCoordinateReferenceSystem +maximumScale() double +minimumScale() double +autreFonctionsPourToutesLesCouches() } class QgsVectorLayer{ +featureCount() int +startEditing() bool +commitChanges() bool +autreFonctionsPourUneCoucheVecteur() } class QgsRasterLayer{ +bandCount() int +largeur int +hauteur int +autreFonctionsPourUneCoucheRaster() } QgsMapLayer <-- QgsVectorLayer QgsMapLayer <-- QgsRasterLayer

L'objet QgsVectorLayer h\u00e9rite de QgsMapLayer qui est une classe commune avec QgsRasterLayer.

API QgsMapLayer C++, API QgsMapLayer Python

Tip

On peut d\u00e9sormais regarder la documentation CPP de QGIS et Qt pour voir l'ensemble des membres, y compris les membres h\u00e9rit\u00e9s. QgsVectorLayer CPP ou QComboBox

Regardons la fonction isinstance qui permet de tester si un objet est une instance d'une classe :

isinstance(communes, QgsVectorLayer)\nTrue\nisinstance(communes, QgsRasterLayer)\nFalse\nisinstance(communes, QgsMapLayer)\nTrue\n
communes.setMinimumScale(2000000)\ncommunes.setMaximumScale(500000)\ncommunes.setScaleBasedVisibility(True)\n# communes.triggerRepaint()\n

Important

Un raccourci \u00e0 savoir, dans la console :

iface.activeLayer()\n

Cela retourne la couche QgsMapLayer active dans la l\u00e9gende !

"},{"location":"console/#code","title":"Code","text":"

Petit r\u00e9capitulatif \u00e0 tester pour voir si cela fonctionne correctement !

from pathlib import Path\ndossier = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\nproject = QgsProject.instance()\nracine = Path(project.absoluteFilePath()).parent\nchemin = racine.joinpath(dossier, thematique)\nfichier_shape = chemin.joinpath(f'{couche}.shp')\n\nprint(layer.featureCount())\nprint(layer.crs().authid())\nprint('Est en m\u00e8tre : {}'.format(layer.crs().mapUnits() ==  QgsUnitTypes.DistanceMeters))\nprint(layer.name())\nlayer.setScaleBasedVisibility(True)\nlayer.setMaximumScale(500000)\nlayer.setMinimumScale(2000000)\nlayer.triggerRepaint()\n
"},{"location":"console/#transition-vers-le-script-avec-le-parcourir-des-entites","title":"Transition vers le script, avec le parcourir des entit\u00e9s","text":"

Ajouter \u00e9galement la couche ARRONDISSEMENT et s\u00e9lectionner l\u00e0.

On souhaite d\u00e9sormais it\u00e9rer sur les polygones et les faire clignoter depuis la console. Nous allons donc avoir besoin de la m\u00e9thode getFeatures() qui fait partie de QgsVectorLayer.

layer = iface.activeLayer()\nfeatures = layer.getFeatures()\nfeatures\nfeature = QgsFeature()\nfeatures.nextFeature(feature)\niface.mapCanvas().flashFeatureIds(layer, [feature.id()])\n

Note, nous pouvons concat\u00e9ner les deux derni\u00e8res lignes \u00e0 l'aide du caract\u00e8re ; pour que cela soit plus pratique.

Ce code est plus pour la partie \"amusante\" pour montrer les limites de la console. Nous allons d\u00e9sormais utiliser un script Python dans le prochain chapitre.

Petite chose suppl\u00e9mentaire avant de passer aux scripts, on souhaite d\u00e9sormais afficher le nom des arrondissements \u00e0 l'aide d'une boucle for.

layer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    # On peut traiter l'entit\u00e9 courante gr\u00e2ce \u00e0 la variable \"feature\".\n    # Pour acc\u00e9der \u00e0 un attribut en particulier, on peut y acc\u00e9der avec des crochets.\n    pass\n

Noter l'apparition de ... au lieu de >>> apr\u00e8s avoir \u00e9crit la premi\u00e8re ligne du for. Il faut faire une indentation obligatoire !

Pour afficher un attribut, on peut faire print(feature['NOM_ARR']) pour afficher le contenu de l'attribut NOM_ARR.

"},{"location":"documentation/","title":"Documentation et liens utiles","text":"

Tip

QGIS est en train de migrer vers la librairie Qt version 6. QGIS 3.42 va certainement avoir un support pour Qt6 et pouvoir faire des premiers tests PyQGIS. Lire le chapitre sur les migrations majeures de PyQGIS.

Voici une liste de liens pour la documentation, tous en anglais, sauf le cookbook :

Voici une liste non exhaustive de blog-post utiles pour manipuler PyQGIS, tous en anglais :

Autre lien pour l'apprentissage de Python (sans QGIS) en fran\u00e7ais :

Tip

QGIS 3.42 va int\u00e9grer un outil pour avoir l'aide d'une classe directement depuis une variable. Voir la d\u00e9mo de QGIS 3.42.

"},{"location":"ecriture-classe-poo/","title":"\u00c9criture de notre classe en POO","text":""},{"location":"ecriture-classe-poo/#notion-sur-la-poo-en-python","title":"Notion sur la POO en Python","text":"

Le framework Processing utilise le concept de la Programmation Orient\u00e9e Objet. Il existe un tutoriel sur le site d'OpenClassRooms sur le sujet.

Mais depuis le d\u00e9but de la formation, nous l'utilisons sans trop le savoir. Les objets Qgs*, comme QgsVectorLayer utilisent le principe de la POO.

On a pu cr\u00e9er des objets QgsVectorLayer en appelant son constructeur :

from qgis.core import QgsVectorLayer\n\nlayer = QgsVectorLayer(\"C:/chemin/vers/un/fichier.gpkg|layername=communes\", \"communes\", \"ogr\")\n

et ensuite, on a pu appeler des m\u00e9thodes sur cet objet, comme :

layer.setName(\"Communes\")\nlayer.name()  # Retourne \"Communes\"\n

Tip

Vous pouvez relire le passage sur la POO en d\u00e9but de formation.

"},{"location":"ecriture-classe-poo/#exemple","title":"Exemple","text":"

Nous allons faire un \"tr\u00e8s\" petit exemple rapide. \u00c9crivons notre premier jeu vid\u00e9o en console ! \ud83c\udfae

from time import sleep\n\nMAX_ENERGIE = 20\n\n\nclass Personnage:\n\n    \"\"\" Classe repr\u00e9sentant un personnage du jeu vid\u00e9o. \"\"\"\n\n    def __init__(self, un_nom, energie=MAX_ENERGIE):\n        \"\"\" Constructeur. \"\"\"\n        self.nom = un_nom\n        self.energie = energie\n\n    def marcher(self):\n        \"\"\" Permet au personnage de marcher.\n\n        Cela d\u00e9pense de l'\u00e9nergie.\n        \"\"\"\n        cout = 5\n        if self.energie >= cout:\n            print(f\"{self.nom} marche.\")\n            self.energie -= cout\n        else:\n            print(f\"{self.nom} ne peut pas marcher car il n'a pas assez d'\u00e9nergie.\")\n\n    def courir(self):\n        \"\"\" Permet au personnage de courir.\n\n        Cela d\u00e9pense de l'\u00e9nergie.\n        \"\"\"\n        cout = 10\n        if self.energie >= cout:\n            print(f\"{self.nom} court.\")\n            self.energie -= cout\n        else:\n            print(f\"{self.nom} ne peut pas courir car il n\\'a pas assez d\\'\u00e9nergie.\")\n\n    def dormir(self):\n        \"\"\" Permet au personnage de dormir et restaurer le niveau maximum d'\u00e9nergie.\"\"\"\n        print(f\"{self.nom} dort et fait le plein d'\u00e9nergie.\")\n        for i in range(2):\n            print('...')\n            sleep(1)\n        self.energie = MAX_ENERGIE\n\n    def manger(self):\n        \"\"\" Permet au personnage de manger et d'augmenter de 10 points le niveau d'\u00e9nergie.\"\"\"\n        energie = 10\n        print(f\"{self.nom} mange et r\u00e9cup\u00e8re {energie} points d'\u00e9nergie.\")\n        if self.energie <= MAX_ENERGIE - energie:\n            self.energie += energie\n        else:\n            self.energie = MAX_ENERGIE\n\n    def __repr__(self):\n        return f\"<Personnage: '{self.nom}' avec {self.energie} points d'\u00e9nergie>\"\n
"},{"location":"ecriture-classe-poo/#utilisation-de-notre-classe","title":"Utilisation de notre classe","text":"
a = Personnage('Dark Vador')\ndir(a)\nhelp(a)\n

Que remarquons-nous ?

Solution
a = Personnage('Dark Vador')\na.courir()\na.dormir()\na.manger()\nprint(a)\n

Afficher le nom du personnage (et juste son nom, pas la phrase de pr\u00e9sentation)

"},{"location":"ecriture-classe-poo/#ajouter-dautres-methodes","title":"Ajouter d'autres m\u00e9thodes","text":"

Ajoutons une m\u00e9thode dialoguer pour discuter avec un autre personnage.

Tip

def dialoguer(self, autre_personnage):\n    \"\"\" Permet de dialoguer avec un autre personnage. \"\"\"\n    pass\n
  1. \u00c9crire le code la fonction \u00e0 l'aide d'un print pour commencer disant que X dialogue avec Y.
  2. V\u00e9rifier le niveau d'\u00e9nergie avant de dialoguer ! Difficile de discuter si on n'a plus d'\u00e9nergie \ud83d\ude09
  3. Garder son code \u00e0 gauche, on peut utiliser une instruction return

Nous pouvons d\u00e9sormais utiliser le constructeur afin de cr\u00e9er deux instances de notre classe.

b = Personnage('Luke')\nb.dialoguer(a)\n
Solution pour la m\u00e9thode dialoguer()
def dialoguer(self, autre_personnage):\n    if self.energie <= 0:\n        print(f\"{self.nom} ne peut pas dialoguer car il n'a pas assez d'\u00e9nergie.\")\n        return\n\n    print(f\"{self.nom} dialogue avec {autre_personnage.nom} et ils \u00e9changent des informations secr\u00e8tes\")\n

Continuons notre classe pour la gestion de son inventaire. Admettons que notre personnage puisse ramasser des objets afin de les mettre dans son sac \u00e0 dos.

  1. Il va falloir ajouter une nouvelle propri\u00e9t\u00e9 \u00e0 notre classe de type list que l'on peut nommer inventaire. Par d\u00e9faut, son inventaire sera vide.
  2. Ajoutons 3 m\u00e9thodes : ramasser, deposer et utiliser. Pour le moment, pour faciliter l'exercice, utilisons une cha\u00eene de caract\u00e8re pour d\u00e9signer l'objet. Ces m\u00e9thodes vont interagir avec notre inventaire \u00e0 l'aide des m\u00e9thodes remove(), append() que l'on trouve sur une liste.
  3. Pour les m\u00e9thodes deposer et utiliser, nous pouvons avoir \u00e0 cr\u00e9er une autre m\u00e9thode priv\u00e9e afin de v\u00e9rifier l'existence de l'objet dans l'inventaire. Par convention, nous pr\u00e9fixons la m\u00e9thode par _ comme _est_dans_inventaire afin de signaler que c'est une m\u00e9thode dite priv\u00e9e. L'utilisation de cette m\u00e9thode priv\u00e9e est uniquement \u00e0 titre p\u00e9dagogique, on peut vouloir exposer la m\u00e9thode est_dans_inventaire. Cette m\u00e9thode doit renvoyer un bool\u00e9en.
  4. Ajoutons des commentaires et/ou des docstrings, CF m\u00e9mo Python. On peut utiliser la m\u00e9thode help.
  5. Pensons aussi annotations Python

  6. Refaire la commande help(a) pour voir le r\u00e9sultat final \ud83d\ude09

Info

Il est important de comprendre que la POO permet de construire une sorte de bo\u00eete opaque du point de vue de l'utilisateur de la classe. Un peu comme une voiture, elles ont toutes un capot et une p\u00e9dale d'acc\u00e9l\u00e9ration. L'appui sur l'acc\u00e9l\u00e9rateur d\u00e9clenche plusieurs m\u00e9canismes \u00e0 l'int\u00e9rieur de la voiture, mais du point de vue utilisateur, c'est plut\u00f4t simple.

Tip

On peut vite imaginer d'autres classes, comme Arme, car ramasser un bout de bois ou un sabre laser n'a pas le m\u00eame impact lors de son utilisation dans un combat. Le d\u00e9g\u00e2t qu'inflige une arme sur le niveau d'\u00e9nergie de l'autre personnage est une propri\u00e9t\u00e9 de l'arme en question et du niveau du personnage.

"},{"location":"ecriture-classe-poo/#des-idees-pour-continuer-plus-loin","title":"Des id\u00e9es pour continuer plus loin","text":"

Des jeux en Python dans QGIS :

Ou pour le fun avec des expressions :

"},{"location":"ecriture-classe-poo/#solution","title":"Solution","text":"

Sur la classe Personnage ci-dessus :

def _est_dans_inventaire(self, un_objet: str) -> bool:\n    \"\"\" Fonction \"interne\" pour tester si un objet est dans l'inventaire. \"\"\"\n    # On ne souhaite pas qu'un autre personnage puis v\u00e9rifier le contenu d'un inventaire d'un autre.\n    # Il faut plut\u00f4t lui demander :)\n    # Note cette m\u00e9thode n'est pas dans le help()\n    return un_objet in self.inventaire\n\ndef ramasser(self, un_objet: str) -> bool:\n    \"\"\" Ramasser un objet et le mettre dans l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    print(f\"{self.nom} ramasse {un_objet} et le met dans son inventaire.\")\n    self.inventaire.append(un_objet)\n    return True\n\ndef utiliser(self, un_objet: str) -> bool:\n    \"\"\" Utiliser un objet s'il est disponible avant dans l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if self._est_dans_inventaire(un_objet):\n        print(f\"{self.nom} utilise {un_objet}\")\n        return True\n\n    print(f\"{self.nom} ne poss\u00e8de pas {un_objet}\")\n    return False\n\ndef deposer(self, un_objet: str) -> bool:\n    \"\"\" Retirer un objet de l'inventaire.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if not self._est_dans_inventaire(un_objet):\n        return False\n\n    print(f\"{self.nom} d\u00e9pose {un_objet}\")\n    self.inventaire.remove(un_objet)\n    return True\n\ndef donner(self, autre_personnage, un_objet: str) -> bool:\n    \"\"\" Donner un objet \u00e0 un autre personnage.\n\n    Retourne True si l'action est OK\n    \"\"\"\n    if not self._est_dans_inventaire(un_objet):\n        return False\n    self.inventaire.remove(un_objet)\n    autre_personnage.inventaire.append(un_objet)\n    print(f\"{autre_personnage.nom} re\u00e7oit {un_objet} de la part de {self.nom} et le remercie \ud83d\udc4d\")\n    return True\n
"},{"location":"expression/","title":"Expression","text":"

On peut d\u00e9finir sa propre expression QGIS \u00e0 l'aide de Python. Il existe un chapitre dans le Python cookbook

Dans la fen\u00eatre des expressions QGIS, on peut observer la fonction d\u00e9j\u00e0 existante.

feature, parent et context sont des param\u00e8tres particuliers dans la signature de la fonction. Si QGIS trouve le mot-cl\u00e9, il assigne l'objet correspondant :

"},{"location":"expression/#exemple","title":"Exemple","text":"

On souhaite utiliser l'API de Wikip\u00e9dia afin de r\u00e9cup\u00e9rer la description d'un terme.

Par exemple, si on cherche le terme Montpellier avec l'API Wikip\u00e9dia :

https://fr.wikipedia.org/w/api.php?action=query&titles=Montpellier&prop=description&format=json

Il existe plusieurs moyens de faire des requ\u00eates HTTP en Python et/ou PyQGIS. Utilisons la technique Processing avec l'algorithme \"T\u00e9l\u00e9chargeur de fichier\" (graphiquement, il n'est disponible que dans le modeleur) :

search = \"montpellier\"\nresults = processing.run(\n    \"native:filedownloader\",\n    {\n        \"URL\": f\"https://fr.wikipedia.org/w/api.php?action=query&titles={search}&prop=description&format=json\",\n        \"OUTPUT\": \"TEMPORARY_OUTPUT\"\n    }\n)\n

Tip

On peut afficher le panneau de d\u00e9bogage et d\u00e9veloppement de QGIS afin de voir les requ\u00eates HTTP. Il se trouve dans le menu Vue \u25b6 Panneau \u25b6 D\u00e9bogage et d\u00e9veloppement

On va d\u00e9sormais parser le fichier JSON que l'on obtient avec la libraire json afin de r\u00e9cup\u00e9rer la description.

"},{"location":"expression/#memo","title":"M\u00e9mo","text":"

Pour lire un fichier \u00e0 l'aide d'un \"contexte Python\" qui va ouvrir et fermer le fichier :

import json\n\nwith open(\"/mon/fichier.json\") as f:\n    data = json.load(f)\nprint(data)\n

Une m\u00e9thode pour r\u00e9cup\u00e9rer la bonne cl\u00e9, dynamiquement :

pages = data['query']['pages']\nkey = list(pages.keys())[0]\ndescription = pages[key]['description']\nprint(description)\n

Peut-\u00eatre surement plus simple \u00e0 comprendre, avec l'usage d'une boucle for

description = \"\"\nfor page in pages.values():\n    description = page.get('description')\n\nprint(description)\n
Une solution compl\u00e8te pour l'expression QGIS
import json\nimport processing\n\n@qgsfunction(args='auto', group='Formation PyQGIS')\ndef wiki_description(search, feature, parent):\n    \"\"\"Permet de r\u00e9cup\u00e9rer la description Wikipedia\n\n    wiki_description('Paris') \u27a1 'capitale de la France'\n    \"\"\"\n    results = processing.run(\n        \"native:filedownloader\",\n        {\n            \"URL\": f\"https://fr.wikipedia.org/w/api.php?action=query&titles={search}&prop=description&format=json\",\n            \"OUTPUT\": \"TEMPORARY_OUTPUT\"\n        }\n    )\n\n    with open(results['OUTPUT']) as f:\n        data = json.load(f)\n\n    pages = data['query']['pages']\n    # Only the first page will be used\n    for page_id, page in pages.items():\n        description = page.get('description')\n        if page_id == \"-1\":\n            error = page.get('invalidReason')\n            if description:\n                return f'Pas de page, {description}'\n            if not error:\n                return 'Pas de page'\n            else:\n                return f\"Pas de page, erreur {error}\"\n        return description\n
"},{"location":"expression/#fournir-une-expression-depuis-une-extension","title":"Fournir une expression depuis une extension","text":"

Pour le moment, cette expression est dans le dossier de l'utilisateur, dans python \u2192 expressions.

Mais une fois que nous avons une extension g\u00e9n\u00e9rique, nous pouvons l'int\u00e9grer dans un fichier Python de l'extension.

Exemple sur StackExchange

"},{"location":"extension-deploiement/","title":"Comment d\u00e9ployer son extension","text":"

Comme vu dans le chapitre concernant la cr\u00e9ation d'une extension g\u00e9n\u00e9rique, une extension QGIS est un dossier comportant :

Ce dossier doit \u00eatre zipp\u00e9.

Pour du d\u00e9ploiement, nous recommandons l'usage de QGIS-Plugin-CI qui peut faire du packaging, la g\u00e9n\u00e9ration du plugins.xml, envoyer sur plugins.qgis.org etc.

"},{"location":"extension-deploiement/#en-interne","title":"En interne","text":"

Si on souhaite publier en interne, on peut d\u00e9poser son dossier zip sur un serveur et on recommande l'utilisation du fichier plugins.xml qui permet de renseigner \u00e0 QGIS la disponibilit\u00e9 d'une extension.

Exemple avec l'installation de PgMetadata et son fichier plugins.xml

Il est possible de prot\u00e9ger son d\u00e9p\u00f4t avec un login/mot de passe.

"},{"location":"extension-deploiement/#tutoriel-pour-installer-un-depot","title":"Tutoriel pour installer un d\u00e9p\u00f4t","text":"

Notre tutoriel pour l'installation d'un d\u00e9p\u00f4t, avec ou sans mot de passe.

"},{"location":"extension-deploiement/#pluginsqgisorg","title":"plugins.qgis.org","text":"

Plus simple pour le d\u00e9ploiement, car le d\u00e9p\u00f4t plugins.qgis.org est par d\u00e9faut dans les installations de QGIS. Il faut cependant que le code source soit disponible sur internet.

Lire les recommandations pour la publication sur ce d\u00e9p\u00f4t :

"},{"location":"extension-generique/","title":"La base pour cr\u00e9er une extension","text":""},{"location":"extension-generique/#une-extension-nest-quun-zip-pour-fournir-du-code-python","title":"Une extension n'est qu'un ZIP pour fournir du code Python","text":"

Comme vu dans Le Python dans QGIS, une extension peut \u00eatre sous diff\u00e9rente forme :

"},{"location":"extension-generique/#modele-de-base","title":"Mod\u00e8le de base","text":"

Pour cr\u00e9er une extension dans QGIS, il existe deux fa\u00e7ons de d\u00e9marrer :

  1. T\u00e9l\u00e9charger le ZIP
  2. Installer le depuis le gestionnaire des extensions, \u00e0 l'aide du ZIP

Tip

Pour trouver le profil courant, dans QGIS, Pr\u00e9f\u00e9rences -> Profils Utilisateurs -> Ouvrir le dossier du profil actif.

"},{"location":"extension-generique/#le-fichier-metadatatxt","title":"Le fichier metadata.txt","text":"

Liste des valeurs possibles dans un fichier metadata.txt

"},{"location":"extension-generique/#exemple-dune-extension-processing-et-graphique","title":"Exemple d'une extension Processing et graphique","text":"

Exemple d'utilisation d'un panneau qui pr\u00e9sentent les algorithmes \"Processing\" :

"},{"location":"extension-generique/#extensions-utiles","title":"Extensions utiles","text":""},{"location":"extension-generique/#plugin-reloader","title":"Plugin reloader","text":"

Indispensable

Le \"Plugin Reloader\" est une extension indispensable pour d\u00e9velopper une extension pour recharger son extension. Elle est disponible dans le gestionnaire des extensions.

"},{"location":"extension-generique/#pyqgis-resource-browser","title":"PyQGIS Resource Browser","text":"

Utile pour l'ergonomie

Permet d'aller chercher des ic\u00f4nes d\u00e9j\u00e0 existantes dans la libraire QGIS et Qt

"},{"location":"extension-generique/#first-aid","title":"First aid","text":"

Utile pour aller plus loin

Extension pour d\u00e9bugger en cas d'une erreur Python

"},{"location":"extension-generique/#apprendre-dune-autre-extension","title":"Apprendre d'une autre extension","text":"

Comme les extensions sur qgis.org sont disponibles sur internet, on peut regarder le code source pour comprendre.

Pensez \u00e0 ouvrir le dossier de votre profil QGIS en suivant l'astuce ci-dessus puis dans python/plugins.

"},{"location":"extension-graphique/","title":"Cr\u00e9er une extension QGIS avec une interface graphique","text":"

Pour faire ce chapitre, il faut d'abord avoir une extension de base, \u00e0 l'aide du chapitre pr\u00e9c\u00e9dent.

"},{"location":"extension-graphique/#qtdesigner","title":"QtDesigner","text":""},{"location":"extension-graphique/#premier-dialogue","title":"Premier dialogue","text":"

Cr\u00e9ons un fichier QtDesigner comme-ceci :

"},{"location":"extension-graphique/#decouverte-de-linterface-qtdesigner","title":"D\u00e9couverte de l'interface QtDesigner","text":"

Tip

Privil\u00e9gier aussi les \"Item Widgets (Item Based)\" plut\u00f4t que les \"Item Views (Model Based) pour d\u00e9buter.

"},{"location":"extension-graphique/#ajoutons-les-widgets","title":"Ajoutons les \"widgets\"","text":"

Important

Ne tenez pas compte de l'alignement des widgets pour le moment. On fait juste un placement \"rapide\" vertical des widgets.

Dans l'ordre vertical, ce sont ces classes :

Classe QLabel QLineEdit QgsMapLayerComboBox QPlainTextEdit Vertical spacer QDialogButtonBox

Une fois que l'ensemble des \"widgets\" sont pr\u00e9sents, on peut faire un clic droit \u00e0 droite sur notre QDialog, puis Mise en page et enfin Verticalement \ud83d\ude80

"},{"location":"extension-graphique/#ajout-dun-bouton-dans-notre-buttonbox-en-bas","title":"Ajout d'un bouton dans notre \"ButtonBox\" en bas","text":"

Ajoutons le bouton d'aide, dans les propri\u00e9t\u00e9s de notre widget QDialogButtonBox.

"},{"location":"extension-graphique/#nommage-de-nos-widgets","title":"Nommage de nos \"widgets\"","text":"

Pour chacun de nos widgets, changeons le nom par d\u00e9faut de l'objet, propri\u00e9t\u00e9 objectName tout en haut :

Classe Nom par d\u00e9faut de objectName Nouveau nom pour objectName QLineEdit lineEdit input_text QgsMapLayerComboBox mMapLayerComboBox couche QPlainTextEdit plainTextEdit metadata QDialogButtonBox buttonBox button_box

Cette propri\u00e9t\u00e9 objectName est tr\u00e8s importante, car elle d\u00e9termine l'appellation de notre propri\u00e9t\u00e9 dans l'objet self pour la suite du TP.

"},{"location":"extension-graphique/#astuces","title":"Astuces","text":"

Pourquoi supprimer les signaux de QtDesigner ?

Un fichier QtDesigner est \"gros\" fichier XML. Il est difficile, dans le temps, de suivre ces modifications, changement\u2026

Il est, \u00e0 mon avis, plus simple de garder le fichier XML le plus l\u00e9ger possible, et de garder la logique dans le code Python. Un signal en XML, c'est plusieurs lignes dans le fichier UI, alors que en Python, c'est une seule ligne.

On peut t\u00e9l\u00e9charger la solution si besoin.

"},{"location":"extension-graphique/#la-classe-qui-accompagne","title":"La classe qui accompagne","text":"

Cr\u00e9ons un fichier dialog.py avec le contenu suivant :

# Les imports\nfrom qgis.core import Qgis\nfrom qgis.utils import iface\nfrom qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox\nfrom qgis.PyQt import uic\nfrom pathlib import Path\n\n# Permettre d'aller chercher le fichier UI correspondant\nfolder = Path(__file__).resolve().parent\nui_file = folder.joinpath('dialog.ui')\nui_class, _ = uic.loadUiType(ui_file)\n\n\nclass MonDialog(ui_class, QDialog):\n\n    \"\"\" Classe qui repr\u00e9sente le dialogue de l'extension. \"\"\"\n\n    def __init__(self, parent: QDialog):\n        \"\"\" Constructeur. \"\"\"\n        super().__init__(parent)  # Appel du constructeur parent\n        self.parent = parent  # Stockage du parent dans self, on va l'utiliser plus tard, si besoin\n        self.setupUi(self)  # Fichier de QtDesigner\n\n        # Quelques propri\u00e9t\u00e9s facultatives\n        self.setWindowTitle(\"Notre super machine \u00e0 caf\u00e9\")\n        # self.setModal(False)  # Utile plus bas si on souhaite ouvrir une autre fen\u00eatre par-dessus\n

Modifions la m\u00e9thode run du fichier __init__.py en

    def run(self):\n        \"\"\" Lors du clic sur le bouton pour lancer la fen\u00eatre de l'extension. \"\"\"\n        from .dialog import MonDialog\n        dialog = MonDialog(self.iface.mainWindow())\n        dialog.show()\n

Relan\u00e7ons l'extension \u00e0 l'aide du \"plugin reloader\" et cliquons sur le bouton.

"},{"location":"extension-graphique/#les-signaux-et-les-slots","title":"Les signaux et les slots","text":""},{"location":"extension-graphique/#signaux-des-boutons-de-la-fenetre","title":"Signaux des boutons de la fen\u00eatre","text":"

Tip

N'h\u00e9sitez pas \u00e0 relire le chapitre sur les signaux.

Connectons le signal clicked du bouton \"Annuler\" dans le constructeur __init__ :

self.button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close)\n

On dit que clicked est un signal, auquel on connecte le slot close.

Connectons-le signal clicked du bouton \"Accepter\" \u00e0 notre propre slot (qui est une fonction) :

self.button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.click_ok)\n

Ensuite, ajoutons notre propre fonction click_ok pour quitter la fen\u00eatre et en affichant la saisie de l'utilisateur dans la QgsMessageBar de QGIS.

Le widget de saisie est un QLineEdit : documentation Qt

def click_ok(self):\n    \"\"\" Clic sur le bouton OK afin de fermer la fen\u00eatre. \"\"\"\n    message = self.input_text.text()\n    iface.messageBar().pushMessage('Notre extension', message, Qgis.Success)\n    self.accept()\n

Faire le test dans QGIS avec une saisie de l'utilisateur et fermer la fen\u00eatre.

"},{"location":"extension-graphique/#clic-sur-le-bouton-daide","title":"Clic sur le bouton d'aide","text":"
# Dans le constructeur :\nself.button_box.button(QDialogButtonBox.StandardButton.Help).clicked.connect(self.open_help)\n\n# Puis la fonction :\ndef open_help(self):\n    \"\"\" Open the online help. \"\"\"\n    from qgis.PyQt.QtGui import QDesktopServices\n    from qgis.PyQt.QtCore import QUrl\n    QDesktopServices.openUrl(QUrl('https://www.youtube.com/watch?v=AdQ3JDLlmPI'))\n
"},{"location":"extension-graphique/#signaux-et-proprietes-du-formulaire-de-saisie","title":"Signaux et propri\u00e9t\u00e9s du formulaire de saisie","text":"

Continuons en rendant en lecture seule le gros bloc de texte et affichons \u00e0 l'int\u00e9rieur la description de la couche qui est s\u00e9lectionn\u00e9e dans le menu d\u00e9roulant.

Documentation :

Dans la fonction __init__ du fichier dialog.py :

self.metadata.setReadOnly(True)\nself.couche.layerChanged.connect(self.layer_changed)\n

Et la nouvelle fonction qui va se charger de mettre \u00e0 jour le texte :

def layer_changed(self):\n    \"\"\" Permet de mettre \u00e0 jour l'UI selon la couche dans le menu d\u00e9roulant. \"\"\"\n    self.metadata.clear()\n    layer = self.couche.currentLayer()\n    self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\n

Danger

Que remarquez-vous en terme d'ergonomie, \u00e0 l'ouverture de la fen\u00eatre ?

La solution plus compl\u00e8te
layer = self.couche.currentLayer()\nif layer:\n    self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\nelse:\n    self.metadata.appendPlainText(\"Pas de couche\")\n

On peut donc d\u00e9sormais cumuler l'ensemble des chapitres pr\u00e9c\u00e9dents pour lancer des algorithmes, manipuler les donn\u00e9es, etc.

Bonus

Le texte actuel concernant les m\u00e9tadonn\u00e9es est \"limit\u00e9\". N'h\u00e9sitez pas \u00e0 compl\u00e9ter, un peu comme lors de l'exercice avec le fichier CSV, pour rappel :

"},{"location":"extension-graphique/#solution","title":"Solution","text":"Afficher
from qgis.core import Qgis\nfrom qgis.utils import iface\nfrom qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox\nfrom qgis.PyQt import uic\nfrom pathlib import Path\n\nfolder = Path(__file__).resolve().parent\nui_file = folder.joinpath('dialog.ui')\nui_class, _ = uic.loadUiType(ui_file)\n\n\nclass MonDialog(ui_class, QDialog):\n\n    \"\"\" Classe qui repr\u00e9sente le dialogue de l'extension. \"\"\"\n\n    def __init__(self, parent=None):\n        \"\"\" Constructeur. \"\"\"\n        _ = parent\n        # TODO CORRECTION\n        super().__init__()\n        self.setupUi(self)  # Fichier de QtDesigner\n\n        # Connectons les signaux\n        self.button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.click_ok)\n        self.button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close)\n\n        self.metadata.setReadOnly(True)\n        self.couche.layerChanged.connect(self.layer_changed)\n        self.layer_changed()\n\n    def click_ok(self):\n        \"\"\" Clic sur le bouton OK afin de fermer la fen\u00eatre. \"\"\"\n        self.close()\n        message = self.input_text.text()\n        iface.messageBar().pushMessage('Notre extension', message, Qgis.Success)\n\n    def layer_changed(self):\n        \"\"\" Permet de mettre \u00e0 jour l'UI selon la couche dans le menu d\u00e9roulant. \"\"\"\n        self.metadata.clear()\n        layer = self.couche.currentLayer()\n        if layer:\n            self.metadata.appendPlainText(f\"{layer.name()} : CRS \u2192 {layer.crs().authid()}\")\n        else:\n            self.metadata.appendPlainText(\"Pas de couche\")\n
"},{"location":"extension-graphique/#organisation-du-code","title":"Organisation du code","text":"

Il ne faut pas h\u00e9siter \u00e0 cr\u00e9er des fichiers Python afin de s\u00e9parer le code.

On peut aussi cr\u00e9er des dossiers afin d'y mettre plusieurs fichiers Python. Un dossier en Python se nomme un module. Pour faire un module compatible, pour Python, il faut toujours avoir un fichier __init__.py m\u00eame s\u2019il n'y a rien dedans.

Warning

Il ne faut vraiment pas oublier le fichier __init__.py. Cela peut emp\u00eacher Python de fonctionner correctement. Un bon IDE peut signaler ce genre d'erreur.

Dans l'exemple ci-dessus, on peut diviser le code du fichier __init__.py :

def classFactory(iface):\n    # from minimal_plugin.plugin import MinimalPlugin  # Import absolue\n    from .plugin import MinimalPlugin  # Import relatif\n    return MinimalPlugin(iface)\n

En faisant un couper/coller, enlever la classe MinimalPlugin du fichier __init__.py.

Tip

On essaie souvent d'avoir une classe par fichier en Python.

Cr\u00e9er un fichier plugin.py et ajouter le contenu en collant. Il est bien de v\u00e9rifier les imports dans les deux fichiers.

"},{"location":"extension-graphique/#un-dossier-resources","title":"Un dossier \"resources\"","text":"

On peut cr\u00e9er un fichier qgis_plugin_tools.py \u00e0 la racine de notre extension afin d'y ajouter des outils :

\"\"\"Tools to work with resources files.\"\"\"\n\nfrom pathlib import Path\n\n\ndef plugin_path(*args) -> Path:\n    \"\"\"Return the path to the plugin root folder.\"\"\"\n    path = Path(__file__).resolve().parent\n    for item in args:\n        path = path.joinpath(item)\n\n    return path\n\n\ndef resources_path(*args) -> Path:\n    \"\"\"Return the path to the plugin resources folder.\"\"\"\n    return plugin_path(\"resources\", *args)\n\n# On peut ajouter ici une m\u00e9thode qui charge un fichier UI qui se trouve dans le dossier \"UI\"\n# et retourne la classe directement.\n

On peut ensuite cr\u00e9er un dossier resources puis icons afin d'y d\u00e9placer un fichier PNG, JPG, SVG.

Warning

Attention \u00e0 la taille de vos fichiers pour une petite ic\u00f4ne \ud83d\ude09

Pour les experts, solution pour faire une fonction qui charge un fichier UI
def load_ui(*args):\n    \"\"\"Get compiled UI file.\n\n    :param args List of path elements e.g. ['img', 'logos', 'image.png']\n    \"\"\"\n    ui_class, _ = uic.loadUiType(str(resources_path(\"ui\", *args)))\n    return ui_class\n
"},{"location":"extension-graphique/#dans-une-extension-graphique-pour-les-icones","title":"Dans une extension graphique pour les ic\u00f4nes","text":"

Lien documentation QAction

# En haut du fichier, on ajoute les imports n\u00e9cessaires\nfrom qgis.PyQt.QtGui import QIcon\nfrom .qgis_plugin_tools import resources_path\n\n# Plus bas dans le code\n# Quand n\u00e9cessaire, \u00e0 remplacer la QAction existante. Il s'agit du premier param\u00e8tre avec QIcon\nself.action = QAction(\n    QIcon(str(resources_path('icons', 'icon.svg'))),\n    'Go!',\n    self.iface.mainWindow())\n

Tip

Ce qu'il faut retenir, c'est l'usage de QIcon(str(resources_path('icons', 'icon.svg'))) si l'on souhaite utiliser une ic\u00f4ne dans autre endroit de l'extension.

"},{"location":"extension-graphique/#dans-une-extension-processing","title":"Dans une extension \"Processing\"","text":"

Dans le provider et les algorithmes :

# En haut du fichier\nfrom ..qgis_plugin_tools import resources_path\n\n# Dans la classe, on ajoute/modifie la m\u00e9thode 'icon'\ndef icon(self) -> QIcon:\n    return QIcon(str(resources_path(\"icons\", \"icon.png\")))\n
"},{"location":"extension-graphique/#utilisation-dune-icone-provenant-de-qgis","title":"Utilisation d'une ic\u00f4ne provenant de QGIS","text":"

\u00c0 l'aide de l'extension \"PyQGIS Resource Browser\", rechercher une ic\u00f4ne concordant avec bouton :

On peut ensuite faire un clic-droit, puis coller son chemin.

Ensuite, quand on souhaite utiliser l'ic\u00f4ne :

# En haut\nfrom qgis.PyQt.QtGui import QIcon\n\n# Ensuite\nicon = QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\")\n\n# Par exemple sur un bouton QPushButton\nself.un_bouton.setIcon(QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\"))\n
"},{"location":"extension-graphique/#ajouter-un-bouton-pour-lancer-processing","title":"Ajouter un bouton pour lancer Processing","text":"

Nous souhaitons ajouter 2 boutons :

Classe objectName QPushButton btn_traitement_1 QPushButton btn_traitement_2

On peut faire la mise en page vertical dans le QGroupBox.

Ajoutons les ic\u00f4nes et infobulles si n\u00e9cessaires, dans le constructeur :

self.btn_traitement_1.setToolTip(\"Permet de lancer l'algorithme des tampons sur la couche ci-dessus avec un buffer de 2km\")\nself.btn_traitement_1.setIcon(QIcon(\":/images/themes/default/algorithms/mAlgorithmBuffer.svg\"))\n# self.btn_traitement_1.clicked.connect(self.traitement_1_clicked)\n
"},{"location":"extension-graphique/#lancer-le-dialogue-de-processing","title":"Lancer le dialogue de Processing","text":"

Pour les imports :

from qgis.core import QgsVectorLayer\nfrom qgis import processing\n

Pour le code dans la fonction :

def traitement_1_clicked(self):\n    \"\"\" Lancement de la fen\u00eatre de QGIS Processing. \"\"\"\n    layer = self.couche.currentLayer()\n\n    # Les pr\u00e9-requis pour continuer\n    # On sort de la fonction si on ne peut pas continuer\n    if not isinstance(layer, QgsVectorLayer):\n        return\n\n    if not layer.isSpatial():\n        return\n\n    dialog = processing.createAlgorithmDialog(\n        \"native:buffer\",\n        {\n            'INPUT': layer,\n            'DISTANCE': 2000,\n            'OUTPUT': 'TEMPORARY_OUTPUT'\n        }\n    )\n    dialog.show()\n

Pour rappel, nous ne sommes pas oblig\u00e9 d'ouvrir la fen\u00eatre de Processing, on peut directement faire processing.run, lire le chapitre pr\u00e9c\u00e9dent. Il ne faut pas oublier de donner la variable layer \u00e0 notre INPUT si vous copiez/coller le code de processing.run du chapitre pr\u00e9c\u00e9dent.

"},{"location":"extension-processing/","title":"Cr\u00e9er une extension QGIS pour Processing","text":"

Pour faire ce chapitre, il faut :

  1. Avoir une extension de base, \u00e0 l'aide du chapitre pr\u00e9c\u00e9dent
  2. Faire la mise \u00e0 jour en extension Processing \u00e0 l'aide de la documentation QGIS
"},{"location":"fonctions-scripts/","title":"Organisation du code dans un script avec des fonctions","text":""},{"location":"fonctions-scripts/#communication-avec-lutilisateur-des-erreurs-et-des-logs","title":"Communication avec l'utilisateur des erreurs et des logs","text":"

Avant de commencer \u00e0 vraiment \u00e9crire un script avec des fonctions, regardons comment communiquer des informations \u00e0 l'utilisateur.

Cookbook

Lien vers le Python cookbook qui pr\u00e9sente cette partie plus pr\u00e9cis\u00e9ment.

"},{"location":"fonctions-scripts/#la-barre-de-message","title":"La barre de message","text":"

On peut envoyer des messages vers l'utilisateur avec l'utilisation de la messageBar de la classe QgisInterface CPP/PyQGIS :

iface.messageBar().pushMessage('Erreur','On peut afficher une erreur', Qgis.Critical)\niface.messageBar().pushMessage('Avertissement','ou un avertissement', Qgis.Warning)\niface.messageBar().pushMessage('Information','ou une information', Qgis.Info)\niface.messageBar().pushMessage('Succ\u00e8s','ou un succ\u00e8s', Qgis.Success)\n

Cette fonction prend 3 param\u00e8tres :

  1. un titre
  2. un message
  3. un niveau d'alerte

On peut voir dans la classe de QgsMessageBar qu'il existe aussi pushSuccess qui est une alternative par exemple.

"},{"location":"fonctions-scripts/#journal-des-logs","title":"Journal des logs","text":"

On peut aussi \u00e9crire des logs comme ceci (plus discret, mais plus verbeux) :

QgsMessageLog.logMessage('Une erreur est survenue','Notre outil', Qgis.Critical)\nQgsMessageLog.logMessage('Un avertissement','Notre outil', Qgis.Warning)\nQgsMessageLog.logMessage('Une information','Notre outil', Qgis.Info)\nQgsMessageLog.logMessage('Un succ\u00e8s','Notre outil', Qgis.Success)\n

Cette fonction prend 3 param\u00e8tres :

"},{"location":"fonctions-scripts/#des-fonctions-pour-simplifier-le-code","title":"Des fonctions pour simplifier le code","text":""},{"location":"fonctions-scripts/#une-fonction-pour-charger-une-couche","title":"Une fonction pour charger UNE couche","text":"

La console, c'est bien, mais c'est tr\u00e8s limitant. Passons \u00e0 l'\u00e9criture d'un script qui va nous faciliter l'organisation du code.

  1. Red\u00e9marrer QGIS (afin de vider l'ensemble des variables que l'on a dans notre console)
  2. N'ouvrez pas le projet pr\u00e9c\u00e9dent !
  3. Ouvrer la console, puis cliquer sur Afficher l'\u00e9diteur
  4. Copier/coller le script ci-dessous
  5. Ex\u00e9cuter le
# En haut du script, ce sont souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\nprojet_qgis = QgsProject.instance().absoluteFilePath()\nif not projet_qgis:\n    iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\nelse:\n    racine = Path(projet_qgis).parent\n    fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n    if not fichier_shape.exists():\n        iface.messageBar().pushMessage('Erreur de chargement', f'Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n    else:\n        layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n        if not layer.isValid():\n            iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n        else:\n            QgsProject.instance().addMapLayer(layer)\n            iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n    print('Fin du script si on a un projet')\n

Tip

Pour d\u00e9sindenter le code, MAJ + TAB.

# Avec annotations Python\ndef charger_couche(bd_topo: str, thematique: str, couche: str):\n    ...\n\n# Sans annotations Python\ndef charger_couche(bd_topo, thematique, couche):\n    ...\n

Tip

Le mot-cl\u00e9 pass (ou encore ... qui est synonyme) ne sert \u00e0 rien. C'est un mot-cl\u00e9 Python pour rendre un bloc valide mais ne faisant rien. On peut le supprimer le bloc n'est pas vide.

On peut ajouter une docstring \u00e0 notre fonction, juste en dessous du def, avec des indentations :

\"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n

Afficher la solution interm\u00e9diaire
# En haut du script, ce souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\nthematique = 'ADMINISTRATIF'\ncouche = 'COMMUNE'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\ndef charger_couche(bd_topo, thematique, couche):\n    \"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n    projet_qgis = QgsProject.instance().absoluteFilePath()\n    if not projet_qgis:\n        iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\n    else:\n        racine = Path(projet_qgis).parent\n        fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n        if not fichier_shape.exists():\n            iface.messageBar().pushMessage('Erreur de chargement', f'Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n        else:\n            layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n            if not layer.isValid():\n                iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n            else:\n                QgsProject.instance().addMapLayer(layer)\n                iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n        print('Fin du script si on a un projet')\n\n# Appel de notre fonction\ncharger_couche(bd_topo, thematique, couche)\n

Am\u00e9liorons encore cette solution interm\u00e9diaire avec la gestion des erreurs avec l'instruction return

On peut garder le code le plus \u00e0 gauche possible gr\u00e2ce \u00e0 return qui ordonne la sortie de la fonction.

Afficher une des solutions finales
# En haut du script, ce souvent des variables \u00e0 modifier\nbd_topo = 'BD_TOPO'\n\n# Puis place au script\n# En th\u00e9orie, pas besoin de modification, en dessous pour un \"utilisateur final\" du script\n\nfrom pathlib import Path\n\ndef charger_couche(bd_topo, thematique, couche):\n    \"\"\" Fonction qui charge une couche de la BD TOPO, selon une th\u00e9matique. \"\"\"\n    projet_qgis = QgsProject.instance().absoluteFilePath()\n    if not projet_qgis:\n        iface.messageBar().pushMessage('Erreur de chargement','Le projet n\\'est pas enregistr\u00e9', Qgis.Critical)\n        return False\n\n    racine = Path(projet_qgis).parent\n    fichier_shape = racine.joinpath(bd_topo, thematique, f'{couche}.shp')\n    if not fichier_shape.exists():\n        iface.messageBar().pushMessage('Erreur de chargement','Le chemin n\\'existe pas: \"{fichier_shape}\"', Qgis.Critical)\n        return False\n\n    layer = QgsVectorLayer(str(fichier_shape), couche, 'ogr')\n    if not layer.isValid():\n        iface.messageBar().pushMessage('Erreur de chargement','La couche n\\'est pas valide', Qgis.Critical)\n        return False\n\n    QgsProject.instance().addMapLayer(layer)\n    iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n    # return True\n    return layer\n\n# Appel de notre fonction\ncharger_couche(bd_topo, 'ADMINISTRATIF', 'COMMUNE')\ncharger_couche(bd_topo, 'ADMINISTRATIF', 'ARRONDISSEMENT')\n
"},{"location":"fonctions-scripts/#une-fonction-pour-lister-les-couches-dune-thematique","title":"Une fonction pour lister LES couches d'UNE th\u00e9matique","text":"

Essayons de faire une fonction qui liste les shapefiles d'une certaine th\u00e9matique \ud83d\ude80

Plus pr\u00e9cis\u00e9ment, on souhaite une liste de cha\u00eenes de caract\u00e8res : ['COMMUNE', 'EPCI'].

Dans l'objet Path, il existe une m\u00e9thode iterdir(). Par exemple, pour it\u00e9rer sur le dossier courant de l'utilisateur :

from pathlib import Path\n\ndossier = Path.home()\nfor fichier in dossier.iterdir():\n    print(fichier)\n

Tip

Il faut se r\u00e9f\u00e9rer \u00e0 la documentation du module pathlib pour comprendre le fonctionnement de cette classe.

Voici la signature de la fonction que l'on souhaite :

def liste_shapefiles(bd_topo: str, thematique: str):\n    \"\"\" Lister les shapefiles d'une th\u00e9matique dans la BDTopo. \"\"\"\n    ...\n

Petit m\u00e9mo pour cet exercice :

Correction
def liste_shapefiles(bd_topo: str, thematique: str):\n    \"\"\" Lister les shapefiles d'une th\u00e9matique dans la BDTopo. \"\"\"\n    racine = Path(QgsProject.instance().absoluteFilePath()).parent\n    dossier = racine.joinpath(bd_topo, thematique)\n    shapes = []\n    for file in dossier.iterdir():\n        if file.suffix.lower() == '.shp':\n            shapes.append(file.stem)\n    return shapes\n\nshapes = liste_shapefiles(bd_topo, 'ADMINISTRATIF')\nprint(shapes)\n

On a d\u00e9sormais deux fonctions : liste_shapefiles et charger_couche.

Il est d\u00e9sormais simple de charger toutes une th\u00e9matique de notre BDTopo :

thematique = 'ADMINISTRATIF'\nshapes = liste_shapesfiles(bd_topo, thematique)\nfor shape in shapes:\n    charger_couche(bd_topo, thematique, shape)\n

Success

On a termin\u00e9 avec ces deux fonctions, c'\u00e9tait pour manipuler les fonctions \ud83d\ude0e

"},{"location":"fonctions-scripts/#pour-les-curieux","title":"Pour les curieux \ud83e\udd2d","text":"

Zoomer sur l'emprise d'une couche, sans la charger dans la l\u00e9gende

Example
  1. Modifions la signature de la fonction, en ajoutant un bool\u00e9en si on souhaite la couche dans la l\u00e9gende :
    def charger_couche(bd_topo, thematique, couche, ajouter_dans_legende = True):\n
    Puis dans cette m\u00eame fonction, utilisons cette variable :
    if ajouter_dans_legende:\n    QgsProject.instance().addMapLayer(layer)\n    iface.messageBar().pushMessage('Bravo','Well done! \ud83d\udc4d', Qgis.Success)\n# return True\nreturn layer\n

Puis on peut ordonner au QgsMapCanvas de zoomer sur une emprise :

hydro = charger_couche(bd_topo, 'ZONES_REGLEMENTEES', 'PARC_OU_RESERVE', False)\niface.mapCanvas().setExtent(hydro.extent())\n

Ne pas oublier de tenir compte d'une projection diff\u00e9rente entre le canevas et la couche.

TODO, \u00e0 adapter, mais le code est la pour faire une reprojection entre 2 CRS

extent = iface.activeLayer().extent()\ncrs_layer = iface.activeLayer().crs()\ncrs = iface.mapCanvas().mapSettings().destinationCrs()\ntransformer = QgsCoordinateTransform(crs_layer, crs, QgsProject.instance())\nnew_extent = transformer.transform(extent)\niface.mapCanvas().setExtent(new_extent)\n

"},{"location":"fonctions-scripts/#extraction-des-informations-sous-forme-dun-fichier-csv","title":"Extraction des informations sous forme d'un fichier CSV.","text":""},{"location":"fonctions-scripts/#introduction","title":"Introduction","text":"

On souhaite d\u00e9sormais r\u00e9aliser une fonction d'export des m\u00e9tadonn\u00e9es de nos couches au format CSV, avec des tabulations comme s\u00e9parateur et son CSVT.

Il existe d\u00e9j\u00e0 un module CSV dans Python pour nous aider \u00e0 \u00e9crire un fichier de type CSV, mais nous n'allons pas l'utiliser.

Nous allons plut\u00f4t utiliser l'API QGIS pour :

  1. Cr\u00e9er une nouvelle couche en m\u00e9moire comportant les diff\u00e9rentes informations que l'on souhaite exporter
  2. Puis, nous allons utiliser l'API pour exporter cette couche m\u00e9moire au format CSV (l'\u00e9quivalent dans QGIS de l'action Exporter la couche).

Les diff\u00e9rents champs qui devront \u00eatre export\u00e9s sont :

"},{"location":"fonctions-scripts/#exemple-de-sortie","title":"Exemple de sortie","text":"nom type projection nombre_entite encodage source seuil_de_visibilite couche_1 Line EPSG:4326 5 UTF-8 /tmp/...geojson False couche_2 Tab No geometry 0 /tmp/...shp True"},{"location":"fonctions-scripts/#petit-memo-avec-des-exemples","title":"Petit m\u00e9mo avec des exemples","text":"

Pour cr\u00e9er une couche tabulaire en m\u00e9moire, code qui vient du cookbook :

layer_info = QgsVectorLayer('None', 'info', 'memory')\n

La liste des couches :

layers = QgsProject.instance().mapLayers()\n

Cr\u00e9er une entit\u00e9 ayant d\u00e9j\u00e0 les champs pr\u00e9configur\u00e9s d'une couche vecteur, et y affecter des valeurs :

feature = QgsFeature(objet_qgsvectorlayer.fields())\nfeature['nom'] = \"NOM\"\n

Obtenir le dossier du projet actuel :

projet_qgis = Path(QgsProject.instance().fileName())\ndossier_qgis = projet_qgis.parent\n

Afficher la g\u00e9om\u00e9trie, sous sa forme \"humaine\", en cha\u00eene de caract\u00e8re, avec l'aide de QgsWkbTypes :

QgsWkbTypes.geometryDisplayString(vector_layer.geometryType())\n

Pour utiliser une session d'\u00e9dition, on peut faire :

layer.startEditing()  # D\u00e9but de la session\nlayer.commitChanges()  # Fin de la session en enregistrant\nlayer.rollback()  # Fin de la session en annulant les modifications\n

"},{"location":"fonctions-scripts/#les-contextes-python","title":"Les contextes Python","text":"

On peut \u00e9galement faire une session d'\u00e9dition avec un \"contexte Python\" :

from qgis.core import edit\n\nwith edit(layer):\n    # Faire une \u00e9dition sur la couche\n    pass\n\n# \u00c0 la fin du bloc d'indentation, la session d'\u00e9dition est automatiquement close, m\u00eame en cas d'erreur Python\n
Exemple de l'utilisation d'un contexte Python avec la session d'\u00e9dition

Sans contexte, la couche reste en mode \u00e9dition en cas d'erreur fatale Python

layer = iface.activeLayer()\n\nlayer.startEditing()\nprint(\"D\u00e9but de la session\")\n# Code inutile, mais qui va volontairement faire une exception Python\na = 10 / 0\n\nprint(\"Fin de la session\")\nlayer.commitChanges()\nprint(\"Fin du script\")\n

Mais utilisons d\u00e9sormais un contexte Python \u00e0 l'aide dewith, sur une couche qui n'est pas en \u00e9dition :

layer = iface.activeLayer()\n\nwith edit(layer):\n    print(\"D\u00e9but de la session\")\n    # Code inutile, mais qui va volontairement faire une exception Python\n    a = 10 / 0\n\nprint(\"Fin du script\")\n

On peut lire le code comme En \u00e9ditant la couche \"layer\", faire :.

"},{"location":"fonctions-scripts/#petit-memo-des-classes","title":"Petit m\u00e9mo des classes","text":"

Nous allons avoir besoin de plusieurs classes dans l'API QGIS :

Pour le type de champ, on va avoir besoin de l'API Qt \u00e9galement :

Note

Note perso, je pense qu'avec la migration vers Qt6, cela va pouvoir se simplifier un peu pour les QVariant...

"},{"location":"fonctions-scripts/#etapes","title":"\u00c9tapes","text":"

Il va y avoir plusieurs \u00e9tapes dans ce script :

  1. Cr\u00e9er une couche en m\u00e9moire
  2. Ajouter des champs \u00e0 cette couche en utilisant une session d'\u00e9dition
  3. R\u00e9cup\u00e9rer la liste des couches pr\u00e9sentes dans la l\u00e9gende
  4. It\u00e9rer sur les couches pour ajouter ligne par ligne les m\u00e9tadonn\u00e9es dans une session d'\u00e9dition
  5. Enregistrer en CSV la couche m\u00e9moire

Tip

Pour d\u00e9boguer, on peut afficher la couche m\u00e9moire en question avec QgsProject.instance().addMapLayer(layer_info)

"},{"location":"fonctions-scripts/#solution-possible","title":"Solution possible","text":"
from qgis.core import edit\n\n# Cr\u00e9ation de la couche m\u00e9moire\nlayer_info = QgsVectorLayer('None', 'info', 'memory')\n# QgsProject.instance().addMapLayer(layer_info)\n\n# Ajout des champs\nwith edit(layer_info):\n    layer_info.addAttribute(QgsField('nom', QVariant.String))\n    layer_info.addAttribute(QgsField('type', QVariant.String))\n    layer_info.addAttribute(QgsField('projection', QVariant.String))\n    layer_info.addAttribute(QgsField('nombre_entit\u00e9', QVariant.Int))\n    layer_info.addAttribute(QgsField('encodage', QVariant.String))\n    layer_info.addAttribute(QgsField('seuil', QVariant.Bool))\n    layer_info.addAttribute(QgsField('source', QVariant.String))\n\nlayers = QgsProject.instance().mapLayers()\nif not layers:\n    iface.messageBar().pushMessage('Pas de couche', \"Attention, il n'a pas de couche\", Qgis.Warning)\n\n# It\u00e9ration sur l'ensemble des couches du projet\nfor layer in layers.values():\n    feature = QgsFeature(layer_info.fields())\n    feature['nom'] = layer.name()\n    feature['type'] = QgsWkbTypes.geometryDisplayString(layer.geometryType())\n    feature['nombre_entit\u00e9'] = layer.featureCount()\n    feature['encodage'] = layer.dataProvider().encoding()\n    feature['projection'] = layer.crs().authid()\n    feature['seuil'] = layer.hasScaleBasedVisibility()\n    feature['source'] = layer.publicSource()\n\n    with edit(layer_info):\n        layer_info.addFeature(feature)\n\n# Export de la couche m\u00e9moire au format CSV\noptions = QgsVectorFileWriter.SaveVectorOptions()\noptions.driverName = 'CSV'\noptions.fileEncoding = 'UTF-8'\noptions.layerOptions = ['CREATE_CSVT=YES', 'SEPARATOR=TAB']\n\nbase_name = QgsProject.instance().baseName()\nracine = Path(QgsProject.instance().absoluteFilePath()).parent\noutput_file = racine.joinpath(f'{base_name}.csv')\n\nQgsVectorFileWriter.writeAsVectorFormatV3(\n    layer_info,\n    str(output_file),\n    QgsProject.instance().transformContext(),\n    options,\n)\n

Warning

Ajouter une couche raster et retester le script ... surprise \ud83c\udf81

Pour les experts, ajouter un alias ou un commentaire sur un champ

field = QgsField(\n    'seuil_visibilite',\n    QVariant.Bool,\n    comment=\"Champ contenant le seuil de visibilit\u00e9\")\nfield.setAlias(\"Seuil de visibilit\u00e9\")\nlayer_info.addAttribute(field)\n
Ceci dit, cela d\u00e9pend dans quel format on exporte la couche, dans l'exercice, on fait du CSV, donc on perd ces informations.

Tip

Pour obtenir en Python la liste des fournisseurs GDAL/OGR :

from osgeo import ogr\n[ogr.GetDriver(i).GetDescription() for i in range(ogr.GetDriverCount())]    \n
ou dans le menu Pr\u00e9f\u00e9rences \u27a1 Options \u27a1 GDAL \u27a1 Pilotes vecteurs

"},{"location":"fonctions-scripts/#finalisation","title":"Finalisation","text":"

Id\u00e9alement, il faut v\u00e9rifier le r\u00e9sultat de l'enregistrement du fichier. Les diff\u00e9rentes m\u00e9thodes writeAsVectorFormat retournent syst\u00e9matiquement un tuple avec un code d'erreur et un message si n\u00e9cessaire, voir la documentation.

Pour s'en rendre compte, on peut ajouter une variable result = QgsVectorFileWriter.writeAsVectorFormatV3(...). Puis de faire un print(result) pour s'en rendre compte. On peut tenir compte donc ce tuple :

De plus, en cas de succ\u00e8s, il est pratique d'avertir l'utilisateur. On peut aussi fournir un lien pour ouvrir l'explorateur de fichier :

# Affichage d'un message \u00e0 l'utilisateur\niface.messageBar().pushSuccess(\n    \"Export OK des couches \ud83d\udc4d\",\n    (\n        \"Le fichier CSV a \u00e9t\u00e9 enregistr\u00e9 dans \"\n        \"<a href=\\\"{}\\\">{}</a>\"\n    ).format(output_file.parent, output_file)\n)\n
Pour ajouter le support du message d'erreur
if result[0] != QgsVectorFileWriter.WriterError.NoError:\n    print(f\"Erreur : {result[1]}\")\nelse:\n    # Affichage d'un message \u00e0 l'utilisateur\n    iface.messageBar().pushSuccess(\n        \"Export OK des couches \ud83d\udc4d\",\n        (\n            \"Le fichier CSV a \u00e9t\u00e9 enregistr\u00e9 dans \"\n            \"<a href=\\\"{}\\\">{}</a>\"\n        ).format(output_file.parent, output_file)\n    )\n
"},{"location":"formulaire/","title":"Formulaire","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

On peut personnaliser un formulaire avec :

Tip

Le blog de Nathan est une bonne ressource concernant les formulaires et QtDesigner pour cette partie la, mais cela commence \u00e0 \u00eatre vieux.

Sur la couche Geopackage, dans les propri\u00e9t\u00e9s de la couche \u27a1 Formulaire d'attributs, cliquer sur le petit logo Python en haut bleu et jaune. Choisir l'option Fournir le code dans cette bo\u00eete de dialogue.

Dans le nom de la fonction, mettre my_form_open qui correspond \u00e0 l'exemple du code en dessous.

La fonction my_form_open sera donc ex\u00e9cut\u00e9 par d\u00e9faut lors de l'ouverture du formulaire. On remarque qu'il y a trois param\u00e8tres qui sont donn\u00e9s :

Dans l'objet dialog :

Pour information :

Afficher
from qgis.PyQt.QtCore import QUrl\nfrom qgis.PyQt.QtWidgets import QWidget, QDialogButtonBox\nfrom qgis.PyQt.QtGui import QDesktopServices\n\ndef my_form_open(dialog, layer, feature):\n    button_box = dialog.findChild(QDialogButtonBox)\n    button_box.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Help|QDialogButtonBox.Ok)\n\n    button_box.button(QDialogButtonBox.Help).clicked.connect(open_help)\n\ndef open_help():\n    QDesktopServices.openUrl(QUrl('https://docs.3liz.org/'))\n

type_field = dialog.findChild(QLineEdit, \"type\")\ntype_field.setStyleSheet(\"background-color: rgba(255, 107, 107, 150);\")\n
* On souhaite rendre le champ en rouge seulement s'il y a une condition :

type_field = dialog.findChild(QLineEdit, \"type\")\ntype_field.textChanged.connect(type_field_changed)\n\ndef type_field_changed():\n    type_field = dialog.findChild(QLineEdit, \"type\")\n    if type_field.text() not in ('studio', 'appartement', 'maison'):\n        type_field.setStyleSheet(\"background-color: rgba(255, 107, 107, 150);\")\n        type_field.setToolTip(\"La valeur doit \u00eatre 'studio', 'appartement' ou 'maison'.\")\n    else:\n        type_field.setStyleSheet(\"\")\n        type_field.setToolTip()\n

Info

Notons que ce ne sont que des exemples des fonctionnalit\u00e9s Python. On peut faire ces masques de saisie \u00e0 l'aide des expressions QGIS ou simplement en changeant le type de widget pour un champ en particulier.

"},{"location":"ide-git/","title":"Python avanc\u00e9","text":""},{"location":"ide-git/#utilisation-dun-ide","title":"Utilisation d'un IDE","text":"

Pour \u00e9crire du code Python, on peut utiliser n'importe quel \u00e9diteur de texte brut quelque soit l'OS. Cependant, l'utilisation d'un \u00e9diteur de texte qui \"comprend\" le code Python est vivement recommand\u00e9, car il peut vous signaler quelques erreurs facilement d\u00e9tectables, telles que les imports manquants. Comme \u00e9diteur de texte, il en existe plusieurs.

Si vous souhaitez faire plus de programmation, nous vous recommandons l'utilisation d'un IDE. Il embarque l'\u00e9diteur de texte ci-dessus, mais poss\u00e8de aussi des outils de debugs et d'assistance dans l'\u00e9criture du code comme l'autocompl\u00e9tion.

En IDE gratuit, il existe :

Un IDE est outil tr\u00e8s complet pour d\u00e9veloppement. Il est possible de coder en Python avec un \u00e9diteur de texte, mais si possible qui sait quand m\u00eame faire de la coloration syntaxique du code Python est vraiment un plus (NotePad++\u2026).

"},{"location":"ide-git/#lancer-un-script-python-dans-la-console","title":"Lancer un script Python dans la console","text":"

Si vous utilisez un IDE pour \u00e9crire du code Python, vous pouvez lancer le code Python dans la console Python \u00e0 l'aide de cette astuce.

"},{"location":"ide-git/#utilisation-de-git","title":"Utilisation de GIT","text":"

Il est vivement recommand\u00e9 d'utiliser GIT :

La documentation : https://git-scm.com/docs/

Les commandes les plus utiles :

Liens vers OpenClassRooms :

"},{"location":"legende/","title":"Objectif","text":"

\u00c0 travers ce TP, on va traiter plusieurs points :

METTRE PHOTO l\u00e9gende

"},{"location":"legende/#donnees","title":"Donn\u00e9es","text":"

On peut placer les deux fichiers l'un \u00e0 c\u00f4t\u00e9 de l'autre. Ouvrir dans QGIS le fichier GPKG seulement.

"},{"location":"legende/#objectif_1","title":"Objectif","text":"
from pathlib import Path\n\nlayer = iface.activeLayer()\n\nparent_folder = Path(layer.source()).parent\n\nprint(parent_folder)\n\nfichier_ods = parent_folder / \"base_cc_comparateur.ods\"\nprint(fichier_ods.is_file())\n\ntableur = QgsVectorLayer(str(fichier_ods), \"tableur\", \"ogr\")\nprint(tableur.isValid())\n
"},{"location":"memo-python/","title":"Introduction au language Python","text":""},{"location":"memo-python/#quest-ce-que-python","title":"Qu'est-ce que Python ?","text":"

Exemple d'un code qui d\u00e9clare une variable et compare si sa valeur est sup\u00e9rieur \u00e0 5 afin d'afficher un message :

# D\u00e9claration d'une variable de type entier\nx = 10\n\n# D\u00e9claration d'une variable cha\u00eene de caract\u00e8re\ninfo = 'X est sup\u00e9rieur \u00e0 5'\n\nif x > 5:\n    print(info)\n
"},{"location":"memo-python/#versions","title":"Versions","text":""},{"location":"memo-python/#rappel-de-base-sur-python","title":"Rappel de base sur Python","text":""},{"location":"memo-python/#la-console","title":"La console","text":"

Pour la suite de la formation, nous allons utiliser la console Python de QGIS.

Dans le menu Extensions \u27a1 Console Python.

Tip

Souvent, avec Windows, il y a un conflit avec un raccourci clavier pour taper le caract\u00e8re { ou } dans la console.

Ces caract\u00e8res sont utilis\u00e9s en Python. Il est donc conseill\u00e9 de supprimer ce raccourci clavier. Il s'agit du \"zoom + secondaire\" dans QGIS \u2192 menu Pr\u00e9f\u00e9rences \u27a1 Raccourcis clavier.

"},{"location":"memo-python/#les-types-de-donnees","title":"Les types de donn\u00e9es","text":"

Une variable peut contenir un entier, un bool\u00e9en (True ou False), cha\u00eene de caract\u00e8res, nombre d\u00e9cimal, un objet... Il y a un faible typage des variables, c'est-\u00e0-dire qu'une variable peut changer de type au cours de l'ex\u00e9cution du programme.

# Pour cr\u00e9er une variable, on d\u00e9clare juste le nom de la variable ainsi que sa valeur :\ncompteur = 0\n

Nous allons par la suite utiliser type(variable) pour v\u00e9rifier le type de la variable.

mon_compteur = 0\ntype(mon_compteur)\n<class 'int'>\n\nest_valide = False\ntype(est_valide)\n<class 'bool'>\n\nnom_couche = 'communes'\ntype(nom_couche)\n<class 'str'>\n\nnom_couche = \"communes\"\ntype(nom_couche)\n<class 'str'>\n\ntexte = 'Bonjour je m\\'appelle \"Olivier\"'\ntype(texte)\n<class 'str'>\n\ndensite = 3.5\ntype(densite)\n<class 'float'>\n\nnom_couche = None\ntype(nom_couche)\n<class 'NoneType'>\n
"},{"location":"memo-python/#les-structures-de-donnees","title":"Les structures de donn\u00e9es","text":"

Il existe quatre types de structure de donn\u00e9es :

Tip

Documentation Python sur les listes

# Cr\u00e9er une liste vide\nnombres = []\ntype(nombres)\n<class 'list'>\n\n# Cr\u00e9er une liste avec des \u00e9l\u00e9ments \u00e0 l'int\u00e9rieur\nmois = ['janvier', 'f\u00e9vrier', 'mars']\n\n# Ajouter un \u00e9l\u00e9ment\nmois.append('avril')\n# Ajouter une autre liste\nmois.extend(['mai', 'juin'])\n\n# Nombre de mois\nlen(mois)\n\n# Supprimer un \u00e9l\u00e9ment\ndel mois[1]\n\n# On peut acc\u00e9der \u00e0 un \u00e9l\u00e9ment avec un \"index\" \u00e0 l'aide de []\nmois[2]\n\n# Attention \u00e0 l'index maximum\nmois[12]\nTraceback (most recent call last):\n  File \"/usr/lib/python3.12/code.py\", line 90, in runcode\n    exec(code, self.locals)\n  File \"<input>\", line 1, in <module>\nIndexError: tuple index out of range\n

Tip

Documentation Python sur les tuples

liste = ('route double sens', 'route sens unique')\ntype(liste)\n<class 'tuple'>\nlen(liste)\n2\nliste[0]\n\nliste[5]\nTraceback (most recent call last):\n  File \"/usr/lib/python3.12/code.py\", line 90, in runcode\n    exec(code, self.locals)\n  File \"<input>\", line 1, in <module>\nIndexError: tuple index out of range\n

Attention, les dictionnaires ne sont pas ordonn\u00e9s, de fa\u00e7on native, m\u00eame depuis Python 3.9. Si vraiment, il y a besoin, il existe une classe OrderedDict, mais ce n'est pas une structure de donn\u00e9es native dans Python. C'est un objet qu'il faut importer.

commune = {}\ntype(commune)\n# <class 'dict'>\ncommune['nom'] = 'Besan\u00e7on'\ncommune['code_insee'] = 25056\ncommune['est_prefecture'] = True\n\n# Ou directement lors de la cr\u00e9ation de la variable :\ncommune = {'nom': 'Besan\u00e7on', 'code_insee': 25056, 'est_prefecture': True}\n\n# Lire le contenu d'une cl\u00e9 :\nprint(commune['nom'])\nprint(commune['population'])  # Leve une erreur IndexError\nprint(commune.get('population'))  # Imprime None\n
"},{"location":"memo-python/#les-commentaires","title":"Les commentaires","text":"

Pour commenter le code dans un script, pas dans la console :

# Ceci est un commentaire sur une ligne\n\n\"\"\" Ces lignes sont r\u00e9serv\u00e9s pour la documentation de l'API et ne doivent pas \u00eatre des lignes de commentaires. \"\"\"\n
"},{"location":"memo-python/#arithmetique","title":"Arithm\u00e9tique","text":"
a = 10\n# Op\u00e9rateurs de base\nb = a + 1\nc = a - 1\nd = a * 2\ne = a / 2\n\n# Les espaces ne sont pas importants\n# 1+2 ou 1 + 2 sont \u00e9quivalents\n\n# Il est souvent utile de faire de l'incr\u00e9mentation ou d\u00e9cr\u00e9mentation d'une variable :\na = a + 1\n# mais on \u00e9crit plus souvent\na += 1\n# On peut changer le pas d'incr\u00e9mentation ou alors faire de la d\u00e9cr\u00e9mentation\na -= 1 # Diminuer de 1\na += 5 # Incr\u00e9menter de 5\n\n\na = 10\nf = a % 3  # Fonction \"modulo\", r\u00e9sultat 1\ng = a ** 2  # Fonction puissance, r\u00e9sultat 100\n
"},{"location":"memo-python/#concatener-des-chaines-et-des-variables","title":"Concat\u00e9ner des cha\u00eenes et des variables","text":"

Concat\u00e9ner, c'est assembler des cha\u00eenes de caract\u00e8res dans une seule et m\u00eame sortie. On peut concat\u00e9ner des variables entre elles ou du texte.

Il existe plein de mani\u00e8res de faire, mais certaines sont plus pratiques que d'autres

# Non recommand\u00e9\na = 'bon'\nb = 'jour'\na + b  # 'bonjour'\nc = 1\na + c  # Erreur\na + str(c)  # Marche\n

\u00c0 l'ancienne avec %

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = 'Bonjour %s !' % prenom\nbienvenue = 'Bonjour %s, nous sommes le %s novembre' % (prenom, numero_jour)\n

Nouveau avec {} et format

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = 'Bonjour {} !'.format(prenom)\nbienvenue = 'Bonjour {}, nous sommes le {} novembre'.format(prenom, numero_jour)\nbienvenue = 'Bonjour {prenom}, nous sommes le {jour} novembre'.format(prenom=prenom, jour=numero_jour)\n

Warning

Attention \u00e0 la port\u00e9e des variables.

Encore plus moderne avec fstring

prenom = 'Pierre'\nnumero_jour = 2\nbienvenue = f'Bonjour {prenom} !'\nbienvenue = f'Bonjour {prenom}, nous sommes le {numero_jour} novembre'\n

"},{"location":"memo-python/#operateurs-logiques","title":"Op\u00e9rateurs logiques","text":"
a > b\na >= b\na < b\na <= b\na == b\na != b\n\n# Dans plusieurs langages, pour v\u00e9rifier si \"a\" est entre deux bornes :\n0 < a and a < 10\n# En Python, on peut faire\n0 < a < 10\n\n# Pour les objets\na is b\na is not b\na in b\n
"},{"location":"memo-python/#condition","title":"Condition","text":"

Important, Python oblige l'indentation sinon il y a une erreur. Par convention, il s'agit de 4 espaces.

note = 13\nif note >= 16:\n    if note == 20:\n        print('Toutes mes f\u00e9licitations')\n    else:\n        print('F\u00e9licitations')\nelif 14 <= note < 16:\n    print('Tr\u00e8s bien')\nelif 12 <= note < 14:\n    print('Bien')\nelse:\n    print('Peu mieux faire')\n
"},{"location":"memo-python/#boucle-for","title":"Boucle for","text":"

Utile lors que l'on connait le nombre de r\u00e9p\u00e9titions avant l'ex\u00e9cution de la boucle.

countries = ['Allemagne', 'Espagne', 'France']\nfor country in countries:\n    print(f'Pays : {country}')\n\nfor x in range(10):\n    print(x)\n\nregions = {\n    'Auvergne-Rh\u00f4ne-Alpes': 'Lyon',\n    'Bourgogne-Franche-Comt\u00e9': 'Dijon',\n    'Bretagne': 'Rennes',\n    'Centre-Val de Loire': 'Orl\u00e9ans',\n}\n\nfor region in regions:\n    print(region)\n\nfor region in regions.keys():\n    print(region)\n\nfor city in regions.values():\n    print(city)\n\nfor region, city in regions.items():\n    print(f'R\u00e9gion {region} dont le chef lieu est {city}')\n\n# Non recommand\u00e9, mais on peut le rencontrer\nfor region in regions.keys():\n  print(f\"R\u00e9gion {region} dont le chef lieu est {regions[region]}\")\n
"},{"location":"memo-python/#recherche-dun-element","title":"Recherche d'un \u00e9l\u00e9ment","text":"
countries = ['Allemagne', 'Espagne', 'France']\n\n# Solution simple\nif 'Allemagne' in countries:\n    print('Pr\u00e9sent')\nelse:\n    print('Non pr\u00e9sent')\n\n# Plus complexe, avec une fonction pour les minuscules\npresent = False\nfor country in countries:\n    if country.lower() == 'allemagne':\n        present = True\nif present:\n    print('Pr\u00e9sent')\nelse:\n    print('Non pr\u00e9sent')\n\n\n# Le plus pythonique\nfor country in countries:\n    if country.lower() == 'allemagne':\n        print('Pr\u00e9sent')\n        break\nelse:\n    print('Non pr\u00e9sent')\n\n# Encore plus pythonique avec une list-comprehension, voir plus bas\n
"},{"location":"memo-python/#boucle-while","title":"Boucle while","text":"

Contrairement \u00e0 la boucle for, on ne connait pas forc\u00e9ment le nombre d'ex\u00e9cution de la boucle en lisant uniquement la ligne while.

x = 0\nwhile x < 10:\n    print(x)\n    x += 1\n

Error

Attention \u00e0 ne pas faire une boucle infinie !

executer_une_fonction()\nwhile not conditon_echec:\n    executer_une_fonction()\n
"},{"location":"memo-python/#switch","title":"Switch","text":"

Python 3.10 minimum

numero_jour = 2\n\nmatch numero_jour:\n  case 1:\n    print('Lundi')\n  case 2:\n    print('Mardi')\n  case 3:\n    print('Mercredi')\n  case 4:\n    print('Jeudi')\n  case 5:\n    print('Vendredi')\n  case 6:\n    print('Samedi')\n  case 7:\n    print('Dimanche')\n  case _:\n    print('Pas un jour de la semaine')\n
Avant Python 3.10 avec un if elif
numero_jour = 2\n\nif numero_jour == 1:\n    print('Lundi')\nelif numero_jour == 2:\n    print('Mardi')\nelif numero_jour == 3:\n    print('Mercredi')\nelif numero_jour == 4:\n    print('Jeudi')\nelif numero_jour == 5:\n    print('Vendredi')\nelif numero_jour == 6:\n    print('Samedi')\nelif numero_jour == 7:\n    print('Dimanche')\nelse:\n    print('Pas un jour de la semaine')\n
"},{"location":"memo-python/#list-comprehensions","title":"List Comprehensions","text":"

C'est une fa\u00e7on tr\u00e8s pythonique et tr\u00e8s utilis\u00e9e de cr\u00e9er des listes.

"},{"location":"memo-python/#pour-transformer-une-liste-existante-en-la-remplacant","title":"Pour transformer une liste existante, en la rempla\u00e7ant :","text":"
countries = ['Allemagne', 'Espagne', 'France']\ncountries = [c.upper() for c in countries]\n

Par exemple, cr\u00e9er une liste des nombres impairs entre 1 et 9 :

# Non pythonique\nimpair = []\nfor x in range(10):\n    if x % 2:\n        impair.append(x)\n\n# Pythonique\nimpair = [x for x in range(10) if x % 2]\n
"},{"location":"memo-python/#manipulation-sur-les-chaines-de-caracteres","title":"Manipulation sur les cha\u00eenes de caract\u00e8res","text":"

Slicing sur les mois de l'ann\u00e9e :

mois = ['Janvier', 'F\u00e9vrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Octobre', 'Novembre', 'D\u00e9cembre']\n\nmois[0:2]\n['Janvier', 'F\u00e9vrier']\n\nmois[2:]\n['Mars', 'Avril', 'Mai', 'Juin', 'Octobre', 'Novembre', 'D\u00e9cembre']\n\nmois[:-2]\n['Janvier', 'F\u00e9vrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Octobre']\n\nmois[-2:]\n['Novembre', 'D\u00e9cembre']\n
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nlen(alphabet)\n','.join(alphabet)\nalphabet.lower()\nalphabet.upper()\nalphabet[1]  # B\nalphabet[1:3]  # BC\nalphabet[-1]  # Z\nalphabet[-3:]  # XYZ\nalphabet[:6]  # ABCDEF\n
"},{"location":"memo-python/#fonctions","title":"Fonctions","text":"

Une fonction permet de factoriser son code. Elle peut :

def ajouter(x, y):\n    \"\"\"Ajouter deux nombres.\"\"\"\n    return x + y\n\ndef crier(phrase='bonjour'):\n    print(phrase.upper())\n\ndef discuter(texte, personnage='Charles'):\n    \"\"\"Un personnage discute.\"\"\"\n    print('{}: \"{}\"'.format(personnage, texte))\n\ndef discuter(texte, personnage='Charles'):\n    \"\"\"Un personnage discute.\"\"\"\n    return f'{personnage}: \"{texte}\"'\n
def decomposer(entier, diviser_par):\n    \"\"\"Retourne la partie enti\u00e8re et le reste d'une division.\"\"\"\n    partie_entiere = entier / diviser_par\n    reste = entier % diviser_par\n    return int(partie_entiere), reste\n
def une_fonction(*args, **kwargs):\n    print('Les arguments')\n    for arg in args:\n        print(arg)\n    print('Les arguments non nomm\u00e9s')\n    for key, value in kwargs.items():\n        print('{} -> {}'.format(key, value))\n\nune_fonction(1,2,3, text='Ma phrase')\n
"},{"location":"memo-python/#poo-programmation-orientee-objet","title":"POO : Programmation Orient\u00e9e Objet","text":"

Pour l'explication th\u00e9orique, lire l'introduction dans le chapitre de la console.

On peut introduire l'utilisation de la POO \u00e0 l'aide de l'objet Path. La documentation de cette classe se trouve en ligne.

La librairie Path est install\u00e9 de base avec Python.

La programmation orient\u00e9e objet permet de cr\u00e9er un objet (on parle plus pr\u00e9cis\u00e9ment d'instancier) puis on peut appeler des m\u00e9thodes sur cet objet.

Dans une console QGIS :

from pathlib import Path\n# Appel du \"constructeur\"\nchemin = Path('.')\n# La notation . est une cha\u00eene de caract\u00e8re particuli\u00e8re pour un OS demandant le dossier courant de l'ex\u00e9cution.\n# On peut utiliser .. pour faire r\u00e9f\u00e9rence au dossier parent\nprint(chemin.absolute())\nprint(chemin.is_dir())\nun_fichier = chemin / 'mon_projet.qgs'\nprint(un_fichier.exists())\nprint(un_fichier.name)\nprint(un_fichier.name)\nprint(chemin.joinpath('mon_projet.qgs').exists())\n

Tip

Quand l'instruction se termine par des (), on dit que c'est une m\u00e9thode de cet objet. Il s'agit d'une fonction, qui peut prendre ou non des param\u00e8tres et qui peut renvoyer ou non des r\u00e9sultats en sortie. Quand l'instruction ne se termine pas par (), on acc\u00e8de \u00e0 une propri\u00e9t\u00e9 de l'objet.

Pour une application avec des objets QGIS, il faut lire le chapitre suivant sur la console ou encore la partie sur l'\u00e9criture d'un script Processing.

"},{"location":"memo-python/#exceptions","title":"Exceptions","text":"

Lire le chapitre sur le parcours des entit\u00e9s.

"},{"location":"memo-python/#truc-et-astuces","title":"Truc et astuces","text":""},{"location":"memo-python/#passage-par-reference","title":"Passage par r\u00e9f\u00e9rence","text":"

Warning

Attention au passage par r\u00e9f\u00e9rence :

ma_liste_1 = [1, 2, 3]\nma_liste_2 = ma_liste_1\nma_liste_2.append(4)\nprint(ma_liste_2)\nprint(ma_liste_1)\n

"},{"location":"memo-python/#enumerate","title":"Enumerate","text":"

Avoir un compteur lors de l'it\u00e9ration d'une liste :

users = ['Tom', 'James', 'John']\nfor i, user in enumerate(users):\n    print('{} -> {}'.format(i + 1, user))\n

"},{"location":"memo-python/#annotations","title":"Annotations","text":"

Dans la suite de la formation, il est possible de voir des annotations Python. Cela permet de sp\u00e9cifier le type des variables dans les param\u00e8tres des fonctions et/ou de d\u00e9finir le type de retour.

from typing import Tuple\ndef decomposer(entier: int, diviser_par: int) -> Tuple[int, int]:\n    \"\"\"Retourne la partie enti\u00e8re et le reste d'une division.\"\"\"\n    partie_entiere = entier / diviser_par\n    reste = entier % diviser_par\n    return int(partie_entiere), reste\n

Il faut lire la documentation des annotations pour voir les diff\u00e9rentes possibilit\u00e9s.

"},{"location":"memo-python/#signal-et-slot","title":"\"Signal\" et \"slot\"","text":"

Lire le passage dans Signaux et slots.

"},{"location":"memo-python/#terminologie","title":"Terminologie","text":""},{"location":"migration-majeure/","title":"Migration majeure au sein de PyQGIS","text":"

Lire la page recensant les \"breaking changes\" de QGIS de toutes les versions.

"},{"location":"migration-majeure/#qgis-2-qgis-3","title":"QGIS 2 \u2192 QGIS 3","text":"

Petit guide sur une migration vers QGIS 3.

"},{"location":"migration-majeure/#qt-5-qt-6","title":"Qt 5 \u2192 Qt 6","text":"

QGIS tente de passer de la version Qt 5 vers Qt 6, sans passer par la case QGIS 4.0 (et donc non \"cassage\" de l'API QGIS 3.X).

Le travail de migration a commenc\u00e9 depuis QGIS 3.34. Mais \u00e0 l'heure actuelle, fin 2024, il n'existe qu'un binaire de QGIS pour tester Qt 6. C'est pour le moment, \"sous le capot\" de QGIS que cela se passe.

Contrairement au passage Qt 4 vers Qt 5, il est possible de rendre une extension compatible pour les deux versions \u00e0 l'aide d'un script que l'on peut trouver dans l'autre petit guide pour une migration vers Qt 6.

Par exemple, l'extension Lizmap avec le commit qui ajoute la compatibilit\u00e9 Qt 6.

"},{"location":"postgis/","title":"PostGIS","text":""},{"location":"postgis/#psycopg","title":"Psycopg","text":"

En Python, il existe un package d\u00e9di\u00e9 \u00e0 PostgreSQL, il s'agit de Psycopg. Il s'agit d'un package totalement ind\u00e9pendant de QGIS.

Exemple pour r\u00e9cup\u00e9rer les tables pr\u00e9sentes dans une base de donn\u00e9es \u00e0 l'aide de SQL

import psycopg\n\ninspect_schema = \"mon_schema\"\n\nconnection = psycopg.connect(\n    user=\"docker\", password=\"docker\", host=\"db\", port=\"5432\", database=\"gis\"\n)\ncursor = connection.cursor()\ncursor.execute(\n    f\"SELECT table_name FROM information_schema.tables WHERE table_schema = '{inspect_schema}'\"\n)\nrecords = cursor.fetchall()\nprint(records)\n
"},{"location":"postgis/#pyqgis","title":"PyQGIS","text":"

Depuis QGIS 3.16, il existe de plus en plus de m\u00e9thodes dans la classe QgsAbstractDatabaseProviderConnection pour interagir avec une base de donn\u00e9es PostGIS.

from qgis.core import QgsProviderRegistry\n\nmetadata = QgsProviderRegistry.instance().providerMetadata('postgres')\nconnection = metadata.findConnection(\"nom de la connexion PG dans votre panneau\")\n\n# Faire une requ\u00eate SQL (ou plusieurs)\n# Besoin d'\u00e9chapper en utilisant \"\" si votre sch\u00e9ma ou table comporte des majuscules\nresults = connection.executeSql(\"SELECT * FROM \\\"schema\\\".\\\"table\\\";\")\nprint(results)\n\n# Cr\u00e9er un sch\u00e9ma\nconnection.createSchema(\"mon_nouveau_schema\")\n\n# Lister les tables\nprint(connection.tables(\"un_schema\"))\n\n# Afficher une table dans QGIS, cela retourne une cha\u00eene de caract\u00e8re\n# permettant de faire une source de donn\u00e9es pour une QgsVectorLayer\nprint(connection.tableUri(\"schema\", \"table\"))\n

Afficher une table sans g\u00e9om\u00e9trie :

layer = QgsVectorLayer(connection.tableUri(\"schema\", \"table\"), \"Ma table\", \"postgres\")\nlayer.loadDefaultStyle()  # Si un style par d\u00e9faut existe dans votre base PostgreSQL, avec la table layer_styles\nQgsProject.instance().addMapLayer(layer)\n

Afficher une table avec g\u00e9om\u00e9trie en partant de QgsDataSourceUri :

uri = QgsDataSourceUri(connection.uri())\nuri.setSchema('schema')\nuri.setTable('table')\nuri.setKeyColumn('uid')\n\n# Avec une geom si besoin\nuri.setGeometryColumn('geom')\n\nlayer = QgsVectorLayer(uri.uri(), 'Ma table', 'postgres')\nQgsProject.instance().addMapLayer(layer)\n

Afficher le r\u00e9sultat d'un SELECT :

# Notons l'usage des parenth\u00e8ses autour du SELECT\nuri = QgsDataSourceUri(connection.uri())\nuri.setTable('(SELECT * FROM schema.table)')\nuri.setKeyColumn('uid')\n\n# Avec une geom si besoin\nuri.setGeometryColumn('geom')\n\nlayer = QgsVectorLayer(uri.uri(), 'Requ\u00eate SELECT', 'postgres')\nQgsProject.instance().addMapLayer(layer)\n

Exemple d'extension

Si besoin, l'extension PgMetadata utilise exclusivement l'API \"Base de donn\u00e9es PG\" de QGIS.

"},{"location":"python-qgis/","title":"Le python dans QGIS","text":"

QGIS permet d'utiliser du Python dans divers emplacement que nous allons voir ci-dessous. Python poss\u00e8de de tr\u00e8s nombreux packages/modules disponibles sur internet qui fournissent des fonctions d\u00e9j\u00e0 \u00e9crites.

"},{"location":"python-qgis/#console","title":"Console","text":"

La console est accessible par le menu Extension -> Console Python. Elle permet l'\u00e9criture de commande simple, une par une. On ne peut pas enregistrer les commandes dans un fichier.

"},{"location":"python-qgis/#script-python","title":"Script Python","text":"

L'\u00e9diteur de script Python est accessible depuis l'ic\u00f4ne d\u00e9di\u00e9e dans la console Python. Il permet un prototypage rapide d'un script. On peut y \u00e9crire du code plus complexe en faisant intervenir des librairies ou des classes.

"},{"location":"python-qgis/#script-processing","title":"Script Processing","text":"

Le menu Traitement dans QGIS donne acc\u00e8s a plusieurs algorithmes d'analyse. Ces algorithms proviennent soient de QGIS, GDAL ou encore de plugins. La bo\u00eete \u00e0 outils de traitements ainsi que le modeleur graphique utilisent le \"framework\" Processing propre \u00e0 QGIS. Ce framework permet de d\u00e9finir les entr\u00e9es et les sorties d'un algorithme. Les algorithms sont donc normalis\u00e9s en suivant tous le m\u00eame mod\u00e8le. Processing impose la fa\u00e7on d'\u00e9crire les scripts.

\u00c9crire un script compatible QGIS Processing permet l'int\u00e9gration dans ce menu, permet \u00e9galement l'utilisation de ce-dernier dans un mod\u00e8le ou encore l'utilisation en mode traitement par lot. Le framework peut aussi g\u00e9n\u00e9rer automatiquement l'interface graphique de l'algorithme et le code est optimis\u00e9.

Il existe un mod\u00e8le par d\u00e9faut que l'on peut utiliser pour d\u00e9marrer l'\u00e9criture d'un script Processing. Depuis la barre d'outils traitements, Cr\u00e9er un nouveau script depuis un mod\u00e8le. Ce mod\u00e8le utilise la syntaxe Programmation Orient\u00e9e Objet. Depuis QGIS 3.6, on peut \u00e9galement utiliser la syntaxe par d\u00e9corateur @alg.

Voir la documentation https://docs.qgis.org/latest/fr/docs/user_manual/processing/scripts.html#the-alg-decorator

"},{"location":"python-qgis/#un-modele-processing-en-python","title":"Un mod\u00e8le Processing en Python","text":"

Depuis QGIS 3.6, on peut d\u00e9sormais exporter un mod\u00e8le de traitement Processing en Python. Il faut faire un clic droit sur un mod\u00e8le dans la bo\u00eete \u00e0 outils puis choisir \"Exporter le mod\u00e8le comme un algorithme Python\". On peut donc modifier ensuite ce fichier Python afin de rajouter de la logique suppl\u00e9mentaire.

"},{"location":"python-qgis/#extension-plugin","title":"Extension (plugin)","text":""},{"location":"python-qgis/#fournisseur-processing-dans-une-extension-processing-provider","title":"Fournisseur Processing dans une extension (Processing Provider)","text":"

Similaire au script Processing, une extension QGIS peut aussi avoir son propre fournisseur d'algorithme.

On peut remarquer les plugins DataPlotly, QuickOSM etc.

Ajout de Processing \u00e0 un plugin QGIS :

Il se peut que certaines extensions ne soient que des fournisseurs Processing.

"},{"location":"python-qgis/#expressions","title":"Expressions","text":"

Les expressions sont souvent pr\u00e9sentes dans QGIS. On peut les utiliser dans nombreux endroits, pour faire des s\u00e9lections, des conditions, etc. On peut \u00e9galement les utiliser \u00e0 chaque fois que vous pouvez voir ce symbole :

Un plugin, ou m\u00eame simplement un utilisateur, peut enregistrer ses propres expressions. Ci-dessous, le plugin InaSAFE:

"},{"location":"python-qgis/#macros","title":"Macros","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

Accessible depuis les propri\u00e9t\u00e9s du projet, dans l'onglet Macros. On peut lancer du code Python automatiquement soit :

"},{"location":"python-qgis/#formulaire","title":"Formulaire","text":"

Warning

Pensez \u00e0 autoriser les macros dans les Propri\u00e9t\u00e9s de QGIS \u27a1 G\u00e9n\u00e9ral \u27a1 Fichiers du projet \u27a1 Activer les macros

On peut personnaliser un formulaire par l'ajout de logique Python. Cependant, dans QGIS 3, l'utilisation de Python n'est plus forc\u00e9ment n\u00e9cessaire, on peut d\u00e9sormais utiliser des expressions (recommand\u00e9).

"},{"location":"python-qgis/#actions","title":"Actions","text":"

Les actions sont des petits traitements que l'on peut lancer soit depuis la table attributaire ou depuis le canevas. Par exemple, on peut ouvrir un lien WEB ou un PDF en fonction d'un attribut d'une entit\u00e9. Il est possible d'\u00e9crire les actions en Python.

Pour la cr\u00e9ation :

Pour l'utilisation c\u00f4t\u00e9 utilisateur :

"},{"location":"python-qgis/#applicationscript-independant","title":"Application/script ind\u00e9pendant","text":"

Sans lancer QGIS graphiquement, on peut utiliser la librairie QGIS dans nos scripts Python. On peut donc cr\u00e9er notre propre application graphique ou notre propre ex\u00e9cutable et ainsi utiliser les fonctions de QGIS. On peut donc faire un programme en ligne de commande qui effectue une certaine op\u00e9ration dans un r\u00e9pertoire donn\u00e9.

Depuis QGIS 3.16, nous pouvons lancer un mod\u00e8le ou un script Processing depuis la ligne de commande depuis l'outil qgis_process.

"},{"location":"python-qgis/#le-fichier-startuppy","title":"Le fichier \"startup.py\"","text":"

Si l'on place un fichier nomm\u00e9 startup.py dans le dossier Python du profil de l'utilisateur, QGIS va le lancer automatiquement \u00e0 chaque ouverture de QGIS.

"},{"location":"script-processing/","title":"Processing","text":"

Processing est un framework pour faire des algorithmes dans QGIS.

Toute la boite \u00e0 outils Traitement dans QGIS sont des bas\u00e9s sur \"Processing\".

Note, depuis QGIS 3.6, il existe d\u00e9sormais une autre syntaxe pour \u00e9crire script Processing \u00e0 l'aide des d\u00e9corateurs Python. Lire sur Docteur Python pour les d\u00e9corateurs

"},{"location":"script-processing/#documentation","title":"Documentation","text":"

Pour l'\u00e9criture d'un script Processing, tant en utilisant la POO ou la version avec les d\u00e9corateurs, il y a une page sur la documentation.

"},{"location":"script-processing/#utiliser-processing-en-python-avec-un-algorithme-existant","title":"Utiliser Processing en Python avec un algorithme existant","text":"

On peut appeler un traitement en ligne de commande Python :

result = processing.run(\n    \"native:buffer\", \n    {\n        'INPUT': '/chemin/vers/HYDROGRAPHIE/CANALISATION_EAU.shp',\n        'DISTANCE': 10,\n        'SEGMENTS': 5,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 2,\n        'DISSOLVE': False,\n        'OUTPUT': 'TEMPORARY_OUTPUT',\n    }\n)\n# print(result)\n

Tip

Pour obtenir l'identifiant de l'algorithme, laissez la souris sur le nom de l'algorithme pour avoir son info-bulle dans le panneau traitement.

Idem pour les identifiants des param\u00e8tres, dans la fen\u00eatre de l'algorithme.

Lien vers la documentation de Processing en console

QgsProject.instance().addMapLayer(result['OUTPUT'])\n

Pour obtenir la description d'un algorithme :

processing.algorithmHelp(\"native:buffer\")\n

Exercice, faire une 3 tampons sur la m\u00eame couche vecteur, distance 10, 20 et 30 m\u00e8tres, avec une fonction.

def tampon(distance):\n    result = processing.run(\n        \"native:buffer\", \n        {\n            'INPUT':'/chemin/vers/HYDROGRAPHIE/BARRAGE.shp',\n            'DISTANCE':distance,\n            'SEGMENTS':5,\n            'END_CAP_STYLE':0,\n            'JOIN_STYLE':0,\n            'MITER_LIMIT':2,\n            'DISSOLVE':False,\n            'OUTPUT':'TEMPORARY_OUTPUT'\n        }\n    )\n    QgsProject.instance().addMapLayer(result['OUTPUT'])\n\nfor x in [10, 20, 30]:\n    tampon(x)\n

Warning

Attention si utilisation de iface.activeLayer() qui va \u00eatre modifi\u00e9 si utilisation de QgsProject.instance().addMapLayer(). Il peut \u00eatre n\u00e9cessaire d'extraire la s\u00e9lection de la couche hors de la boucle.

Tip

Il existe aussi processing.runandLoadResults qui permet de charger directement les r\u00e9sultats, comme QGIS en mode graphique.

"},{"location":"script-processing/#lancer-linterface-graphique-de-notre-algorithme","title":"Lancer l'interface graphique de notre algorithme","text":"

Au lieu de processing.run, on peut cr\u00e9er uniquement le dialogue. Il faut alors l'afficher manuellement.

dialog = processing.createAlgorithmDialog(\n    \"native:buffer\",\n    {\n        'INPUT': '/data/lines.shp',\n        'DISTANCE': 100.0,\n        'SEGMENTS': 10,\n        'DISSOLVE': True,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 10,\n        'OUTPUT': '/data/buffers.shp'\n    }\n)\ndialog.show()\n

Ou alors directement lancer ex\u00e9cution du dialogue :

processing.execAlgorithmDialog(\n    \"native:buffer\",\n    {\n        'INPUT': '/data/lines.shp',\n        'DISTANCE': 100.0,\n        'SEGMENTS': 10,\n        'DISSOLVE': True,\n        'END_CAP_STYLE': 0,\n        'JOIN_STYLE': 0,\n        'MITER_LIMIT': 10,\n        'OUTPUT': '/data/buffers.shp'\n    }\n)\n
"},{"location":"script-processing/#convertir-un-modele-processing-en-python","title":"Convertir un mod\u00e8le Processing en python","text":"

Il est possible de convertir un mod\u00e8le Processing en script Python. On peut alors le modifier avec plus de finesse.

On ne peut pas reconvertir un script Python en mod\u00e8le.

"},{"location":"script-processing/#manipulation","title":"Manipulation","text":"
  1. Ouvrir en mod\u00e8le, par exemple ce fichier model3
  2. Depuis le mod\u00e8le, cliquer sur le bouton \"Convertir en script Processing\".
  3. Ouvrir en parall\u00e8le le script Processing Python de QGIS (Bo\u00eete \u00e0 outil \u2192 Python \u2192 Cr\u00e9er un nouveau script depuis un mod\u00e8le)

Avec QGIS < 3.40.2

Le mod\u00e8le par d\u00e9faut dans QGIS est un plus compr\u00e9hensible dans QGIS >= 3.40.2. On peut le r\u00e9cup\u00e9rer manuellement.

"},{"location":"script-processing/#utiliser-un-script-processing-dans-une-action","title":"Utiliser un script Processing dans une action","text":"

On peut utiliser processing.run() dans le code d'une action, pour faire une zone tampon sur un point en particulier par exemple.

On peut lancer, graphiquement depuis la bo\u00eete \u00e0 outil Processing, une zone tampon, avec une s\u00e9lection. Regardons ensuite dans l'historique Processing pour voir comment QGIS a pu sp\u00e9cifier la s\u00e9lection dans son appel PyQGIS.

On note l'usage d'une nouvelle classe QgsProcessingFeatureSourceDefinition.

On souhaite donc pouvoir faire une zone tampon personnalis\u00e9e en cliquant sur un point \u00e0 l'aide d'une action.

Il faut donc revoir le code dans le chapitre actions pour voir comment cr\u00e9er une action. Pour utiliser la s\u00e9lection, nous allons faire dans l'action :

from qgis.core import QgsProject, QgsVectorLayer\n\nlayer: QgsVectorLayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nlayer.selectByIds([int('[% $id %]')])\n# Ajouter ici le code processing.run avec une s\u00e9lection\nlayer.removeSelection()\n

On peut compl\u00e9ter l'action avec un processing.run en utilisant uniquement l'entit\u00e9 en s\u00e9lection.

Solution
import processing\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nlayer.selectByIds([int('[% $id %]')])\n\nresult = processing.run(\n    \"native:buffer\",\n    {\n        'INPUT':QgsProcessingFeatureSourceDefinition(layer.source(), selectedFeaturesOnly=True),\n        'DISTANCE':1000,\n        'SEGMENTS':5,\n        'END_CAP_STYLE':0,\n        'JOIN_STYLE':0,\n        'MITER_LIMIT':2,\n        'DISSOLVE':False,\n        'OUTPUT':'TEMPORARY_OUTPUT'\n    }\n)\nQgsProject.instance().addMapLayer(result['OUTPUT'])\n\nlayer.removeSelection()\n
"},{"location":"script-processing/#introduction-aux-decorateurs","title":"Introduction aux d\u00e9corateurs","text":"

Comme mentionn\u00e9 au d\u00e9but de ce chapitre, il est possible de ne pas utiliser la POO pour \u00e9crire un \"Script Processing\" mais plut\u00f4t l'\u00e9criture \u00e0 l'aide des d\u00e9corateurs.

C'est \"cens\u00e9\" \u00eatre plus simple, pour \u00e9viter de voir l'aspect de la programmation orient\u00e9 objet avec la complexit\u00e9 des classes.

Info

Cependant, cette syntaxe n'est pas compatible avec une extension Processing.

Dans la documentation QGIS, on trouve :

Le code suivant utilise le d\u00e9corateur @alg :

from qgis import processing\nfrom qgis.processing import alg\n\n\n@alg(name='bufferrasteralg', label='Buffer and export to raster (alg)',\n     group='examplescripts', group_label='Example scripts')\n@alg.input(type=alg.SOURCE, name='INPUT', label='Input vector layer')\n@alg.input(type=alg.RASTER_LAYER_DEST, name='OUTPUT',\n           label='Raster output')\n@alg.input(type=alg.VECTOR_LAYER_DEST, name='BUFFER_OUTPUT',\n           label='Buffer output')\n@alg.input(type=alg.DISTANCE, name='BUFFERDIST', label='BUFFER DISTANCE',\n           default=1.0)\n@alg.input(type=alg.DISTANCE, name='CELLSIZE', label='RASTER CELL SIZE',\n           default=10.0)\n@alg.output(type=alg.NUMBER, name='NUMBEROFFEATURES',\n            label='Number of features processed')\ndef bufferrasteralg(instance, parameters, context, feedback, inputs):\n   \"\"\"\n   Description of the algorithm.\n   (If there is no comment here, you will get an error)\n   \"\"\"\n   input_featuresource = instance.parameterAsSource(parameters,\n                                                    'INPUT', context)\n   numfeatures = input_featuresource.featureCount()\n   bufferdist = instance.parameterAsDouble(parameters, 'BUFFERDIST',\n                                           context)\n   rastercellsize = instance.parameterAsDouble(parameters, 'CELLSIZE',\n                                               context)\n\n   if feedback.isCanceled():\n      return {}\n\n   params = {\n      'INPUT': parameters['INPUT'],\n      'OUTPUT': parameters['BUFFER_OUTPUT'],\n      'DISTANCE': bufferdist,\n      'SEGMENTS': 10,\n      'DISSOLVE': True,\n      'END_CAP_STYLE': 0,\n      'JOIN_STYLE': 0,\n      'MITER_LIMIT': 10\n   }\n   buffer_result = processing.run(\n      'native:buffer',\n      params,\n      is_child_algorithm=True,\n      context=context,\n      feedback=feedback)\n\n   if feedback.isCanceled():\n      return {}\n\n   params = {\n      'LAYER': buffer_result['OUTPUT'],\n      'EXTENT': buffer_result['OUTPUT'],\n      'MAP_UNITS_PER_PIXEL': rastercellsize,\n      'OUTPUT': parameters['OUTPUT']\n   }\n   rasterized_result = processing.run(\n      'qgis:rasterize',\n      params,\n      is_child_algorithm=True, context=context,\n      feedback=feedback)\n\n   if feedback.isCanceled():\n      return {}\n\n   results = {\n      'OUTPUT': rasterized_result['OUTPUT'],\n      'BUFFER_OUTPUT': buffer_result['OUTPUT'],\n      'NUMBEROFFEATURES': numfeatures,\n   }\n   return results\n
"},{"location":"selection-parcours-entites/","title":"Fonctions sur une couche vecteur","text":""},{"location":"selection-parcours-entites/#utilisation-des-expressions-qgis","title":"Utilisation des expressions QGIS","text":"

Tip

Adapter le num\u00e9ro des codes INSEE ou des d\u00e9partements selon votre BDTOPO \ud83d\ude09

"},{"location":"selection-parcours-entites/#selection-dentite","title":"S\u00e9lection d'entit\u00e9","text":"

Nous souhaitons s\u00e9lectionner les entit\u00e9s dont le code INSEE commence par 77. Commen\u00e7ons par faire cela graphiquement dans QGIS Bureautique. \u00c0 l'aide d'une expression QGIS, s\u00e9lectionner les codes INSEE qui commencent par 77 (\u00e0 choisir un code INSEE propre au jeu de donn\u00e9es).

Solution en mode graphique :

\"INSEE_COM\" LIKE '77%'\n

Nous allons faire la m\u00eame chose, mais en utilisant Python. Pensez \u00e0 d\u00e9s\u00e9lectionner les entit\u00e9s.

Il va falloir \"\u00e9chapper\" un caract\u00e8re \u00e0 l'aide de \\. Voir la page Wikip\u00e9dia sur l'\u00e9chappement ou ce meme pour les devs \ud83e\udee2

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nlayer.removeSelection()\nlayer.selectByExpression(f\"\\\"INSEE_COM\\\" LIKE '77%'\")\n# layer.selectByExpression(f'\"INSEE_COM\" LIKE \\'77%\\'')  # R\u00e9sultat identique\nlayer.invertSelection()\nlayer.removeSelection()\n

Le raccourci iface.activeLayer() est tr\u00e8s pratique, mais de temps en temps, on a besoin de plusieurs couches qui sont d\u00e9j\u00e0 dans la l\u00e9gende. Il existe dans QgsProject plusieurs m\u00e9thodes pour r\u00e9cup\u00e9rer des couches dans la l\u00e9gende :

from qgis.core import QgsProject\n\nprojet = QgsProject.instance()\ncommunes = projet.mapLayersByName('communes')[0]\ninsee = projet.mapLayersByName('tableau INSEE')\n

Notons le s dans mapLayersByName. Il peut y avoir plusieurs couches avec ce m\u00eame nom de couche. La fonction retourne donc une liste de couches. Il convient alors de regarder si la liste est vide ou si elle contient plusieurs couches avec len(communes) par exemple.

Warning

mapLayersByName fait uniquement une recherche stricte, sensible \u00e0 la casse. Il faut passer par du code Python \"pure\" en it\u00e9rant sur l'ensemble des couches, ind\u00e9pendamment de leur nom si l'on souhaite faire une recherche plus fine. Si vraiment, on a besoin, on peut utiliser le module re (lien du Docteur Python).

from qgis.core import QgsProject\n\nprojet = QgsProject.instance()\ncommunes = projet.mapLayersByName('communes')\n\nif len(communes) == 0:\n    print(\"Pas de couches dans la l\u00e9gende qui se nomme 'communes'\")\n    layer = None\nelif len(communes) >= 1:\n    # TODO FIX ME, pas forc\u00e9ment la bonne couche 'communes'\n    layer = communes[0]\n
"},{"location":"selection-parcours-entites/#exemple-dune-selection-avec-un-export","title":"Exemple d'une s\u00e9lection avec un export","text":"

On souhaite pouvoir exporter les communes par d\u00e9partement. On peut cr\u00e9er une variable depts = ('34', '30') puis boucler dessus pour exporter les entit\u00e9s s\u00e9lectionn\u00e9es dans un nouveau fichier.

from pathlib import Path\nfrom qgis.core import QgsProject, QgsVectorFileWriter\nfrom qgis.utils import iface\n\nlayer = iface.activeLayer()\n\noptions = QgsVectorFileWriter.SaveVectorOptions()\noptions.driverName = 'ESRI Shapefile'\noptions.fileEncoding = 'UTF-8'\noptions.onlySelectedFeatures = True  # Nouvelle option pour la s\u00e9lection\n\ndepts = ('34', '30')\nfor dept in depts:\n    print(f\"Dept {dept}\")\n    layer.selectByExpression(f\"\\\"INSEE_DEP\\\"  =  '{dept}'\")\n    result = QgsVectorFileWriter.writeAsVectorFormatV3(\n        layer,\n        str(Path(QgsProject.instance().homePath()).joinpath(f'{dept}.shp')),\n        QgsProject.instance().transformContext(),\n        options\n    )\n    print(result)\n    if result[0] == QgsVectorFileWriter.WriterError.NoError:\n        print(\" \u2192 OK\")\n

Bonus

Si l'on souhaite parcourir automatiquement les d\u00e9partements existants, on peut r\u00e9cup\u00e9rer les valeurs uniques. Pour cela, il faut modifier deux lignes :

index = layer.fields().indexFromName(\"INSEE_DEP\")\nfor dept in layer.uniqueValues(index):\n
"},{"location":"selection-parcours-entites/#boucler-sur-les-entites-dune-couche-sans-expression","title":"Boucler sur les entit\u00e9s d'une couche sans expression","text":"

Si besoin, pour que la suite de l'exercice soit plus rapide, on peut utiliser une couche ARRONDISSEMENT par exemple.

On peut parcourir les entit\u00e9s d'une couche QgsVectorLayer \u00e0 l'aide de getFeatures().

Info

Avec PyQGIS, on peut acc\u00e9der aux attributs d'une QgsFeature simplement avec l'op\u00e9rateur [] sur l'objet courant comme s'il s'agissait d'un dictionnaire Python :

# Pour acc\u00e9der au champ \"NOM\" de l'entit\u00e9 \"feature\" :\nprint(feature['NOM'])\n

On peut le voir dans les exemples attribute de QgsFeature : https://qgis.org/pyqgis/3.34/core/QgsFeature.html#qgis.core.QgsFeature.attribute

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    print(feature)\n    print(feature['NOM'])\n    print(feature.attribute('NOM'))\n
"},{"location":"selection-parcours-entites/#boucler-sur-les-entites-a-laide-dune-expression","title":"Boucler sur les entit\u00e9s \u00e0 l'aide d'une expression","text":"

L'objectif est d'afficher dans la console le nom des communes dont la code d\u00e9partement INSEE_DEP correspond uniquement \u00e0 un seul d\u00e9partement arbitraire.

L'exemple \u00e0 ne pas faire, m\u00eame si cela fonctionne (car on peut l'optimiser tr\u00e8s facilement) :

from qgis.utils import iface\n\nlayer = iface.activeLayer()\nfor feature in layer.getFeatures():\n    if feature['INSEE_DEP'] == '84':\n        print(f'{feature['NOM']} : d\u00e9partement {feature['INSEE_DEP']}')\n
  1. Imaginons qu'il s'agisse d'une couche PostgreSQL, sur un serveur distant
  2. On demande \u00e0 QGIS de r\u00e9cup\u00e9rer l'ensemble de la table distante, \u00e9quivalent \u00e0 SELECT * FROM ma_table
  3. Puis, on filtre dans QGIS (toute la donn\u00e9e est pr\u00e9sente dans QGIS Bureautique d\u00e9sormais)

Tip

Ce qui prend du temps lors de l'ex\u00e9cution, c'est surtout le print en lui-m\u00eame. Si vous n'utilisez pas print, mais un autre traitement, cela sera plus rapide. Un simple print ralenti l'ex\u00e9cution d'un script.

"},{"location":"selection-parcours-entites/#optimisation-de-la-requete","title":"Optimisation de la requ\u00eate","text":"

Dans la documentation, observez bien la signature de la fonction getFeatures. Que remarquez-vous ? Utilisons donc une expression pour limiter les r\u00e9sultats.

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest\n\nlayer = iface.activeLayer()\n\nrequest = QgsFeatureRequest()\n# \u00c9quivalent \u00e0 SELECT * FROM ma_table WHERE \"INSEE_DEP\" = '84'\nrequest.setFilterExpression('\"INSEE_DEP\" = \\'84\\'')\n\nfor feature in layer.getFeatures(request):\n    print(f'{feature['NOM']} : d\u00e9partement {feature['INSEE_DEP']}')\n

Nous pouvons accessoirement ordonner les r\u00e9sultats et surtout encore optimiser la requ\u00eate en :

La solution pour les experts
request = QgsFeatureRequest()\nrequest.setFilterExpression('\"INSEE_DEP\" = \\'84\\'')\nrequest.addOrderBy('NOM')\nrequest.setFlags(QgsFeatureRequest.NoGeometry)\n# request.setSubsetOfAttributes([1, 4]) autre mani\u00e8re moins pratique, historique\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\n# # \u00c9quivalent \u00e0 SELECT NOM, POPULATION FROM ma_table WHERE \"INSEE_DEP\" = '84' ORDER BY NOM\nfor feature in layer.getFeatures(request):\n    print('{commune} : {nombre} habitants'.format(commune=feature['NOM'], nombre=feature['POPULATION']))\n
"},{"location":"selection-parcours-entites/#enregistrement-dune-requete-dans-une-couche-en-memoire","title":"Enregistrement d'une requ\u00eate dans une couche en m\u00e9moire","text":"

Si l'on souhaite \"enregistrer\" le r\u00e9sultat de cette expression QGIS, on peut la mat\u00e9rialiser dans une nouvelle couche :

memory_layer = layer.materialize(request)\nQgsProject.instance().addMapLayer(memory_layer)\n

Warning

Attention \u00e0 la ligne iface.activeLayer() qui peut changer lors de l'ajout d'une nouvelle couche dans la l\u00e9gende.

Regardons le r\u00e9sultat et corrigeons ce probl\u00e8me d'export afin d'obtenir les g\u00e9om\u00e9tries et les attributs, il faut supprimer la ligne NoGeometry si vous l'avez.

"},{"location":"selection-parcours-entites/#valeur-null","title":"Valeur NULL","text":"

En PyQGIS, il existe la valeur NULL qui peut \u00eatre pr\u00e9sente dans la table attributaire d'une couche vecteur.

from qgis.PyQt.QtCore import NULL\n\nif feature['nom_attribut'] == NULL:\n    # Traiter la valeur NULL\n    pass\nelse:\n    # Continuer\n    pass\n
"},{"location":"selection-parcours-entites/#calculer-un-champ-densite","title":"Calculer un champ \"densite\"","text":"

Nous souhaitons avoir une colonne densite dans notre table attributaire, avec la densit\u00e9 de population.

Mais regardons avant la gestion des erreurs lors d'un traitement. En effet, nous allons vouloir \"caster\" (transformer le type) de la variable population en entier, mais attention, il y a des valeurs NC dans les valeurs.

_Note, il n'y a d\u00e9sormais plus de valeur NC dans le champ POPULATION dans la donn\u00e9e, mais imaginons. Il peut s'agir d'une autre couche dont on ne connait pas la provenance et le contenu.

"},{"location":"selection-parcours-entites/#les-exceptions-en-python","title":"Les exceptions en Python","text":"

Avant de traiter cet exercice, nous devons voir ce qu'est une exception en Python.

\u00c0 plusieurs reprises depuis le d\u00e9but de la formation, il est fort \u00e0 parier que nous ayons des messages en rouges dans la console de temps en temps. Ce sont des exceptions. C'est une notion de programmation qui existe dans beaucoup de languages.

Dans le langage informatique, une exception peut-\u00eatre :

Essayons dans la console de faire une op\u00e9ration 10 / 2 :

10 / 2\n

Essayons cette fois-ci 10 / 0, ce qui est math\u00e9matiquement impossible :

10 / 0\n

Passons cette fois-ci dans un script pour que cela soit plus simple, et voir que le script s'arr\u00eate brutalement \ud83d\ude09

print('D\u00e9but')\nprint(10 / 2)\nprint('Fin')\n

On peut \"attraper\" cette erreur Python \u00e0 l'aide d'un try ... except... :

print('D\u00e9but')\ntry:\n    print(10 / 2)\nexcept ZeroDivisionError:\n    print('Ceci est une division par z\u00e9ro !')\nprint('Fin')\n

Le try permet d'essayer le code qui suit. Le except permet d'attraper en filtrant s'il y a des exceptions et de traiter l'erreur si besoin.

Tip

On peut avoir une ou plusieurs lignes de code dans chacun de ces blocs. On peut appeler des fonctions, etc.

"},{"location":"selection-parcours-entites/#une-exception-remonte-le-fil-dexecution-du-programme","title":"Une exception remonte le fil d'ex\u00e9cution du programme","text":"

Important, une exception remonte tant qu'elle n'est pas attrap\u00e9e :

def function_3():\n    print(\"D\u00e9but fonction 3\")\n    a = 10\n    b = 2\n    print(f\"\u2192 {a} / {b} = {a/b}\")\n    print(\"Fin fonction 3\")\n\ndef function_2():\n    print(\"D\u00e9but fonction 2\")\n    function_3()\n    print(\"Fin fonction 2\")\n\ndef function_1():\n    print(\"D\u00e9but fonction 1\")\n    function_2()\n    print(\"Fin fonction 1\")\n\nfunction_1()\n

Testons d\u00e9sormais d'attraper l'erreur dans la fonction 1 :

try:\n    function_2()\nexcept ZeroDivisionError:\n    print(\"Fin de l'exception\")\n

On voit que Python, quand il peut, nous indique la \"stacktrace\" ou encore \"traceback\", c'est-\u00e0-dire une sorte de fil d'ariane.

"},{"location":"selection-parcours-entites/#heritage-des-exceptions","title":"H\u00e9ritage des exceptions","text":"

Toutes les exceptions h\u00e9ritent de Exception donc le code ci-dessous fonctionne, mais n'est pas recommand\u00e9, car il masque d'autres erreurs :

try:\n    a = 10 / 5\n\n    mois = ['janvier', 'f\u00e9vrier']\n    b = mois[0]\n\nexcept Exception:\n    print('Erreur inconnue')\n

On peut par contre \"encha\u00eener\" les exceptions, afin de filtrer progressivement les exceptions.

try:\n    a = 10 / 5\n    # a = 10 / 0\n    # a = 10 / int(\"NC\")\n    # a = 10 / \"NC\"\nexcept ZeroDivisionError:\n    print('Erreur, division par 0')\nexcept ValueError:\n    print(\"Erreur, il n'y a avait pas que des chiffres.\")\nexcept Exception:\n    print('Erreur inconnue')\n

Il existe d'autres mots-cl\u00e9s en Python pour les exceptions comme finally: et else:. Voir un autre tutoriel.

\u00c9videment, on peut v\u00e9rifier la valeur de b en amont si c'est \u00e9gal \u00e0 0. Mais ceci est pour pr\u00e9senter le concept des exceptions en Python.

"},{"location":"selection-parcours-entites/#retour-a-lexercice","title":"Retour \u00e0 l'exercice","text":"

On souhaite donc savoir si un nombre est transformable en entier, dans le cas de la population (s'il y a NC par exemple) :

int('10')\nint('NC')\n

Correction possible de l'exercice :

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest\n\nlayer = iface.activeLayer()\nrequest = QgsFeatureRequest()\n# request.setLimit(5)  # Pour aller plus vite si-besoin\nrequest.addOrderBy('NOM')\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\nfor feature in layer.getFeatures(request):\n    area = feature.geometry().area() / 1000000\n    try:\n        population = int(feature['POPULATION'])\n        # Par exemple, pour nettoyer une cha\u00eene de caract\u00e8re en Python des espaces avant/apr\u00e8s : \"  Bonjour  \".strip()\n        # \"rstrip\"/\"lstrip\" existent \u00e9galement\n    except ValueError:\n        population = 0\n\n    densite = population/area\n\n    print(f\"{feature['NOM']} : {densite} habitants/km\u00b2\")\n

Nous souhaitons enregistrer ces informations dans une vraie table avec un nouveau champ densite_population.

Solution possible :

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest, QgsField, edit\n\nfrom qgis.PyQt.QtCore import QVariant\n\nlayer = iface.activeLayer()\n\nif 'densite' not in layer.fields().names():\n    with edit(layer):\n        field = QgsField('densite', QVariant.Double, prec=2, len=2)\n        layer.addAttribute(field)\n\nindex = layer.fields().indexFromName('densite')\nlayer.startEditing()\nrequest = QgsFeatureRequest()\n# request.setLimit(5)  # Pour aller plus vite si-besoin\nrequest.addOrderBy('NOM')\nrequest.setSubsetOfAttributes(['NOM', 'POPULATION'], layer.fields())\n# SELECT NOM, POPULATION FROM communes\nfor feature in layer.getFeatures(request):\n    area = feature.geometry().area() / 1000000\n    try:\n        population = int(feature['POPULATION'])\n    except ValueError:\n        population = 0\n\n    densite = population/area\n\n    # Cette ligne n'aura aucun effet, contrairement \u00e0 l'exercice d'export au format CSV pr\u00e9c\u00e9dent.\n    # https://docs.3liz.org/formation-pyqgis/fonctions-scripts/#solution-possible\n    # La variable \"feature\" est une copie, comme un peu le r\u00e9sultat du SELECT * FROM ma_table LIMIT 5\n    # Un SELECT est en lecture seule. Ce n'est pas comme \u00e7a que cela se passe, c'est pour imager.\n    feature['densite'] = densite\n\n    # Uniquement l'appel \u00e0 \"changeAttributeValue\" fonctionne\n    # Pour information, il existe \"changeGeometry\" pour la m\u00eame raison\n    # Un peu comme la commande SQL UPDATE, sur une entit\u00e9 existante, bien qu'il ne faille pas oublier la session d'\u00e9dition.\n    layer.changeAttributeValue(feature.id(), index, densite)\n    # print('{commune} : {densite} habitants/km\u00b2'.format(commune=feature['NOM'], densite=round(population/area,2)))\n\nlayer.commitChanges()\n
"},{"location":"selection-parcours-entites/#calculer-deux-champs-en-utilisant-la-geometrie-et-une-reprojection-a-la-volee","title":"Calculer deux champs en utilisant la g\u00e9om\u00e9trie et une reprojection \u00e0 la vol\u00e9e","text":"

Manipulons d\u00e9sormais la g\u00e9om\u00e9trie en ajoutant le centro\u00efde de la commune dans une colonne latitude et longitude en degr\u00e9es.

Warning

TODO, en cours de correction, suppression de la variable petite_communes

from qgis.utils import iface\nfrom qgis.core import QgsFeatureRequest, QgsField, edit\n\nlayer = iface.activeLayer()\n\nrequest = QgsFeatureRequest()\nrequest.setFilterExpression('to_int( \"POPULATION\" ) < 1000')\npetites_communes = layer.materialize(request)\n\nwith edit(petites_communes):\n    petites_communes.addAttribute(QgsField('densite_population', QVariant.Double))\n\n    # /!\\ Ajouter les 2 lignes ci-dessous\n    petites_communes.addAttribute(QgsField('longitude', QVariant.Double))\n    petites_communes.addAttribute(QgsField('latitude', QVariant.Double))\n\nrequest = QgsFeatureRequest()\nrequest.setSubsetOfAttributes([4])\n\n# /!\\ Ajouter les 2 lignes ci-dessous \u00e0 propos de la transformation\ntransform = QgsCoordinateTransform(\n    QgsCoordinateReferenceSystem(\"EPSG:2154\"), QgsCoordinateReferenceSystem(\"EPSG:4326\"), QgsProject.instance())\n\nwith edit(petites_communes):\n    for feature in petites_communes.getFeatures(request):\n        area = feature.geometry().area() / 1000000\n        population = int(feature['POPULATION'])\n        densite=population/area\n        petites_communes.changeAttributeValue(feature.id(), 5, densite)\n\n        # /!\\ Ajouter les lignes ci-dessous\n        geom = feature.geometry()\n        # La transformation affecte directement l'objet Python en cours, mais pas l'entit\u00e9 dans la couche\n        geom.transform(transform)\n        centroid = geom.centroid().asPoint()\n        petites_communes.changeAttributeValue(feature.id(), 6, centroid.x())\n        petites_communes.changeAttributeValue(feature.id(), 7, centroid.y())\n\nQgsProject.instance().addMapLayer(petites_communes)\n
"},{"location":"signal-slot/","title":"Les signaux et les slots","text":""},{"location":"signal-slot/#definition","title":"D\u00e9finition","text":"

Nous avons pu voir dans la documentation des librairies Qt et QGIS, il y a une section Signals.

Chaque objet \u00e9met tr\u00e8s souvent un signal d\u00e8s qu'une action est faite sur l'objet. Cela sert \u00e0 d\u00e9clencher du code Python lorsqu'un signal pr\u00e9cis est \u00e9mis.

Par exemple sur la documentation de QgsMapLayer, on peut chercher le tableau signals.

Info

Comme on peut le voir dans la documentation CPP, c'est d\u00e9sormais dans la classe QgsMapLayer et non QgsVectorLayer depuis QGIS 3.22. \ud83e\uddd0

La plupart des signaux sont aux pass\u00e9s, par exemple crsChanged, nameChanged. Cependant, certaines sont dans un \"futur\" proche comme willBeDeleted.

"},{"location":"signal-slot/#syntaxe","title":"Syntaxe","text":"

On dit que l'on souhaite connecter un signal \u00e0 une fonction/slot :

variable_de_lobjet.nom_du_signal.connect(nom_de_la_fonction)\n

Danger

Il ne faut pas \u00e9crire nom_de_la_fonction() car on ne souhaite pas appeler la fonction, juste connecter.

Cela sera Python, plus tard, quand le signal sera \u00e9mis, que la fonction sera r\u00e9ellement appel\u00e9e.

"},{"location":"signal-slot/#exemple","title":"Exemple","text":"

Par exemple, dans la classe QgsMapLayer, cherchons un signal qui est \u00e9mis apr\u00e8s (before) que la session d'\u00e9dition commence. Il s'agit de editingStarted.

Affichons un message \u00e0 l'utilisateur lors du d\u00e9but et de la fin d'une session d'\u00e9dition.

Tip

On profite de cet exemple pour voir comment \u00e9valuer une expression QGIS \u00e0 l'aide des diff\u00e9rents contextes.

def user_from_qgis() -> str:\n    \"\"\" \u00c0 l'aide d'une expression QGIS, r\u00e9cup\u00e9ration du nom d'utilisateur.\"\"\"\n    context = QgsExpressionContext()\n    context.appendScope(QgsExpressionContextUtils.globalScope())\n    # On peut th\u00e9oriquement s'arr\u00eater \u00e0 ce niveau la concernant l'ajout des \"scopes\" avec le GlobalScope\n    # Mais on peut ajouter d'autre \"scope\", comme ajouter celui du projet :\n    # context.appendScope(QgsExpressionContextUtils.projectScope(project))\n    # context.appendScope(QgsExpressionContextUtils.layerScope(layer))\n\n    # On peut ensuite \u00e9valuer l'expression QGIS\n    # \"@user_account_name\" ou encore \"@user_full_name\"\n    expression = QgsExpression(\"@user_full_name\")\n    return expression.evaluate(context)\n\ndef user_from_os() -> str:\n    \"\"\" \u00c0 l'aide de l'OS, retourne le nom d'utilisateur.\"\"\"\n    import os\n    return os.getlogin()\n\ndef we_are_watching_you():\n    \"\"\" Just warn the user about the editing session.\"\"\"\n    current_user = user_from_qgis()\n    # Attention aux effets si on lance le code plusieurs fois !\n    # print(\"Hello \ud83d\ude09\")\n    iface.messageBar().pushMessage('Hey',f'Be careful <strong>{current_user}</strong> while you are editing \ud83e\uddd0', Qgis.Warning)\n\ndef thanks():\n    iface.messageBar().pushMessage('Hey', \"Thanks \ud83d\ude09\", Qgis.Success)\n\n\nlayer = iface.activeLayer()\n\nlayer.beforeEditingStarted.connect(we_are_watching_you)\nlayer.editingStopped.connect(thanks)\n
"},{"location":"standalone/","title":"Librairie QGIS","text":""},{"location":"standalone/#qgis-process","title":"QGIS Process","text":"

Depuis QGIS 3.16, il existe un outil qgis_process qui permet de lancer QGIS Processing en ligne de commande.

Quelques rappels pour utiliser la ligne de commande sous Windows :

Dans le shell OSGEO, taper :

cd C:/Program Files/QGIS 3.14/bin/\n# Il peut s'agit du chemin ci-dessous\ncd C:\\OSGeo4W\\apps\\qgis-ltr\\bin\\\n

On doit avoir d\u00e9sormais un ex\u00e9cutable qgis_process-qgis-ltr.bat

qgis_process-qgis-ltr.bat\nqgis_process-qgis-ltr.bat --help\nqgis_process-qgis-ltr.bat list\n

On peut lancer les algorithmes, les mod\u00e8les, les scripts Python qui sont dans la version graphique de QGIS Processing.

On peut donc lancer en ligne de commande, ou alors avec notre propre ic\u00f4ne sur son bureau un ex\u00e9cutable.

qgis_process help qgis:buffer\nqgis_process run qgis:buffer -- INPUT=/home/etienne/source.shp DISTANCE=2 OUTPUT=/tmp/sortie.gpkg\n

L'id\u00e9e de QGIS Process est soit de faire un petit ex\u00e9cutable ou alors de lancer le programme \u00e0 intervalle de temps r\u00e9gulier.

"},{"location":"standalone/#standalone-application","title":"Standalone application","text":"

Il est possible de faire un programme qui ne se lance pas dans QGIS Bureautique mais qui utilise la librairie QGIS qui se trouve sur l'ordinateur.

Warning

Gr\u00e2ce \u00e0 qgis_process, c'est exemple d'application standalone perd la plupart de son int\u00e9r\u00eat. Pour lancer QGIS en ligne de commande pour faire des traitements, il est d\u00e9sormais fortement conseill\u00e9 d'utiliser qgis_process.

On peut donc cr\u00e9er son propre programme, en ligne de commande ou avec une interface graphique qui utilise le moteur de QGIS en arri\u00e8re-plan pour utiliser ce que sait d\u00e9j\u00e0 faire QGIS.

Exemple sur le gist de Thomas Gratier

# Code borrowed from https://subscription.packtpub.com/book/application_development/9781783984985/1/ch01lvl1sec18/creating-a-standalone-application\n# and upgraded for QGIS 3.0\nimport os\nimport sys\nimport shutil\nimport tempfile\nimport urllib.request\nfrom zipfile import ZipFile\nfrom glob import glob\n\nfrom qgis.core import (QgsApplication, QgsCoordinateReferenceSystem, QgsFeature,\n                   QgsGeometry, QgsProject, QgsRasterLayer, QgsVectorLayer)\nfrom qgis.gui import QgsLayerTreeMapCanvasBridge, QgsMapCanvas\nfrom qgis.PyQt.QtCore import Qt\n# Unused so commented\n# from qgis.PyQt.QtGui import *\n\napp = QgsApplication([], True)\n# On Windows : https://gis.stackexchange.com/questions/334172/creating-standalone-application-in-qgis\n# On Linux, didn't need to set it so commented\n# app.setPrefixPath(\"C:/Program Files/QGIS Brighton/apps/qgis\", True)\napp.initQgis()\ncanvas = QgsMapCanvas()\ncanvas.setWindowTitle(\"PyQGIS Standalone Application Example\")\ncanvas.setCanvasColor(Qt.white)\ncrs = QgsCoordinateReferenceSystem('EPSG:3857')\nproject = QgsProject.instance()\ncanvas.setDestinationCrs(crs)\n\nurlWithParams = 'type=xyz&url=https://a.tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=19&zmin=0&crs=EPSG3857'\nrlayer2 = QgsRasterLayer(urlWithParams, 'OpenStreetMap', 'wms')\n\nif rlayer2.isValid():\n    project.addMapLayer(rlayer2)\nelse:\n    print('invalid layer')\n\n# Download shp ne_10m_admin_0_countries.shp and associated files in the same directory\nurl = \"https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip\"\nif not glob(\"ne_10m_admin_0_countries.*\"):\n    with urllib.request.urlopen(url) as response:\n        with tempfile.NamedTemporaryFile(delete=False) as tmp_file:\n            shutil.copyfileobj(response, tmp_file)\n        with ZipFile(tmp_file.name, 'r') as zipObj:\n            # Extract all the contents of zip file in current directory\n            zipObj.extractall()\n\nlayer_shp = QgsVectorLayer(os.path.join(os.path.dirname(__file__), \"ne_10m_admin_0_countries.shp\"), \"Natural Earth\", \"ogr\")\nif not layer_shp.isValid():\n  print(\"Layer failed to load!\")\n\nproject.addMapLayer(layer_shp)\n\nprint(layer_shp.crs().authid())\nprint(rlayer2.crs().authid())\ncanvas.setExtent(layer_shp.extent())\ncanvas.setLayers([rlayer2, layer_shp])\ncanvas.zoomToFullExtent()\n# canvas.freeze(True)\ncanvas.show()\ncanvas.refresh()\n# canvas.freeze(False)\ncanvas.repaint()\nbridge = QgsLayerTreeMapCanvasBridge(\n    project.layerTreeRoot(),\n    canvas\n)\n\ndef run_when_project_saved():\n    print('Saved')\n\nproject.projectSaved.connect(run_when_project_saved)\n\nproject.write('my_new_qgis_project.qgz')\n\ndef run_when_application_state_changed(state):\n    print('State changed', state)\n\napp.applicationStateChanged.connect(run_when_application_state_changed)\n\nexitcode = app.exec()\nQgsApplication.exitQgis()\nsys.exit(True)\n
"},{"location":"symbologie/","title":"Symbologie","text":"

\u00c9tant donn\u00e9 que la symbologie pouvant \u00eatre complexe dans QGIS Bureautique, avec les diff\u00e9rents types de symbologie, les diff\u00e9rents niveaux de symbole, les ensembles de r\u00e8gles avec des filtres etc, il n'est pas forc\u00e9ment simple de s'y retrouver dans l'API PyQGIS \u00e9galement.

"},{"location":"symbologie/#utilisation-dun-qml-au-lieu-de-pyqgis","title":"Utilisation d'un QML au lieu de PyQGIS","text":"

On peut se passer de PyQGIS pour fournir une symbologie \u00e0 l'aide d'un fichier QML, si on ne souhaite pas faire \u00e7a en Python enti\u00e8rement.

Regarder les m\u00e9thodes loadNamedStyle de la classe QgsMapLayer.

from pathlib import Path\n\nlayer = iface.activeLayer()\n\nqml = Path(\"path_to_qml\")\nif qml.exists():\n    layer.loadNamedStyle(str(qml))\n    iface.legendInterface().refreshLayerSymbology(layer)\n
"},{"location":"symbologie/#classes-utiles-en-pyqgis","title":"Classes utiles en PyQGIS","text":"

Voir les graphiques d'h\u00e9ritage sur :

"},{"location":"symbologie/#afficher-les-infos-de-la-symbologie","title":"Afficher les infos de la symbologie","text":"
layer = iface.activeLayer()\nrenderer = layer.renderer()\nprint(renderer.dump())\n

SINGLE: FILL SYMBOL (1 layers) color 125,139,143,255

layer = iface.activeLayer()\nrenderer = layer.renderer()\nprint(renderer.symbol().symbolLayers()[0].properties())\n

{'border_width_map_unit_scale': '3x:0,0,0,0,0,0', 'color': '125,139,143,255', 'joinstyle': 'bevel', 'offset': '0,0', 'offset_map_unit_scale': '3x:0,0,0,0,0,0', 'offset_unit': 'MM', 'outline_color': '35,35,35,255', 'outline_style': 'solid', 'outline_width': '0.26', 'outline_width_unit': 'MM', 'style': 'solid'}

"},{"location":"symbologie/#affecter-une-symbologie-a-une-couche","title":"Affecter une symbologie \u00e0 une couche","text":"

Il peut \u00eatre tr\u00e8s pratique de partir d'une symbologie existante, faite via l'interface graphique, puis de l'exporter pour voir les propri\u00e9t\u00e9s.

"},{"location":"symbologie/#un-symbole-ponctuel-unique-simple","title":"Un symbole ponctuel unique simple","text":"
from qgis.core import QgsMarkerSymbol, QgsSingleSymbolRenderer\n\nsymbol = QgsMarkerSymbol.createSimple(\n    {\n        \"name\": \"circle\",\n        \"color\": \"yellow\",\n        \"size\": 3,\n    }\n)\nrenderer = QgsSingleSymbolRenderer(symbol)\nlayer = iface.activeLayer()\nlayer.setRenderer(renderer)\n# layer.triggerRepaint()  # If necessary\n
"},{"location":"symbologie/#un-symbole-lineaire-unique-sous-forme-de-fleche","title":"Un symbole lin\u00e9aire unique sous forme de fl\u00e8che","text":"
from qgis.core import QgsApplication, QgsSymbol, Qgis, QgsSingleSymbolRenderer\nfrom qgis.PyQt.QtGui import QColor\n\n# Quelques propri\u00e9t\u00e9s d'une fl\u00e8che si besoin de surcharger. Utiliser le code PyQGIS pour r\u00e9cup\u00e9rer la liste des propri\u00e9t\u00e9s.\nARROW = {\n    'arrow_start_width': '1',\n    'arrow_start_width_unit': 'MM',\n    'arrow_start_width_unit_scale': '3x:0,0,0,0,0,0',\n    'arrow_type': '0',\n}\n\nregistry = QgsApplication.symbolLayerRegistry()\nline_metadata = registry.symbolLayerMetadata('ArrowLine')\nline_layer = line_metadata.createSymbolLayer(ARROW)\nline_layer.setColor(QColor('#33a02c'))\n\nsymbol = QgsSymbol.defaultSymbol(Qgis.GeometryType.LineGeometry)\nsymbol.deleteSymbolLayer(0)\nsymbol.appendSymbolLayer(line_layer)\n\n\nrenderer = QgsSingleSymbolRenderer(symbol)\nlayer = iface.activeLayer()\nlayer.setRenderer(renderer)\n
"}]} \ No newline at end of file