[{"data":1,"prerenderedAt":1719},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure":3},{"id":4,"title":5,"body":6,"description":1712,"extension":1713,"meta":1714,"navigation":530,"path":1715,"seo":1716,"stem":1717,"__hash__":1718},"docs\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Findex.md","Plugin Boilerplate & Structure",{"type":7,"value":8,"toc":1689},"minimark",[9,13,21,30,35,38,63,66,86,90,93,103,108,160,164,261,265,273,280,358,364,367,444,450,453,1362,1366,1385,1389,1393,1427,1435,1462,1466,1499,1529,1533,1564,1568,1603,1607,1610,1677,1685],[10,11,5],"h1",{"id":12},"plugin-boilerplate-structure",[14,15,16,17,20],"p",{},"A well-defined ",[18,19,5],"strong",{}," is the foundation of any maintainable PyQGIS extension. QGIS expects a predictable file hierarchy, standardized metadata, and explicit lifecycle hooks. When these elements are correctly implemented, the plugin manager can load, initialize, and unload your code without conflicts. This guide outlines the architectural standards, provides tested initialization patterns, and details the step-by-step workflow required to build a production-ready QGIS plugin.",[14,22,23,24,29],{},"For developers entering the ecosystem, understanding how ",[25,26,28],"a",{"href":27},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development"," operates at the framework level is essential before customizing the boilerplate. The structure below aligns with QGIS 3.x standards and Python 3 requirements.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[14,36,37],{},"Before generating or modifying a plugin skeleton, ensure your environment meets the following baseline requirements:",[39,40,41,48,54,57,60],"ul",{},[42,43,44,47],"li",{},[18,45,46],{},"QGIS 3.x"," installed (LTS or current release)",[42,49,50,53],{},[18,51,52],{},"Python 3.10+"," environment (bundled with modern QGIS releases)",[42,55,56],{},"A code editor with Python syntax highlighting and linting (VS Code, PyCharm, or similar)",[42,58,59],{},"Basic familiarity with object-oriented Python and Qt signal\u002Fslot architecture",[42,61,62],{},"Write access to the QGIS profile directory",[14,64,65],{},"The QGIS profile directory typically resides at:",[39,67,68,78],{},[42,69,70,73,74],{},[18,71,72],{},"Windows:"," ",[75,76,77],"code",{},"%APPDATA%\\QGIS\\QGIS3\\profiles\\default\\python\\plugins\\",[42,79,80,73,83],{},[18,81,82],{},"macOS\u002FLinux:",[75,84,85],{},"~\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002F",[31,87,89],{"id":88},"standard-directory-architecture","Standard Directory Architecture",[14,91,92],{},"A compliant plugin directory must contain specific files that QGIS scans during startup. The following tree represents the minimal viable structure:",[94,95,100],"pre",{"className":96,"code":98,"language":99},[97],"language-text","my_plugin\u002F\n├── __init__.py\n├── metadata.txt\n├── main_plugin.py\n├── resources.qrc\n├── ui\u002F\n│ └── main_dialog.ui\n├── i18n\u002F\n│ └── my_plugin_en.ts\n└── icons\u002F\n └── icon.png\n","text",[75,101,98],{"__ignoreMap":102},"",[14,104,105],{},[18,106,107],{},"File Responsibilities:",[39,109,110,120,126,132,138,148,154],{},[42,111,112,115,116,119],{},[75,113,114],{},"__init__.py",": Entry point that exposes the ",[75,117,118],{},"classFactory"," function to QGIS.",[42,121,122,125],{},[75,123,124],{},"metadata.txt",": Plain-text configuration file containing plugin name, version, author, and QGIS compatibility flags.",[42,127,128,131],{},[75,129,130],{},"main_plugin.py",": Core logic handling GUI initialization, toolbar\u002Fmenu integration, and cleanup routines.",[42,133,134,137],{},[75,135,136],{},"resources.qrc",": Qt resource compiler file bundling icons and UI assets.",[42,139,140,143,144,147],{},[75,141,142],{},"ui\u002F",": Contains ",[75,145,146],{},".ui"," XML files generated by Qt Designer.",[42,149,150,153],{},[75,151,152],{},"i18n\u002F",": Translation files for internationalization.",[42,155,156,159],{},[75,157,158],{},"icons\u002F",": Raster or SVG assets for toolbar buttons and plugin manager listings.",[31,161,163],{"id":162},"step-by-step-implementation-workflow","Step-by-Step Implementation Workflow",[165,166,167,177,201,212,234,251],"ol",{},[42,168,169,172,173,176],{},[18,170,171],{},"Create the Root Directory:"," Name it using lowercase letters and underscores (e.g., ",[75,174,175],{},"spatial_analyzer","). Avoid spaces or special characters.",[42,178,179,185,186,189,190,189,193,196,197,200],{},[18,180,181,182,184],{},"Generate ",[75,183,124],{},":"," Populate required keys. QGIS will reject plugins missing ",[75,187,188],{},"name",", ",[75,191,192],{},"version",[75,194,195],{},"qgisMinimumVersion",", or ",[75,198,199],{},"description",".",[42,202,203,208,209,211],{},[18,204,205,206,184],{},"Implement ",[75,207,114],{}," Define a single ",[75,210,118],{}," function that returns your main plugin class instance.",[42,213,214,219,220,223,224,189,227,230,231,200],{},[18,215,216,217,184],{},"Build ",[75,218,130],{}," Define a standard Python class that accepts ",[75,221,222],{},"iface",". Implement ",[75,225,226],{},"initGui()",[75,228,229],{},"unload()",", and ",[75,232,233],{},"run()",[42,235,236,239,240,243,244,246,247,250],{},[18,237,238],{},"Compile Resources:"," Use ",[75,241,242],{},"pyrcc5"," to convert ",[75,245,136],{}," into a Python-importable module (",[75,248,249],{},"resources_rc.py",").",[42,252,253,256,257,260],{},[18,254,255],{},"Load & Test:"," Enable the plugin in QGIS via ",[75,258,259],{},"Plugins → Manage and Install Plugins → Installed → Enable",". Monitor the Python Console for traceback output.",[31,262,264],{"id":263},"core-file-breakdown-tested-code-patterns","Core File Breakdown & Tested Code Patterns",[266,267,269,270,272],"h3",{"id":268},"_1-entry-point-__init__py","1. Entry Point (",[75,271,114],{},")",[14,274,275,276,279],{},"This file must be lightweight. QGIS calls ",[75,277,278],{},"classFactory(iface)"," during plugin discovery.",[94,281,285],{"className":282,"code":283,"language":284,"meta":102,"style":102},"language-python shiki shiki-themes github-dark","def classFactory(iface):\n \"\"\"\n Factory function required by QGIS.\n :param iface: QgsInterface instance providing access to the QGIS API\n :return: Main plugin class instance\n \"\"\"\n from .main_plugin import SpatialAnalyzerPlugin\n return SpatialAnalyzerPlugin(iface)\n","python",[75,286,287,304,311,317,323,329,334,349],{"__ignoreMap":102},[288,289,292,296,300],"span",{"class":290,"line":291},"line",1,[288,293,295],{"class":294},"snl16","def",[288,297,299],{"class":298},"svObZ"," classFactory",[288,301,303],{"class":302},"s95oV","(iface):\n",[288,305,307],{"class":290,"line":306},2,[288,308,310],{"class":309},"sU2Wk"," \"\"\"\n",[288,312,314],{"class":290,"line":313},3,[288,315,316],{"class":309}," Factory function required by QGIS.\n",[288,318,320],{"class":290,"line":319},4,[288,321,322],{"class":309}," :param iface: QgsInterface instance providing access to the QGIS API\n",[288,324,326],{"class":290,"line":325},5,[288,327,328],{"class":309}," :return: Main plugin class instance\n",[288,330,332],{"class":290,"line":331},6,[288,333,310],{"class":309},[288,335,337,340,343,346],{"class":290,"line":336},7,[288,338,339],{"class":294}," from",[288,341,342],{"class":302}," .main_plugin ",[288,344,345],{"class":294},"import",[288,347,348],{"class":302}," SpatialAnalyzerPlugin\n",[288,350,352,355],{"class":290,"line":351},8,[288,353,354],{"class":294}," return",[288,356,357],{"class":302}," SpatialAnalyzerPlugin(iface)\n",[266,359,361,362,272],{"id":360},"_2-metadata-configuration-metadatatxt","2. Metadata Configuration (",[75,363,124],{},[14,365,366],{},"Use strict key-value formatting. Do not include blank lines between keys.",[94,368,372],{"className":369,"code":370,"language":371,"meta":102,"style":102},"language-ini shiki shiki-themes github-dark","[general]\nname=Spatial Analyzer\nqgisMinimumVersion=3.16\ndescription=Provides advanced spatial analysis tools for vector layers.\nversion=1.0.0\nauthor=Your Name\nemail=your.email@example.com\nabout=This plugin extends QGIS with custom geoprocessing workflows.\ntracker=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\u002Fissues\nrepository=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\nicon=icons\u002Ficon.png\nexperimental=False\ndeprecated=False\n","ini",[75,373,374,379,384,389,394,399,404,409,414,420,426,432,438],{"__ignoreMap":102},[288,375,376],{"class":290,"line":291},[288,377,378],{},"[general]\n",[288,380,381],{"class":290,"line":306},[288,382,383],{},"name=Spatial Analyzer\n",[288,385,386],{"class":290,"line":313},[288,387,388],{},"qgisMinimumVersion=3.16\n",[288,390,391],{"class":290,"line":319},[288,392,393],{},"description=Provides advanced spatial analysis tools for vector layers.\n",[288,395,396],{"class":290,"line":325},[288,397,398],{},"version=1.0.0\n",[288,400,401],{"class":290,"line":331},[288,402,403],{},"author=Your Name\n",[288,405,406],{"class":290,"line":336},[288,407,408],{},"email=your.email@example.com\n",[288,410,411],{"class":290,"line":351},[288,412,413],{},"about=This plugin extends QGIS with custom geoprocessing workflows.\n",[288,415,417],{"class":290,"line":416},9,[288,418,419],{},"tracker=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\u002Fissues\n",[288,421,423],{"class":290,"line":422},10,[288,424,425],{},"repository=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\n",[288,427,429],{"class":290,"line":428},11,[288,430,431],{},"icon=icons\u002Ficon.png\n",[288,433,435],{"class":290,"line":434},12,[288,436,437],{},"experimental=False\n",[288,439,441],{"class":290,"line":440},13,[288,442,443],{},"deprecated=False\n",[266,445,447,448,272],{"id":446},"_3-main-plugin-class-main_pluginpy","3. Main Plugin Class (",[75,449,130],{},[14,451,452],{},"The following pattern demonstrates safe GUI registration, robust path resolution, and clean teardown.",[94,454,456],{"className":282,"code":455,"language":284,"meta":102,"style":102},"import os\nfrom qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication\nfrom qgis.PyQt.QtGui import QIcon\nfrom qgis.PyQt.QtWidgets import QAction, QApplication\nfrom qgis.core import QgsProject, QgsApplication\nfrom qgis.gui import QgsMessageBar\n\nclass SpatialAnalyzerPlugin:\n def __init__(self, iface):\n self.iface = iface\n self.plugin_dir = os.path.dirname(__file__)\n self.actions = []\n self.menu = self.tr(\"&Spatial Analyzer\")\n self.toolbar = self.iface.addToolBar(\"SpatialAnalyzer\")\n self.toolbar.setObjectName(\"SpatialAnalyzer\")\n \n # Initialize translation with safe fallback\n locale = QSettings().value(\"locale\u002FuserLocale\", \"en_US\")[0:2]\n locale_path = os.path.join(self.plugin_dir, \"i18n\", f\"spatial_analyzer_{locale}.qm\")\n if os.path.exists(locale_path):\n self.translator = QTranslator()\n self.translator.load(locale_path)\n QCoreApplication.installTranslator(self.translator)\n\n def tr(self, message):\n return QCoreApplication.translate(\"SpatialAnalyzerPlugin\", message)\n\n def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None):\n icon = QIcon(icon_path)\n action = QAction(icon, text, parent)\n action.triggered.connect(callback)\n action.setEnabled(enabled_flag)\n if status_tip:\n action.setStatusTip(status_tip)\n if whats_this:\n action.setWhatsThis(whats_this)\n if add_to_toolbar:\n self.toolbar.addAction(action)\n if add_to_menu:\n self.iface.addPluginToMenu(self.menu, action)\n self.actions.append(action)\n return action\n\n def initGui(self):\n \"\"\"Initialize GUI elements when plugin loads.\"\"\"\n icon_path = os.path.join(self.plugin_dir, \"icons\", \"icon.png\")\n self.add_action(\n icon_path,\n text=self.tr(\"Run Analysis\"),\n callback=self.run,\n parent=self.iface.mainWindow()\n )\n # For developers focusing on interface layout, integrating \n # [Qt Designer for GIS Interfaces](\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F) \n # ensures dialogs remain responsive, theme-compliant, and easier to maintain.\n\n def unload(self):\n \"\"\"Remove GUI elements and clean up resources.\"\"\"\n for action in self.actions:\n self.iface.removePluginMenu(self.menu, action)\n self.iface.removeToolBarIcon(action)\n # Safely remove the custom toolbar\n self.iface.mainWindow().removeToolBar(self.toolbar)\n self.actions.clear()\n\n def run(self):\n \"\"\"Execute plugin logic.\"\"\"\n layer = self.iface.activeLayer()\n if not layer:\n self.iface.messageBar().pushMessage(\n \"Warning\", \"Please select a vector layer first.\",\n level=QgsMessageBar.WARNING, duration=3\n )\n return\n # Core processing logic goes here\n self.iface.messageBar().pushMessage(\n \"Success\", f\"Analysis initialized for {layer.name()}\",\n level=QgsMessageBar.SUCCESS, duration=3\n )\n",[75,457,458,465,478,490,502,514,526,532,543,555,569,587,599,618,638,650,656,663,696,738,747,760,768,779,784,795,809,814,869,880,891,897,903,911,917,925,931,939,947,955,968,976,984,989,1000,1006,1030,1038,1044,1063,1076,1089,1095,1101,1107,1113,1118,1128,1134,1150,1162,1170,1176,1189,1197,1202,1212,1218,1231,1242,1250,1264,1288,1293,1299,1305,1312,1337,1357],{"__ignoreMap":102},[288,459,460,462],{"class":290,"line":291},[288,461,345],{"class":294},[288,463,464],{"class":302}," os\n",[288,466,467,470,473,475],{"class":290,"line":306},[288,468,469],{"class":294},"from",[288,471,472],{"class":302}," qgis.PyQt.QtCore ",[288,474,345],{"class":294},[288,476,477],{"class":302}," QSettings, QTranslator, QCoreApplication\n",[288,479,480,482,485,487],{"class":290,"line":313},[288,481,469],{"class":294},[288,483,484],{"class":302}," qgis.PyQt.QtGui ",[288,486,345],{"class":294},[288,488,489],{"class":302}," QIcon\n",[288,491,492,494,497,499],{"class":290,"line":319},[288,493,469],{"class":294},[288,495,496],{"class":302}," qgis.PyQt.QtWidgets ",[288,498,345],{"class":294},[288,500,501],{"class":302}," QAction, QApplication\n",[288,503,504,506,509,511],{"class":290,"line":325},[288,505,469],{"class":294},[288,507,508],{"class":302}," qgis.core ",[288,510,345],{"class":294},[288,512,513],{"class":302}," QgsProject, QgsApplication\n",[288,515,516,518,521,523],{"class":290,"line":331},[288,517,469],{"class":294},[288,519,520],{"class":302}," qgis.gui ",[288,522,345],{"class":294},[288,524,525],{"class":302}," QgsMessageBar\n",[288,527,528],{"class":290,"line":336},[288,529,531],{"emptyLinePlaceholder":530},true,"\n",[288,533,534,537,540],{"class":290,"line":351},[288,535,536],{"class":294},"class",[288,538,539],{"class":298}," SpatialAnalyzerPlugin",[288,541,542],{"class":302},":\n",[288,544,545,548,552],{"class":290,"line":416},[288,546,547],{"class":294}," def",[288,549,551],{"class":550},"sDLfK"," __init__",[288,553,554],{"class":302},"(self, iface):\n",[288,556,557,560,563,566],{"class":290,"line":422},[288,558,559],{"class":550}," self",[288,561,562],{"class":302},".iface ",[288,564,565],{"class":294},"=",[288,567,568],{"class":302}," iface\n",[288,570,571,573,576,578,581,584],{"class":290,"line":428},[288,572,559],{"class":550},[288,574,575],{"class":302},".plugin_dir ",[288,577,565],{"class":294},[288,579,580],{"class":302}," os.path.dirname(",[288,582,583],{"class":550},"__file__",[288,585,586],{"class":302},")\n",[288,588,589,591,594,596],{"class":290,"line":434},[288,590,559],{"class":550},[288,592,593],{"class":302},".actions ",[288,595,565],{"class":294},[288,597,598],{"class":302}," []\n",[288,600,601,603,606,608,610,613,616],{"class":290,"line":440},[288,602,559],{"class":550},[288,604,605],{"class":302},".menu ",[288,607,565],{"class":294},[288,609,559],{"class":550},[288,611,612],{"class":302},".tr(",[288,614,615],{"class":309},"\"&Spatial Analyzer\"",[288,617,586],{"class":302},[288,619,621,623,626,628,630,633,636],{"class":290,"line":620},14,[288,622,559],{"class":550},[288,624,625],{"class":302},".toolbar ",[288,627,565],{"class":294},[288,629,559],{"class":550},[288,631,632],{"class":302},".iface.addToolBar(",[288,634,635],{"class":309},"\"SpatialAnalyzer\"",[288,637,586],{"class":302},[288,639,641,643,646,648],{"class":290,"line":640},15,[288,642,559],{"class":550},[288,644,645],{"class":302},".toolbar.setObjectName(",[288,647,635],{"class":309},[288,649,586],{"class":302},[288,651,653],{"class":290,"line":652},16,[288,654,655],{"class":302}," \n",[288,657,659],{"class":290,"line":658},17,[288,660,662],{"class":661},"sAwPA"," # Initialize translation with safe fallback\n",[288,664,666,669,671,674,677,679,682,685,688,690,693],{"class":290,"line":665},18,[288,667,668],{"class":302}," locale ",[288,670,565],{"class":294},[288,672,673],{"class":302}," QSettings().value(",[288,675,676],{"class":309},"\"locale\u002FuserLocale\"",[288,678,189],{"class":302},[288,680,681],{"class":309},"\"en_US\"",[288,683,684],{"class":302},")[",[288,686,687],{"class":550},"0",[288,689,184],{"class":302},[288,691,692],{"class":550},"2",[288,694,695],{"class":302},"]\n",[288,697,699,702,704,707,710,713,716,718,721,724,727,730,733,736],{"class":290,"line":698},19,[288,700,701],{"class":302}," locale_path ",[288,703,565],{"class":294},[288,705,706],{"class":302}," os.path.join(",[288,708,709],{"class":550},"self",[288,711,712],{"class":302},".plugin_dir, ",[288,714,715],{"class":309},"\"i18n\"",[288,717,189],{"class":302},[288,719,720],{"class":294},"f",[288,722,723],{"class":309},"\"spatial_analyzer_",[288,725,726],{"class":550},"{",[288,728,729],{"class":302},"locale",[288,731,732],{"class":550},"}",[288,734,735],{"class":309},".qm\"",[288,737,586],{"class":302},[288,739,741,744],{"class":290,"line":740},20,[288,742,743],{"class":294}," if",[288,745,746],{"class":302}," os.path.exists(locale_path):\n",[288,748,750,752,755,757],{"class":290,"line":749},21,[288,751,559],{"class":550},[288,753,754],{"class":302},".translator ",[288,756,565],{"class":294},[288,758,759],{"class":302}," QTranslator()\n",[288,761,763,765],{"class":290,"line":762},22,[288,764,559],{"class":550},[288,766,767],{"class":302},".translator.load(locale_path)\n",[288,769,771,774,776],{"class":290,"line":770},23,[288,772,773],{"class":302}," QCoreApplication.installTranslator(",[288,775,709],{"class":550},[288,777,778],{"class":302},".translator)\n",[288,780,782],{"class":290,"line":781},24,[288,783,531],{"emptyLinePlaceholder":530},[288,785,787,789,792],{"class":290,"line":786},25,[288,788,547],{"class":294},[288,790,791],{"class":298}," tr",[288,793,794],{"class":302},"(self, message):\n",[288,796,798,800,803,806],{"class":290,"line":797},26,[288,799,354],{"class":294},[288,801,802],{"class":302}," QCoreApplication.translate(",[288,804,805],{"class":309},"\"SpatialAnalyzerPlugin\"",[288,807,808],{"class":302},", message)\n",[288,810,812],{"class":290,"line":811},27,[288,813,531],{"emptyLinePlaceholder":530},[288,815,817,819,822,825,827,830,833,835,837,840,842,844,847,849,852,855,857,859,862,864,866],{"class":290,"line":816},28,[288,818,547],{"class":294},[288,820,821],{"class":298}," add_action",[288,823,824],{"class":302},"(self, icon_path, text, callback, enabled_flag",[288,826,565],{"class":294},[288,828,829],{"class":550},"True",[288,831,832],{"class":302},", add_to_menu",[288,834,565],{"class":294},[288,836,829],{"class":550},[288,838,839],{"class":302},", add_to_toolbar",[288,841,565],{"class":294},[288,843,829],{"class":550},[288,845,846],{"class":302},", status_tip",[288,848,565],{"class":294},[288,850,851],{"class":550},"None",[288,853,854],{"class":302},", whats_this",[288,856,565],{"class":294},[288,858,851],{"class":550},[288,860,861],{"class":302},", parent",[288,863,565],{"class":294},[288,865,851],{"class":550},[288,867,868],{"class":302},"):\n",[288,870,872,875,877],{"class":290,"line":871},29,[288,873,874],{"class":302}," icon ",[288,876,565],{"class":294},[288,878,879],{"class":302}," QIcon(icon_path)\n",[288,881,883,886,888],{"class":290,"line":882},30,[288,884,885],{"class":302}," action ",[288,887,565],{"class":294},[288,889,890],{"class":302}," QAction(icon, text, parent)\n",[288,892,894],{"class":290,"line":893},31,[288,895,896],{"class":302}," action.triggered.connect(callback)\n",[288,898,900],{"class":290,"line":899},32,[288,901,902],{"class":302}," action.setEnabled(enabled_flag)\n",[288,904,906,908],{"class":290,"line":905},33,[288,907,743],{"class":294},[288,909,910],{"class":302}," status_tip:\n",[288,912,914],{"class":290,"line":913},34,[288,915,916],{"class":302}," action.setStatusTip(status_tip)\n",[288,918,920,922],{"class":290,"line":919},35,[288,921,743],{"class":294},[288,923,924],{"class":302}," whats_this:\n",[288,926,928],{"class":290,"line":927},36,[288,929,930],{"class":302}," action.setWhatsThis(whats_this)\n",[288,932,934,936],{"class":290,"line":933},37,[288,935,743],{"class":294},[288,937,938],{"class":302}," add_to_toolbar:\n",[288,940,942,944],{"class":290,"line":941},38,[288,943,559],{"class":550},[288,945,946],{"class":302},".toolbar.addAction(action)\n",[288,948,950,952],{"class":290,"line":949},39,[288,951,743],{"class":294},[288,953,954],{"class":302}," add_to_menu:\n",[288,956,958,960,963,965],{"class":290,"line":957},40,[288,959,559],{"class":550},[288,961,962],{"class":302},".iface.addPluginToMenu(",[288,964,709],{"class":550},[288,966,967],{"class":302},".menu, action)\n",[288,969,971,973],{"class":290,"line":970},41,[288,972,559],{"class":550},[288,974,975],{"class":302},".actions.append(action)\n",[288,977,979,981],{"class":290,"line":978},42,[288,980,354],{"class":294},[288,982,983],{"class":302}," action\n",[288,985,987],{"class":290,"line":986},43,[288,988,531],{"emptyLinePlaceholder":530},[288,990,992,994,997],{"class":290,"line":991},44,[288,993,547],{"class":294},[288,995,996],{"class":298}," initGui",[288,998,999],{"class":302},"(self):\n",[288,1001,1003],{"class":290,"line":1002},45,[288,1004,1005],{"class":309}," \"\"\"Initialize GUI elements when plugin loads.\"\"\"\n",[288,1007,1009,1012,1014,1016,1018,1020,1023,1025,1028],{"class":290,"line":1008},46,[288,1010,1011],{"class":302}," icon_path ",[288,1013,565],{"class":294},[288,1015,706],{"class":302},[288,1017,709],{"class":550},[288,1019,712],{"class":302},[288,1021,1022],{"class":309},"\"icons\"",[288,1024,189],{"class":302},[288,1026,1027],{"class":309},"\"icon.png\"",[288,1029,586],{"class":302},[288,1031,1033,1035],{"class":290,"line":1032},47,[288,1034,559],{"class":550},[288,1036,1037],{"class":302},".add_action(\n",[288,1039,1041],{"class":290,"line":1040},48,[288,1042,1043],{"class":302}," icon_path,\n",[288,1045,1047,1051,1053,1055,1057,1060],{"class":290,"line":1046},49,[288,1048,1050],{"class":1049},"s9osk"," text",[288,1052,565],{"class":294},[288,1054,709],{"class":550},[288,1056,612],{"class":302},[288,1058,1059],{"class":309},"\"Run Analysis\"",[288,1061,1062],{"class":302},"),\n",[288,1064,1066,1069,1071,1073],{"class":290,"line":1065},50,[288,1067,1068],{"class":1049}," callback",[288,1070,565],{"class":294},[288,1072,709],{"class":550},[288,1074,1075],{"class":302},".run,\n",[288,1077,1079,1082,1084,1086],{"class":290,"line":1078},51,[288,1080,1081],{"class":1049}," parent",[288,1083,565],{"class":294},[288,1085,709],{"class":550},[288,1087,1088],{"class":302},".iface.mainWindow()\n",[288,1090,1092],{"class":290,"line":1091},52,[288,1093,1094],{"class":302}," )\n",[288,1096,1098],{"class":290,"line":1097},53,[288,1099,1100],{"class":661}," # For developers focusing on interface layout, integrating \n",[288,1102,1104],{"class":290,"line":1103},54,[288,1105,1106],{"class":661}," # [Qt Designer for GIS Interfaces](\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F) \n",[288,1108,1110],{"class":290,"line":1109},55,[288,1111,1112],{"class":661}," # ensures dialogs remain responsive, theme-compliant, and easier to maintain.\n",[288,1114,1116],{"class":290,"line":1115},56,[288,1117,531],{"emptyLinePlaceholder":530},[288,1119,1121,1123,1126],{"class":290,"line":1120},57,[288,1122,547],{"class":294},[288,1124,1125],{"class":298}," unload",[288,1127,999],{"class":302},[288,1129,1131],{"class":290,"line":1130},58,[288,1132,1133],{"class":309}," \"\"\"Remove GUI elements and clean up resources.\"\"\"\n",[288,1135,1137,1140,1142,1145,1147],{"class":290,"line":1136},59,[288,1138,1139],{"class":294}," for",[288,1141,885],{"class":302},[288,1143,1144],{"class":294},"in",[288,1146,559],{"class":550},[288,1148,1149],{"class":302},".actions:\n",[288,1151,1153,1155,1158,1160],{"class":290,"line":1152},60,[288,1154,559],{"class":550},[288,1156,1157],{"class":302},".iface.removePluginMenu(",[288,1159,709],{"class":550},[288,1161,967],{"class":302},[288,1163,1165,1167],{"class":290,"line":1164},61,[288,1166,559],{"class":550},[288,1168,1169],{"class":302},".iface.removeToolBarIcon(action)\n",[288,1171,1173],{"class":290,"line":1172},62,[288,1174,1175],{"class":661}," # Safely remove the custom toolbar\n",[288,1177,1179,1181,1184,1186],{"class":290,"line":1178},63,[288,1180,559],{"class":550},[288,1182,1183],{"class":302},".iface.mainWindow().removeToolBar(",[288,1185,709],{"class":550},[288,1187,1188],{"class":302},".toolbar)\n",[288,1190,1192,1194],{"class":290,"line":1191},64,[288,1193,559],{"class":550},[288,1195,1196],{"class":302},".actions.clear()\n",[288,1198,1200],{"class":290,"line":1199},65,[288,1201,531],{"emptyLinePlaceholder":530},[288,1203,1205,1207,1210],{"class":290,"line":1204},66,[288,1206,547],{"class":294},[288,1208,1209],{"class":298}," run",[288,1211,999],{"class":302},[288,1213,1215],{"class":290,"line":1214},67,[288,1216,1217],{"class":309}," \"\"\"Execute plugin logic.\"\"\"\n",[288,1219,1221,1224,1226,1228],{"class":290,"line":1220},68,[288,1222,1223],{"class":302}," layer ",[288,1225,565],{"class":294},[288,1227,559],{"class":550},[288,1229,1230],{"class":302},".iface.activeLayer()\n",[288,1232,1234,1236,1239],{"class":290,"line":1233},69,[288,1235,743],{"class":294},[288,1237,1238],{"class":294}," not",[288,1240,1241],{"class":302}," layer:\n",[288,1243,1245,1247],{"class":290,"line":1244},70,[288,1246,559],{"class":550},[288,1248,1249],{"class":302},".iface.messageBar().pushMessage(\n",[288,1251,1253,1256,1258,1261],{"class":290,"line":1252},71,[288,1254,1255],{"class":309}," \"Warning\"",[288,1257,189],{"class":302},[288,1259,1260],{"class":309},"\"Please select a vector layer first.\"",[288,1262,1263],{"class":302},",\n",[288,1265,1267,1270,1272,1275,1278,1280,1283,1285],{"class":290,"line":1266},72,[288,1268,1269],{"class":1049}," level",[288,1271,565],{"class":294},[288,1273,1274],{"class":302},"QgsMessageBar.",[288,1276,1277],{"class":550},"WARNING",[288,1279,189],{"class":302},[288,1281,1282],{"class":1049},"duration",[288,1284,565],{"class":294},[288,1286,1287],{"class":550},"3\n",[288,1289,1291],{"class":290,"line":1290},73,[288,1292,1094],{"class":302},[288,1294,1296],{"class":290,"line":1295},74,[288,1297,1298],{"class":294}," return\n",[288,1300,1302],{"class":290,"line":1301},75,[288,1303,1304],{"class":661}," # Core processing logic goes here\n",[288,1306,1308,1310],{"class":290,"line":1307},76,[288,1309,559],{"class":550},[288,1311,1249],{"class":302},[288,1313,1315,1318,1320,1322,1325,1327,1330,1332,1335],{"class":290,"line":1314},77,[288,1316,1317],{"class":309}," \"Success\"",[288,1319,189],{"class":302},[288,1321,720],{"class":294},[288,1323,1324],{"class":309},"\"Analysis initialized for ",[288,1326,726],{"class":550},[288,1328,1329],{"class":302},"layer.name()",[288,1331,732],{"class":550},[288,1333,1334],{"class":309},"\"",[288,1336,1263],{"class":302},[288,1338,1340,1342,1344,1346,1349,1351,1353,1355],{"class":290,"line":1339},78,[288,1341,1269],{"class":1049},[288,1343,565],{"class":294},[288,1345,1274],{"class":302},[288,1347,1348],{"class":550},"SUCCESS",[288,1350,189],{"class":302},[288,1352,1282],{"class":1049},[288,1354,565],{"class":294},[288,1356,1287],{"class":550},[288,1358,1360],{"class":290,"line":1359},79,[288,1361,1094],{"class":302},[266,1363,1365],{"id":1364},"extending-the-boilerplate","Extending the Boilerplate",[14,1367,1368,1369,1373,1374,1378,1379,1382,1383,200],{},"Once the base structure is stable, you can branch into specialized implementations. For instance, registering a custom UI action often involves ",[25,1370,1372],{"href":1371},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreating-a-custom-toolbar-button-in-qgis\u002F","Creating a custom toolbar button in QGIS"," to ensure consistent icon scaling, hover states, and toggle behavior. Similarly, if your plugin requires field calculator integration, you will need to adapt the initialization sequence to support ",[25,1375,1377],{"href":1376},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreating-custom-expression-functions-for-qgis\u002F","Creating custom expression functions for QGIS",", which requires explicit registration with ",[75,1380,1381],{},"QgsExpression.registerFunction()"," during ",[75,1384,226],{},[31,1386,1388],{"id":1387},"common-errors-resolution-strategies","Common Errors & Resolution Strategies",[266,1390,1392],{"id":1391},"_1-plugin-not-appearing-in-manager","1. Plugin Not Appearing in Manager",[14,1394,1395,1398,1399,1402,1403,1405,1406,1408,1409,1412,1413,1415,1416,1418,1419,1422,1423,1426],{},[18,1396,1397],{},"Symptom:"," The directory exists but QGIS ignores it.\n",[18,1400,1401],{},"Cause:"," Missing ",[75,1404,124],{},", malformed keys, or ",[75,1407,195],{}," higher than the installed QGIS version.\n",[18,1410,1411],{},"Fix:"," Verify ",[75,1414,124],{}," syntax. Ensure ",[75,1417,195],{}," matches or is lower than your QGIS build. Check the Python Console (",[75,1420,1421],{},"Plugins → Python Console",") for ",[75,1424,1425],{},"PluginManager"," warnings.",[266,1428,1430,1431,1434],{"id":1429},"_2-modulenotfounderror-or-import-failures","2. ",[75,1432,1433],{},"ModuleNotFoundError"," or Import Failures",[14,1436,1437,73,1439,1442,1444,1445,1447,1448,1450,1451,1454,1455,1457,1458,1461],{},[18,1438,1397],{},[75,1440,1441],{},"ImportError: cannot import name 'main_plugin'",[18,1443,1401],{}," Relative import issues or missing ",[75,1446,114],{}," in subdirectories.\n",[18,1449,1411],{}," Always use relative imports (",[75,1452,1453],{},"from .main_plugin import ...","). Ensure every Python-containing directory has an empty ",[75,1456,114],{},". Avoid absolute paths; QGIS modifies ",[75,1459,1460],{},"sys.path"," dynamically during plugin loading.",[266,1463,1465],{"id":1464},"_3-ui-dialog-fails-to-load","3. UI Dialog Fails to Load",[14,1467,1468,73,1470,1473,1474,1476,1477,1479,1480,1483,1484,1486,1487,1490,1491,1494,1495,1498],{},[18,1469,1397],{},[75,1471,1472],{},"QFile::open: No such file or directory"," when loading ",[75,1475,146],{}," files.\n",[18,1478,1401],{}," Incorrect working directory assumption. ",[75,1481,1482],{},"QgsApplication"," does not guarantee the current working directory matches the plugin folder.\n",[18,1485,1411],{}," Resolve paths using ",[75,1488,1489],{},"os.path.dirname(__file__)"," before passing them to ",[75,1492,1493],{},"QUiLoader"," or ",[75,1496,1497],{},"uic.loadUi",". Example:",[94,1500,1502],{"className":282,"code":1501,"language":284,"meta":102,"style":102},"ui_path = os.path.join(os.path.dirname(__file__), \"ui\", \"main_dialog.ui\")\n",[75,1503,1504],{"__ignoreMap":102},[288,1505,1506,1509,1511,1514,1516,1519,1522,1524,1527],{"class":290,"line":291},[288,1507,1508],{"class":302},"ui_path ",[288,1510,565],{"class":294},[288,1512,1513],{"class":302}," os.path.join(os.path.dirname(",[288,1515,583],{"class":550},[288,1517,1518],{"class":302},"), ",[288,1520,1521],{"class":309},"\"ui\"",[288,1523,189],{"class":302},[288,1525,1526],{"class":309},"\"main_dialog.ui\"",[288,1528,586],{"class":302},[266,1530,1532],{"id":1531},"_4-toolbarmenu-items-persist-after-unload","4. Toolbar\u002FMenu Items Persist After Unload",[14,1534,1535,1537,1538,73,1540,1542,1543,1545,1546,1549,1550,1552,1553,1555,1556,1559,1560,1563],{},[18,1536,1397],{}," Buttons remain visible after disabling the plugin.\n",[18,1539,1401],{},[75,1541,229],{}," does not explicitly remove registered actions.\n",[18,1544,1411],{}," Store all ",[75,1547,1548],{},"QAction"," instances in a list during ",[75,1551,226],{}," and iterate through them in ",[75,1554,229],{},", calling both ",[75,1557,1558],{},"removePluginMenu()"," and ",[75,1561,1562],{},"removeToolBarIcon()",". Never rely on QGIS to garbage-collect UI references automatically.",[266,1565,1567],{"id":1566},"_5-resource-compilation-errors","5. Resource Compilation Errors",[14,1569,1570,73,1572,1574,1575,1577,1578,1580,1581,1584,1585,1587,1588,1591,1592,1595,1596,1599,1600,1602],{},[18,1571,1397],{},[75,1573,242],{}," fails or icons appear as broken placeholders.\n",[18,1576,1401],{}," Invalid paths in ",[75,1579,136],{}," or missing ",[75,1582,1583],{},"qrc"," file in the plugin directory.\n",[18,1586,1411],{}," Run compilation from the plugin root: ",[75,1589,1590],{},"pyrcc5 resources.qrc -o resources_rc.py",". Verify all ",[75,1593,1594],{},"\u003Cfile>"," tags in the ",[75,1597,1598],{},".qrc"," use paths relative to the ",[75,1601,1598],{}," location.",[31,1604,1606],{"id":1605},"workflow-validation-checklist","Workflow Validation Checklist",[14,1608,1609],{},"Before considering the boilerplate complete, verify the following:",[39,1611,1614,1625,1636,1647,1659,1665,1671],{"className":1612},[1613],"contains-task-list",[42,1615,1618,73,1622,1624],{"className":1616},[1617],"task-list-item",[1619,1620],"input",{"disabled":530,"type":1621},"checkbox",[75,1623,124],{}," contains all required fields and valid version strings",[42,1626,1628,73,1630,1632,1633,1635],{"className":1627},[1617],[1619,1629],{"disabled":530,"type":1621},[75,1631,114],{}," exposes exactly one ",[75,1634,118],{}," function",[42,1637,1639,73,1641,1643,1644,1646],{"className":1638},[1617],[1619,1640],{"disabled":530,"type":1621},[75,1642,226],{}," registers actions and ",[75,1645,229],{}," removes them symmetrically",[42,1648,1650,1652,1653,1655,1656],{"className":1649},[1617],[1619,1651],{"disabled":530,"type":1621}," All file paths resolve using ",[75,1654,583],{}," rather than ",[75,1657,1658],{},"os.getcwd()",[42,1660,1662,1664],{"className":1661},[1617],[1619,1663],{"disabled":530,"type":1621}," Plugin loads without traceback in a fresh QGIS profile",[42,1666,1668,1670],{"className":1667},[1617],[1619,1669],{"disabled":530,"type":1621}," Toolbar and menu items disappear completely after disabling",[42,1672,1674,1676],{"className":1673},[1617],[1619,1675],{"disabled":530,"type":1621}," Translation files load gracefully when missing",[14,1678,1679,1680,1684],{},"Once validation passes, the structure is ready for advanced feature integration and distribution. Properly organizing your codebase at this stage prevents technical debt and simplifies the transition to ",[25,1681,1683],{"href":1682},"\u002Fqgis-plugin-development\u002Fplugin-packaging-deployment\u002F","Plugin Packaging & Deployment",", where standardized archives, dependency management, and repository submission protocols take precedence.",[1686,1687,1688],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":102,"searchDepth":306,"depth":306,"links":1690},[1691,1692,1693,1694,1703,1711],{"id":33,"depth":306,"text":34},{"id":88,"depth":306,"text":89},{"id":162,"depth":306,"text":163},{"id":263,"depth":306,"text":264,"children":1695},[1696,1698,1700,1702],{"id":268,"depth":313,"text":1697},"1. Entry Point (__init__.py)",{"id":360,"depth":313,"text":1699},"2. Metadata Configuration (metadata.txt)",{"id":446,"depth":313,"text":1701},"3. Main Plugin Class (main_plugin.py)",{"id":1364,"depth":313,"text":1365},{"id":1387,"depth":306,"text":1388,"children":1704},[1705,1706,1708,1709,1710],{"id":1391,"depth":313,"text":1392},{"id":1429,"depth":313,"text":1707},"2. ModuleNotFoundError or Import Failures",{"id":1464,"depth":313,"text":1465},{"id":1531,"depth":313,"text":1532},{"id":1566,"depth":313,"text":1567},{"id":1605,"depth":306,"text":1606},"A well-defined Plugin Boilerplate & Structure is the foundation of any maintainable PyQGIS extension. QGIS expects a predictable file hierarchy, standardized metadata, and explicit lifecycle hooks. When these elements are correctly implemented, the plugin manager can load, initialize, and unload your code without conflicts. This guide outlines the architectural standards, provides tested initialization patterns, and details the step-by-step workflow required to build a production-ready QGIS plugin.","md",{},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure",{"title":5,"description":1712},"qgis-plugin-development\u002Fplugin-boilerplate-structure\u002Findex","UXEXJwGt7AZvWwzfFYK7uJ54E6ozQ9ngFSa9El-E0_E",1777824788893]