[{"data":1,"prerenderedAt":2777},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure":3},{"id":4,"title":5,"body":6,"description":2766,"extension":2767,"meta":2768,"navigation":892,"path":2773,"seo":2774,"stem":2775,"__hash__":2776},"docs\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Findex.md","QGIS Plugin Boilerplate & Structure",{"type":7,"value":8,"toc":2738},"minimark",[9,14,44,57,253,258,261,294,297,316,320,323,332,337,385,400,404,498,502,510,516,593,599,602,679,692,698,716,824,827,1803,1828,1832,1844,1902,1923,1927,1952,1956,1960,1994,2002,2029,2033,2061,2091,2095,2125,2129,2164,2168,2171,2238,2242,2245,2393,2399,2403,2457,2475,2479,2500,2523,2556,2572,2592,2615,2628,2640,2646,2655,2661,2667,2671,2734],[10,11,13],"h1",{"id":12},"plugin-boilerplate-structure","Plugin Boilerplate & Structure",[15,16,17,18,23,24,28,29,33,34,38,39,43],"p",{},"A well-defined plugin boilerplate and directory 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 sits within the broader ",[19,20,22],"a",{"href":21},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development"," track: it establishes the skeleton every later stage builds on — the visual interface you design in ",[19,25,27],{"href":26},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for QGIS interfaces",", the algorithms you expose through ",[19,30,32],{"href":31},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002F","processing provider plugins",", the automated checks in ",[19,35,37],{"href":36},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","testing and CI for plugins",", and the release you eventually send to the ",[19,40,42],{"href":41},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002F","QGIS plugin repository",".",[15,45,46,47,51,52,56],{},"This page targets QGIS 3.x on Python 3 and pins its examples to the 3.34 LTR line, noting API differences where they matter. Read it top to bottom to build a correct skeleton by hand, then branch into the focused recipes linked throughout — ",[19,48,50],{"href":49},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreate-qgis-plugin-with-plugin-builder\u002F","scaffolding with Plugin Builder 3"," if you would rather generate the tree, and ",[19,53,55],{"href":54},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","adding a toolbar button"," once the class is in place.",[58,59,64,68,72,79,88,97,103,108,113,118,122,126,129,134,139,143,147,151,155,159,164,169,172,176,180,183,187,191,207,213,219,224,229,233,237,240,245,248],"svg",{"viewBox":60,"role":61,"ariaLabel":62,"xmlns":63},"0 0 760 420","img","QGIS plugin directory structure and how its files relate","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[65,66,67],"title",{},"Plugin directory structure and file relationships",[69,70,71],"desc",{},"metadata.txt is read first by QGIS; __init__.py exposes classFactory which returns the main module; the main module loads the .ui file and the compiled resources_rc.py generated from resources.qrc; icons feed resources; tests exercise the main module.",[73,74],"rect",{"x":75,"y":75,"width":76,"height":77,"fill":78},"0","760","420","#f6f3ea",[73,80],{"x":81,"y":81,"width":82,"height":83,"rx":84,"fill":85,"stroke":86,"style":87},"24","220","56","8","#fffdf7","#b45309","stroke-width:2.5",[89,90,96],"text",{"x":91,"y":92,"style":93,"fill":94,"textAnchor":95},"134","48","text-anchor:middle;font-size:13px;font-weight:bold;font-family:monospace","#17211d","middle","metadata.txt",[89,98,102],{"x":91,"y":99,"style":100,"fill":101,"textAnchor":95},"66","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","name \u002F version \u002F minVer",[73,104],{"x":81,"y":105,"width":82,"height":83,"rx":84,"fill":106,"stroke":107,"style":87},"112","#26322d","#0f766e",[89,109,112],{"x":91,"y":110,"style":93,"fill":111,"textAnchor":95},"136","#d9f99d","__init__.py",[89,114,117],{"x":91,"y":115,"style":116,"fill":111,"textAnchor":95},"154","text-anchor:middle;font-size:11px;font-family:monospace","classFactory(iface)",[73,119],{"x":120,"y":105,"width":82,"height":83,"rx":84,"fill":85,"stroke":121,"style":87},"300","#2563eb",[89,123,125],{"x":124,"y":110,"style":93,"fill":94,"textAnchor":95},"410","main_plugin.py",[89,127,128],{"x":124,"y":115,"style":100,"fill":101,"textAnchor":95},"initGui \u002F unload \u002F run",[73,130],{"x":120,"y":131,"width":82,"height":132,"rx":84,"fill":85,"stroke":133,"style":87},"208","50","#15803d",[89,135,138],{"x":124,"y":136,"style":137,"fill":94,"textAnchor":95},"230","text-anchor:middle;font-size:12px;font-weight:bold;font-family:monospace","ui\u002Fmain_dialog.ui",[89,140,142],{"x":124,"y":141,"style":100,"fill":101,"textAnchor":95},"247","loadUiType() at runtime",[73,144],{"x":145,"y":105,"width":146,"height":132,"rx":84,"fill":85,"stroke":107,"style":87},"576","160",[89,148,150],{"x":149,"y":91,"style":137,"fill":94,"textAnchor":95},"656","resources.qrc",[89,152,154],{"x":149,"y":153,"style":100,"fill":101,"textAnchor":95},"151","asset manifest",[73,156],{"x":145,"y":157,"width":146,"height":132,"rx":84,"fill":106,"stroke":158,"style":87},"200","#22c55e",[89,160,163],{"x":149,"y":161,"style":162,"fill":111,"textAnchor":95},"222","text-anchor:middle;font-size:11px;font-weight:bold;font-family:monospace","resources_rc.py",[89,165,168],{"x":149,"y":166,"style":167,"fill":111,"textAnchor":95},"239","text-anchor:middle;font-size:10px;font-family:sans-serif","via pyrcc5",[73,170],{"x":145,"y":171,"width":146,"height":132,"rx":84,"fill":85,"stroke":86,"style":87},"290",[89,173,175],{"x":149,"y":174,"style":137,"fill":94,"textAnchor":95},"312","icons\u002Ficon.png",[89,177,179],{"x":149,"y":178,"style":100,"fill":101,"textAnchor":95},"329","toolbar \u002F manager",[73,181],{"x":120,"y":182,"width":82,"height":132,"rx":84,"fill":85,"stroke":121,"style":87},"304",[89,184,186],{"x":124,"y":185,"style":137,"fill":94,"textAnchor":95},"326","tests\u002Ftest_main.py",[89,188,190],{"x":124,"y":189,"style":100,"fill":101,"textAnchor":95},"343","exercises main module",[192,193,194],"defs",{},[195,196,203],"marker",{"id":197,"viewBox":198,"refX":199,"refY":200,"markerWidth":201,"markerHeight":201,"orient":202},"arrowb","0 0 10 10","9","5","7","auto-start-reverse",[204,205],"path",{"d":206,"fill":101},"M0,0 L10,5 L0,10 z",[208,209],"line",{"x1":91,"y1":210,"x2":91,"y2":211,"stroke":101,"style":212},"80","108","stroke-width:2.5;marker-end:url(#arrowb)",[89,214,218],{"x":157,"y":215,"style":216,"fill":101,"textAnchor":217},"98","text-anchor:start;font-size:10px;font-family:sans-serif","start","QGIS reads first",[208,220],{"x1":221,"y1":222,"x2":223,"y2":222,"stroke":101,"style":212},"244","140","296",[89,225,228],{"x":226,"y":227,"style":167,"fill":101,"textAnchor":95},"270","130","returns",[208,230],{"x1":124,"y1":231,"x2":124,"y2":232,"stroke":101,"style":212},"168","204",[208,234],{"x1":149,"y1":235,"x2":149,"y2":236,"stroke":101,"style":212},"162","196",[208,238],{"x1":149,"y1":171,"x2":149,"y2":239,"stroke":101,"style":212},"254",[208,241],{"x1":145,"y1":242,"x2":243,"y2":146,"stroke":101,"style":244},"225","522","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#arrowb)",[208,246],{"x1":124,"y1":182,"x2":124,"y2":247,"stroke":101,"style":244},"262",[89,249,252],{"x":91,"y":250,"style":251,"fill":101,"textAnchor":217},"392","text-anchor:start;font-size:11px;font-family:sans-serif","Solid arrows: load-time wiring. Dashed: build \u002F test dependencies.",[254,255,257],"h2",{"id":256},"prerequisites","Prerequisites",[15,259,260],{},"Before generating or modifying a plugin skeleton, ensure your environment meets the following baseline requirements:",[262,263,264,277,283,286,291],"ul",{},[265,266,267,271,272,276],"li",{},[268,269,270],"strong",{},"QGIS 3.x"," installed — pin against the current LTR (3.34) so your ",[273,274,275],"code",{},"qgisMinimumVersion"," and API assumptions match the release most users run.",[265,278,279,282],{},[268,280,281],{},"Python 3.9+"," environment (bundled with modern QGIS releases).",[265,284,285],{},"A code editor with Python syntax highlighting and linting (VS Code, PyCharm, or similar).",[265,287,288,289,43],{},"Basic familiarity with object-oriented Python and Qt signal\u002Fslot architecture — the same signal wiring covered in ",[19,290,27],{"href":26},[265,292,293],{},"Write access to the QGIS profile directory.",[15,295,296],{},"The QGIS profile directory typically resides at:",[262,298,299,308],{},[265,300,301,304,305],{},[268,302,303],{},"Windows:"," ",[273,306,307],{},"%APPDATA%\\QGIS\\QGIS3\\profiles\\default\\python\\plugins\\",[265,309,310,304,313],{},[268,311,312],{},"macOS\u002FLinux:",[273,314,315],{},"~\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002F",[254,317,319],{"id":318},"standard-directory-architecture","Standard Directory Architecture",[15,321,322],{},"A compliant plugin directory must contain specific files that QGIS scans during startup. The following tree represents the minimal viable structure:",[324,325,329],"pre",{"className":326,"code":328,"language":89},[327],"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",[273,330,328],{"__ignoreMap":331},"",[15,333,334],{},[268,335,336],{},"File Responsibilities:",[262,338,339,348,353,358,363,373,379],{},[265,340,341,343,344,347],{},[273,342,112],{},": Entry point that exposes the ",[273,345,346],{},"classFactory"," function to QGIS.",[265,349,350,352],{},[273,351,96],{},": Plain-text configuration file containing plugin name, version, author, and QGIS compatibility flags.",[265,354,355,357],{},[273,356,125],{},": Core logic handling GUI initialization, toolbar\u002Fmenu integration, and cleanup routines.",[265,359,360,362],{},[273,361,150],{},": Qt resource compiler file bundling icons and UI assets.",[265,364,365,368,369,372],{},[273,366,367],{},"ui\u002F",": Contains ",[273,370,371],{},".ui"," XML files generated by Qt Designer.",[265,374,375,378],{},[273,376,377],{},"i18n\u002F",": Translation files for internationalization.",[265,380,381,384],{},[273,382,383],{},"icons\u002F",": Raster or SVG assets for toolbar buttons and plugin manager listings.",[15,386,387,388,391,392,395,396,399],{},"This layout is exactly what a scaffolding tool emits: if you run ",[19,389,390],{"href":49},"Plugin Builder 3",", it generates the same tree plus a ",[273,393,394],{},"Makefile"," and a ",[273,397,398],{},"test\u002F"," package. Building it by hand once, however, makes every later file feel obvious rather than magic.",[254,401,403],{"id":402},"step-by-step-implementation-workflow","Step-by-Step Implementation Workflow",[405,406,407,417,439,450,472,488],"ol",{},[265,408,409,412,413,416],{},[268,410,411],{},"Create the Root Directory:"," Name it using lowercase letters and underscores (e.g., ",[273,414,415],{},"spatial_analyzer","). Avoid spaces or special characters — the folder name becomes the Python package name.",[265,418,419,425,426,429,430,429,433,435,436,43],{},[268,420,421,422,424],{},"Generate ",[273,423,96],{},":"," Populate required keys. QGIS will reject plugins missing ",[273,427,428],{},"name",", ",[273,431,432],{},"version",[273,434,275],{},", or ",[273,437,438],{},"description",[265,440,441,446,447,449],{},[268,442,443,444,424],{},"Implement ",[273,445,112],{}," Define a single ",[273,448,346],{}," function that returns your main plugin class instance.",[265,451,452,457,458,461,462,429,465,468,469,43],{},[268,453,454,455,424],{},"Build ",[273,456,125],{}," Define a standard Python class that accepts ",[273,459,460],{},"iface",". Implement ",[273,463,464],{},"initGui()",[273,466,467],{},"unload()",", and ",[273,470,471],{},"run()",[265,473,474,477,478,481,482,484,485,487],{},[268,475,476],{},"Compile Resources:"," Use ",[273,479,480],{},"pyrcc5"," to convert ",[273,483,150],{}," into a Python-importable module (",[273,486,163],{},").",[265,489,490,493,494,497],{},[268,491,492],{},"Load & Test:"," Enable the plugin in QGIS via ",[273,495,496],{},"Plugins → Manage and Install Plugins → Installed → Enable",". Monitor the Python Console for traceback output.",[254,499,501],{"id":500},"core-file-breakdown-tested-code-patterns","Core File Breakdown & Tested Code Patterns",[503,504,506,507,509],"h3",{"id":505},"_1-entry-point-__init__py","1. Entry Point (",[273,508,112],{},")",[15,511,512,513,515],{},"This file must be lightweight. QGIS calls ",[273,514,117],{}," during plugin discovery.",[324,517,521],{"className":518,"code":519,"language":520,"meta":331,"style":331},"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",[273,522,523,539,546,552,558,564,569,584],{"__ignoreMap":331},[524,525,527,531,535],"span",{"class":208,"line":526},1,[524,528,530],{"class":529},"snl16","def",[524,532,534],{"class":533},"svObZ"," classFactory",[524,536,538],{"class":537},"s95oV","(iface):\n",[524,540,542],{"class":208,"line":541},2,[524,543,545],{"class":544},"sU2Wk","    \"\"\"\n",[524,547,549],{"class":208,"line":548},3,[524,550,551],{"class":544},"    Factory function required by QGIS.\n",[524,553,555],{"class":208,"line":554},4,[524,556,557],{"class":544},"    :param iface: QgsInterface instance providing access to the QGIS API\n",[524,559,561],{"class":208,"line":560},5,[524,562,563],{"class":544},"    :return: Main plugin class instance\n",[524,565,567],{"class":208,"line":566},6,[524,568,545],{"class":544},[524,570,572,575,578,581],{"class":208,"line":571},7,[524,573,574],{"class":529},"    from",[524,576,577],{"class":537}," .main_plugin ",[524,579,580],{"class":529},"import",[524,582,583],{"class":537}," SpatialAnalyzerPlugin\n",[524,585,587,590],{"class":208,"line":586},8,[524,588,589],{"class":529},"    return",[524,591,592],{"class":537}," SpatialAnalyzerPlugin(iface)\n",[503,594,596,597,509],{"id":595},"_2-metadata-configuration-metadatatxt","2. Metadata Configuration (",[273,598,96],{},[15,600,601],{},"Use strict key-value formatting. Do not include blank lines between keys.",[324,603,607],{"className":604,"code":605,"language":606,"meta":331,"style":331},"language-ini shiki shiki-themes github-dark","[general]\nname=Spatial Analyzer\nqgisMinimumVersion=3.34\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",[273,608,609,614,619,624,629,634,639,644,649,655,661,667,673],{"__ignoreMap":331},[524,610,611],{"class":208,"line":526},[524,612,613],{},"[general]\n",[524,615,616],{"class":208,"line":541},[524,617,618],{},"name=Spatial Analyzer\n",[524,620,621],{"class":208,"line":548},[524,622,623],{},"qgisMinimumVersion=3.34\n",[524,625,626],{"class":208,"line":554},[524,627,628],{},"description=Provides advanced spatial analysis tools for vector layers.\n",[524,630,631],{"class":208,"line":560},[524,632,633],{},"version=1.0.0\n",[524,635,636],{"class":208,"line":566},[524,637,638],{},"author=Your Name\n",[524,640,641],{"class":208,"line":571},[524,642,643],{},"email=your.email@example.com\n",[524,645,646],{"class":208,"line":586},[524,647,648],{},"about=This plugin extends QGIS with custom geoprocessing workflows.\n",[524,650,652],{"class":208,"line":651},9,[524,653,654],{},"tracker=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\u002Fissues\n",[524,656,658],{"class":208,"line":657},10,[524,659,660],{},"repository=https:\u002F\u002Fgithub.com\u002Fyourname\u002Fspatial-analyzer\n",[524,662,664],{"class":208,"line":663},11,[524,665,666],{},"icon=icons\u002Ficon.png\n",[524,668,670],{"class":208,"line":669},12,[524,671,672],{},"experimental=False\n",[524,674,676],{"class":208,"line":675},13,[524,677,678],{},"deprecated=False\n",[15,680,681,682,684,685,688,689,43],{},"Pinning ",[273,683,275],{}," to ",[273,686,687],{},"3.34"," targets the current LTR. If you rely on an API added in a later feature release, raise this value — QGIS hides the plugin from users on older builds rather than letting it fail at runtime. The full set of keys the official repository validates is covered in ",[19,690,691],{"href":41},"publishing to the QGIS plugin repository",[503,693,695,696,509],{"id":694},"_3-main-plugin-class-main_pluginpy","3. Main Plugin Class (",[273,697,125],{},[15,699,700,701,703,704,706,707,709,710,712,713,715],{},"The lifecycle here is the heart of the boilerplate: QGIS instantiates the class, calls ",[273,702,464],{}," to register interface elements, invokes ",[273,705,471],{}," when the user triggers your action, and calls ",[273,708,467],{}," when the plugin is disabled. Every element you add in ",[273,711,464],{}," must be removed in ",[273,714,467],{}," — the two methods are mirror images, and treating them that way is what keeps the interface clean.",[58,717,720,723,726,729,736,741,745,750,753,755,758,762,765,768,771,774,777,783,787,793,796,800,805,811,815,820],{"viewBox":718,"role":61,"ariaLabel":719,"xmlns":63},"0 0 760 250","Lifecycle of a QGIS plugin class: __init__ builds state, initGui registers the interface, run executes on each user trigger, and unload symmetrically removes what initGui added",[65,721,722],{},"Plugin class lifecycle and the initGui \u002F unload mirror",[69,724,725],{},"QGIS instantiates the plugin class (__init__), then calls initGui() to add actions, toolbar and menu entries. run() executes each time the user triggers the action and repeats. When the plugin is disabled QGIS calls unload(), which removes every element initGui() added — the two methods are mirror images.",[73,727],{"x":75,"y":75,"width":76,"height":728,"fill":78},"250",[192,730,731],{},[195,732,734],{"id":733,"viewBox":198,"refX":199,"refY":200,"markerWidth":201,"markerHeight":201,"orient":202},"arrl",[204,735],{"d":206,"fill":101},[73,737],{"x":738,"y":739,"width":146,"height":740,"rx":84,"fill":85,"stroke":86,"style":87},"20","44","78",[89,742,744],{"x":743,"y":210,"style":93,"fill":94,"textAnchor":95},"100","__init__(iface)",[89,746,749],{"x":743,"y":747,"style":748,"fill":101,"textAnchor":95},"102","text-anchor:middle;font-size:10.5px;font-family:sans-serif","store iface, build state",[73,751],{"x":752,"y":739,"width":146,"height":740,"rx":84,"fill":85,"stroke":133,"style":87},"210",[89,754,464],{"x":171,"y":210,"style":93,"fill":94,"textAnchor":95},[89,756,757],{"x":171,"y":747,"style":748,"fill":101,"textAnchor":95},"add action \u002F toolbar \u002F menu",[73,759],{"x":760,"y":739,"width":761,"height":740,"rx":84,"fill":85,"stroke":121,"style":87},"400","150",[89,763,471],{"x":764,"y":210,"style":93,"fill":94,"textAnchor":95},"475",[89,766,767],{"x":764,"y":747,"style":748,"fill":101,"textAnchor":95},"user triggers work",[73,769],{"x":770,"y":739,"width":146,"height":740,"rx":84,"fill":85,"stroke":107,"style":87},"580",[89,772,467],{"x":773,"y":210,"style":93,"fill":94,"textAnchor":95},"660",[89,775,776],{"x":773,"y":747,"style":748,"fill":101,"textAnchor":95},"remove action \u002F toolbar",[208,778],{"x1":779,"y1":780,"x2":781,"y2":780,"stroke":101,"style":782},"180","83","206","stroke-width:2.5;marker-end:url(#arrl)",[208,784],{"x1":785,"y1":780,"x2":786,"y2":780,"stroke":101,"style":782},"370","396",[89,788,792],{"x":789,"y":790,"style":791,"fill":101,"textAnchor":95},"383","74","text-anchor:middle;font-size:9.5px;font-family:sans-serif","run",[208,794],{"x1":795,"y1":780,"x2":145,"y2":780,"stroke":101,"style":782},"550",[89,797,799],{"x":798,"y":790,"style":791,"fill":101,"textAnchor":95},"563","off",[204,801],{"d":802,"fill":803,"stroke":121,"style":804},"M455,44 C455,16 495,16 495,42","none","stroke-width:2;marker-end:url(#arrl)",[89,806,810],{"x":807,"y":808,"style":809,"fill":101,"textAnchor":217},"508","26","text-anchor:start;font-size:9.5px;font-family:sans-serif","repeats each call",[204,812],{"d":813,"fill":803,"stroke":101,"style":814},"M290,122 C290,196 660,196 660,124","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#arrl)",[89,816,819],{"x":764,"y":817,"style":818,"fill":94,"textAnchor":95},"216","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","mirror images",[89,821,823],{"x":764,"y":822,"style":167,"fill":101,"textAnchor":95},"234","every element added in initGui() has a matching removal in unload()",[15,825,826],{},"The following pattern demonstrates safe GUI registration, robust path resolution, and clean teardown.",[324,828,830],{"className":518,"code":829,"language":520,"meta":331,"style":331},"import os\nfrom qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication\nfrom qgis.PyQt.QtGui import QIcon\nfrom qgis.PyQt.QtWidgets import QAction\nfrom qgis.core import Qgis\n\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(\n        self,\n        icon_path,\n        text,\n        callback,\n        enabled_flag=True,\n        add_to_menu=True,\n        add_to_toolbar=True,\n        status_tip=None,\n        whats_this=None,\n        parent=None,\n    ):\n        icon = QIcon(icon_path)\n        action = QAction(icon, text, parent)\n        action.triggered.connect(callback)\n        action.setEnabled(enabled_flag)\n\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\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\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\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\",\n                \"Please select a vector layer first.\",\n                level=Qgis.Warning,\n                duration=3,\n            )\n            return\n\n        # Core processing logic goes here\n        self.iface.messageBar().pushMessage(\n            \"Success\",\n            f\"Analysis initialized for {layer.name()}\",\n            level=Qgis.Success,\n            duration=3,\n        )\n",[273,831,832,839,852,864,876,888,894,898,909,921,935,953,965,985,1005,1017,1022,1029,1061,1103,1112,1126,1134,1145,1150,1161,1176,1181,1192,1198,1204,1210,1216,1230,1242,1254,1267,1279,1291,1297,1308,1319,1325,1331,1336,1344,1350,1358,1364,1372,1380,1388,1401,1406,1414,1422,1427,1438,1444,1468,1476,1482,1501,1514,1527,1533,1538,1548,1554,1571,1583,1591,1596,1602,1615,1623,1628,1638,1644,1657,1668,1676,1684,1692,1703,1716,1722,1728,1733,1739,1746,1754,1775,1786,1798],{"__ignoreMap":331},[524,833,834,836],{"class":208,"line":526},[524,835,580],{"class":529},[524,837,838],{"class":537}," os\n",[524,840,841,844,847,849],{"class":208,"line":541},[524,842,843],{"class":529},"from",[524,845,846],{"class":537}," qgis.PyQt.QtCore ",[524,848,580],{"class":529},[524,850,851],{"class":537}," QSettings, QTranslator, QCoreApplication\n",[524,853,854,856,859,861],{"class":208,"line":548},[524,855,843],{"class":529},[524,857,858],{"class":537}," qgis.PyQt.QtGui ",[524,860,580],{"class":529},[524,862,863],{"class":537}," QIcon\n",[524,865,866,868,871,873],{"class":208,"line":554},[524,867,843],{"class":529},[524,869,870],{"class":537}," qgis.PyQt.QtWidgets ",[524,872,580],{"class":529},[524,874,875],{"class":537}," QAction\n",[524,877,878,880,883,885],{"class":208,"line":560},[524,879,843],{"class":529},[524,881,882],{"class":537}," qgis.core ",[524,884,580],{"class":529},[524,886,887],{"class":537}," Qgis\n",[524,889,890],{"class":208,"line":566},[524,891,893],{"emptyLinePlaceholder":892},true,"\n",[524,895,896],{"class":208,"line":571},[524,897,893],{"emptyLinePlaceholder":892},[524,899,900,903,906],{"class":208,"line":586},[524,901,902],{"class":529},"class",[524,904,905],{"class":533}," SpatialAnalyzerPlugin",[524,907,908],{"class":537},":\n",[524,910,911,914,918],{"class":208,"line":651},[524,912,913],{"class":529},"    def",[524,915,917],{"class":916},"sDLfK"," __init__",[524,919,920],{"class":537},"(self, iface):\n",[524,922,923,926,929,932],{"class":208,"line":657},[524,924,925],{"class":916},"        self",[524,927,928],{"class":537},".iface ",[524,930,931],{"class":529},"=",[524,933,934],{"class":537}," iface\n",[524,936,937,939,942,944,947,950],{"class":208,"line":663},[524,938,925],{"class":916},[524,940,941],{"class":537},".plugin_dir ",[524,943,931],{"class":529},[524,945,946],{"class":537}," os.path.dirname(",[524,948,949],{"class":916},"__file__",[524,951,952],{"class":537},")\n",[524,954,955,957,960,962],{"class":208,"line":669},[524,956,925],{"class":916},[524,958,959],{"class":537},".actions ",[524,961,931],{"class":529},[524,963,964],{"class":537}," []\n",[524,966,967,969,972,974,977,980,983],{"class":208,"line":675},[524,968,925],{"class":916},[524,970,971],{"class":537},".menu ",[524,973,931],{"class":529},[524,975,976],{"class":916}," self",[524,978,979],{"class":537},".tr(",[524,981,982],{"class":544},"\"&Spatial Analyzer\"",[524,984,952],{"class":537},[524,986,988,990,993,995,997,1000,1003],{"class":208,"line":987},14,[524,989,925],{"class":916},[524,991,992],{"class":537},".toolbar ",[524,994,931],{"class":529},[524,996,976],{"class":916},[524,998,999],{"class":537},".iface.addToolBar(",[524,1001,1002],{"class":544},"\"SpatialAnalyzer\"",[524,1004,952],{"class":537},[524,1006,1008,1010,1013,1015],{"class":208,"line":1007},15,[524,1009,925],{"class":916},[524,1011,1012],{"class":537},".toolbar.setObjectName(",[524,1014,1002],{"class":544},[524,1016,952],{"class":537},[524,1018,1020],{"class":208,"line":1019},16,[524,1021,893],{"emptyLinePlaceholder":892},[524,1023,1025],{"class":208,"line":1024},17,[524,1026,1028],{"class":1027},"sjoCn","        # Initialize translation with safe fallback\n",[524,1030,1032,1035,1037,1040,1043,1045,1048,1051,1053,1055,1058],{"class":208,"line":1031},18,[524,1033,1034],{"class":537},"        locale ",[524,1036,931],{"class":529},[524,1038,1039],{"class":537}," QSettings().value(",[524,1041,1042],{"class":544},"\"locale\u002FuserLocale\"",[524,1044,429],{"class":537},[524,1046,1047],{"class":544},"\"en_US\"",[524,1049,1050],{"class":537},")[",[524,1052,75],{"class":916},[524,1054,424],{"class":537},[524,1056,1057],{"class":916},"2",[524,1059,1060],{"class":537},"]\n",[524,1062,1064,1067,1069,1072,1075,1078,1081,1083,1086,1089,1092,1095,1098,1101],{"class":208,"line":1063},19,[524,1065,1066],{"class":537},"        locale_path ",[524,1068,931],{"class":529},[524,1070,1071],{"class":537}," os.path.join(",[524,1073,1074],{"class":916},"self",[524,1076,1077],{"class":537},".plugin_dir, ",[524,1079,1080],{"class":544},"\"i18n\"",[524,1082,429],{"class":537},[524,1084,1085],{"class":529},"f",[524,1087,1088],{"class":544},"\"spatial_analyzer_",[524,1090,1091],{"class":916},"{",[524,1093,1094],{"class":537},"locale",[524,1096,1097],{"class":916},"}",[524,1099,1100],{"class":544},".qm\"",[524,1102,952],{"class":537},[524,1104,1106,1109],{"class":208,"line":1105},20,[524,1107,1108],{"class":529},"        if",[524,1110,1111],{"class":537}," os.path.exists(locale_path):\n",[524,1113,1115,1118,1121,1123],{"class":208,"line":1114},21,[524,1116,1117],{"class":916},"            self",[524,1119,1120],{"class":537},".translator ",[524,1122,931],{"class":529},[524,1124,1125],{"class":537}," QTranslator()\n",[524,1127,1129,1131],{"class":208,"line":1128},22,[524,1130,1117],{"class":916},[524,1132,1133],{"class":537},".translator.load(locale_path)\n",[524,1135,1137,1140,1142],{"class":208,"line":1136},23,[524,1138,1139],{"class":537},"            QCoreApplication.installTranslator(",[524,1141,1074],{"class":916},[524,1143,1144],{"class":537},".translator)\n",[524,1146,1148],{"class":208,"line":1147},24,[524,1149,893],{"emptyLinePlaceholder":892},[524,1151,1153,1155,1158],{"class":208,"line":1152},25,[524,1154,913],{"class":529},[524,1156,1157],{"class":533}," tr",[524,1159,1160],{"class":537},"(self, message):\n",[524,1162,1164,1167,1170,1173],{"class":208,"line":1163},26,[524,1165,1166],{"class":529},"        return",[524,1168,1169],{"class":537}," QCoreApplication.translate(",[524,1171,1172],{"class":544},"\"SpatialAnalyzerPlugin\"",[524,1174,1175],{"class":537},", message)\n",[524,1177,1179],{"class":208,"line":1178},27,[524,1180,893],{"emptyLinePlaceholder":892},[524,1182,1184,1186,1189],{"class":208,"line":1183},28,[524,1185,913],{"class":529},[524,1187,1188],{"class":533}," add_action",[524,1190,1191],{"class":537},"(\n",[524,1193,1195],{"class":208,"line":1194},29,[524,1196,1197],{"class":537},"        self,\n",[524,1199,1201],{"class":208,"line":1200},30,[524,1202,1203],{"class":537},"        icon_path,\n",[524,1205,1207],{"class":208,"line":1206},31,[524,1208,1209],{"class":537},"        text,\n",[524,1211,1213],{"class":208,"line":1212},32,[524,1214,1215],{"class":537},"        callback,\n",[524,1217,1219,1222,1224,1227],{"class":208,"line":1218},33,[524,1220,1221],{"class":537},"        enabled_flag",[524,1223,931],{"class":529},[524,1225,1226],{"class":916},"True",[524,1228,1229],{"class":537},",\n",[524,1231,1233,1236,1238,1240],{"class":208,"line":1232},34,[524,1234,1235],{"class":537},"        add_to_menu",[524,1237,931],{"class":529},[524,1239,1226],{"class":916},[524,1241,1229],{"class":537},[524,1243,1245,1248,1250,1252],{"class":208,"line":1244},35,[524,1246,1247],{"class":537},"        add_to_toolbar",[524,1249,931],{"class":529},[524,1251,1226],{"class":916},[524,1253,1229],{"class":537},[524,1255,1257,1260,1262,1265],{"class":208,"line":1256},36,[524,1258,1259],{"class":537},"        status_tip",[524,1261,931],{"class":529},[524,1263,1264],{"class":916},"None",[524,1266,1229],{"class":537},[524,1268,1270,1273,1275,1277],{"class":208,"line":1269},37,[524,1271,1272],{"class":537},"        whats_this",[524,1274,931],{"class":529},[524,1276,1264],{"class":916},[524,1278,1229],{"class":537},[524,1280,1282,1285,1287,1289],{"class":208,"line":1281},38,[524,1283,1284],{"class":537},"        parent",[524,1286,931],{"class":529},[524,1288,1264],{"class":916},[524,1290,1229],{"class":537},[524,1292,1294],{"class":208,"line":1293},39,[524,1295,1296],{"class":537},"    ):\n",[524,1298,1300,1303,1305],{"class":208,"line":1299},40,[524,1301,1302],{"class":537},"        icon ",[524,1304,931],{"class":529},[524,1306,1307],{"class":537}," QIcon(icon_path)\n",[524,1309,1311,1314,1316],{"class":208,"line":1310},41,[524,1312,1313],{"class":537},"        action ",[524,1315,931],{"class":529},[524,1317,1318],{"class":537}," QAction(icon, text, parent)\n",[524,1320,1322],{"class":208,"line":1321},42,[524,1323,1324],{"class":537},"        action.triggered.connect(callback)\n",[524,1326,1328],{"class":208,"line":1327},43,[524,1329,1330],{"class":537},"        action.setEnabled(enabled_flag)\n",[524,1332,1334],{"class":208,"line":1333},44,[524,1335,893],{"emptyLinePlaceholder":892},[524,1337,1339,1341],{"class":208,"line":1338},45,[524,1340,1108],{"class":529},[524,1342,1343],{"class":537}," status_tip:\n",[524,1345,1347],{"class":208,"line":1346},46,[524,1348,1349],{"class":537},"            action.setStatusTip(status_tip)\n",[524,1351,1353,1355],{"class":208,"line":1352},47,[524,1354,1108],{"class":529},[524,1356,1357],{"class":537}," whats_this:\n",[524,1359,1361],{"class":208,"line":1360},48,[524,1362,1363],{"class":537},"            action.setWhatsThis(whats_this)\n",[524,1365,1367,1369],{"class":208,"line":1366},49,[524,1368,1108],{"class":529},[524,1370,1371],{"class":537}," add_to_toolbar:\n",[524,1373,1375,1377],{"class":208,"line":1374},50,[524,1376,1117],{"class":916},[524,1378,1379],{"class":537},".toolbar.addAction(action)\n",[524,1381,1383,1385],{"class":208,"line":1382},51,[524,1384,1108],{"class":529},[524,1386,1387],{"class":537}," add_to_menu:\n",[524,1389,1391,1393,1396,1398],{"class":208,"line":1390},52,[524,1392,1117],{"class":916},[524,1394,1395],{"class":537},".iface.addPluginToMenu(",[524,1397,1074],{"class":916},[524,1399,1400],{"class":537},".menu, action)\n",[524,1402,1404],{"class":208,"line":1403},53,[524,1405,893],{"emptyLinePlaceholder":892},[524,1407,1409,1411],{"class":208,"line":1408},54,[524,1410,925],{"class":916},[524,1412,1413],{"class":537},".actions.append(action)\n",[524,1415,1417,1419],{"class":208,"line":1416},55,[524,1418,1166],{"class":529},[524,1420,1421],{"class":537}," action\n",[524,1423,1425],{"class":208,"line":1424},56,[524,1426,893],{"emptyLinePlaceholder":892},[524,1428,1430,1432,1435],{"class":208,"line":1429},57,[524,1431,913],{"class":529},[524,1433,1434],{"class":533}," initGui",[524,1436,1437],{"class":537},"(self):\n",[524,1439,1441],{"class":208,"line":1440},58,[524,1442,1443],{"class":544},"        \"\"\"Initialize GUI elements when plugin loads.\"\"\"\n",[524,1445,1447,1450,1452,1454,1456,1458,1461,1463,1466],{"class":208,"line":1446},59,[524,1448,1449],{"class":537},"        icon_path ",[524,1451,931],{"class":529},[524,1453,1071],{"class":537},[524,1455,1074],{"class":916},[524,1457,1077],{"class":537},[524,1459,1460],{"class":544},"\"icons\"",[524,1462,429],{"class":537},[524,1464,1465],{"class":544},"\"icon.png\"",[524,1467,952],{"class":537},[524,1469,1471,1473],{"class":208,"line":1470},60,[524,1472,925],{"class":916},[524,1474,1475],{"class":537},".add_action(\n",[524,1477,1479],{"class":208,"line":1478},61,[524,1480,1481],{"class":537},"            icon_path,\n",[524,1483,1485,1489,1491,1493,1495,1498],{"class":208,"line":1484},62,[524,1486,1488],{"class":1487},"s9osk","            text",[524,1490,931],{"class":529},[524,1492,1074],{"class":916},[524,1494,979],{"class":537},[524,1496,1497],{"class":544},"\"Run Analysis\"",[524,1499,1500],{"class":537},"),\n",[524,1502,1504,1507,1509,1511],{"class":208,"line":1503},63,[524,1505,1506],{"class":1487},"            callback",[524,1508,931],{"class":529},[524,1510,1074],{"class":916},[524,1512,1513],{"class":537},".run,\n",[524,1515,1517,1520,1522,1524],{"class":208,"line":1516},64,[524,1518,1519],{"class":1487},"            parent",[524,1521,931],{"class":529},[524,1523,1074],{"class":916},[524,1525,1526],{"class":537},".iface.mainWindow(),\n",[524,1528,1530],{"class":208,"line":1529},65,[524,1531,1532],{"class":537},"        )\n",[524,1534,1536],{"class":208,"line":1535},66,[524,1537,893],{"emptyLinePlaceholder":892},[524,1539,1541,1543,1546],{"class":208,"line":1540},67,[524,1542,913],{"class":529},[524,1544,1545],{"class":533}," unload",[524,1547,1437],{"class":537},[524,1549,1551],{"class":208,"line":1550},68,[524,1552,1553],{"class":544},"        \"\"\"Remove GUI elements and clean up resources.\"\"\"\n",[524,1555,1557,1560,1563,1566,1568],{"class":208,"line":1556},69,[524,1558,1559],{"class":529},"        for",[524,1561,1562],{"class":537}," action ",[524,1564,1565],{"class":529},"in",[524,1567,976],{"class":916},[524,1569,1570],{"class":537},".actions:\n",[524,1572,1574,1576,1579,1581],{"class":208,"line":1573},70,[524,1575,1117],{"class":916},[524,1577,1578],{"class":537},".iface.removePluginMenu(",[524,1580,1074],{"class":916},[524,1582,1400],{"class":537},[524,1584,1586,1588],{"class":208,"line":1585},71,[524,1587,1117],{"class":916},[524,1589,1590],{"class":537},".iface.removeToolBarIcon(action)\n",[524,1592,1594],{"class":208,"line":1593},72,[524,1595,893],{"emptyLinePlaceholder":892},[524,1597,1599],{"class":208,"line":1598},73,[524,1600,1601],{"class":1027},"        # Safely remove the custom toolbar\n",[524,1603,1605,1607,1610,1612],{"class":208,"line":1604},74,[524,1606,925],{"class":916},[524,1608,1609],{"class":537},".iface.mainWindow().removeToolBar(",[524,1611,1074],{"class":916},[524,1613,1614],{"class":537},".toolbar)\n",[524,1616,1618,1620],{"class":208,"line":1617},75,[524,1619,925],{"class":916},[524,1621,1622],{"class":537},".actions.clear()\n",[524,1624,1626],{"class":208,"line":1625},76,[524,1627,893],{"emptyLinePlaceholder":892},[524,1629,1631,1633,1636],{"class":208,"line":1630},77,[524,1632,913],{"class":529},[524,1634,1635],{"class":533}," run",[524,1637,1437],{"class":537},[524,1639,1641],{"class":208,"line":1640},78,[524,1642,1643],{"class":544},"        \"\"\"Execute plugin logic.\"\"\"\n",[524,1645,1647,1650,1652,1654],{"class":208,"line":1646},79,[524,1648,1649],{"class":537},"        layer ",[524,1651,931],{"class":529},[524,1653,976],{"class":916},[524,1655,1656],{"class":537},".iface.activeLayer()\n",[524,1658,1660,1662,1665],{"class":208,"line":1659},80,[524,1661,1108],{"class":529},[524,1663,1664],{"class":529}," not",[524,1666,1667],{"class":537}," layer:\n",[524,1669,1671,1673],{"class":208,"line":1670},81,[524,1672,1117],{"class":916},[524,1674,1675],{"class":537},".iface.messageBar().pushMessage(\n",[524,1677,1679,1682],{"class":208,"line":1678},82,[524,1680,1681],{"class":544},"                \"Warning\"",[524,1683,1229],{"class":537},[524,1685,1687,1690],{"class":208,"line":1686},83,[524,1688,1689],{"class":544},"                \"Please select a vector layer first.\"",[524,1691,1229],{"class":537},[524,1693,1695,1698,1700],{"class":208,"line":1694},84,[524,1696,1697],{"class":1487},"                level",[524,1699,931],{"class":529},[524,1701,1702],{"class":537},"Qgis.Warning,\n",[524,1704,1706,1709,1711,1714],{"class":208,"line":1705},85,[524,1707,1708],{"class":1487},"                duration",[524,1710,931],{"class":529},[524,1712,1713],{"class":916},"3",[524,1715,1229],{"class":537},[524,1717,1719],{"class":208,"line":1718},86,[524,1720,1721],{"class":537},"            )\n",[524,1723,1725],{"class":208,"line":1724},87,[524,1726,1727],{"class":529},"            return\n",[524,1729,1731],{"class":208,"line":1730},88,[524,1732,893],{"emptyLinePlaceholder":892},[524,1734,1736],{"class":208,"line":1735},89,[524,1737,1738],{"class":1027},"        # Core processing logic goes here\n",[524,1740,1742,1744],{"class":208,"line":1741},90,[524,1743,925],{"class":916},[524,1745,1675],{"class":537},[524,1747,1749,1752],{"class":208,"line":1748},91,[524,1750,1751],{"class":544},"            \"Success\"",[524,1753,1229],{"class":537},[524,1755,1757,1760,1763,1765,1768,1770,1773],{"class":208,"line":1756},92,[524,1758,1759],{"class":529},"            f",[524,1761,1762],{"class":544},"\"Analysis initialized for ",[524,1764,1091],{"class":916},[524,1766,1767],{"class":537},"layer.name()",[524,1769,1097],{"class":916},[524,1771,1772],{"class":544},"\"",[524,1774,1229],{"class":537},[524,1776,1778,1781,1783],{"class":208,"line":1777},93,[524,1779,1780],{"class":1487},"            level",[524,1782,931],{"class":529},[524,1784,1785],{"class":537},"Qgis.Success,\n",[524,1787,1789,1792,1794,1796],{"class":208,"line":1788},94,[524,1790,1791],{"class":1487},"            duration",[524,1793,931],{"class":529},[524,1795,1713],{"class":916},[524,1797,1229],{"class":537},[524,1799,1801],{"class":208,"line":1800},95,[524,1802,1532],{"class":537},[15,1804,1805,1806,1809,1810,1813,1814,1816,1817,1820,1821,1824,1825,43],{},"The ",[273,1807,1808],{},"add_action()"," helper is deliberately generic so that every button, menu entry, and shortcut passes through one registration path that also appends to ",[273,1811,1812],{},"self.actions",". That single list is what makes ",[273,1815,467],{}," reliable. For the full breakdown of registering a single ",[273,1818,1819],{},"QAction"," — including mirroring it into a plugin menu and wiring the ",[273,1822,1823],{},"triggered"," signal — follow ",[19,1826,1827],{"href":54},"Add a Toolbar Button to a QGIS Plugin",[503,1829,1831],{"id":1830},"_4-wiring-in-a-dialog","4. Wiring in a Dialog",[15,1833,1834,1835,1837,1838,1841,1842,424],{},"Most plugins do more than fire a message bar notification: they open a dialog built in Qt Designer. Keep the layout in a ",[273,1836,371],{}," file and load it at runtime rather than compiling it, so a ",[273,1839,1840],{},"pyuic5"," version mismatch never breaks an end-user install. Instantiate the dialog inside ",[273,1843,471],{},[324,1845,1847],{"className":518,"code":1846,"language":520,"meta":331,"style":331},"def run(self):\n    from .ui.main_dialog import SpatialAnalyzerDialog\n    dlg = SpatialAnalyzerDialog(self.iface.mainWindow())\n    if dlg.exec_():\n        # read validated inputs from dlg, then dispatch the work\n        ...\n",[273,1848,1849,1857,1869,1884,1892,1897],{"__ignoreMap":331},[524,1850,1851,1853,1855],{"class":208,"line":526},[524,1852,530],{"class":529},[524,1854,1635],{"class":533},[524,1856,1437],{"class":537},[524,1858,1859,1861,1864,1866],{"class":208,"line":541},[524,1860,574],{"class":529},[524,1862,1863],{"class":537}," .ui.main_dialog ",[524,1865,580],{"class":529},[524,1867,1868],{"class":537}," SpatialAnalyzerDialog\n",[524,1870,1871,1874,1876,1879,1881],{"class":208,"line":548},[524,1872,1873],{"class":537},"    dlg ",[524,1875,931],{"class":529},[524,1877,1878],{"class":537}," SpatialAnalyzerDialog(",[524,1880,1074],{"class":916},[524,1882,1883],{"class":537},".iface.mainWindow())\n",[524,1885,1886,1889],{"class":208,"line":554},[524,1887,1888],{"class":529},"    if",[524,1890,1891],{"class":537}," dlg.exec_():\n",[524,1893,1894],{"class":208,"line":560},[524,1895,1896],{"class":1027},"        # read validated inputs from dlg, then dispatch the work\n",[524,1898,1899],{"class":208,"line":566},[524,1900,1901],{"class":916},"        ...\n",[15,1903,1904,1905,429,1908,1911,1912,1914,1915,1918,1919,1922],{},"The dialog class itself — ",[273,1906,1907],{},"uic.loadUiType()",[273,1909,1910],{},"setupUi()",", promoted QGIS widgets, and signal connections — is covered end to end in ",[19,1913,27],{"href":26},". Keep the heavy geoprocessing off the constructor and out of the main thread: long-running work belongs in a ",[273,1916,1917],{},"QgsTask"," or, better, in a reusable algorithm registered through a ",[19,1920,1921],{"href":31},"processing provider plugin",", which gives you batch execution and Model Builder support for free.",[503,1924,1926],{"id":1925},"_5-extending-the-boilerplate","5. Extending the Boilerplate",[15,1928,1929,1930,1933,1934,1936,1937,1939,1940,1942,1943,1945,1946,1948,1949,1951],{},"Once the base structure is stable, you can branch into specialized implementations. If your plugin requires field calculator integration, adapt the initialization sequence to support custom expression functions, which requires explicit registration with ",[273,1931,1932],{},"QgsExpression.registerFunction()"," during ",[273,1935,464],{}," and deregistration in ",[273,1938,467],{}," — the same symmetric setup\u002Fteardown discipline the rest of the class already follows. As soon as the module has real logic, add coverage for it; the headless test harness in ",[19,1941,37],{"href":36}," mocks ",[273,1944,460],{}," so ",[273,1947,464],{},"\u002F",[273,1950,467],{}," can be exercised without a running desktop.",[254,1953,1955],{"id":1954},"common-errors-resolution-strategies","Common Errors & Resolution Strategies",[503,1957,1959],{"id":1958},"_1-plugin-not-appearing-in-manager","1. Plugin Not Appearing in Manager",[15,1961,1962,1965,1966,1969,1970,1972,1973,1975,1976,1979,1980,1982,1983,1985,1986,1989,1990,1993],{},[268,1963,1964],{},"Symptom:"," The directory exists but QGIS ignores it.\n",[268,1967,1968],{},"Cause:"," Missing ",[273,1971,96],{},", malformed keys, or ",[273,1974,275],{}," higher than the installed QGIS version.\n",[268,1977,1978],{},"Fix:"," Verify ",[273,1981,96],{}," syntax. Ensure ",[273,1984,275],{}," matches or is lower than your QGIS build. Check ",[273,1987,1988],{},"View > Log Messages > Python Errors"," for ",[273,1991,1992],{},"PluginManager"," warnings.",[503,1995,1997,1998,2001],{"id":1996},"_2-modulenotfounderror-or-import-failures","2. ",[273,1999,2000],{},"ModuleNotFoundError"," or Import Failures",[15,2003,2004,304,2006,2009,2011,2012,2014,2015,2017,2018,2021,2022,2024,2025,2028],{},[268,2005,1964],{},[273,2007,2008],{},"ImportError: cannot import name 'main_plugin'",[268,2010,1968],{}," Relative import issues or missing ",[273,2013,112],{}," in subdirectories.\n",[268,2016,1978],{}," Always use relative imports (",[273,2019,2020],{},"from .main_plugin import ...","). Ensure every Python-containing directory has an empty ",[273,2023,112],{},". Avoid absolute paths; QGIS modifies ",[273,2026,2027],{},"sys.path"," dynamically during plugin loading.",[503,2030,2032],{"id":2031},"_3-ui-dialog-fails-to-load","3. UI Dialog Fails to Load",[15,2034,2035,304,2037,2040,2041,2043,2044,2046,2047,2050,2051,2053,2054,2057,2058,2060],{},[268,2036,1964],{},[273,2038,2039],{},"QFile::open: No such file or directory"," when loading ",[273,2042,371],{}," files.\n",[268,2045,1968],{}," Incorrect working directory assumption. ",[273,2048,2049],{},"QgsApplication"," does not guarantee the current working directory matches the plugin folder.\n",[268,2052,1978],{}," Resolve paths using ",[273,2055,2056],{},"os.path.dirname(__file__)"," before passing them to ",[273,2059,1907],{},". Example:",[324,2062,2064],{"className":518,"code":2063,"language":520,"meta":331,"style":331},"ui_path = os.path.join(os.path.dirname(__file__), \"ui\", \"main_dialog.ui\")\n",[273,2065,2066],{"__ignoreMap":331},[524,2067,2068,2071,2073,2076,2078,2081,2084,2086,2089],{"class":208,"line":526},[524,2069,2070],{"class":537},"ui_path ",[524,2072,931],{"class":529},[524,2074,2075],{"class":537}," os.path.join(os.path.dirname(",[524,2077,949],{"class":916},[524,2079,2080],{"class":537},"), ",[524,2082,2083],{"class":544},"\"ui\"",[524,2085,429],{"class":537},[524,2087,2088],{"class":544},"\"main_dialog.ui\"",[524,2090,952],{"class":537},[503,2092,2094],{"id":2093},"_4-toolbarmenu-items-persist-after-unload","4. Toolbar\u002FMenu Items Persist After Unload",[15,2096,2097,2099,2100,304,2102,2104,2105,2107,2108,2110,2111,2113,2114,2116,2117,2120,2121,2124],{},[268,2098,1964],{}," Buttons remain visible after disabling the plugin.\n",[268,2101,1968],{},[273,2103,467],{}," does not explicitly remove registered actions.\n",[268,2106,1978],{}," Store all ",[273,2109,1819],{}," instances in a list during ",[273,2112,464],{}," and iterate through them in ",[273,2115,467],{},", calling both ",[273,2118,2119],{},"removePluginMenu()"," and ",[273,2122,2123],{},"removeToolBarIcon()",". Never rely on QGIS to garbage-collect UI references automatically.",[503,2126,2128],{"id":2127},"_5-resource-compilation-errors","5. Resource Compilation Errors",[15,2130,2131,304,2133,2135,2136,2138,2139,2141,2142,2145,2146,2148,2149,2152,2153,2156,2157,2160,2161,2163],{},[268,2132,1964],{},[273,2134,480],{}," fails or icons appear as broken placeholders.\n",[268,2137,1968],{}," Invalid paths in ",[273,2140,150],{}," or missing ",[273,2143,2144],{},"qrc"," file in the plugin directory.\n",[268,2147,1978],{}," Run compilation from the plugin root: ",[273,2150,2151],{},"pyrcc5 resources.qrc -o resources_rc.py",". Verify all ",[273,2154,2155],{},"\u003Cfile>"," tags in the ",[273,2158,2159],{},".qrc"," use paths relative to the ",[273,2162,2159],{}," location.",[254,2165,2167],{"id":2166},"workflow-validation-checklist","Workflow Validation Checklist",[15,2169,2170],{},"Before considering the boilerplate complete, verify the following:",[262,2172,2175,2186,2197,2208,2220,2226,2232],{"className":2173},[2174],"contains-task-list",[265,2176,2179,304,2183,2185],{"className":2177},[2178],"task-list-item",[2180,2181],"input",{"disabled":892,"type":2182},"checkbox",[273,2184,96],{}," contains all required fields and valid version strings",[265,2187,2189,304,2191,2193,2194,2196],{"className":2188},[2178],[2180,2190],{"disabled":892,"type":2182},[273,2192,112],{}," exposes exactly one ",[273,2195,346],{}," function",[265,2198,2200,304,2202,2204,2205,2207],{"className":2199},[2178],[2180,2201],{"disabled":892,"type":2182},[273,2203,464],{}," registers actions and ",[273,2206,467],{}," removes them symmetrically",[265,2209,2211,2213,2214,2216,2217],{"className":2210},[2178],[2180,2212],{"disabled":892,"type":2182}," All file paths resolve using ",[273,2215,949],{}," rather than ",[273,2218,2219],{},"os.getcwd()",[265,2221,2223,2225],{"className":2222},[2178],[2180,2224],{"disabled":892,"type":2182}," Plugin loads without traceback in a fresh QGIS profile",[265,2227,2229,2231],{"className":2228},[2178],[2180,2230],{"disabled":892,"type":2182}," Toolbar and menu items disappear completely after disabling",[265,2233,2235,2237],{"className":2234},[2178],[2180,2236],{"disabled":892,"type":2182}," Translation files load gracefully when missing",[254,2239,2241],{"id":2240},"the-load-sequence-and-what-fails-where","The load sequence, and what fails where",[15,2243,2244],{},"A plugin passes through four stages before it is usable, and each stage has its own failure symptom. Recognising which stage failed narrows the cause immediately.",[15,2246,2247],{},[58,2248,2250,2253,2256,2258,2266,2271,2320,2325,2329,2333,2384,2387,2389,2391],{"viewBox":718,"role":61,"ariaLabel":2249,"xmlns":63},"The four stages of loading a plugin — discovery, import, classFactory and initGui — with the symptom of a failure at each",[65,2251,2252],{},"Four load stages and their failure symptoms",[69,2254,2255],{},"QGIS first discovers the plugin by finding metadata.txt in a folder under the profile plugins directory; a failure here means the plugin never appears in the manager. It then imports the package; a failure shows an error in the manager and the plugin cannot be enabled. It then calls classFactory; a failure shows a traceback on enabling. Finally it calls initGui; a failure means the plugin is loaded but has no toolbar entry.",[73,2257],{"x":75,"y":75,"width":76,"height":728,"fill":78},[192,2259,2260],{},[195,2261,2263],{"id":2262,"viewBox":198,"refX":84,"refY":200,"markerWidth":201,"markerHeight":201,"orient":202},"loadSeqArrow",[204,2264],{"d":2265,"fill":107},"M0 0 L10 5 L0 10 z",[89,2267,2270],{"x":2268,"y":808,"style":2269,"fill":94,"textAnchor":95},"380","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","The symptom tells you which stage failed",[2272,2273,2274,2283,2287,2291,2293,2297,2300,2303,2307,2310,2313,2317],"g",{},[73,2275],{"x":2276,"y":2277,"width":2278,"height":2279,"rx":2280,"fill":85,"stroke":2281,"style":2282},"12","52","176","60","10","#59645f","stroke-width:2",[89,2284,2286],{"x":743,"y":2285,"style":818,"fill":94,"textAnchor":95},"76","1 · discovery",[89,2288,2290],{"x":743,"y":2289,"style":167,"fill":101,"textAnchor":95},"96","metadata.txt found",[73,2292],{"x":157,"y":2277,"width":2278,"height":2279,"rx":2280,"fill":85,"stroke":2281,"style":2282},[89,2294,2296],{"x":2295,"y":2285,"style":818,"fill":94,"textAnchor":95},"288","2 · import",[89,2298,2299],{"x":2295,"y":2289,"style":167,"fill":101,"textAnchor":95},"the package imports",[73,2301],{"x":2302,"y":2277,"width":2278,"height":2279,"rx":2280,"fill":85,"stroke":2281,"style":2282},"388",[89,2304,2306],{"x":2305,"y":2285,"style":818,"fill":94,"textAnchor":95},"476","3 · classFactory",[89,2308,2309],{"x":2305,"y":2289,"style":167,"fill":101,"textAnchor":95},"returns an instance",[73,2311],{"x":145,"y":2277,"width":2312,"height":2279,"rx":2280,"fill":106,"stroke":107,"style":87},"172",[89,2314,2316],{"x":2315,"y":2285,"style":818,"fill":111,"textAnchor":95},"662","4 · initGui",[89,2318,2319],{"x":2315,"y":2289,"style":167,"fill":111,"textAnchor":95},"actions appear",[208,2321],{"x1":2322,"y1":2323,"x2":236,"y2":2323,"stroke":107,"style":2324},"188","82","stroke-width:2.5;marker-end:url(#loadSeqArrow)",[208,2326],{"x1":2327,"y1":2323,"x2":2328,"y2":2323,"stroke":107,"style":2324},"376","384",[208,2330],{"x1":2331,"y1":2323,"x2":2332,"y2":2323,"stroke":107,"style":2324},"564","572",[2272,2334,2335,2340,2344,2348,2351,2353,2356,2359,2362,2364,2367,2370,2373,2375,2378,2381],{},[73,2336],{"x":2276,"y":2337,"width":2278,"height":2338,"rx":84,"fill":85,"stroke":2339,"style":2282},"144","86","#b91c1c",[89,2341,2343],{"x":743,"y":231,"style":2342,"fill":2339,"textAnchor":95},"text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","not in the manager",[89,2345,2347],{"x":743,"y":2346,"style":167,"fill":101,"textAnchor":95},"190","wrong folder, or",[89,2349,2350],{"x":743,"y":131,"style":167,"fill":101,"textAnchor":95},"metadata.txt missing",[73,2352],{"x":157,"y":2337,"width":2278,"height":2338,"rx":84,"fill":85,"stroke":2339,"style":2282},[89,2354,2355],{"x":2295,"y":231,"style":2342,"fill":2339,"textAnchor":95},"cannot be enabled",[89,2357,2358],{"x":2295,"y":2346,"style":167,"fill":101,"textAnchor":95},"a syntax error or a",[89,2360,2361],{"x":2295,"y":131,"style":167,"fill":101,"textAnchor":95},"missing dependency",[73,2363],{"x":2302,"y":2337,"width":2278,"height":2338,"rx":84,"fill":85,"stroke":86,"style":2282},[89,2365,2366],{"x":2305,"y":231,"style":2342,"fill":86,"textAnchor":95},"a traceback on enable",[89,2368,2369],{"x":2305,"y":2346,"style":167,"fill":101,"textAnchor":95},"classFactory raised or",[89,2371,2372],{"x":2305,"y":131,"style":167,"fill":101,"textAnchor":95},"returned nothing",[73,2374],{"x":145,"y":2337,"width":2312,"height":2338,"rx":84,"fill":85,"stroke":86,"style":2282},[89,2376,2377],{"x":2315,"y":231,"style":2342,"fill":86,"textAnchor":95},"enabled but invisible",[89,2379,2380],{"x":2315,"y":2346,"style":167,"fill":101,"textAnchor":95},"initGui raised, often",[89,2382,2383],{"x":2315,"y":131,"style":167,"fill":101,"textAnchor":95},"on a missing icon",[208,2385],{"x1":743,"y1":105,"x2":743,"y2":222,"stroke":2339,"style":2386},"stroke-width:1.6;stroke-dasharray:4 3",[208,2388],{"x1":2295,"y1":105,"x2":2295,"y2":222,"stroke":2339,"style":2386},[208,2390],{"x1":2305,"y1":105,"x2":2305,"y2":222,"stroke":86,"style":2386},[208,2392],{"x1":2315,"y1":105,"x2":2315,"y2":222,"stroke":86,"style":2386},[15,2394,2395,2396,2398],{},"The fourth is the one that looks most mysterious: the plugin is listed, ticked and apparently loaded, yet nothing appears in the interface. That is nearly always ",[273,2397,464],{}," raising partway through — commonly on an icon path that does not resolve — leaving the actions created before the exception installed and everything after it missing.",[254,2400,2402],{"id":2401},"key-takeaways","Key Takeaways",[262,2404,2405,2417,2426,2436,2442],{},[265,2406,2407,2408,2410,2411,2413,2414,2416],{},"The directory is a contract: QGIS reads ",[273,2409,96],{}," first, imports ",[273,2412,112],{},", and expects ",[273,2415,117],{}," to return your main class. Get those three right and the plugin manager can see your code.",[265,2418,2419,2420,2422,2423,2425],{},"Keep ",[273,2421,112],{}," trivial and defer the real import into ",[273,2424,346],{}," so a heavy module never loads at scan time and one broken plugin cannot take down the manager.",[265,2427,2428,2120,2430,2432,2433,2435],{},[273,2429,464],{},[273,2431,467],{}," are mirror images. Route every interface element through one ",[273,2434,1808],{},"-style helper that also records it, so teardown is a simple loop.",[265,2437,2438,2439,2441],{},"Resolve every path from ",[273,2440,2056],{},", never the current working directory — QGIS does not guarantee where it runs from.",[265,2443,2444,2445,2447,2448,2450,2451,2453,2454,2456],{},"Ship ",[273,2446,371],{}," files raw and load them at runtime; only ",[273,2449,150],{}," needs the ",[273,2452,480],{}," step. Pin ",[273,2455,275],{}," to the LTR you test against.",[15,2458,2459,2460,2463,2464,2467,2468,2471,2472,487],{},"With a clean skeleton in place, the next steps are the interface (",[19,2461,2462],{"href":26},"Qt Designer","), reusable logic (",[19,2465,2466],{"href":31},"processing providers","), automated checks (",[19,2469,2470],{"href":36},"testing and CI","), and release (",[19,2473,2474],{"href":41},"publishing",[254,2476,2478],{"id":2477},"frequently-asked-questions","Frequently Asked Questions",[15,2480,2481,2490,2491,2493,2494,2496,2497,2499],{},[268,2482,2483,2484,2486,2487,2489],{},"Why must ",[273,2485,112],{}," only contain the ",[273,2488,346],{}," function and a deferred import?","\nQGIS imports ",[273,2492,112],{}," during plugin discovery, before the user enables the plugin. Keeping it lightweight and importing your main module inside ",[273,2495,346],{}," (",[273,2498,2020],{},") prevents heavy modules from loading at scan time and stops an import error in one plugin from breaking the entire plugin manager.",[15,2501,2502,2508,2509,429,2511,429,2513,468,2515,2517,2518,1989,2520,2522],{},[268,2503,2504,2505,2507],{},"What happens if ",[273,2506,96],{}," is missing or has a malformed key?","\nQGIS silently skips the directory and the plugin never appears in the plugin manager. Required keys are ",[273,2510,428],{},[273,2512,432],{},[273,2514,275],{},[273,2516,438],{},"; missing any one, or adding blank lines between keys, causes the parser to reject the file. Check ",[273,2519,1988],{},[273,2521,1992],{}," warnings when a folder is ignored.",[15,2524,2525,2531,2532,2534,2535,2537,2538,2540,2541,2543,2544,2546,2547,2549,2550,2552,2553,2555],{},[268,2526,2527,2528,2530],{},"Do I need to compile ",[273,2529,371],{}," files into the plugin structure?","\nNo. The recommended pattern loads the ",[273,2533,371],{}," file at runtime with ",[273,2536,1907],{},", so you ship the raw ",[273,2539,371],{}," inside the ",[273,2542,367],{}," folder rather than a compiled module. You only run ",[273,2545,480],{}," to turn ",[273,2548,150],{}," into ",[273,2551,163],{},", since Qt's resource system cannot read ",[273,2554,2159],{}," directly at runtime.",[15,2557,2558,2561,2562,2565,2566,2568,2569,2571],{},[268,2559,2560],{},"Where should the plugin folder live during development?","\nPlace it in the active QGIS profile's ",[273,2563,2564],{},"python\u002Fplugins\u002F"," directory — ",[273,2567,307],{}," on Windows or ",[273,2570,315],{}," on macOS\u002FLinux. The folder name becomes the import package name, so use lowercase letters and underscores with no spaces.",[15,2573,2574,2577,2578,2580,2581,2583,2584,2586,2587,2120,2589,2591],{},[268,2575,2576],{},"Why do toolbar buttons sometimes remain after I disable a plugin?","\nQGIS does not automatically garbage-collect ",[273,2579,1819],{}," references you added in ",[273,2582,464],{},". Store every action in a list and, in ",[273,2585,467],{},", call both ",[273,2588,2119],{},[273,2590,2123],{}," for each, then remove any custom toolbar you created. Symmetric setup and teardown is the only reliable way to keep the interface clean.",[15,2593,2594,2597,2598,429,2600,429,2602,1948,2605,2608,2609,2611,2612,2614],{},[268,2595,2596],{},"Does this boilerplate change between QGIS 3.x versions?","\nThe core contract — ",[273,2599,96],{},[273,2601,346],{},[273,2603,2604],{},"initGui",[273,2606,2607],{},"unload"," — has been stable across the entire QGIS 3 series. What shifts is the API surface you call inside ",[273,2610,471],{},": enums and helper classes occasionally move between feature releases. Pin ",[273,2613,275],{}," to the LTR you develop against (3.34 here) and consult the QGIS API changelog before adopting methods introduced in a newer build.",[15,2616,2617,2620,2621,2623,2624,2627],{},[268,2618,2619],{},"Why does my plugin not appear in the Plugin Manager?","\nQGIS discovers plugins by looking for a ",[273,2622,96],{}," inside a folder directly under the profile's ",[273,2625,2626],{},"python\u002Fplugins"," directory. A missing metadata file, or a folder nested one level too deep, means the plugin is never seen at all — there is no error because nothing was found to report on.",[15,2629,2630,2633,2634,2636,2637,2639],{},[268,2631,2632],{},"What must classFactory return?","\nAn instance of your plugin class, constructed with the ",[273,2635,460],{}," argument it receives. Returning ",[273,2638,1264],{},", or raising anything, produces a traceback when the user ticks the plugin and leaves it disabled.",[15,2641,2642,2645],{},[268,2643,2644],{},"Is unload() really necessary?","\nYes, for anything you intend to develop iteratively. Without it a reload leaves duplicate toolbar buttons, doubled signal handlers and orphaned map tools, and the resulting behaviour is far more confusing than the original bug you were trying to fix.",[15,2647,2648,2651,2652,2654],{},[268,2649,2650],{},"Where does QGIS look for plugins?","\nInside the active user profile, under ",[273,2653,2626],{},". The profile directory is shown by Settings, User Profiles, Open Active Profile Folder, which is more reliable than guessing the path for your platform.",[15,2656,2657,2660],{},[268,2658,2659],{},"Does the folder name matter?","\nYes — it becomes the Python package name, so it must be a valid identifier and must match the name used when the plugin is packaged for the repository.",[15,2662,2663,2666],{},[268,2664,2665],{},"Can two plugins share a folder name?","\nNo. The folder name is the Python package name and the repository identifier, so a collision means one of the two will not load. Prefix an organisation-specific name to make an accidental clash unlikely.",[254,2668,2670],{"id":2669},"related-guides","Related Guides",[262,2672,2673,2681,2687,2700,2709,2715,2721,2727],{},[265,2674,2675,304,2678,2680],{},[268,2676,2677],{},"Up:",[19,2679,22],{"href":21}," — the parent guide covering the full plugin journey.",[265,2682,2683,2686],{},[19,2684,2685],{"href":49},"Create a QGIS Plugin with Plugin Builder 3"," — scaffold this exact structure automatically.",[265,2688,2689,2691,2692,2694,2695,2697,2698,43],{},[19,2690,1827],{"href":54}," — register a ",[273,2693,1819],{}," in ",[273,2696,464],{}," and clean it up in ",[273,2699,467],{},[265,2701,2702,2705,2706,2708],{},[19,2703,2704],{"href":26},"Qt Designer for QGIS Plugin Interfaces"," — build the dialog your ",[273,2707,471],{}," method opens.",[265,2710,2711,2714],{},[19,2712,2713],{"href":31},"Processing Provider Plugins for QGIS"," — expose reusable algorithms in the Processing Toolbox.",[265,2716,2717,2720],{},[19,2718,2719],{"href":36},"Testing & CI for QGIS Plugins"," — exercise the lifecycle headlessly.",[265,2722,2723,2726],{},[19,2724,2725],{"href":41},"Publishing to the QGIS Plugin Repository"," — metadata, versioning, and the approval workflow.",[265,2728,2729,2733],{},[19,2730,2732],{"href":2731},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-plugin-icon-and-resources-pyqgis\u002F","Add a Plugin Icon and Resources File"," — icons that resolve wherever the plugin is installed, and survive packaging.",[2735,2736,2737],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":331,"searchDepth":541,"depth":541,"links":2739},[2740,2741,2742,2743,2753,2761,2762,2763,2764,2765],{"id":256,"depth":541,"text":257},{"id":318,"depth":541,"text":319},{"id":402,"depth":541,"text":403},{"id":500,"depth":541,"text":501,"children":2744},[2745,2747,2749,2751,2752],{"id":505,"depth":548,"text":2746},"1. Entry Point (__init__.py)",{"id":595,"depth":548,"text":2748},"2. Metadata Configuration (metadata.txt)",{"id":694,"depth":548,"text":2750},"3. Main Plugin Class (main_plugin.py)",{"id":1830,"depth":548,"text":1831},{"id":1925,"depth":548,"text":1926},{"id":1954,"depth":541,"text":1955,"children":2754},[2755,2756,2758,2759,2760],{"id":1958,"depth":548,"text":1959},{"id":1996,"depth":548,"text":2757},"2. ModuleNotFoundError or Import Failures",{"id":2031,"depth":548,"text":2032},{"id":2093,"depth":548,"text":2094},{"id":2127,"depth":548,"text":2128},{"id":2166,"depth":541,"text":2167},{"id":2240,"depth":541,"text":2241},{"id":2401,"depth":541,"text":2402},{"id":2477,"depth":541,"text":2478},{"id":2669,"depth":541,"text":2670},"Understand the QGIS plugin boilerplate structure. Set up metadata.txt, classFactory, initGui, and unload to build a correctly structured QGIS plugin.","md",{"slug":12,"type":2769,"breadcrumb":2770,"datePublished":2771,"dateModified":2772},"guide","Boilerplate & Structure","2025-09-15","2026-07-18","\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure",{"title":5,"description":2766},"qgis-plugin-development\u002Fplugin-boilerplate-structure\u002Findex","YQBEYTW5l0TiqtV4jVT6k_fnpHPt4WvIjJXpsHcpQ90",1787823360573]