[{"data":1,"prerenderedAt":1723},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis":3},{"id":4,"title":5,"body":6,"description":1712,"extension":1713,"meta":1714,"navigation":292,"path":1719,"seo":1720,"stem":1721,"__hash__":1722},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002Findex.md","Create a Custom Map Tool in PyQGIS",{"type":7,"value":8,"toc":1700},"minimark",[9,13,30,39,171,176,210,214,221,502,535,539,546,891,910,914,1026,1029,1152,1185,1189,1194,1288,1291,1434,1457,1461,1467,1530,1535,1539,1594,1598,1604,1608,1616,1626,1632,1638,1647,1656,1662,1671,1675,1696],[10,11,5],"h1",{"id":12},"create-a-custom-map-tool-in-pyqgis",[14,15,16,17,21,22,25,26,29],"p",{},"The first custom map tool most people write crashes QGIS outright, and the reason is not in the tool at all. ",[18,19,20],"code",{},"QgsMapCanvas.setMapTool()"," stores a non-owning pointer, so if the only Python reference to the tool is a local variable inside the function that installed it, the object is collected as soon as that function returns — and the next mouse move dereferences freed memory. The fix is one character: ",[18,23,24],{},"self.tool"," rather than ",[18,27,28],{},"tool",".",[14,31,32,33,38],{},"This page is a focused recipe within ",[34,35,37],"a",{"href":36},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",". It covers a minimal working tool, wiring it to a checkable toolbar button so the interface state stays honest, and the unload discipline that lets the plugin be reloaded during development.",[14,40,41],{},[42,43,48,52,56,63,80,89,99,105,110,115,120,125,130,134,138,141,144,151,155,162,167],"svg",{"viewBox":44,"role":45,"ariaLabel":46,"xmlns":47},"0 0 760 254","img","The ownership relationship between a plugin, its tool and the canvas, showing that the canvas holds a non-owning pointer so the plugin must hold the only strong reference","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[49,50,51],"title",{},"Who owns the map tool",[53,54,55],"desc",{},"The plugin instance holds a strong reference to the tool, keeping it alive. The canvas holds only a weak, non-owning pointer used to deliver events. If the plugin's reference is a local variable that goes out of scope, the tool is garbage collected while the canvas still points at it, and the next event dereferences freed memory.",[57,58],"rect",{"x":59,"y":59,"width":60,"height":61,"fill":62},"0","760","254","#f6f3ea",[64,65,66],"defs",{},[67,68,75],"marker",{"id":69,"viewBox":70,"refX":71,"refY":72,"markerWidth":73,"markerHeight":73,"orient":74},"toolOwnArrow","0 0 10 10","8","5","7","auto-start-reverse",[76,77],"path",{"d":78,"fill":79},"M0 0 L10 5 L0 10 z","#2f3b35",[81,82,88],"text",{"x":83,"y":84,"style":85,"fill":86,"textAnchor":87},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The canvas points at the tool but does not keep it alive",[57,90],{"x":91,"y":92,"width":93,"height":94,"rx":95,"fill":96,"stroke":97,"style":98},"16","76","168","80","10","#fffdf7","#15803d","stroke-width:2.5",[81,100,104],{"x":101,"y":102,"style":103,"fill":97,"textAnchor":87},"100","104","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","plugin instance",[81,106,109],{"x":101,"y":107,"style":108,"fill":79,"textAnchor":87},"126","text-anchor:middle;font-size:11px;font-family:monospace","self.tool = MyTool()",[81,111,114],{"x":101,"y":112,"style":113,"fill":97,"textAnchor":87},"144","text-anchor:middle;font-size:10.5px;font-family:sans-serif","strong reference",[57,116],{"x":117,"y":92,"width":93,"height":94,"rx":95,"fill":118,"stroke":119,"style":98},"296","#26322d","#0f766e",[81,121,124],{"x":83,"y":122,"style":103,"fill":123,"textAnchor":87},"106","#d9f99d","MyTool",[81,126,129],{"x":83,"y":127,"style":128,"fill":123,"textAnchor":87},"130","text-anchor:middle;font-size:11px;font-family:sans-serif","QgsMapTool subclass",[57,131],{"x":132,"y":92,"width":93,"height":94,"rx":95,"fill":96,"stroke":133,"style":98},"576","#b45309",[81,135,137],{"x":136,"y":102,"style":103,"fill":133,"textAnchor":87},"660","QgsMapCanvas",[81,139,140],{"x":136,"y":107,"style":128,"fill":79,"textAnchor":87},"delivers events",[81,142,143],{"x":136,"y":112,"style":113,"fill":133,"textAnchor":87},"non-owning pointer",[145,146],"line",{"x1":147,"y1":148,"x2":149,"y2":148,"stroke":97,"style":150},"184","116","292","stroke-width:3;marker-end:url(#toolOwnArrow)",[145,152],{"x1":132,"y1":148,"x2":153,"y2":148,"stroke":133,"style":154},"468","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#toolOwnArrow)",[57,156],{"x":157,"y":158,"width":159,"height":160,"rx":71,"fill":96,"stroke":161,"style":98},"152","188","456","48","#b91c1c",[81,163,166],{"x":83,"y":164,"style":165,"fill":161,"textAnchor":87},"208","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","tool = MyTool()  — a local variable",[81,168,170],{"x":83,"y":169,"style":128,"fill":79,"textAnchor":87},"227","collected on return; the next mouse move crashes QGIS",[172,173,175],"h2",{"id":174},"prerequisites","Prerequisites",[177,178,179,187,202],"ul",{},[180,181,182,186],"li",{},[183,184,185],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, running as a desktop application.",[180,188,189,190,193,194,197,198,29],{},"A plugin skeleton with ",[18,191,192],{},"initGui()"," and ",[18,195,196],{},"unload()"," — see ",[34,199,201],{"href":200},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",[180,203,204,205,209],{},"A toolbar action to trigger the tool; ",[34,206,208],{"href":207},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","Add a Toolbar Button to a QGIS Plugin"," covers creating one.",[172,211,213],{"id":212},"a-minimal-working-tool","A minimal working tool",[14,215,216,217,220],{},"Subclass ",[18,218,219],{},"QgsMapTool",", override the event handlers you need, and emit a signal rather than acting directly.",[222,223,228],"pre",{"className":224,"code":225,"language":226,"meta":227,"style":227},"language-python shiki shiki-themes github-dark","from qgis.core import QgsPointXY\nfrom qgis.gui import QgsMapTool\nfrom qgis.PyQt.QtCore import Qt, pyqtSignal\nfrom qgis.PyQt.QtGui import QCursor\n\n\nclass PointPickTool(QgsMapTool):\n    \"\"\"Emit the map coordinate of each left click.\"\"\"\n\n    pointPicked = pyqtSignal(QgsPointXY)\n\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.canvas = canvas\n\n    def activate(self):\n        super().activate()\n        self.canvas.setCursor(QCursor(Qt.CrossCursor))\n\n    def canvasReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n        self.pointPicked.emit(self.toMapCoordinates(event.pos()))\n\n    def deactivate(self):\n        super().deactivate()\n        self.deactivated.emit()\n","python","",[18,229,230,248,261,274,287,294,299,317,324,329,341,346,359,374,388,393,404,412,420,425,436,451,457,471,476,486,494],{"__ignoreMap":227},[231,232,234,238,242,245],"span",{"class":145,"line":233},1,[231,235,237],{"class":236},"snl16","from",[231,239,241],{"class":240},"s95oV"," qgis.core ",[231,243,244],{"class":236},"import",[231,246,247],{"class":240}," QgsPointXY\n",[231,249,251,253,256,258],{"class":145,"line":250},2,[231,252,237],{"class":236},[231,254,255],{"class":240}," qgis.gui ",[231,257,244],{"class":236},[231,259,260],{"class":240}," QgsMapTool\n",[231,262,264,266,269,271],{"class":145,"line":263},3,[231,265,237],{"class":236},[231,267,268],{"class":240}," qgis.PyQt.QtCore ",[231,270,244],{"class":236},[231,272,273],{"class":240}," Qt, pyqtSignal\n",[231,275,277,279,282,284],{"class":145,"line":276},4,[231,278,237],{"class":236},[231,280,281],{"class":240}," qgis.PyQt.QtGui ",[231,283,244],{"class":236},[231,285,286],{"class":240}," QCursor\n",[231,288,290],{"class":145,"line":289},5,[231,291,293],{"emptyLinePlaceholder":292},true,"\n",[231,295,297],{"class":145,"line":296},6,[231,298,293],{"emptyLinePlaceholder":292},[231,300,302,305,309,312,314],{"class":145,"line":301},7,[231,303,304],{"class":236},"class",[231,306,308],{"class":307},"svObZ"," PointPickTool",[231,310,311],{"class":240},"(",[231,313,219],{"class":307},[231,315,316],{"class":240},"):\n",[231,318,320],{"class":145,"line":319},8,[231,321,323],{"class":322},"sU2Wk","    \"\"\"Emit the map coordinate of each left click.\"\"\"\n",[231,325,327],{"class":145,"line":326},9,[231,328,293],{"emptyLinePlaceholder":292},[231,330,332,335,338],{"class":145,"line":331},10,[231,333,334],{"class":240},"    pointPicked ",[231,336,337],{"class":236},"=",[231,339,340],{"class":240}," pyqtSignal(QgsPointXY)\n",[231,342,344],{"class":145,"line":343},11,[231,345,293],{"emptyLinePlaceholder":292},[231,347,349,352,356],{"class":145,"line":348},12,[231,350,351],{"class":236},"    def",[231,353,355],{"class":354},"sDLfK"," __init__",[231,357,358],{"class":240},"(self, canvas):\n",[231,360,362,365,368,371],{"class":145,"line":361},13,[231,363,364],{"class":354},"        super",[231,366,367],{"class":240},"().",[231,369,370],{"class":354},"__init__",[231,372,373],{"class":240},"(canvas)\n",[231,375,377,380,383,385],{"class":145,"line":376},14,[231,378,379],{"class":354},"        self",[231,381,382],{"class":240},".canvas ",[231,384,337],{"class":236},[231,386,387],{"class":240}," canvas\n",[231,389,391],{"class":145,"line":390},15,[231,392,293],{"emptyLinePlaceholder":292},[231,394,396,398,401],{"class":145,"line":395},16,[231,397,351],{"class":236},[231,399,400],{"class":307}," activate",[231,402,403],{"class":240},"(self):\n",[231,405,407,409],{"class":145,"line":406},17,[231,408,364],{"class":354},[231,410,411],{"class":240},"().activate()\n",[231,413,415,417],{"class":145,"line":414},18,[231,416,379],{"class":354},[231,418,419],{"class":240},".canvas.setCursor(QCursor(Qt.CrossCursor))\n",[231,421,423],{"class":145,"line":422},19,[231,424,293],{"emptyLinePlaceholder":292},[231,426,428,430,433],{"class":145,"line":427},20,[231,429,351],{"class":236},[231,431,432],{"class":307}," canvasReleaseEvent",[231,434,435],{"class":240},"(self, event):\n",[231,437,439,442,445,448],{"class":145,"line":438},21,[231,440,441],{"class":236},"        if",[231,443,444],{"class":240}," event.button() ",[231,446,447],{"class":236},"!=",[231,449,450],{"class":240}," Qt.LeftButton:\n",[231,452,454],{"class":145,"line":453},22,[231,455,456],{"class":236},"            return\n",[231,458,460,462,465,468],{"class":145,"line":459},23,[231,461,379],{"class":354},[231,463,464],{"class":240},".pointPicked.emit(",[231,466,467],{"class":354},"self",[231,469,470],{"class":240},".toMapCoordinates(event.pos()))\n",[231,472,474],{"class":145,"line":473},24,[231,475,293],{"emptyLinePlaceholder":292},[231,477,479,481,484],{"class":145,"line":478},25,[231,480,351],{"class":236},[231,482,483],{"class":307}," deactivate",[231,485,403],{"class":240},[231,487,489,491],{"class":145,"line":488},26,[231,490,364],{"class":354},[231,492,493],{"class":240},"().deactivate()\n",[231,495,497,499],{"class":145,"line":496},27,[231,498,379],{"class":354},[231,500,501],{"class":240},".deactivated.emit()\n",[14,503,504,507,508,511,512,193,515,518,519,522,523,527,528,531,532,534],{},[183,505,506],{},"Breakdown:"," ",[18,509,510],{},"pyqtSignal(QgsPointXY)"," declares the tool's output without binding it to any particular consumer, which is what makes the tool reusable across dialogs and dock widgets. ",[18,513,514],{},"activate()",[18,516,517],{},"deactivate()"," are called by the canvas when the tool is installed or replaced — always call ",[18,520,521],{},"super()"," so the base class housekeeping runs. Handling ",[524,525,526],"em",{},"release"," rather than press follows every built-in tool: it lets a user press, change their mind, drag away and release harmlessly. ",[18,529,530],{},"self.deactivated.emit()"," in ",[18,533,517],{}," is what lets a checkable toolbar button untick itself when the user picks a different tool.",[172,536,538],{"id":537},"wire-it-to-a-checkable-action","Wire it to a checkable action",[14,540,541,542,545],{},"A tool button that stays pressed after the user has switched to pan is worse than no button. Making the action checkable and listening for ",[18,543,544],{},"deactivated"," keeps the two in step.",[222,547,549],{"className":224,"code":548,"language":226,"meta":227,"style":227},"from qgis.PyQt.QtWidgets import QAction\nfrom qgis.PyQt.QtGui import QIcon\n\n\nclass MyPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.tool = None\n        self.action = None\n\n    def initGui(self):\n        self.action = QAction(QIcon(\":\u002Fplugins\u002Fmyplugin\u002Fpick.svg\"), \"Pick a point\",\n                              self.iface.mainWindow())\n        self.action.setCheckable(True)\n        self.action.triggered.connect(self.toggle_tool)\n        self.iface.addToolBarIcon(self.action)\n\n        self.tool = PointPickTool(self.iface.mapCanvas())\n        self.tool.setAction(self.action)\n        self.tool.pointPicked.connect(self.on_point_picked)\n\n    def toggle_tool(self, checked):\n        canvas = self.iface.mapCanvas()\n        if checked:\n            canvas.setMapTool(self.tool)\n        else:\n            canvas.unsetMapTool(self.tool)\n\n    def on_point_picked(self, point):\n        self.iface.messageBar().pushInfo(\n            \"Picked\", f\"{point.x():.2f}, {point.y():.2f}\"\n        )\n",[18,550,551,563,574,578,582,592,601,613,625,636,640,649,672,680,693,705,717,721,737,748,760,764,774,787,794,804,811,820,825,836,844,885],{"__ignoreMap":227},[231,552,553,555,558,560],{"class":145,"line":233},[231,554,237],{"class":236},[231,556,557],{"class":240}," qgis.PyQt.QtWidgets ",[231,559,244],{"class":236},[231,561,562],{"class":240}," QAction\n",[231,564,565,567,569,571],{"class":145,"line":250},[231,566,237],{"class":236},[231,568,281],{"class":240},[231,570,244],{"class":236},[231,572,573],{"class":240}," QIcon\n",[231,575,576],{"class":145,"line":263},[231,577,293],{"emptyLinePlaceholder":292},[231,579,580],{"class":145,"line":276},[231,581,293],{"emptyLinePlaceholder":292},[231,583,584,586,589],{"class":145,"line":289},[231,585,304],{"class":236},[231,587,588],{"class":307}," MyPlugin",[231,590,591],{"class":240},":\n",[231,593,594,596,598],{"class":145,"line":296},[231,595,351],{"class":236},[231,597,355],{"class":354},[231,599,600],{"class":240},"(self, iface):\n",[231,602,603,605,608,610],{"class":145,"line":301},[231,604,379],{"class":354},[231,606,607],{"class":240},".iface ",[231,609,337],{"class":236},[231,611,612],{"class":240}," iface\n",[231,614,615,617,620,622],{"class":145,"line":319},[231,616,379],{"class":354},[231,618,619],{"class":240},".tool ",[231,621,337],{"class":236},[231,623,624],{"class":354}," None\n",[231,626,627,629,632,634],{"class":145,"line":326},[231,628,379],{"class":354},[231,630,631],{"class":240},".action ",[231,633,337],{"class":236},[231,635,624],{"class":354},[231,637,638],{"class":145,"line":331},[231,639,293],{"emptyLinePlaceholder":292},[231,641,642,644,647],{"class":145,"line":343},[231,643,351],{"class":236},[231,645,646],{"class":307}," initGui",[231,648,403],{"class":240},[231,650,651,653,655,657,660,663,666,669],{"class":145,"line":348},[231,652,379],{"class":354},[231,654,631],{"class":240},[231,656,337],{"class":236},[231,658,659],{"class":240}," QAction(QIcon(",[231,661,662],{"class":322},"\":\u002Fplugins\u002Fmyplugin\u002Fpick.svg\"",[231,664,665],{"class":240},"), ",[231,667,668],{"class":322},"\"Pick a point\"",[231,670,671],{"class":240},",\n",[231,673,674,677],{"class":145,"line":361},[231,675,676],{"class":354},"                              self",[231,678,679],{"class":240},".iface.mainWindow())\n",[231,681,682,684,687,690],{"class":145,"line":376},[231,683,379],{"class":354},[231,685,686],{"class":240},".action.setCheckable(",[231,688,689],{"class":354},"True",[231,691,692],{"class":240},")\n",[231,694,695,697,700,702],{"class":145,"line":390},[231,696,379],{"class":354},[231,698,699],{"class":240},".action.triggered.connect(",[231,701,467],{"class":354},[231,703,704],{"class":240},".toggle_tool)\n",[231,706,707,709,712,714],{"class":145,"line":395},[231,708,379],{"class":354},[231,710,711],{"class":240},".iface.addToolBarIcon(",[231,713,467],{"class":354},[231,715,716],{"class":240},".action)\n",[231,718,719],{"class":145,"line":406},[231,720,293],{"emptyLinePlaceholder":292},[231,722,723,725,727,729,732,734],{"class":145,"line":414},[231,724,379],{"class":354},[231,726,619],{"class":240},[231,728,337],{"class":236},[231,730,731],{"class":240}," PointPickTool(",[231,733,467],{"class":354},[231,735,736],{"class":240},".iface.mapCanvas())\n",[231,738,739,741,744,746],{"class":145,"line":422},[231,740,379],{"class":354},[231,742,743],{"class":240},".tool.setAction(",[231,745,467],{"class":354},[231,747,716],{"class":240},[231,749,750,752,755,757],{"class":145,"line":427},[231,751,379],{"class":354},[231,753,754],{"class":240},".tool.pointPicked.connect(",[231,756,467],{"class":354},[231,758,759],{"class":240},".on_point_picked)\n",[231,761,762],{"class":145,"line":438},[231,763,293],{"emptyLinePlaceholder":292},[231,765,766,768,771],{"class":145,"line":453},[231,767,351],{"class":236},[231,769,770],{"class":307}," toggle_tool",[231,772,773],{"class":240},"(self, checked):\n",[231,775,776,779,781,784],{"class":145,"line":459},[231,777,778],{"class":240},"        canvas ",[231,780,337],{"class":236},[231,782,783],{"class":354}," self",[231,785,786],{"class":240},".iface.mapCanvas()\n",[231,788,789,791],{"class":145,"line":473},[231,790,441],{"class":236},[231,792,793],{"class":240}," checked:\n",[231,795,796,799,801],{"class":145,"line":478},[231,797,798],{"class":240},"            canvas.setMapTool(",[231,800,467],{"class":354},[231,802,803],{"class":240},".tool)\n",[231,805,806,809],{"class":145,"line":488},[231,807,808],{"class":236},"        else",[231,810,591],{"class":240},[231,812,813,816,818],{"class":145,"line":496},[231,814,815],{"class":240},"            canvas.unsetMapTool(",[231,817,467],{"class":354},[231,819,803],{"class":240},[231,821,823],{"class":145,"line":822},28,[231,824,293],{"emptyLinePlaceholder":292},[231,826,828,830,833],{"class":145,"line":827},29,[231,829,351],{"class":236},[231,831,832],{"class":307}," on_point_picked",[231,834,835],{"class":240},"(self, point):\n",[231,837,839,841],{"class":145,"line":838},30,[231,840,379],{"class":354},[231,842,843],{"class":240},".iface.messageBar().pushInfo(\n",[231,845,847,850,853,856,859,862,865,868,871,873,875,878,880,882],{"class":145,"line":846},31,[231,848,849],{"class":322},"            \"Picked\"",[231,851,852],{"class":240},", ",[231,854,855],{"class":236},"f",[231,857,858],{"class":322},"\"",[231,860,861],{"class":354},"{",[231,863,864],{"class":240},"point.x()",[231,866,867],{"class":236},":.2f",[231,869,870],{"class":354},"}",[231,872,852],{"class":322},[231,874,861],{"class":354},[231,876,877],{"class":240},"point.y()",[231,879,867],{"class":236},[231,881,870],{"class":354},[231,883,884],{"class":322},"\"\n",[231,886,888],{"class":145,"line":887},32,[231,889,890],{"class":240},"        )\n",[14,892,893,507,895,897,898,901,902,905,906,909],{},[183,894,506],{},[18,896,24],{}," on the plugin instance is the strong reference that keeps the object alive — this is the line that prevents the crash. ",[18,899,900],{},"setAction()"," on the tool tells QGIS which action represents it, and QGIS then unchecks that action automatically when another tool takes over, so the button never lies about what is active. ",[18,903,904],{},"unsetMapTool()"," restores whatever tool was active before rather than leaving the canvas with nothing. ",[18,907,908],{},"messageBar().pushInfo()"," is the idiomatic way to report a result without a modal dialog interrupting the user.",[172,911,913],{"id":912},"handle-activation-state-cleanly","Handle activation state cleanly",[14,915,916],{},[42,917,920,923,926,929,936,939,945,949,952,956,959,962,965,968,973,978,982,986,989,993,996,1000,1006,1010,1015,1017,1022],{"viewBox":918,"role":45,"ariaLabel":919,"xmlns":47},"0 0 760 250","The tool state machine: idle, active and replaced, with the transitions triggered by the toolbar action, another tool being chosen, and plugin unload",[49,921,922],{},"The states a map tool moves through",[53,924,925],{},"A tool starts idle with its action unchecked. Clicking the action calls setMapTool and moves it to active, where activate runs and the cursor changes. Choosing a different tool moves it back to idle via deactivate, which also unchecks the action. Plugin unload calls unsetMapTool and disconnects the signals, ending in a destroyed state.",[57,927],{"x":59,"y":59,"width":60,"height":928,"fill":62},"250",[64,930,931],{},[67,932,934],{"id":933,"viewBox":70,"refX":71,"refY":72,"markerWidth":73,"markerHeight":73,"orient":74},"toolStateArrow",[76,935],{"d":78,"fill":79},[81,937,938],{"x":83,"y":84,"style":85,"fill":86,"textAnchor":87},"Every arrow out of \"active\" must leave the canvas usable",[57,940],{"x":941,"y":942,"width":943,"height":92,"rx":95,"fill":96,"stroke":944,"style":98},"40","94","164","#59645f",[81,946,948],{"x":947,"y":947,"style":103,"fill":86,"textAnchor":87},"122","idle",[81,950,951],{"x":947,"y":112,"style":128,"fill":79,"textAnchor":87},"action unchecked",[81,953,955],{"x":947,"y":954,"style":128,"fill":79,"textAnchor":87},"160","tool alive, not installed",[57,957],{"x":958,"y":942,"width":943,"height":92,"rx":95,"fill":118,"stroke":119,"style":98},"298",[81,960,961],{"x":83,"y":947,"style":103,"fill":123,"textAnchor":87},"active",[81,963,964],{"x":83,"y":112,"style":128,"fill":123,"textAnchor":87},"activate() ran",[81,966,967],{"x":83,"y":954,"style":128,"fill":123,"textAnchor":87},"receiving events",[57,969],{"x":970,"y":971,"width":972,"height":92,"rx":95,"fill":96,"stroke":133,"style":98},"556","46","180",[81,974,977],{"x":975,"y":976,"style":103,"fill":133,"textAnchor":87},"646","74","replaced",[81,979,981],{"x":975,"y":980,"style":128,"fill":79,"textAnchor":87},"96","another tool chosen",[81,983,985],{"x":975,"y":984,"style":128,"fill":79,"textAnchor":87},"112","deactivate() runs",[57,987],{"x":970,"y":988,"width":972,"height":92,"rx":95,"fill":96,"stroke":161,"style":98},"146",[81,990,992],{"x":975,"y":991,"style":103,"fill":161,"textAnchor":87},"174","unloaded",[81,994,904],{"x":975,"y":995,"style":128,"fill":79,"textAnchor":87},"196",[81,997,999],{"x":975,"y":998,"style":128,"fill":79,"textAnchor":87},"212","signals disconnected",[145,1001],{"x1":1002,"y1":1003,"x2":1004,"y2":1003,"stroke":97,"style":1005},"204","120","294","stroke-width:2.5;marker-end:url(#toolStateArrow)",[81,1007,1009],{"x":1008,"y":984,"style":113,"fill":97,"textAnchor":87},"249","setMapTool",[145,1011],{"x1":1012,"y1":984,"x2":1013,"y2":1014,"stroke":133,"style":1005},"462","552","88",[145,1016],{"x1":1012,"y1":988,"x2":1013,"y2":972,"stroke":161,"style":1005},[76,1018],{"d":1019,"fill":1020,"stroke":133,"style":1021},"M646 122 Q646 240 380 240 Q160 240 122 176","none","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#toolStateArrow)",[81,1023,1025],{"x":83,"y":1024,"style":113,"fill":133,"textAnchor":87},"234","action unchecks itself via setAction()",[14,1027,1028],{},"The unload path is where a development loop lives or dies. A plugin that leaves its tool installed and its signals connected will behave strangely the moment it is reloaded.",[222,1030,1032],{"className":224,"code":1031,"language":226,"meta":227,"style":227},"    def unload(self):\n        canvas = self.iface.mapCanvas()\n        if canvas.mapTool() is self.tool:\n            canvas.unsetMapTool(self.tool)\n        try:\n            self.tool.pointPicked.disconnect(self.on_point_picked)\n        except TypeError:\n            pass                       # already disconnected\n        self.iface.removeToolBarIcon(self.action)\n        self.action.deleteLater()\n        self.tool = None\n        self.action = None\n",[18,1033,1034,1043,1053,1068,1076,1083,1095,1105,1114,1125,1132,1142],{"__ignoreMap":227},[231,1035,1036,1038,1041],{"class":145,"line":233},[231,1037,351],{"class":236},[231,1039,1040],{"class":307}," unload",[231,1042,403],{"class":240},[231,1044,1045,1047,1049,1051],{"class":145,"line":250},[231,1046,778],{"class":240},[231,1048,337],{"class":236},[231,1050,783],{"class":354},[231,1052,786],{"class":240},[231,1054,1055,1057,1060,1063,1065],{"class":145,"line":263},[231,1056,441],{"class":236},[231,1058,1059],{"class":240}," canvas.mapTool() ",[231,1061,1062],{"class":236},"is",[231,1064,783],{"class":354},[231,1066,1067],{"class":240},".tool:\n",[231,1069,1070,1072,1074],{"class":145,"line":276},[231,1071,815],{"class":240},[231,1073,467],{"class":354},[231,1075,803],{"class":240},[231,1077,1078,1081],{"class":145,"line":289},[231,1079,1080],{"class":236},"        try",[231,1082,591],{"class":240},[231,1084,1085,1088,1091,1093],{"class":145,"line":296},[231,1086,1087],{"class":354},"            self",[231,1089,1090],{"class":240},".tool.pointPicked.disconnect(",[231,1092,467],{"class":354},[231,1094,759],{"class":240},[231,1096,1097,1100,1103],{"class":145,"line":301},[231,1098,1099],{"class":236},"        except",[231,1101,1102],{"class":354}," TypeError",[231,1104,591],{"class":240},[231,1106,1107,1110],{"class":145,"line":319},[231,1108,1109],{"class":236},"            pass",[231,1111,1113],{"class":1112},"sjoCn","                       # already disconnected\n",[231,1115,1116,1118,1121,1123],{"class":145,"line":326},[231,1117,379],{"class":354},[231,1119,1120],{"class":240},".iface.removeToolBarIcon(",[231,1122,467],{"class":354},[231,1124,716],{"class":240},[231,1126,1127,1129],{"class":145,"line":331},[231,1128,379],{"class":354},[231,1130,1131],{"class":240},".action.deleteLater()\n",[231,1133,1134,1136,1138,1140],{"class":145,"line":343},[231,1135,379],{"class":354},[231,1137,619],{"class":240},[231,1139,337],{"class":236},[231,1141,624],{"class":354},[231,1143,1144,1146,1148,1150],{"class":145,"line":348},[231,1145,379],{"class":354},[231,1147,631],{"class":240},[231,1149,337],{"class":236},[231,1151,624],{"class":354},[14,1153,1154,1156,1157,1160,1161,1164,1165,1168,1169,1171,1172,1175,1176,1179,1180,1184],{},[183,1155,506],{}," Checking ",[18,1158,1159],{},"canvas.mapTool() is self.tool"," avoids unsetting a tool the user has since switched away from, which would rip the pan tool out from under them. ",[18,1162,1163],{},"disconnect()"," raises ",[18,1166,1167],{},"TypeError"," when the connection is already gone, so the guard makes ",[18,1170,196],{}," safe to call twice. ",[18,1173,1174],{},"deleteLater()"," schedules the Qt object for destruction on the event loop rather than freeing it while a signal may still be in flight. Setting the attributes to ",[18,1177,1178],{},"None"," releases the last Python references. This discipline is what makes ",[34,1181,1183],{"href":1182},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting"," actually work.",[172,1186,1188],{"id":1187},"choose-the-right-base-class","Choose the right base class",[14,1190,1191,1193],{},[18,1192,219],{}," is the general case, and QGIS ships three more specialised bases that remove boilerplate when they fit. Starting from the right one can halve the code.",[14,1195,1196],{},[42,1197,1200,1203,1206,1209,1212,1217,1224,1228,1232,1277,1282,1285],{"viewBox":1198,"role":45,"ariaLabel":1199,"xmlns":47},"0 0 760 256","Four map tool base classes compared by what they provide out of the box: QgsMapTool, QgsMapToolEmitPoint, QgsMapToolExtent and QgsMapToolIdentify",[49,1201,1202],{},"Four base classes and what each gives you",[53,1204,1205],{},"QgsMapTool is the general base with nothing implemented, suited to custom drag and key handling. QgsMapToolEmitPoint adds a canvasClicked signal carrying the map point. QgsMapToolExtent adds rubber-band rectangle dragging and an extentChanged signal. QgsMapToolIdentify adds feature identification against visible layers. Each row notes when to choose it.",[57,1207],{"x":59,"y":59,"width":60,"height":1208,"fill":62},"256",[81,1210,1211],{"x":83,"y":84,"style":85,"fill":86,"textAnchor":87},"Start from the closest base, not always the most general",[145,1213],{"x1":998,"y1":1214,"x2":1215,"y2":1214,"stroke":944,"style":1216},"66","744","stroke-width:1.5",[81,1218,1223],{"x":1219,"y":1220,"style":1221,"fill":86,"textAnchor":1222},"200","58","text-anchor:end;font-size:11.5px;font-weight:bold;font-family:sans-serif","end","base class",[81,1225,1227],{"x":1226,"y":1220,"style":165,"fill":86,"textAnchor":87},"400","gives you",[81,1229,1231],{"x":1230,"y":1220,"style":165,"fill":86,"textAnchor":87},"640","choose it when",[1233,1234,1236,1239,1243,1246,1251,1254,1257,1261,1264,1267,1271,1274],"g",{"style":1235},"font-size:11px;font-family:sans-serif",[81,1237,219],{"x":1219,"y":980,"style":1238,"fill":119,"textAnchor":1222},"text-anchor:end;font-weight:bold",[81,1240,1242],{"x":1226,"y":980,"style":1241,"fill":79,"textAnchor":87},"text-anchor:middle","nothing — you override all",[81,1244,1245],{"x":1230,"y":980,"style":1241,"fill":79,"textAnchor":87},"drag, keys, custom feedback",[81,1247,1250],{"x":1219,"y":1248,"style":1238,"fill":1249,"textAnchor":1222},"136","#2563eb","QgsMapToolEmitPoint",[81,1252,1253],{"x":1226,"y":1248,"style":1241,"fill":79,"textAnchor":87},"canvasClicked(point, button)",[81,1255,1256],{"x":1230,"y":1248,"style":1241,"fill":79,"textAnchor":87},"you only need one coordinate",[81,1258,1260],{"x":1219,"y":1259,"style":1238,"fill":133,"textAnchor":1222},"176","QgsMapToolExtent",[81,1262,1263],{"x":1226,"y":1259,"style":1241,"fill":79,"textAnchor":87},"rubber-band drag + extentChanged",[81,1265,1266],{"x":1230,"y":1259,"style":1241,"fill":79,"textAnchor":87},"the user drags a box",[81,1268,1270],{"x":1219,"y":1269,"style":1238,"fill":97,"textAnchor":1222},"216","QgsMapToolIdentify",[81,1272,1273],{"x":1226,"y":1269,"style":1241,"fill":79,"textAnchor":87},"identify across visible layers",[81,1275,1276],{"x":1230,"y":1269,"style":1241,"fill":79,"textAnchor":87},"clicking reports features",[145,1278],{"x1":998,"y1":1279,"x2":1215,"y2":1279,"stroke":944,"style":1280,"strokeOpacity":1281},"110","stroke-width:0.8",0.4,[145,1283],{"x1":998,"y1":1284,"x2":1215,"y2":1284,"stroke":944,"style":1280,"strokeOpacity":1281},"150",[145,1286],{"x1":998,"y1":1287,"x2":1215,"y2":1287,"stroke":944,"style":1280,"strokeOpacity":1281},"190",[14,1289,1290],{},"For the single-coordinate case, the specialised base removes the class entirely:",[222,1292,1294],{"className":224,"code":1293,"language":226,"meta":227,"style":227},"from qgis.gui import QgsMapToolEmitPoint\nfrom qgis.PyQt.QtCore import Qt\n\nself.tool = QgsMapToolEmitPoint(self.iface.mapCanvas())\nself.tool.setAction(self.action)\nself.tool.canvasClicked.connect(self.on_clicked)\n\n\ndef on_clicked(self, point, button):\n    if button != Qt.LeftButton:\n        return\n    self.iface.messageBar().pushInfo(\"Picked\", f\"{point.x():.2f}, {point.y():.2f}\")\n",[18,1295,1296,1307,1318,1322,1337,1347,1359,1363,1367,1378,1390,1395],{"__ignoreMap":227},[231,1297,1298,1300,1302,1304],{"class":145,"line":233},[231,1299,237],{"class":236},[231,1301,255],{"class":240},[231,1303,244],{"class":236},[231,1305,1306],{"class":240}," QgsMapToolEmitPoint\n",[231,1308,1309,1311,1313,1315],{"class":145,"line":250},[231,1310,237],{"class":236},[231,1312,268],{"class":240},[231,1314,244],{"class":236},[231,1316,1317],{"class":240}," Qt\n",[231,1319,1320],{"class":145,"line":263},[231,1321,293],{"emptyLinePlaceholder":292},[231,1323,1324,1326,1328,1330,1333,1335],{"class":145,"line":276},[231,1325,467],{"class":354},[231,1327,619],{"class":240},[231,1329,337],{"class":236},[231,1331,1332],{"class":240}," QgsMapToolEmitPoint(",[231,1334,467],{"class":354},[231,1336,736],{"class":240},[231,1338,1339,1341,1343,1345],{"class":145,"line":289},[231,1340,467],{"class":354},[231,1342,743],{"class":240},[231,1344,467],{"class":354},[231,1346,716],{"class":240},[231,1348,1349,1351,1354,1356],{"class":145,"line":296},[231,1350,467],{"class":354},[231,1352,1353],{"class":240},".tool.canvasClicked.connect(",[231,1355,467],{"class":354},[231,1357,1358],{"class":240},".on_clicked)\n",[231,1360,1361],{"class":145,"line":301},[231,1362,293],{"emptyLinePlaceholder":292},[231,1364,1365],{"class":145,"line":319},[231,1366,293],{"emptyLinePlaceholder":292},[231,1368,1369,1372,1375],{"class":145,"line":326},[231,1370,1371],{"class":236},"def",[231,1373,1374],{"class":307}," on_clicked",[231,1376,1377],{"class":240},"(self, point, button):\n",[231,1379,1380,1383,1386,1388],{"class":145,"line":331},[231,1381,1382],{"class":236},"    if",[231,1384,1385],{"class":240}," button ",[231,1387,447],{"class":236},[231,1389,450],{"class":240},[231,1391,1392],{"class":145,"line":343},[231,1393,1394],{"class":236},"        return\n",[231,1396,1397,1400,1403,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426,1428,1430,1432],{"class":145,"line":348},[231,1398,1399],{"class":354},"    self",[231,1401,1402],{"class":240},".iface.messageBar().pushInfo(",[231,1404,1405],{"class":322},"\"Picked\"",[231,1407,852],{"class":240},[231,1409,855],{"class":236},[231,1411,858],{"class":322},[231,1413,861],{"class":354},[231,1415,864],{"class":240},[231,1417,867],{"class":236},[231,1419,870],{"class":354},[231,1421,852],{"class":322},[231,1423,861],{"class":354},[231,1425,877],{"class":240},[231,1427,867],{"class":236},[231,1429,870],{"class":354},[231,1431,858],{"class":322},[231,1433,692],{"class":240},[14,1435,1436,507,1438,1440,1441,1444,1445,1448,1449,852,1451,1453,1454,1456],{},[183,1437,506],{},[18,1439,1250],{}," already converts the click to map coordinates and emits it, so there is no subclass, no ",[18,1442,1443],{},"canvasReleaseEvent",", and no conversion code to get wrong. The ",[18,1446,1447],{},"button"," argument comes free, which is what makes the right-click filter a single line. Everything else — the strong reference on ",[18,1450,467],{},[18,1452,900],{}," for the checkable button, disconnecting in ",[18,1455,196],{}," — applies exactly as it does to a hand-written subclass.",[172,1458,1460],{"id":1459},"qgis-version-compatibility","QGIS version compatibility",[14,1462,1463,1464,1466],{},"The examples target ",[183,1465,185],{}," (Python 3.12).",[1468,1469,1470,1486],"table",{},[1471,1472,1473],"thead",{},[1474,1475,1476,1480,1483],"tr",{},[1477,1478,1479],"th",{},"QGIS version",[1477,1481,1482],{},"Python",[1477,1484,1485],{},"Notes",[1487,1488,1489,1506,1517],"tbody",{},[1474,1490,1491,1495,1498],{},[1492,1493,1494],"td",{},"3.28 LTR",[1492,1496,1497],{},"3.9",[1492,1499,1500,1501,193,1503,1505],{},"Identical API. ",[18,1502,900],{},[18,1504,544],{}," available throughout 3.x.",[1474,1507,1508,1511,1514],{},[1492,1509,1510],{},"3.34 LTR",[1492,1512,1513],{},"3.12",[1492,1515,1516],{},"Baseline for this page.",[1474,1518,1519,1522,1524],{},[1492,1520,1521],{},"3.40 \u002F 3.44",[1492,1523,1513],{},[1492,1525,1526,1529],{},[18,1527,1528],{},"QgsMapToolHandler"," offers a higher-level way to bind a tool to an action; the manual form still works.",[14,1531,1532,1534],{},[18,1533,219],{},", its event handlers and the canvas install methods are unchanged across the 3.x line.",[172,1536,1538],{"id":1537},"troubleshooting","Troubleshooting",[177,1540,1541,1547,1555,1565,1571,1583],{},[180,1542,1543,1546],{},[183,1544,1545],{},"QGIS crashes when the tool activates."," The tool was a local variable and has been garbage collected. Store it on the plugin instance.",[180,1548,1549,507,1552,1554],{},[183,1550,1551],{},"The toolbar button stays pressed after switching tools.",[18,1553,900],{}," was not called on the tool, so QGIS cannot untick it.",[180,1556,1557,1560,1561,1564],{},[183,1558,1559],{},"Nothing happens on click."," The tool was created but never installed, or another tool replaced it. Print ",[18,1562,1563],{},"canvas.mapTool()"," to see what is actually active.",[180,1566,1567,1570],{},[183,1568,1569],{},"The first click works and then nothing does."," An exception was raised inside the handler. Qt swallows it to the log rather than surfacing it — check the Log Messages panel.",[180,1572,1573,1576,1577,1580,1581,29],{},[183,1574,1575],{},"After a reload, every click fires twice."," The previous instance's ",[18,1578,1579],{},"pointPicked"," connection was never disconnected. Disconnect in ",[18,1582,196],{},[180,1584,1585,507,1588,1590,1591,29],{},[183,1586,1587],{},"The cursor never changes.",[18,1589,514],{}," was overridden without calling ",[18,1592,1593],{},"super().activate()",[172,1595,1597],{"id":1596},"conclusion","Conclusion",[14,1599,1600,1601,1603],{},"A custom map tool is a small class plus two pieces of bookkeeping: keep a strong reference on the plugin instance so the object outlives the call that installed it, and bind the tool to its action with ",[18,1602,900],{}," so the interface reflects reality. Getting the unload path right costs six lines and makes the whole development loop pleasant.",[172,1605,1607],{"id":1606},"frequently-asked-questions","Frequently Asked Questions",[14,1609,1610,1613,1614,29],{},[183,1611,1612],{},"Why does QGIS crash when I activate my map tool?","\nThe canvas keeps a non-owning pointer to the tool. If your only reference was a local variable, Python collects the object as soon as the function returns and the next event dereferences freed memory. Store it as ",[18,1615,24],{},[14,1617,1618,1621,1622,1625],{},[183,1619,1620],{},"How do I make my toolbar button untick when the user picks another tool?","\nCall ",[18,1623,1624],{},"tool.setAction(action)"," on a checkable action. QGIS then unchecks that action automatically whenever a different tool becomes active.",[14,1627,1628,1631],{},[183,1629,1630],{},"Should I handle canvasPressEvent or canvasReleaseEvent?","\nRelease, for a click. It matches every built-in tool and lets a user abort by dragging away before letting go. Use press when you need to start a drag.",[14,1633,1634,1637],{},[183,1635,1636],{},"What must unload() do for a map tool?","\nUnset the tool if it is still active, disconnect every signal you connected, remove the toolbar icon, and drop the references. Skipping the disconnect is what causes duplicate handlers after a plugin reload.",[14,1639,1640,1643,1644,1646],{},[183,1641,1642],{},"How do I make my tool the active one when the plugin loads?","\nDo not. Taking over the canvas without the user asking is disorienting, and it overrides whatever they were doing. Install the tool only when its action is triggered, and restore the previous tool with ",[18,1645,904],{}," when the action is unchecked.",[14,1648,1649,1652,1653,1655],{},[183,1650,1651],{},"Can two plugins have map tools active at the same time?","\nNo. The canvas holds exactly one active tool, and installing a second replaces the first. That is why ",[18,1654,900],{}," matters: it lets QGIS untick your toolbar button automatically when another plugin's tool takes over, so your interface never claims to be active when it is not.",[14,1657,1658,1661],{},[183,1659,1660],{},"Should the tool do the work, or emit a signal?","\nEmit a signal. A tool that reports what happened stays reusable across dialogs and panels, while one that acts directly is welded to whatever it acts on and cannot be reused.",[14,1663,1664,1667,1668,1670],{},[183,1665,1666],{},"Can a map tool outlive the plugin that created it?","\nIt should not. Unset it and drop the reference in ",[18,1669,196],{},", or the canvas keeps pointing at an object your plugin no longer manages.",[172,1672,1674],{"id":1673},"related","Related",[177,1676,1677,1682,1688,1692],{},[180,1678,1679,1681],{},[34,1680,37],{"href":36}," — the guide this recipe belongs to",[180,1683,1684],{},[34,1685,1687],{"href":1686},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcapture-map-click-coordinates-pyqgis\u002F","Capture Map Click Coordinates in PyQGIS",[180,1689,1690],{},[34,1691,208],{"href":207},[180,1693,1694],{},[34,1695,1183],{"href":1182},[1697,1698,1699],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":227,"searchDepth":250,"depth":250,"links":1701},[1702,1703,1704,1705,1706,1707,1708,1709,1710,1711],{"id":174,"depth":250,"text":175},{"id":212,"depth":250,"text":213},{"id":537,"depth":250,"text":538},{"id":912,"depth":250,"text":913},{"id":1187,"depth":250,"text":1188},{"id":1459,"depth":250,"text":1460},{"id":1537,"depth":250,"text":1538},{"id":1596,"depth":250,"text":1597},{"id":1606,"depth":250,"text":1607},{"id":1673,"depth":250,"text":1674},"Build a QgsMapTool subclass in PyQGIS, wire it to a checkable toolbar action, keep a reference so QGIS does not crash, and restore the previous tool on unload.","md",{"slug":1715,"type":1716,"breadcrumb":1717,"datePublished":1718,"dateModified":1718},"create-custom-map-tool-pyqgis","article","Create a Map Tool","2026-08-01","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis",{"title":5,"description":1712},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002Findex","a8qAGJSvfNvyayeaFGZ9tGzEBI02Mxx7iT1jcCuFv1k",1785585990473]