[{"data":1,"prerenderedAt":1841},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis":3},{"id":4,"title":5,"body":6,"description":1830,"extension":1831,"meta":1832,"navigation":411,"path":1837,"seo":1838,"stem":1839,"__hash__":1840},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002Findex.md","Connect to Layer and Project Signals in PyQGIS",{"type":7,"value":8,"toc":1817},"minimark",[9,13,22,31,174,179,215,219,229,379,460,472,476,479,678,699,703,706,911,928,933,937,1069,1223,1250,1254,1260,1422,1560,1579,1583,1589,1641,1644,1648,1711,1715,1730,1734,1745,1759,1769,1778,1788,1792,1813],[10,11,5],"h1",{"id":12},"connect-to-layer-and-project-signals-in-pyqgis",[14,15,16,17,21],"p",{},"A plugin that reads the project once and caches what it found will be wrong within minutes: the user adds a layer, removes another, starts an edit session, changes the selection. Signals are how QGIS tells you those things happened — and the two ways they go wrong are both about lifetime. Act on a layer ",[18,19,20],"em",{},"after"," it has been destroyed and QGIS crashes. Forget to disconnect on unload and a reloaded plugin runs every handler twice.",[14,23,24,25,30],{},"This page is a focused recipe within ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",". It covers the signals worth knowing, the crucial difference between the two layer-removal signals, connecting to layers that do not exist yet, and disconnecting cleanly.",[14,32,33],{},[34,35,40,44,48,55,72,81,88,98,105,111,115,119,122,125,132,137,141,145,150,154,158,162,165,169,172],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 254","img","A timeline of layer removal showing that layerWillBeRemoved fires while the layer is still valid and layersRemoved fires after it has been destroyed","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"The only safe moment to release a layer reference",[45,46,47],"desc",{},"A timeline of a layer being removed. layerWillBeRemoved fires first, while the object is still valid and can be safely inspected or disconnected. The layer is then destroyed. layersRemoved fires afterwards, carrying only identifiers, and touching the layer object from that handler dereferences freed memory.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","254","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"sigTimeArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#59645f",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One of these hands you a live object; the other hands you a name",[82,83],"line",{"x1":84,"y1":85,"x2":86,"y2":85,"stroke":71,"style":87},"40","132","720","stroke-width:2.5;marker-end:url(#sigTimeArrow)",[49,89],{"x":90,"y":91,"width":92,"height":93,"rx":94,"fill":95,"fillOpacity":96,"stroke":95,"style":97},"60","120","280","24","4","#15803d",0.25,"stroke-width:1.5",[73,99,104],{"x":100,"y":101,"style":102,"fill":103,"textAnchor":79},"200","137","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","layer still valid",[49,106],{"x":107,"y":91,"width":108,"height":93,"rx":94,"fill":109,"fillOpacity":110,"stroke":109,"style":97},"400","300","#b91c1c",0.2,[73,112,114],{"x":113,"y":101,"style":102,"fill":103,"textAnchor":79},"550","layer destroyed",[116,117],"circle",{"cx":100,"cy":85,"r":118,"fill":95},"9",[116,120],{"cx":121,"cy":85,"r":118,"fill":109},"370",[116,123],{"cx":113,"cy":85,"r":118,"fill":124},"#b45309",[49,126],{"x":127,"y":128,"width":129,"height":128,"rx":63,"fill":130,"stroke":95,"style":131},"80","52","240","#fffdf7","stroke-width:2.5",[73,133,136],{"x":100,"y":134,"style":135,"fill":95,"textAnchor":79},"74","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","layerWillBeRemoved(layer)",[73,138,140],{"x":100,"y":139,"style":102,"fill":103,"textAnchor":79},"93","inspect it, disconnect from it",[82,142],{"x1":100,"y1":143,"x2":100,"y2":91,"stroke":95,"style":144},"104","stroke-width:2;marker-end:url(#sigTimeArrow)",[49,146],{"x":108,"y":147,"width":148,"height":128,"rx":63,"fill":130,"stroke":109,"style":149},"176","140","stroke-width:2",[73,151,153],{"x":121,"y":152,"style":135,"fill":109,"textAnchor":79},"198","deleted",[73,155,157],{"x":121,"y":156,"style":102,"fill":103,"textAnchor":79},"217","C++ object gone",[82,159],{"x1":121,"y1":160,"x2":121,"y2":161,"stroke":109,"style":144},"144","172",[49,163],{"x":164,"y":128,"width":129,"height":128,"rx":63,"fill":130,"stroke":124,"style":131},"440",[73,166,168],{"x":167,"y":134,"style":135,"fill":124,"textAnchor":79},"560","layersRemoved(ids)",[73,170,171],{"x":167,"y":139,"style":102,"fill":103,"textAnchor":79},"strings only — never look it up",[82,173],{"x1":113,"y1":143,"x2":113,"y2":91,"stroke":124,"style":144},[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,182,190,207],"ul",{},[183,184,185,189],"li",{},[186,187,188],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[183,191,192,193,197,198,201,202,206],{},"A plugin skeleton with ",[194,195,196],"code",{},"initGui()"," and ",[194,199,200],{},"unload()"," — see ",[26,203,205],{"href":204},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",".",[183,208,209,210,214],{},"Familiarity with Qt signals and slots; ",[26,211,213],{"href":212},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces"," covers the basics.",[175,216,218],{"id":217},"the-signals-worth-knowing","The signals worth knowing",[14,220,221,224,225,228],{},[194,222,223],{},"QgsProject"," reports structural changes; a ",[194,226,227],{},"QgsVectorLayer"," reports changes to itself.",[230,231,232,251],"table",{},[233,234,235],"thead",{},[236,237,238,242,245,248],"tr",{},[239,240,241],"th",{},"Signal",[239,243,244],{},"Emitted by",[239,246,247],{},"Carries",[239,249,250],{},"Use it for",[252,253,254,271,286,301,316,332,350,365],"tbody",{},[236,255,256,262,265,268],{},[257,258,259],"td",{},[194,260,261],{},"layersAdded",[257,263,264],{},"project",[257,266,267],{},"list of layers",[257,269,270],{},"wiring up a newly loaded layer",[236,272,273,278,280,283],{},[257,274,275],{},[194,276,277],{},"layerWillBeRemoved",[257,279,264],{},[257,281,282],{},"the layer",[257,284,285],{},"releasing your reference safely",[236,287,288,293,295,298],{},[257,289,290],{},[194,291,292],{},"layersRemoved",[257,294,264],{},[257,296,297],{},"list of IDs",[257,299,300],{},"cleaning up keyed caches",[236,302,303,308,310,313],{},[257,304,305],{},[194,306,307],{},"cleared",[257,309,264],{},[257,311,312],{},"nothing",[257,314,315],{},"resetting when a new project opens",[236,317,318,323,326,329],{},[257,319,320],{},[194,321,322],{},"selectionChanged",[257,324,325],{},"layer",[257,327,328],{},"added \u002F removed IDs",[257,330,331],{},"keeping a panel in step",[236,333,334,343,345,347],{},[257,335,336,339,340],{},[194,337,338],{},"editingStarted"," \u002F ",[194,341,342],{},"editingStopped",[257,344,325],{},[257,346,312],{},[257,348,349],{},"enabling or disabling actions",[236,351,352,357,359,362],{},[257,353,354],{},[194,355,356],{},"attributeValueChanged",[257,358,325],{},[257,360,361],{},"fid, index, value",[257,363,364],{},"live validation",[236,366,367,372,374,376],{},[257,368,369],{},[194,370,371],{},"layerModified",[257,373,325],{},[257,375,312],{},[257,377,378],{},"marking a view dirty",[380,381,386],"pre",{"className":382,"code":383,"language":384,"meta":385,"style":385},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nproject.layersAdded.connect(self.on_layers_added)\nproject.layerWillBeRemoved.connect(self.on_layer_will_be_removed)\nproject.cleared.connect(self.on_project_cleared)\n","python","",[194,387,388,406,413,425,438,449],{"__ignoreMap":385},[389,390,392,396,400,403],"span",{"class":82,"line":391},1,[389,393,395],{"class":394},"snl16","from",[389,397,399],{"class":398},"s95oV"," qgis.core ",[389,401,402],{"class":394},"import",[389,404,405],{"class":398}," QgsProject\n",[389,407,409],{"class":82,"line":408},2,[389,410,412],{"emptyLinePlaceholder":411},true,"\n",[389,414,416,419,422],{"class":82,"line":415},3,[389,417,418],{"class":398},"project ",[389,420,421],{"class":394},"=",[389,423,424],{"class":398}," QgsProject.instance()\n",[389,426,428,431,435],{"class":82,"line":427},4,[389,429,430],{"class":398},"project.layersAdded.connect(",[389,432,434],{"class":433},"sDLfK","self",[389,436,437],{"class":398},".on_layers_added)\n",[389,439,441,444,446],{"class":82,"line":440},5,[389,442,443],{"class":398},"project.layerWillBeRemoved.connect(",[389,445,434],{"class":433},[389,447,448],{"class":398},".on_layer_will_be_removed)\n",[389,450,452,455,457],{"class":82,"line":451},6,[389,453,454],{"class":398},"project.cleared.connect(",[389,456,434],{"class":433},[389,458,459],{"class":398},".on_project_cleared)\n",[14,461,462,465,466,468,469,471],{},[186,463,464],{},"Breakdown:"," ",[194,467,261],{}," passes a list, not a single layer, because loading a multi-layer GeoPackage adds several at once — a handler written for one silently processes only the first. ",[194,470,307],{}," fires when a new project is opened or the current one is closed, and is the signal that should reset every cache your plugin holds; without it a plugin keeps showing data from the previous project.",[175,473,475],{"id":474},"never-touch-a-removed-layer","Never touch a removed layer",[14,477,478],{},"The two removal signals look interchangeable and are not.",[380,480,482],{"className":382,"code":481,"language":384,"meta":385,"style":385},"class MyPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.tracked = {}          # layer id -> some cached state\n\n    def on_layer_will_be_removed(self, layer_id):\n        layer = QgsProject.instance().mapLayer(layer_id)\n        if layer is not None:\n            try:\n                layer.selectionChanged.disconnect(self.on_selection_changed)\n            except TypeError:\n                pass\n        self.tracked.pop(layer_id, None)\n\n    def on_layers_removed(self, layer_ids):\n        for layer_id in layer_ids:\n            self.tracked.pop(layer_id, None)   # ids only — do not look them up\n",[194,483,484,496,507,520,536,540,550,561,581,589,600,611,617,631,636,647,662],{"__ignoreMap":385},[389,485,486,489,493],{"class":82,"line":391},[389,487,488],{"class":394},"class",[389,490,492],{"class":491},"svObZ"," MyPlugin",[389,494,495],{"class":398},":\n",[389,497,498,501,504],{"class":82,"line":408},[389,499,500],{"class":394},"    def",[389,502,503],{"class":433}," __init__",[389,505,506],{"class":398},"(self, iface):\n",[389,508,509,512,515,517],{"class":82,"line":415},[389,510,511],{"class":433},"        self",[389,513,514],{"class":398},".iface ",[389,516,421],{"class":394},[389,518,519],{"class":398}," iface\n",[389,521,522,524,527,529,532],{"class":82,"line":427},[389,523,511],{"class":433},[389,525,526],{"class":398},".tracked ",[389,528,421],{"class":394},[389,530,531],{"class":398}," {}          ",[389,533,535],{"class":534},"sjoCn","# layer id -> some cached state\n",[389,537,538],{"class":82,"line":440},[389,539,412],{"emptyLinePlaceholder":411},[389,541,542,544,547],{"class":82,"line":451},[389,543,500],{"class":394},[389,545,546],{"class":491}," on_layer_will_be_removed",[389,548,549],{"class":398},"(self, layer_id):\n",[389,551,553,556,558],{"class":82,"line":552},7,[389,554,555],{"class":398},"        layer ",[389,557,421],{"class":394},[389,559,560],{"class":398}," QgsProject.instance().mapLayer(layer_id)\n",[389,562,564,567,570,573,576,579],{"class":82,"line":563},8,[389,565,566],{"class":394},"        if",[389,568,569],{"class":398}," layer ",[389,571,572],{"class":394},"is",[389,574,575],{"class":394}," not",[389,577,578],{"class":433}," None",[389,580,495],{"class":398},[389,582,584,587],{"class":82,"line":583},9,[389,585,586],{"class":394},"            try",[389,588,495],{"class":398},[389,590,592,595,597],{"class":82,"line":591},10,[389,593,594],{"class":398},"                layer.selectionChanged.disconnect(",[389,596,434],{"class":433},[389,598,599],{"class":398},".on_selection_changed)\n",[389,601,603,606,609],{"class":82,"line":602},11,[389,604,605],{"class":394},"            except",[389,607,608],{"class":433}," TypeError",[389,610,495],{"class":398},[389,612,614],{"class":82,"line":613},12,[389,615,616],{"class":394},"                pass\n",[389,618,620,622,625,628],{"class":82,"line":619},13,[389,621,511],{"class":433},[389,623,624],{"class":398},".tracked.pop(layer_id, ",[389,626,627],{"class":433},"None",[389,629,630],{"class":398},")\n",[389,632,634],{"class":82,"line":633},14,[389,635,412],{"emptyLinePlaceholder":411},[389,637,639,641,644],{"class":82,"line":638},15,[389,640,500],{"class":394},[389,642,643],{"class":491}," on_layers_removed",[389,645,646],{"class":398},"(self, layer_ids):\n",[389,648,650,653,656,659],{"class":82,"line":649},16,[389,651,652],{"class":394},"        for",[389,654,655],{"class":398}," layer_id ",[389,657,658],{"class":394},"in",[389,660,661],{"class":398}," layer_ids:\n",[389,663,665,668,670,672,675],{"class":82,"line":664},17,[389,666,667],{"class":433},"            self",[389,669,624],{"class":398},[389,671,627],{"class":433},[389,673,674],{"class":398},")   ",[389,676,677],{"class":534},"# ids only — do not look them up\n",[14,679,680,465,682,684,685,688,689,691,692,695,696,698],{},[186,681,464],{},[194,683,277],{}," fires ",[18,686,687],{},"before"," destruction and passes the layer's ID, which can still be resolved to a live object — this is the only safe moment to disconnect signals you attached to that layer. ",[194,690,292],{}," fires afterwards; calling ",[194,693,694],{},"mapLayer()"," from it returns ",[194,697,627],{}," at best, and holding a stale Python wrapper and calling a method on it crashes the application. Treating the IDs as opaque strings in that handler is the rule.",[175,700,702],{"id":701},"connect-to-layers-that-do-not-exist-yet","Connect to layers that do not exist yet",[14,704,705],{},"A plugin loaded before the user opens a project has no layers to connect to. Wiring new layers as they arrive covers both cases with one code path.",[380,707,709],{"className":382,"code":708,"language":384,"meta":385,"style":385},"from qgis.core import QgsProject, QgsVectorLayer\n\n\nclass MyPlugin:\n    def initGui(self):\n        project = QgsProject.instance()\n        project.layersAdded.connect(self.wire_layers)\n        self.wire_layers(list(project.mapLayers().values()))   # catch existing ones\n\n    def wire_layers(self, layers):\n        for layer in layers:\n            if not isinstance(layer, QgsVectorLayer):\n                continue\n            layer.selectionChanged.connect(self.on_selection_changed)\n            layer.editingStopped.connect(self.on_editing_stopped)\n\n    def on_selection_changed(self, selected, deselected, clear_and_select):\n        layer = self.sender()\n        self.iface.messageBar().pushInfo(\n            layer.name(), f\"{layer.selectedFeatureCount()} selected\"\n        )\n",[194,710,711,722,726,730,738,748,757,767,783,787,797,808,821,826,835,845,849,859,872,880,905],{"__ignoreMap":385},[389,712,713,715,717,719],{"class":82,"line":391},[389,714,395],{"class":394},[389,716,399],{"class":398},[389,718,402],{"class":394},[389,720,721],{"class":398}," QgsProject, QgsVectorLayer\n",[389,723,724],{"class":82,"line":408},[389,725,412],{"emptyLinePlaceholder":411},[389,727,728],{"class":82,"line":415},[389,729,412],{"emptyLinePlaceholder":411},[389,731,732,734,736],{"class":82,"line":427},[389,733,488],{"class":394},[389,735,492],{"class":491},[389,737,495],{"class":398},[389,739,740,742,745],{"class":82,"line":440},[389,741,500],{"class":394},[389,743,744],{"class":491}," initGui",[389,746,747],{"class":398},"(self):\n",[389,749,750,753,755],{"class":82,"line":451},[389,751,752],{"class":398},"        project ",[389,754,421],{"class":394},[389,756,424],{"class":398},[389,758,759,762,764],{"class":82,"line":552},[389,760,761],{"class":398},"        project.layersAdded.connect(",[389,763,434],{"class":433},[389,765,766],{"class":398},".wire_layers)\n",[389,768,769,771,774,777,780],{"class":82,"line":563},[389,770,511],{"class":433},[389,772,773],{"class":398},".wire_layers(",[389,775,776],{"class":433},"list",[389,778,779],{"class":398},"(project.mapLayers().values()))   ",[389,781,782],{"class":534},"# catch existing ones\n",[389,784,785],{"class":82,"line":583},[389,786,412],{"emptyLinePlaceholder":411},[389,788,789,791,794],{"class":82,"line":591},[389,790,500],{"class":394},[389,792,793],{"class":491}," wire_layers",[389,795,796],{"class":398},"(self, layers):\n",[389,798,799,801,803,805],{"class":82,"line":602},[389,800,652],{"class":394},[389,802,569],{"class":398},[389,804,658],{"class":394},[389,806,807],{"class":398}," layers:\n",[389,809,810,813,815,818],{"class":82,"line":613},[389,811,812],{"class":394},"            if",[389,814,575],{"class":394},[389,816,817],{"class":433}," isinstance",[389,819,820],{"class":398},"(layer, QgsVectorLayer):\n",[389,822,823],{"class":82,"line":619},[389,824,825],{"class":394},"                continue\n",[389,827,828,831,833],{"class":82,"line":633},[389,829,830],{"class":398},"            layer.selectionChanged.connect(",[389,832,434],{"class":433},[389,834,599],{"class":398},[389,836,837,840,842],{"class":82,"line":638},[389,838,839],{"class":398},"            layer.editingStopped.connect(",[389,841,434],{"class":433},[389,843,844],{"class":398},".on_editing_stopped)\n",[389,846,847],{"class":82,"line":649},[389,848,412],{"emptyLinePlaceholder":411},[389,850,851,853,856],{"class":82,"line":664},[389,852,500],{"class":394},[389,854,855],{"class":491}," on_selection_changed",[389,857,858],{"class":398},"(self, selected, deselected, clear_and_select):\n",[389,860,862,864,866,869],{"class":82,"line":861},18,[389,863,555],{"class":398},[389,865,421],{"class":394},[389,867,868],{"class":433}," self",[389,870,871],{"class":398},".sender()\n",[389,873,875,877],{"class":82,"line":874},19,[389,876,511],{"class":433},[389,878,879],{"class":398},".iface.messageBar().pushInfo(\n",[389,881,883,886,889,893,896,899,902],{"class":82,"line":882},20,[389,884,885],{"class":398},"            layer.name(), ",[389,887,888],{"class":394},"f",[389,890,892],{"class":891},"sU2Wk","\"",[389,894,895],{"class":433},"{",[389,897,898],{"class":398},"layer.selectedFeatureCount()",[389,900,901],{"class":433},"}",[389,903,904],{"class":891}," selected\"\n",[389,906,908],{"class":82,"line":907},21,[389,909,910],{"class":398},"        )\n",[14,912,913,915,916,919,920,923,924,927],{},[186,914,464],{}," Calling ",[194,917,918],{},"wire_layers()"," with the current layers immediately after connecting means the plugin behaves the same whether it was enabled before or after the project loaded. The ",[194,921,922],{},"isinstance"," check skips rasters, which do not have these signals. ",[194,925,926],{},"self.sender()"," inside a slot returns the object that emitted it, which is how one handler serves every layer without a lambda per connection — and lambdas here would be a leak, since each would hold a reference to its layer.",[14,929,930,932],{},[194,931,322],{}," passes three arguments: the newly selected IDs, the deselected IDs, and whether the selection was replaced wholesale. A handler that only needs the count can ignore them, but for an incremental view they avoid re-reading the whole selection.",[175,934,936],{"id":935},"disconnect-on-unload","Disconnect on unload",[14,938,939],{},[34,940,943,946,949,952,959,962,969,975,983,988,991,996,1000,1006,1010,1014,1017,1021,1024,1027,1032,1037,1041,1044,1047,1051,1058,1062,1065],{"viewBox":941,"role":37,"ariaLabel":942,"xmlns":39},"0 0 760 250","A plugin reloaded without disconnecting leaves the old instance's handlers connected, so each signal reaches two handlers and every action runs twice",[41,944,945],{},"What a missing disconnect does on reload",[45,947,948],{},"On the left, a plugin that disconnects on unload leaves a single handler connected after a reload, and one signal produces one action. On the right, a plugin that does not disconnect leaves the previous instance's handler connected alongside the new one, so one signal reaches two handlers and the action runs twice.",[49,950],{"x":51,"y":51,"width":52,"height":951,"fill":54},"250",[56,953,954],{},[59,955,957],{"id":956,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"sigDupArrow",[68,958],{"d":70,"fill":103},[73,960,961],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"The old instance is gone from your code but not from the signal",[49,963],{"x":964,"y":965,"width":966,"height":967,"rx":968,"fill":130,"stroke":95,"style":131},"16","46","356","188","10",[73,970,974],{"x":971,"y":972,"style":973,"fill":95,"textAnchor":79},"194","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","disconnect() in unload()",[49,976],{"x":977,"y":978,"width":979,"height":980,"rx":63,"fill":981,"stroke":982,"style":149},"44","94","112","48","#26322d","#0f766e",[73,984,322],{"x":985,"y":986,"style":102,"fill":987,"textAnchor":79},"100","123","#d9f99d",[49,989],{"x":990,"y":978,"width":979,"height":980,"rx":63,"fill":130,"stroke":95,"style":149},"232",[73,992,995],{"x":993,"y":994,"style":102,"fill":103,"textAnchor":79},"288","115","handler v2",[73,997,999],{"x":993,"y":85,"style":998,"fill":71,"textAnchor":79},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","the only one",[82,1001],{"x1":1002,"y1":1003,"x2":1004,"y2":1003,"stroke":95,"style":1005},"156","118","228","stroke-width:2;marker-end:url(#sigDupArrow)",[73,1007,1009],{"x":971,"y":1008,"style":135,"fill":95,"textAnchor":79},"182","one signal → one action",[73,1011,1013],{"x":971,"y":1012,"style":102,"fill":103,"textAnchor":79},"206","what you expect",[49,1015],{"x":1016,"y":965,"width":966,"height":967,"rx":968,"fill":130,"stroke":109,"style":131},"388",[73,1018,1020],{"x":1019,"y":972,"style":973,"fill":109,"textAnchor":79},"566","no disconnect",[49,1022],{"x":1023,"y":978,"width":979,"height":980,"rx":63,"fill":981,"stroke":982,"style":149},"416",[73,1025,322],{"x":1026,"y":986,"style":102,"fill":987,"textAnchor":79},"472",[49,1028],{"x":1029,"y":1030,"width":979,"height":977,"rx":63,"fill":130,"stroke":109,"style":1031},"604","72","stroke-width:2;stroke-dasharray:5 4",[73,1033,1036],{"x":1034,"y":1035,"style":102,"fill":103,"textAnchor":79},"660","90","handler v1",[73,1038,1040],{"x":1034,"y":1039,"style":998,"fill":109,"textAnchor":79},"107","still attached",[49,1042],{"x":1029,"y":1043,"width":979,"height":977,"rx":63,"fill":130,"stroke":109,"style":149},"128",[73,1045,995],{"x":1034,"y":1046,"style":102,"fill":103,"textAnchor":79},"146",[73,1048,1050],{"x":1034,"y":1049,"style":998,"fill":71,"textAnchor":79},"163","the new one",[82,1052],{"x1":1053,"y1":1054,"x2":1055,"y2":1056,"stroke":109,"style":1057},"528","110","600","92","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#sigDupArrow)",[82,1059],{"x1":1053,"y1":1060,"x2":1055,"y2":1061,"stroke":109,"style":1005},"126","148",[73,1063,1064],{"x":1019,"y":100,"style":135,"fill":109,"textAnchor":79},"one signal → two actions",[73,1066,1068],{"x":1019,"y":1067,"style":102,"fill":103,"textAnchor":79},"222","and three after the next reload",[380,1070,1072],{"className":382,"code":1071,"language":384,"meta":385,"style":385},"    def unload(self):\n        project = QgsProject.instance()\n        for signal, slot in (\n            (project.layersAdded, self.wire_layers),\n            (project.layerWillBeRemoved, self.on_layer_will_be_removed),\n            (project.cleared, self.on_project_cleared),\n        ):\n            try:\n                signal.disconnect(slot)\n            except TypeError:\n                pass\n\n        for layer in project.mapLayers().values():\n            if isinstance(layer, QgsVectorLayer):\n                try:\n                    layer.selectionChanged.disconnect(self.on_selection_changed)\n                    layer.editingStopped.disconnect(self.on_editing_stopped)\n                except TypeError:\n                    pass\n",[194,1073,1074,1083,1091,1103,1113,1123,1133,1138,1144,1149,1157,1161,1165,1176,1184,1191,1200,1209,1218],{"__ignoreMap":385},[389,1075,1076,1078,1081],{"class":82,"line":391},[389,1077,500],{"class":394},[389,1079,1080],{"class":491}," unload",[389,1082,747],{"class":398},[389,1084,1085,1087,1089],{"class":82,"line":408},[389,1086,752],{"class":398},[389,1088,421],{"class":394},[389,1090,424],{"class":398},[389,1092,1093,1095,1098,1100],{"class":82,"line":415},[389,1094,652],{"class":394},[389,1096,1097],{"class":398}," signal, slot ",[389,1099,658],{"class":394},[389,1101,1102],{"class":398}," (\n",[389,1104,1105,1108,1110],{"class":82,"line":427},[389,1106,1107],{"class":398},"            (project.layersAdded, ",[389,1109,434],{"class":433},[389,1111,1112],{"class":398},".wire_layers),\n",[389,1114,1115,1118,1120],{"class":82,"line":440},[389,1116,1117],{"class":398},"            (project.layerWillBeRemoved, ",[389,1119,434],{"class":433},[389,1121,1122],{"class":398},".on_layer_will_be_removed),\n",[389,1124,1125,1128,1130],{"class":82,"line":451},[389,1126,1127],{"class":398},"            (project.cleared, ",[389,1129,434],{"class":433},[389,1131,1132],{"class":398},".on_project_cleared),\n",[389,1134,1135],{"class":82,"line":552},[389,1136,1137],{"class":398},"        ):\n",[389,1139,1140,1142],{"class":82,"line":563},[389,1141,586],{"class":394},[389,1143,495],{"class":398},[389,1145,1146],{"class":82,"line":583},[389,1147,1148],{"class":398},"                signal.disconnect(slot)\n",[389,1150,1151,1153,1155],{"class":82,"line":591},[389,1152,605],{"class":394},[389,1154,608],{"class":433},[389,1156,495],{"class":398},[389,1158,1159],{"class":82,"line":602},[389,1160,616],{"class":394},[389,1162,1163],{"class":82,"line":613},[389,1164,412],{"emptyLinePlaceholder":411},[389,1166,1167,1169,1171,1173],{"class":82,"line":619},[389,1168,652],{"class":394},[389,1170,569],{"class":398},[389,1172,658],{"class":394},[389,1174,1175],{"class":398}," project.mapLayers().values():\n",[389,1177,1178,1180,1182],{"class":82,"line":633},[389,1179,812],{"class":394},[389,1181,817],{"class":433},[389,1183,820],{"class":398},[389,1185,1186,1189],{"class":82,"line":638},[389,1187,1188],{"class":394},"                try",[389,1190,495],{"class":398},[389,1192,1193,1196,1198],{"class":82,"line":649},[389,1194,1195],{"class":398},"                    layer.selectionChanged.disconnect(",[389,1197,434],{"class":433},[389,1199,599],{"class":398},[389,1201,1202,1205,1207],{"class":82,"line":664},[389,1203,1204],{"class":398},"                    layer.editingStopped.disconnect(",[389,1206,434],{"class":433},[389,1208,844],{"class":398},[389,1210,1211,1214,1216],{"class":82,"line":861},[389,1212,1213],{"class":394},"                except",[389,1215,608],{"class":433},[389,1217,495],{"class":398},[389,1219,1220],{"class":82,"line":874},[389,1221,1222],{"class":394},"                    pass\n",[14,1224,1225,1227,1228,1230,1231,1233,1234,1237,1238,1241,1242,1244,1245,1249],{},[186,1226,464],{}," Every connection made in ",[194,1229,196],{}," or ",[194,1232,918],{}," needs a matching disconnect, including the per-layer ones. ",[194,1235,1236],{},"disconnect()"," raises ",[194,1239,1240],{},"TypeError"," when the connection is already gone — which happens legitimately, for example when a layer was removed earlier — so the guard makes ",[194,1243,200],{}," idempotent rather than fragile. Iterating the signal\u002Fslot pairs keeps the list visible in one place, which is the practical defence against adding a connection and forgetting its disconnect. This discipline is what makes ",[26,1246,1248],{"href":1247},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting"," usable during development.",[175,1251,1253],{"id":1252},"do-not-do-heavy-work-in-a-signal-handler","Do not do heavy work in a signal handler",[14,1255,1256,1257,1259],{},"Signals fire on the main thread, synchronously, in the middle of whatever QGIS was doing. A handler that reads a large layer or hits a network freezes the interface — and ",[194,1258,356],{}," in particular can fire thousands of times during a single bulk edit.",[14,1261,1262],{},[34,1263,1266,1269,1272,1275,1282,1285,1291,1381,1384,1388,1393,1396,1401,1406,1410,1414,1419],{"viewBox":1264,"role":37,"ariaLabel":1265,"xmlns":39},"0 0 760 244","A bulk edit emitting thousands of attributeValueChanged signals, with a naive handler running per signal versus a debounced handler running once after the burst",[41,1267,1268],{},"Debouncing a burst of signals",[45,1270,1271],{},"A bulk edit emits a dense burst of attributeValueChanged signals. A naive handler recomputes on every one, freezing the interface for the duration. A debounced handler restarts a short single-shot timer on each signal and recomputes only once, after the burst has finished.",[49,1273],{"x":51,"y":51,"width":52,"height":1274,"fill":54},"244",[56,1276,1277],{},[59,1278,1280],{"id":1279,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"sigDebArrow",[68,1281],{"d":70,"fill":95},[73,1283,1284],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"One bulk edit, thousands of signals",[73,1286,1290],{"x":967,"y":972,"style":1287,"fill":1288,"textAnchor":1289},"text-anchor:end;font-size:11.5px;font-weight:bold;font-family:sans-serif","#2563eb","end","signals emitted",[1292,1293,1294,1300,1303,1306,1308,1311,1313,1316,1319,1322,1325,1328,1331,1333,1336,1339,1342,1345,1348,1351,1353,1356,1359,1361,1363,1366,1369,1372,1375,1378],"g",{"fill":1288},[49,1295],{"x":1296,"y":1297,"width":1298,"height":1299},"204","56","3","20",[49,1301],{"x":1302,"y":1297,"width":1298,"height":1299},"212",[49,1304],{"x":1305,"y":1297,"width":1298,"height":1299},"220",[49,1307],{"x":1004,"y":1297,"width":1298,"height":1299},[49,1309],{"x":1310,"y":1297,"width":1298,"height":1299},"236",[49,1312],{"x":1274,"y":1297,"width":1298,"height":1299},[49,1314],{"x":1315,"y":1297,"width":1298,"height":1299},"252",[49,1317],{"x":1318,"y":1297,"width":1298,"height":1299},"260",[49,1320],{"x":1321,"y":1297,"width":1298,"height":1299},"268",[49,1323],{"x":1324,"y":1297,"width":1298,"height":1299},"276",[49,1326],{"x":1327,"y":1297,"width":1298,"height":1299},"284",[49,1329],{"x":1330,"y":1297,"width":1298,"height":1299},"292",[49,1332],{"x":108,"y":1297,"width":1298,"height":1299},[49,1334],{"x":1335,"y":1297,"width":1298,"height":1299},"308",[49,1337],{"x":1338,"y":1297,"width":1298,"height":1299},"316",[49,1340],{"x":1341,"y":1297,"width":1298,"height":1299},"324",[49,1343],{"x":1344,"y":1297,"width":1298,"height":1299},"332",[49,1346],{"x":1347,"y":1297,"width":1298,"height":1299},"340",[49,1349],{"x":1350,"y":1297,"width":1298,"height":1299},"348",[49,1352],{"x":966,"y":1297,"width":1298,"height":1299},[49,1354],{"x":1355,"y":1297,"width":1298,"height":1299},"364",[49,1357],{"x":1358,"y":1297,"width":1298,"height":1299},"372",[49,1360],{"x":75,"y":1297,"width":1298,"height":1299},[49,1362],{"x":1016,"y":1297,"width":1298,"height":1299},[49,1364],{"x":1365,"y":1297,"width":1298,"height":1299},"396",[49,1367],{"x":1368,"y":1297,"width":1298,"height":1299},"404",[49,1370],{"x":1371,"y":1297,"width":1298,"height":1299},"412",[49,1373],{"x":1374,"y":1297,"width":1298,"height":1299},"420",[49,1376],{"x":1377,"y":1297,"width":1298,"height":1299},"428",[49,1379],{"x":1380,"y":1297,"width":1298,"height":1299},"436",[73,1382,1383],{"x":967,"y":85,"style":1287,"fill":109,"textAnchor":1289},"naive handler",[49,1385],{"x":1296,"y":979,"width":1310,"height":76,"rx":94,"fill":109,"fillOpacity":1386,"stroke":109,"style":1387},0.3,"stroke-width:1.8",[73,1389,1392],{"x":1390,"y":1391,"style":102,"fill":103,"textAnchor":79},"322","130","recomputes 30 times — UI frozen",[73,1394,1395],{"x":967,"y":971,"style":1287,"fill":95,"textAnchor":1289},"debounced",[49,1397],{"x":1398,"y":1399,"width":979,"height":76,"rx":94,"fill":95,"fillOpacity":1400,"stroke":95,"style":1387},"452","174",0.4,[73,1402,1405],{"x":1403,"y":1404,"style":102,"fill":103,"textAnchor":79},"508","192","recomputes once",[82,1407],{"x1":164,"y1":972,"x2":164,"y2":1408,"stroke":95,"style":1409},"186","stroke-width:1.6;stroke-dasharray:4 3",[82,1411],{"x1":164,"y1":1408,"x2":1412,"y2":1408,"stroke":95,"style":1413},"448","stroke-width:2;marker-end:url(#sigDebArrow)",[73,1415,1418],{"x":1416,"y":1404,"style":1417,"fill":71},"596","font-size:11px;font-family:sans-serif","200 ms after the last signal",[73,1420,1421],{"x":75,"y":1004,"style":102,"fill":71,"textAnchor":79},"the timer restarts on every signal and only fires when the burst stops",[380,1423,1425],{"className":382,"code":1424,"language":384,"meta":385,"style":385},"from qgis.PyQt.QtCore import QTimer\n\n\nclass MyPlugin:\n    def initGui(self):\n        self._refresh_timer = QTimer(self.iface.mainWindow())\n        self._refresh_timer.setSingleShot(True)\n        self._refresh_timer.setInterval(200)\n        self._refresh_timer.timeout.connect(self.recompute)\n\n    def on_attribute_changed(self, fid, index, value):\n        self._refresh_timer.start()          # restarts if already running\n\n    def recompute(self):\n        ...                                   # the expensive work, once\n",[194,1426,1427,1439,1443,1447,1455,1463,1480,1492,1503,1515,1519,1529,1539,1543,1552],{"__ignoreMap":385},[389,1428,1429,1431,1434,1436],{"class":82,"line":391},[389,1430,395],{"class":394},[389,1432,1433],{"class":398}," qgis.PyQt.QtCore ",[389,1435,402],{"class":394},[389,1437,1438],{"class":398}," QTimer\n",[389,1440,1441],{"class":82,"line":408},[389,1442,412],{"emptyLinePlaceholder":411},[389,1444,1445],{"class":82,"line":415},[389,1446,412],{"emptyLinePlaceholder":411},[389,1448,1449,1451,1453],{"class":82,"line":427},[389,1450,488],{"class":394},[389,1452,492],{"class":491},[389,1454,495],{"class":398},[389,1456,1457,1459,1461],{"class":82,"line":440},[389,1458,500],{"class":394},[389,1460,744],{"class":491},[389,1462,747],{"class":398},[389,1464,1465,1467,1470,1472,1475,1477],{"class":82,"line":451},[389,1466,511],{"class":433},[389,1468,1469],{"class":398},"._refresh_timer ",[389,1471,421],{"class":394},[389,1473,1474],{"class":398}," QTimer(",[389,1476,434],{"class":433},[389,1478,1479],{"class":398},".iface.mainWindow())\n",[389,1481,1482,1484,1487,1490],{"class":82,"line":552},[389,1483,511],{"class":433},[389,1485,1486],{"class":398},"._refresh_timer.setSingleShot(",[389,1488,1489],{"class":433},"True",[389,1491,630],{"class":398},[389,1493,1494,1496,1499,1501],{"class":82,"line":563},[389,1495,511],{"class":433},[389,1497,1498],{"class":398},"._refresh_timer.setInterval(",[389,1500,100],{"class":433},[389,1502,630],{"class":398},[389,1504,1505,1507,1510,1512],{"class":82,"line":583},[389,1506,511],{"class":433},[389,1508,1509],{"class":398},"._refresh_timer.timeout.connect(",[389,1511,434],{"class":433},[389,1513,1514],{"class":398},".recompute)\n",[389,1516,1517],{"class":82,"line":591},[389,1518,412],{"emptyLinePlaceholder":411},[389,1520,1521,1523,1526],{"class":82,"line":602},[389,1522,500],{"class":394},[389,1524,1525],{"class":491}," on_attribute_changed",[389,1527,1528],{"class":398},"(self, fid, index, value):\n",[389,1530,1531,1533,1536],{"class":82,"line":613},[389,1532,511],{"class":433},[389,1534,1535],{"class":398},"._refresh_timer.start()          ",[389,1537,1538],{"class":534},"# restarts if already running\n",[389,1540,1541],{"class":82,"line":619},[389,1542,412],{"emptyLinePlaceholder":411},[389,1544,1545,1547,1550],{"class":82,"line":633},[389,1546,500],{"class":394},[389,1548,1549],{"class":491}," recompute",[389,1551,747],{"class":398},[389,1553,1554,1557],{"class":82,"line":638},[389,1555,1556],{"class":433},"        ...",[389,1558,1559],{"class":534},"                                   # the expensive work, once\n",[14,1561,1562,465,1564,1567,1568,1571,1572,1575,1576,1578],{},[186,1563,464],{},[194,1565,1566],{},"setSingleShot(True)"," means the timer fires once rather than repeating. Calling ",[194,1569,1570],{},"start()"," on an already-running single-shot timer restarts it, which is what collapses a burst into one call — the handler itself does nothing but reset the clock, so it stays cheap enough to run thousands of times. Parenting the timer to the main window ties its lifetime to something that outlives the burst. Two hundred milliseconds is short enough to feel immediate and long enough to swallow a bulk edit. Remember to ",[194,1573,1574],{},"stop()"," and disconnect the timer in ",[194,1577,200],{}," alongside everything else.",[175,1580,1582],{"id":1581},"qgis-version-compatibility","QGIS version compatibility",[14,1584,1585,1586,1588],{},"The examples target ",[186,1587,188],{}," (Python 3.12).",[230,1590,1591,1604],{},[233,1592,1593],{},[236,1594,1595,1598,1601],{},[239,1596,1597],{},"QGIS version",[239,1599,1600],{},"Python",[239,1602,1603],{},"Notes",[252,1605,1606,1617,1628],{},[236,1607,1608,1611,1614],{},[257,1609,1610],{},"3.28 LTR",[257,1612,1613],{},"3.9",[257,1615,1616],{},"Identical signal set and signatures.",[236,1618,1619,1622,1625],{},[257,1620,1621],{},"3.34 LTR",[257,1623,1624],{},"3.12",[257,1626,1627],{},"Baseline for this page.",[236,1629,1630,1633,1635],{},[257,1631,1632],{},"3.40 \u002F 3.44",[257,1634,1624],{},[257,1636,1637,1640],{},[194,1638,1639],{},"QgsProject.layersWillBeRemoved"," added as a batch counterpart; the singular form remains.",[14,1642,1643],{},"The project and vector-layer signals used here have been stable across the whole 3.x line.",[175,1645,1647],{"id":1646},"troubleshooting","Troubleshooting",[180,1649,1650,1662,1670,1679,1687,1702],{},[183,1651,1652,1655,1656,1658,1659,1661],{},[186,1653,1654],{},"QGIS crashes when a layer is removed."," A handler on ",[194,1657,292],{}," looked the layer up and used it. Do that work in ",[194,1660,277],{}," instead.",[183,1663,1664,1667,1668,206],{},[186,1665,1666],{},"Every action happens twice after a reload."," The previous instance's connections were never removed. Disconnect everything in ",[194,1669,200],{},[183,1671,1672,1675,1676,1678],{},[186,1673,1674],{},"A newly added layer is ignored."," The plugin only wired the layers present at startup. Connect to ",[194,1677,261],{}," and wire them as they arrive.",[183,1680,1681,465,1684,1686],{},[186,1682,1683],{},"Only the first layer of a GeoPackage is handled.",[194,1685,261],{}," passes a list; the handler treated it as a single layer.",[183,1688,1689,1694,1695,1698,1699,206],{},[186,1690,1691,1693],{},[194,1692,1236],{}," raises TypeError."," The connection is already gone, which is normal during teardown. Wrap it in a ",[194,1696,1697],{},"try","\u002F",[194,1700,1701],{},"except TypeError",[183,1703,1704,1707,1708,1710],{},[186,1705,1706],{},"The plugin shows stale data after opening a new project."," Nothing is connected to ",[194,1709,307],{},". Reset caches there.",[175,1712,1714],{"id":1713},"conclusion","Conclusion",[14,1716,1717,1718,1720,1721,1723,1724,1726,1727,1729],{},"Signals keep a plugin honest about a project that changes underneath it. Connect to ",[194,1719,261],{}," and wire existing layers through the same function, do all layer-touching cleanup in ",[194,1722,277],{}," while the object is still alive, reset caches on ",[194,1725,307],{},", and disconnect every single connection in ",[194,1728,200],{}," — the last one is what makes the difference between a plugin that reloads cleanly and one that has to be debugged by restarting QGIS.",[175,1731,1733],{"id":1732},"frequently-asked-questions","Frequently Asked Questions",[14,1735,1736,1739,1741,1742,1744],{},[186,1737,1738],{},"What is the difference between layerWillBeRemoved and layersRemoved?",[194,1740,277],{}," fires before the layer is destroyed, so the object can still be resolved and safely disconnected. ",[194,1743,292],{}," fires afterwards and carries only identifiers — looking those up or touching a cached wrapper crashes QGIS.",[14,1746,1747,1750,1751,1753,1754,1698,1756,1758],{},[186,1748,1749],{},"Why does my plugin react twice to everything after a reload?","\nThe previous instance's signal connections are still attached. Disconnect every signal you connect in the matching ",[194,1752,200],{},", wrapping each call in a ",[194,1755,1697],{},[194,1757,1701],{}," so it is safe when the connection has already gone.",[14,1760,1761,1764,1765,1768],{},[186,1762,1763],{},"How do I connect to layers the user adds later?","\nConnect to ",[194,1766,1767],{},"QgsProject.layersAdded"," and do the per-layer wiring there, then call the same function once with the layers already present. That way one code path handles both cases.",[14,1770,1771,1774,1775,1777],{},[186,1772,1773],{},"How do I know which layer emitted a signal?","\nCall ",[194,1776,926],{}," inside the slot. It returns the emitting object, which lets one handler serve every layer without creating a lambda per connection — lambdas would keep references alive and leak.",[14,1779,1780,1783,1784,1787],{},[186,1781,1782],{},"Is there a signal for the project being saved?","\nYes, ",[194,1785,1786],{},"QgsProject.projectSaved",", which fires after a successful write. It is the right place to clear a dirty flag your plugin maintains, and it does not fire when the save fails.",[175,1789,1791],{"id":1790},"related","Related",[180,1793,1794,1799,1803,1807],{},[183,1795,1796,1798],{},[26,1797,29],{"href":28}," — the guide this recipe belongs to",[183,1800,1801],{},[26,1802,205],{"href":204},[183,1804,1805],{},[26,1806,1248],{"href":1247},[183,1808,1809],{},[26,1810,1812],{"href":1811},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","Create a Custom Map Tool in PyQGIS",[1814,1815,1816],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":385,"searchDepth":408,"depth":408,"links":1818},[1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829],{"id":177,"depth":408,"text":178},{"id":217,"depth":408,"text":218},{"id":474,"depth":408,"text":475},{"id":701,"depth":408,"text":702},{"id":935,"depth":408,"text":936},{"id":1252,"depth":408,"text":1253},{"id":1581,"depth":408,"text":1582},{"id":1646,"depth":408,"text":1647},{"id":1713,"depth":408,"text":1714},{"id":1732,"depth":408,"text":1733},{"id":1790,"depth":408,"text":1791},"React to QGIS project and layer changes from a plugin. Connect to layersAdded, selectionChanged and editingStopped, avoid stale references, and disconnect on unload.","md",{"slug":1833,"type":1834,"breadcrumb":1835,"datePublished":1836,"dateModified":1836},"connect-layer-signals-pyqgis","article","Layer & Project Signals","2026-08-01","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis",{"title":5,"description":1830},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002Findex","X5m6MTDmQkm9HT3JomsS44u1e6HJ5pZ9fHgIp-X11pc",1785585990473]