[{"data":1,"prerenderedAt":2928},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction":3},{"id":4,"title":5,"body":6,"description":2918,"extension":2919,"meta":2920,"navigation":328,"path":2924,"seo":2925,"stem":2926,"__hash__":2927},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Findex.md","Custom Map Tools and Canvas Interaction",{"type":7,"value":8,"toc":2903},"minimark",[9,13,27,41,222,227,256,260,270,552,580,583,630,651,655,662,784,880,903,907,913,1295,1330,1334,1337,1475,1512,1516,1519,1598,1649,1679,1683,1686,1801,1817,1821,1824,1898,1926,1932,1966,1988,1992,1995,2527,2557,2560,2636,2641,2645,2648,2720,2723,2727,2778,2782,2792,2808,2823,2839,2848,2852,2899],[10,11,5],"h1",{"id":12},"custom-map-tools-and-canvas-interaction",[14,15,16,17,21,22,26],"p",{},"A plugin that only adds a dialog is a form with a map behind it. The plugins people actually keep installed are the ones that let you ",[18,19,20],"em",{},"point at something"," — click a parcel to see its history, drag a line to profile the terrain beneath it, rubber-band a box to select what falls inside. All of those are ",[23,24,25],"code",{},"QgsMapTool"," subclasses, and the API is small enough to learn in an afternoon once the event flow is clear.",[14,28,29,30,35,36,40],{},"This guide sits inside ",[31,32,34],"a",{"href":33},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development",". It covers the map tool lifecycle, converting screen pixels into map coordinates, drawing transient feedback with rubber bands, identifying the feature under the cursor, and connecting to the signals that tell your plugin the world changed underneath it. It assumes you already have a plugin skeleton — if not, start with ",[31,37,39],{"href":38},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",".",[14,42,43],{},[44,45,50,54,58,65,82,91,101,107,112,116,120,124,131,138,143,147,151,155,159,163,166,169,172,175,181,186,190,198,202,208,212,217],"svg",{"viewBox":46,"role":47,"ariaLabel":48,"xmlns":49},"0 0 760 274","img","The map tool event flow: the user activates a tool, mouse events arrive at the tool's handlers, the tool converts pixels to map coordinates and draws feedback, and deactivation tears the feedback down","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[51,52,53],"title",{},"How a custom map tool receives and handles events",[55,56,57],"desc",{},"A user clicks a toolbar action, which calls setMapTool on the canvas and activates the tool. Mouse press, move and release events are delivered to the tool's canvasPressEvent, canvasMoveEvent and canvasReleaseEvent handlers. Inside those handlers the tool converts pixel positions to map coordinates and updates a rubber band. When another tool is chosen, deactivate fires and the rubber band must be removed.",[59,60],"rect",{"x":61,"y":61,"width":62,"height":63,"fill":64},"0","760","274","#f6f3ea",[66,67,68],"defs",{},[69,70,77],"marker",{"id":71,"viewBox":72,"refX":73,"refY":74,"markerWidth":75,"markerHeight":75,"orient":76},"toolFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[78,79],"path",{"d":80,"fill":81},"M0 0 L10 5 L0 10 z","#2f3b35",[83,84,90],"text",{"x":85,"y":86,"style":87,"fill":88,"textAnchor":89},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Activate, receive events, draw feedback, clean up",[59,92],{"x":93,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":99,"style":100},"14","52","150","64","10","#fffdf7","#2563eb","stroke-width:2.5",[83,102,106],{"x":103,"y":104,"style":105,"fill":88,"textAnchor":89},"89","78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","toolbar action",[83,108,111],{"x":103,"y":109,"style":110,"fill":81,"textAnchor":89},"99","text-anchor:middle;font-size:11px;font-family:sans-serif","setMapTool(tool)",[59,113],{"x":93,"y":114,"width":95,"height":96,"rx":97,"fill":98,"stroke":115,"style":100},"152","#0f766e",[83,117,119],{"x":103,"y":118,"style":105,"fill":88,"textAnchor":89},"178","activate()",[83,121,123],{"x":103,"y":122,"style":110,"fill":81,"textAnchor":89},"199","set the cursor",[59,125],{"x":126,"y":127,"width":128,"height":129,"rx":97,"fill":130,"stroke":115,"style":100},"204","70","196","130","#26322d",[83,132,137],{"x":133,"y":134,"style":135,"fill":136,"textAnchor":89},"302","96","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","#d9f99d","your QgsMapTool",[83,139,142],{"x":133,"y":140,"style":141,"fill":136,"textAnchor":89},"120","text-anchor:middle;font-size:11.5px;font-family:sans-serif","canvasPressEvent",[83,144,146],{"x":133,"y":145,"style":141,"fill":136,"textAnchor":89},"140","canvasMoveEvent",[83,148,150],{"x":133,"y":149,"style":141,"fill":136,"textAnchor":89},"160","canvasReleaseEvent",[83,152,154],{"x":133,"y":153,"style":110,"fill":136,"textAnchor":89},"182","event.pos() is in pixels",[59,156],{"x":157,"y":94,"width":149,"height":96,"rx":97,"fill":98,"stroke":158,"style":100},"440","#b45309",[83,160,162],{"x":161,"y":104,"style":105,"fill":158,"textAnchor":89},"520","toMapCoordinates()",[83,164,165],{"x":161,"y":109,"style":110,"fill":81,"textAnchor":89},"pixels → map units",[59,167],{"x":157,"y":114,"width":149,"height":96,"rx":97,"fill":98,"stroke":168,"style":100},"#15803d",[83,170,171],{"x":161,"y":118,"style":105,"fill":168,"textAnchor":89},"QgsRubberBand",[83,173,174],{"x":161,"y":122,"style":110,"fill":81,"textAnchor":89},"transient feedback",[59,176],{"x":177,"y":178,"width":179,"height":96,"rx":97,"fill":98,"stroke":180,"style":100},"628","102","118","#b91c1c",[83,182,185],{"x":183,"y":184,"style":105,"fill":180,"textAnchor":89},"687","128","deactivate()",[83,187,189],{"x":183,"y":188,"style":110,"fill":81,"textAnchor":89},"149","reset the band",[191,192],"line",{"x1":193,"y1":194,"x2":195,"y2":196,"stroke":81,"style":197},"164","84","200","104","stroke-width:2.5;marker-end:url(#toolFlowArrow)",[191,199],{"x1":193,"y1":200,"x2":195,"y2":201,"stroke":81,"style":197},"184","166",[191,203],{"x1":204,"y1":205,"x2":206,"y2":207,"stroke":81,"style":197},"400","112","436","90",[191,209],{"x1":204,"y1":210,"x2":206,"y2":211,"stroke":81,"style":197},"158","180",[191,213],{"x1":214,"y1":211,"x2":215,"y2":114,"stroke":180,"style":216},"600","624","stroke-width:2.5;stroke-dasharray:5 4;marker-end:url(#toolFlowArrow)",[83,218,221],{"x":85,"y":219,"style":141,"fill":220,"textAnchor":89},"252","#59645f","a tool that does not clean up in deactivate() leaves its band on the canvas forever",[223,224,226],"h2",{"id":225},"prerequisites","Prerequisites",[228,229,230,238,245,253],"ul",{},[231,232,233,237],"li",{},[234,235,236],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, running as a desktop application — map tools need a canvas.",[231,239,240,241,40],{},"A working plugin skeleton with an action in the toolbar; see ",[31,242,244],{"href":243},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","Add a Toolbar Button to a QGIS Plugin",[231,246,247,248,252],{},"Familiarity with Qt signals and slots, since everything here is event-driven — ",[31,249,251],{"href":250},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces"," covers the basics.",[231,254,255],{},"A vector layer loaded, for the identify examples.",[223,257,259],{"id":258},"the-qgsmaptool-lifecycle","The QgsMapTool lifecycle",[14,261,262,263,265,266,269],{},"A map tool is a plain Python class deriving from ",[23,264,25],{}," (or ",[23,267,268],{},"QgsMapToolEmitPoint"," for the common click-a-point case). The canvas holds exactly one active tool at a time and routes mouse and key events to it.",[271,272,277],"pre",{"className":273,"code":274,"language":275,"meta":276,"style":276},"language-python shiki shiki-themes github-dark","from qgis.gui import QgsMapTool\nfrom qgis.PyQt.QtCore import Qt, pyqtSignal\nfrom qgis.PyQt.QtGui import QCursor\n\n\nclass PointPickTool(QgsMapTool):\n    \"\"\"Emits the map coordinate of every left click.\"\"\"\n\n    pointPicked = pyqtSignal(object)\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        point = self.toMapCoordinates(event.pos())\n        self.pointPicked.emit(point)\n\n    def deactivate(self):\n        super().deactivate()\n        self.deactivated.emit()\n","python","",[23,278,279,297,310,323,330,335,353,360,365,384,389,401,416,430,435,446,454,462,467,478,493,499,513,521,526,536,544],{"__ignoreMap":276},[280,281,283,287,291,294],"span",{"class":191,"line":282},1,[280,284,286],{"class":285},"snl16","from",[280,288,290],{"class":289},"s95oV"," qgis.gui ",[280,292,293],{"class":285},"import",[280,295,296],{"class":289}," QgsMapTool\n",[280,298,300,302,305,307],{"class":191,"line":299},2,[280,301,286],{"class":285},[280,303,304],{"class":289}," qgis.PyQt.QtCore ",[280,306,293],{"class":285},[280,308,309],{"class":289}," Qt, pyqtSignal\n",[280,311,313,315,318,320],{"class":191,"line":312},3,[280,314,286],{"class":285},[280,316,317],{"class":289}," qgis.PyQt.QtGui ",[280,319,293],{"class":285},[280,321,322],{"class":289}," QCursor\n",[280,324,326],{"class":191,"line":325},4,[280,327,329],{"emptyLinePlaceholder":328},true,"\n",[280,331,333],{"class":191,"line":332},5,[280,334,329],{"emptyLinePlaceholder":328},[280,336,338,341,345,348,350],{"class":191,"line":337},6,[280,339,340],{"class":285},"class",[280,342,344],{"class":343},"svObZ"," PointPickTool",[280,346,347],{"class":289},"(",[280,349,25],{"class":343},[280,351,352],{"class":289},"):\n",[280,354,356],{"class":191,"line":355},7,[280,357,359],{"class":358},"sU2Wk","    \"\"\"Emits the map coordinate of every left click.\"\"\"\n",[280,361,363],{"class":191,"line":362},8,[280,364,329],{"emptyLinePlaceholder":328},[280,366,368,371,374,377,381],{"class":191,"line":367},9,[280,369,370],{"class":289},"    pointPicked ",[280,372,373],{"class":285},"=",[280,375,376],{"class":289}," pyqtSignal(",[280,378,380],{"class":379},"sDLfK","object",[280,382,383],{"class":289},")\n",[280,385,387],{"class":191,"line":386},10,[280,388,329],{"emptyLinePlaceholder":328},[280,390,392,395,398],{"class":191,"line":391},11,[280,393,394],{"class":285},"    def",[280,396,397],{"class":379}," __init__",[280,399,400],{"class":289},"(self, canvas):\n",[280,402,404,407,410,413],{"class":191,"line":403},12,[280,405,406],{"class":379},"        super",[280,408,409],{"class":289},"().",[280,411,412],{"class":379},"__init__",[280,414,415],{"class":289},"(canvas)\n",[280,417,419,422,425,427],{"class":191,"line":418},13,[280,420,421],{"class":379},"        self",[280,423,424],{"class":289},".canvas ",[280,426,373],{"class":285},[280,428,429],{"class":289}," canvas\n",[280,431,433],{"class":191,"line":432},14,[280,434,329],{"emptyLinePlaceholder":328},[280,436,438,440,443],{"class":191,"line":437},15,[280,439,394],{"class":285},[280,441,442],{"class":343}," activate",[280,444,445],{"class":289},"(self):\n",[280,447,449,451],{"class":191,"line":448},16,[280,450,406],{"class":379},[280,452,453],{"class":289},"().activate()\n",[280,455,457,459],{"class":191,"line":456},17,[280,458,421],{"class":379},[280,460,461],{"class":289},".canvas.setCursor(QCursor(Qt.CrossCursor))\n",[280,463,465],{"class":191,"line":464},18,[280,466,329],{"emptyLinePlaceholder":328},[280,468,470,472,475],{"class":191,"line":469},19,[280,471,394],{"class":285},[280,473,474],{"class":343}," canvasReleaseEvent",[280,476,477],{"class":289},"(self, event):\n",[280,479,481,484,487,490],{"class":191,"line":480},20,[280,482,483],{"class":285},"        if",[280,485,486],{"class":289}," event.button() ",[280,488,489],{"class":285},"!=",[280,491,492],{"class":289}," Qt.LeftButton:\n",[280,494,496],{"class":191,"line":495},21,[280,497,498],{"class":285},"            return\n",[280,500,502,505,507,510],{"class":191,"line":501},22,[280,503,504],{"class":289},"        point ",[280,506,373],{"class":285},[280,508,509],{"class":379}," self",[280,511,512],{"class":289},".toMapCoordinates(event.pos())\n",[280,514,516,518],{"class":191,"line":515},23,[280,517,421],{"class":379},[280,519,520],{"class":289},".pointPicked.emit(point)\n",[280,522,524],{"class":191,"line":523},24,[280,525,329],{"emptyLinePlaceholder":328},[280,527,529,531,534],{"class":191,"line":528},25,[280,530,394],{"class":285},[280,532,533],{"class":343}," deactivate",[280,535,445],{"class":289},[280,537,539,541],{"class":191,"line":538},26,[280,540,406],{"class":379},[280,542,543],{"class":289},"().deactivate()\n",[280,545,547,549],{"class":191,"line":546},27,[280,548,421],{"class":379},[280,550,551],{"class":289},".deactivated.emit()\n",[14,553,554,557,558,561,562,565,566,568,569,571,572,575,576,579],{},[234,555,556],{},"Breakdown:"," ",[23,559,560],{},"pyqtSignal(object)"," lets the tool report results without knowing who is listening, which keeps it reusable — the plugin connects to ",[23,563,564],{},"pointPicked"," and decides what to do. ",[23,567,119],{}," and ",[23,570,185],{}," are called by the canvas when the tool is set or replaced; always call ",[23,573,574],{},"super()"," so the base class housekeeping still runs. Handling the release rather than the press is the convention: it lets a user press, think better of it, drag away and release harmlessly. Filtering on ",[23,577,578],{},"Qt.LeftButton"," leaves the right button free for the context menu.",[14,581,582],{},"Installing it is two lines, and keeping a reference is mandatory:",[271,584,586],{"className":273,"code":585,"language":275,"meta":276,"style":276},"self.tool = PointPickTool(self.iface.mapCanvas())\nself.tool.pointPicked.connect(self.on_point_picked)\nself.iface.mapCanvas().setMapTool(self.tool)\n",[23,587,588,606,618],{"__ignoreMap":276},[280,589,590,593,596,598,601,603],{"class":191,"line":282},[280,591,592],{"class":379},"self",[280,594,595],{"class":289},".tool ",[280,597,373],{"class":285},[280,599,600],{"class":289}," PointPickTool(",[280,602,592],{"class":379},[280,604,605],{"class":289},".iface.mapCanvas())\n",[280,607,608,610,613,615],{"class":191,"line":299},[280,609,592],{"class":379},[280,611,612],{"class":289},".tool.pointPicked.connect(",[280,614,592],{"class":379},[280,616,617],{"class":289},".on_point_picked)\n",[280,619,620,622,625,627],{"class":191,"line":312},[280,621,592],{"class":379},[280,623,624],{"class":289},".iface.mapCanvas().setMapTool(",[280,626,592],{"class":379},[280,628,629],{"class":289},".tool)\n",[14,631,632,634,635,637,638,641,642,645,646,650],{},[234,633,556],{}," Storing the tool on ",[23,636,592],{}," prevents Python from garbage-collecting it while the canvas still holds a pointer, which is a classic cause of a hard crash rather than a clean error. To hand control back, call ",[23,639,640],{},"self.iface.mapCanvas().unsetMapTool(self.tool)"," — usually in the plugin's ",[23,643,644],{},"unload()",". ",[31,647,649],{"href":648},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","Create a Custom Map Tool in PyQGIS"," walks through wiring it to a checkable toolbar button so the button state follows the active tool.",[223,652,654],{"id":653},"from-screen-pixels-to-map-coordinates","From screen pixels to map coordinates",[14,656,657,658,661],{},"Mouse events carry positions in ",[18,659,660],{},"widget pixels"," with the origin at the canvas's top-left corner and y increasing downward. Map coordinates have their origin wherever the CRS puts it and y increasing upward. The conversion is one call, and getting it wrong produces coordinates that look plausible but are mirrored or wildly out of range.",[14,663,664],{},[44,665,668,671,674,677,684,687,693,698,703,705,708,714,720,726,728,734,737,741,745,747,749,754,759,763,766,772,778,781],{"viewBox":666,"role":47,"ariaLabel":667,"xmlns":49},"0 0 760 254","A canvas widget with pixel coordinates on the left and the same view in map coordinates on the right, showing that the y axis is inverted and the origin moves",[51,669,670],{},"Pixel space versus map space",[55,672,673],{},"On the left, a canvas rectangle with its origin at the top-left corner, x increasing to the right and y increasing downward, and a click marked at 240 by 90 pixels. On the right, the same view in map coordinates with the origin at the bottom-left, y increasing upward, and the same click reported as an easting and northing. An arrow between them is labelled toMapCoordinates.",[59,675],{"x":61,"y":61,"width":62,"height":676,"fill":64},"254",[66,678,679],{},[69,680,682],{"id":681,"viewBox":72,"refX":73,"refY":74,"markerWidth":75,"markerHeight":75,"orient":76},"toolCoordArrow",[78,683],{"d":80,"fill":115},[83,685,686],{"x":85,"y":86,"style":87,"fill":88,"textAnchor":89},"The y axis flips — never assume the numbers are interchangeable",[59,688],{"x":689,"y":690,"width":691,"height":692,"rx":97,"fill":98,"stroke":99,"style":100},"20","48","290","176",[83,694,697],{"x":695,"y":696,"style":135,"fill":99,"textAnchor":89},"165","72","event.pos() — pixels",[191,699],{"x1":94,"y1":700,"x2":701,"y2":700,"stroke":99,"style":702},"92","282","stroke-width:2;marker-end:url(#toolCoordArrow)",[191,704],{"x1":94,"y1":700,"x2":94,"y2":126,"stroke":99,"style":702},[706,707],"circle",{"cx":94,"cy":700,"r":74,"fill":99},[83,709,713],{"x":710,"y":711,"style":712,"fill":99},"60","86","font-size:11px;font-family:sans-serif","(0, 0) top-left",[83,715,719],{"x":716,"y":134,"style":717,"fill":99,"textAnchor":718},"286","text-anchor:end;font-size:11px;font-family:sans-serif","end","x →",[83,721,725],{"x":710,"y":722,"style":723,"fill":99,"textAnchor":724},"206","text-anchor:start;font-size:11px;font-family:sans-serif","start","y ↓",[706,727],{"cx":195,"cy":145,"r":75,"fill":158},[83,729,733],{"x":730,"y":731,"style":732,"fill":81},"212","144","font-size:11.5px;font-family:sans-serif","(240, 90)",[59,735],{"x":736,"y":690,"width":691,"height":692,"rx":97,"fill":98,"stroke":168,"style":100},"450",[83,738,740],{"x":739,"y":696,"style":135,"fill":168,"textAnchor":89},"595","map coordinates",[191,742],{"x1":743,"y1":126,"x2":744,"y2":126,"stroke":168,"style":702},"482","712",[191,746],{"x1":743,"y1":126,"x2":743,"y2":700,"stroke":168,"style":702},[706,748],{"cx":743,"cy":126,"r":74,"fill":168},[83,750,753],{"x":751,"y":752,"style":712,"fill":168},"490","220","origin at bottom-left",[83,755,758],{"x":756,"y":757,"style":717,"fill":168,"textAnchor":718},"716","208","easting →",[83,760,762],{"x":761,"y":207,"style":723,"fill":168,"textAnchor":724},"492","northing ↑",[706,764],{"cx":765,"cy":145,"r":75,"fill":158},"630",[83,767,771],{"x":768,"y":769,"style":770,"fill":81,"textAnchor":718},"622","132","text-anchor:end;font-size:11.5px;font-family:sans-serif","(504 812, 5 606 340)",[191,773],{"x1":774,"y1":775,"x2":776,"y2":775,"stroke":115,"style":777},"314","136","446","stroke-width:3;marker-end:url(#toolCoordArrow)",[83,779,162],{"x":85,"y":184,"style":780,"fill":115,"textAnchor":89},"text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif",[83,782,783],{"x":85,"y":149,"style":110,"fill":220,"textAnchor":89},"result is in the canvas CRS",[271,785,787],{"className":273,"code":786,"language":275,"meta":276,"style":276},"def canvasMoveEvent(self, event):\n    point = self.toMapCoordinates(event.pos())\n    crs = self.canvas.mapSettings().destinationCrs().authid()\n    self.iface.mainWindow().statusBar().showMessage(\n        f\"{point.x():.2f}, {point.y():.2f}  [{crs}]\"\n    )\n",[23,788,789,799,810,822,830,875],{"__ignoreMap":276},[280,790,791,794,797],{"class":191,"line":282},[280,792,793],{"class":285},"def",[280,795,796],{"class":343}," canvasMoveEvent",[280,798,477],{"class":289},[280,800,801,804,806,808],{"class":191,"line":299},[280,802,803],{"class":289},"    point ",[280,805,373],{"class":285},[280,807,509],{"class":379},[280,809,512],{"class":289},[280,811,812,815,817,819],{"class":191,"line":312},[280,813,814],{"class":289},"    crs ",[280,816,373],{"class":285},[280,818,509],{"class":379},[280,820,821],{"class":289},".canvas.mapSettings().destinationCrs().authid()\n",[280,823,824,827],{"class":191,"line":325},[280,825,826],{"class":379},"    self",[280,828,829],{"class":289},".iface.mainWindow().statusBar().showMessage(\n",[280,831,832,835,838,841,844,847,850,853,855,858,860,862,865,867,870,872],{"class":191,"line":332},[280,833,834],{"class":285},"        f",[280,836,837],{"class":358},"\"",[280,839,840],{"class":379},"{",[280,842,843],{"class":289},"point.x()",[280,845,846],{"class":285},":.2f",[280,848,849],{"class":379},"}",[280,851,852],{"class":358},", ",[280,854,840],{"class":379},[280,856,857],{"class":289},"point.y()",[280,859,846],{"class":285},[280,861,849],{"class":379},[280,863,864],{"class":358},"  [",[280,866,840],{"class":379},[280,868,869],{"class":289},"crs",[280,871,849],{"class":379},[280,873,874],{"class":358},"]\"\n",[280,876,877],{"class":191,"line":337},[280,878,879],{"class":289},"    )\n",[14,881,882,557,884,886,887,889,890,893,894,897,898,902],{},[234,883,556],{},[23,885,162],{}," is a method on ",[23,888,25],{}," and handles the flip, the pan offset and the current zoom for you. The result is in the ",[234,891,892],{},"canvas CRS",", not the layer's — comparing it directly against a layer geometry in a different CRS is the classic source of a click that never matches anything. When you need layer coordinates, use the two-argument form ",[23,895,896],{},"self.toLayerCoordinates(layer, event.pos())",", which applies the transform as well. ",[31,899,901],{"href":900},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcapture-map-click-coordinates-pyqgis\u002F","Capture Map Click Coordinates in PyQGIS"," covers the CRS handling in detail.",[223,904,906],{"id":905},"drawing-feedback-with-a-rubber-band","Drawing feedback with a rubber band",[14,908,909,910,912],{},"A ",[23,911,171],{}," is a lightweight overlay drawn on top of the canvas. It is not a layer, nothing is stored, and it repaints instantly — exactly what interactive feedback needs.",[271,914,916],{"className":273,"code":915,"language":275,"meta":276,"style":276},"from qgis.core import QgsWkbTypes\nfrom qgis.gui import QgsRubberBand\nfrom qgis.PyQt.QtGui import QColor\n\n\nclass BoxSelectTool(QgsMapTool):\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.canvas = canvas\n        self.origin = None\n        self.band = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)\n        self.band.setColor(QColor(15, 118, 110, 60))\n        self.band.setStrokeColor(QColor(15, 118, 110))\n        self.band.setWidth(2)\n\n    def canvasPressEvent(self, event):\n        self.origin = self.toMapCoordinates(event.pos())\n        self.band.reset(QgsWkbTypes.PolygonGeometry)\n\n    def canvasMoveEvent(self, event):\n        if self.origin is None:\n            return\n        current = self.toMapCoordinates(event.pos())\n        self.band.reset(QgsWkbTypes.PolygonGeometry)\n        for corner in (\n            self.origin,\n            (self.origin.x(), current.y()),\n            current,\n            (current.x(), self.origin.y()),\n        ):\n            self.band.addPoint(QgsPointXY(*corner) if isinstance(corner, tuple) else corner, False)\n        self.band.closePoints()\n\n    def deactivate(self):\n        self.band.reset(QgsWkbTypes.PolygonGeometry)\n        super().deactivate()\n",[23,917,918,930,941,952,956,960,973,981,991,1001,1013,1025,1051,1070,1082,1086,1095,1107,1114,1118,1126,1143,1147,1158,1164,1178,1186,1196,1202,1213,1219,1259,1267,1272,1281,1288],{"__ignoreMap":276},[280,919,920,922,925,927],{"class":191,"line":282},[280,921,286],{"class":285},[280,923,924],{"class":289}," qgis.core ",[280,926,293],{"class":285},[280,928,929],{"class":289}," QgsWkbTypes\n",[280,931,932,934,936,938],{"class":191,"line":299},[280,933,286],{"class":285},[280,935,290],{"class":289},[280,937,293],{"class":285},[280,939,940],{"class":289}," QgsRubberBand\n",[280,942,943,945,947,949],{"class":191,"line":312},[280,944,286],{"class":285},[280,946,317],{"class":289},[280,948,293],{"class":285},[280,950,951],{"class":289}," QColor\n",[280,953,954],{"class":191,"line":325},[280,955,329],{"emptyLinePlaceholder":328},[280,957,958],{"class":191,"line":332},[280,959,329],{"emptyLinePlaceholder":328},[280,961,962,964,967,969,971],{"class":191,"line":337},[280,963,340],{"class":285},[280,965,966],{"class":343}," BoxSelectTool",[280,968,347],{"class":289},[280,970,25],{"class":343},[280,972,352],{"class":289},[280,974,975,977,979],{"class":191,"line":355},[280,976,394],{"class":285},[280,978,397],{"class":379},[280,980,400],{"class":289},[280,982,983,985,987,989],{"class":191,"line":362},[280,984,406],{"class":379},[280,986,409],{"class":289},[280,988,412],{"class":379},[280,990,415],{"class":289},[280,992,993,995,997,999],{"class":191,"line":367},[280,994,421],{"class":379},[280,996,424],{"class":289},[280,998,373],{"class":285},[280,1000,429],{"class":289},[280,1002,1003,1005,1008,1010],{"class":191,"line":386},[280,1004,421],{"class":379},[280,1006,1007],{"class":289},".origin ",[280,1009,373],{"class":285},[280,1011,1012],{"class":379}," None\n",[280,1014,1015,1017,1020,1022],{"class":191,"line":391},[280,1016,421],{"class":379},[280,1018,1019],{"class":289},".band ",[280,1021,373],{"class":285},[280,1023,1024],{"class":289}," QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)\n",[280,1026,1027,1029,1032,1035,1037,1039,1041,1044,1046,1048],{"class":191,"line":403},[280,1028,421],{"class":379},[280,1030,1031],{"class":289},".band.setColor(QColor(",[280,1033,1034],{"class":379},"15",[280,1036,852],{"class":289},[280,1038,179],{"class":379},[280,1040,852],{"class":289},[280,1042,1043],{"class":379},"110",[280,1045,852],{"class":289},[280,1047,710],{"class":379},[280,1049,1050],{"class":289},"))\n",[280,1052,1053,1055,1058,1060,1062,1064,1066,1068],{"class":191,"line":418},[280,1054,421],{"class":379},[280,1056,1057],{"class":289},".band.setStrokeColor(QColor(",[280,1059,1034],{"class":379},[280,1061,852],{"class":289},[280,1063,179],{"class":379},[280,1065,852],{"class":289},[280,1067,1043],{"class":379},[280,1069,1050],{"class":289},[280,1071,1072,1074,1077,1080],{"class":191,"line":432},[280,1073,421],{"class":379},[280,1075,1076],{"class":289},".band.setWidth(",[280,1078,1079],{"class":379},"2",[280,1081,383],{"class":289},[280,1083,1084],{"class":191,"line":437},[280,1085,329],{"emptyLinePlaceholder":328},[280,1087,1088,1090,1093],{"class":191,"line":448},[280,1089,394],{"class":285},[280,1091,1092],{"class":343}," canvasPressEvent",[280,1094,477],{"class":289},[280,1096,1097,1099,1101,1103,1105],{"class":191,"line":456},[280,1098,421],{"class":379},[280,1100,1007],{"class":289},[280,1102,373],{"class":285},[280,1104,509],{"class":379},[280,1106,512],{"class":289},[280,1108,1109,1111],{"class":191,"line":464},[280,1110,421],{"class":379},[280,1112,1113],{"class":289},".band.reset(QgsWkbTypes.PolygonGeometry)\n",[280,1115,1116],{"class":191,"line":469},[280,1117,329],{"emptyLinePlaceholder":328},[280,1119,1120,1122,1124],{"class":191,"line":480},[280,1121,394],{"class":285},[280,1123,796],{"class":343},[280,1125,477],{"class":289},[280,1127,1128,1130,1132,1134,1137,1140],{"class":191,"line":495},[280,1129,483],{"class":285},[280,1131,509],{"class":379},[280,1133,1007],{"class":289},[280,1135,1136],{"class":285},"is",[280,1138,1139],{"class":379}," None",[280,1141,1142],{"class":289},":\n",[280,1144,1145],{"class":191,"line":501},[280,1146,498],{"class":285},[280,1148,1149,1152,1154,1156],{"class":191,"line":515},[280,1150,1151],{"class":289},"        current ",[280,1153,373],{"class":285},[280,1155,509],{"class":379},[280,1157,512],{"class":289},[280,1159,1160,1162],{"class":191,"line":523},[280,1161,421],{"class":379},[280,1163,1113],{"class":289},[280,1165,1166,1169,1172,1175],{"class":191,"line":528},[280,1167,1168],{"class":285},"        for",[280,1170,1171],{"class":289}," corner ",[280,1173,1174],{"class":285},"in",[280,1176,1177],{"class":289}," (\n",[280,1179,1180,1183],{"class":191,"line":538},[280,1181,1182],{"class":379},"            self",[280,1184,1185],{"class":289},".origin,\n",[280,1187,1188,1191,1193],{"class":191,"line":546},[280,1189,1190],{"class":289},"            (",[280,1192,592],{"class":379},[280,1194,1195],{"class":289},".origin.x(), current.y()),\n",[280,1197,1199],{"class":191,"line":1198},28,[280,1200,1201],{"class":289},"            current,\n",[280,1203,1205,1208,1210],{"class":191,"line":1204},29,[280,1206,1207],{"class":289},"            (current.x(), ",[280,1209,592],{"class":379},[280,1211,1212],{"class":289},".origin.y()),\n",[280,1214,1216],{"class":191,"line":1215},30,[280,1217,1218],{"class":289},"        ):\n",[280,1220,1222,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251,1254,1257],{"class":191,"line":1221},31,[280,1223,1182],{"class":379},[280,1225,1226],{"class":289},".band.addPoint(QgsPointXY(",[280,1228,1229],{"class":285},"*",[280,1231,1232],{"class":289},"corner) ",[280,1234,1235],{"class":285},"if",[280,1237,1238],{"class":379}," isinstance",[280,1240,1241],{"class":289},"(corner, ",[280,1243,1244],{"class":379},"tuple",[280,1246,1247],{"class":289},") ",[280,1249,1250],{"class":285},"else",[280,1252,1253],{"class":289}," corner, ",[280,1255,1256],{"class":379},"False",[280,1258,383],{"class":289},[280,1260,1262,1264],{"class":191,"line":1261},32,[280,1263,421],{"class":379},[280,1265,1266],{"class":289},".band.closePoints()\n",[280,1268,1270],{"class":191,"line":1269},33,[280,1271,329],{"emptyLinePlaceholder":328},[280,1273,1275,1277,1279],{"class":191,"line":1274},34,[280,1276,394],{"class":285},[280,1278,533],{"class":343},[280,1280,445],{"class":289},[280,1282,1284,1286],{"class":191,"line":1283},35,[280,1285,421],{"class":379},[280,1287,1113],{"class":289},[280,1289,1291,1293],{"class":191,"line":1290},36,[280,1292,406],{"class":379},[280,1294,543],{"class":289},[14,1296,1297,1299,1300,852,1303,1306,1307,1310,1311,1313,1314,1317,1318,1321,1322,1324,1325,1329],{},[234,1298,556],{}," The geometry type passed to the constructor decides what the band draws — ",[23,1301,1302],{},"PointGeometry",[23,1304,1305],{},"LineGeometry"," or ",[23,1308,1309],{},"PolygonGeometry",". The fill colour's fourth argument is alpha, so ",[23,1312,710],{}," gives a translucent wash that does not hide the map beneath. ",[23,1315,1316],{},"addPoint(..., False)"," suppresses the repaint for every intermediate vertex; ",[23,1319,1320],{},"closePoints()"," triggers it once at the end, which keeps dragging smooth. Resetting in ",[23,1323,185],{}," is what stops an abandoned rectangle from sitting on the canvas for the rest of the session. ",[31,1326,1328],{"href":1327},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fhighlight-feature-with-rubber-band-pyqgis\u002F","Highlight a Feature with a Rubber Band in PyQGIS"," covers the geometry-following variant used for highlighting.",[223,1331,1333],{"id":1332},"identifying-the-feature-under-the-cursor","Identifying the feature under the cursor",[14,1335,1336],{},"Clicks are imprecise, so a hit test against an exact point almost never matches. The convention is to build a small search rectangle around the click, sized in map units derived from the current zoom, and query with that.",[271,1338,1340],{"className":273,"code":1339,"language":275,"meta":276,"style":276},"from qgis.core import QgsFeatureRequest, QgsRectangle\n\n\ndef features_at(self, layer, pos, pixel_radius=6):\n    \"\"\"Features whose geometry falls within pixel_radius of a canvas position.\"\"\"\n    point = self.toLayerCoordinates(layer, pos)\n    radius = self.canvas.mapUnitsPerPixel() * pixel_radius\n    box = QgsRectangle(\n        point.x() - radius, point.y() - radius,\n        point.x() + radius, point.y() + radius,\n    )\n    request = QgsFeatureRequest().setFilterRect(box)\n    return list(layer.getFeatures(request))\n",[23,1341,1342,1353,1357,1361,1378,1383,1394,1411,1421,1437,1450,1454,1464],{"__ignoreMap":276},[280,1343,1344,1346,1348,1350],{"class":191,"line":282},[280,1345,286],{"class":285},[280,1347,924],{"class":289},[280,1349,293],{"class":285},[280,1351,1352],{"class":289}," QgsFeatureRequest, QgsRectangle\n",[280,1354,1355],{"class":191,"line":299},[280,1356,329],{"emptyLinePlaceholder":328},[280,1358,1359],{"class":191,"line":312},[280,1360,329],{"emptyLinePlaceholder":328},[280,1362,1363,1365,1368,1371,1373,1376],{"class":191,"line":325},[280,1364,793],{"class":285},[280,1366,1367],{"class":343}," features_at",[280,1369,1370],{"class":289},"(self, layer, pos, pixel_radius",[280,1372,373],{"class":285},[280,1374,1375],{"class":379},"6",[280,1377,352],{"class":289},[280,1379,1380],{"class":191,"line":332},[280,1381,1382],{"class":358},"    \"\"\"Features whose geometry falls within pixel_radius of a canvas position.\"\"\"\n",[280,1384,1385,1387,1389,1391],{"class":191,"line":337},[280,1386,803],{"class":289},[280,1388,373],{"class":285},[280,1390,509],{"class":379},[280,1392,1393],{"class":289},".toLayerCoordinates(layer, pos)\n",[280,1395,1396,1399,1401,1403,1406,1408],{"class":191,"line":355},[280,1397,1398],{"class":289},"    radius ",[280,1400,373],{"class":285},[280,1402,509],{"class":379},[280,1404,1405],{"class":289},".canvas.mapUnitsPerPixel() ",[280,1407,1229],{"class":285},[280,1409,1410],{"class":289}," pixel_radius\n",[280,1412,1413,1416,1418],{"class":191,"line":362},[280,1414,1415],{"class":289},"    box ",[280,1417,373],{"class":285},[280,1419,1420],{"class":289}," QgsRectangle(\n",[280,1422,1423,1426,1429,1432,1434],{"class":191,"line":367},[280,1424,1425],{"class":289},"        point.x() ",[280,1427,1428],{"class":285},"-",[280,1430,1431],{"class":289}," radius, point.y() ",[280,1433,1428],{"class":285},[280,1435,1436],{"class":289}," radius,\n",[280,1438,1439,1441,1444,1446,1448],{"class":191,"line":386},[280,1440,1425],{"class":289},[280,1442,1443],{"class":285},"+",[280,1445,1431],{"class":289},[280,1447,1443],{"class":285},[280,1449,1436],{"class":289},[280,1451,1452],{"class":191,"line":391},[280,1453,879],{"class":289},[280,1455,1456,1459,1461],{"class":191,"line":403},[280,1457,1458],{"class":289},"    request ",[280,1460,373],{"class":285},[280,1462,1463],{"class":289}," QgsFeatureRequest().setFilterRect(box)\n",[280,1465,1466,1469,1472],{"class":191,"line":418},[280,1467,1468],{"class":285},"    return",[280,1470,1471],{"class":379}," list",[280,1473,1474],{"class":289},"(layer.getFeatures(request))\n",[14,1476,1477,557,1479,1482,1483,1486,1487,1490,1491,1494,1495,1499,1500,1503,1504,645,1507,1511],{},[234,1478,556],{},[23,1480,1481],{},"toLayerCoordinates()"," does the pixel conversion ",[18,1484,1485],{},"and"," the CRS transform into the layer's own system, which is what makes the rectangle comparable to the stored geometry. ",[23,1488,1489],{},"mapUnitsPerPixel()"," converts a comfortable pixel tolerance into map units at the current zoom, so the tool behaves identically whether the user is zoomed to a street or a country. ",[23,1492,1493],{},"setFilterRect()"," pushes the bounding-box test down to the provider, and on an indexed source it is close to free — the same principle as the spatial index in ",[31,1496,1498],{"href":1497},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations and Spatial Predicates",". Note the result is a ",[18,1501,1502],{},"candidate"," list from a box test; if you need exact hits, follow up with ",[23,1505,1506],{},"geometry.intersects()",[31,1508,1510],{"href":1509},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis\u002F","Identify the Feature Under the Cursor in PyQGIS"," builds this into a complete identify tool.",[223,1513,1515],{"id":1514},"snapping-to-existing-geometry","Snapping to existing geometry",[14,1517,1518],{},"A digitising tool that lets the user click \"close enough\" to a vertex and quietly records a point half a metre away produces slivers and gaps. QGIS ships a snapping utility that map tools can borrow, so your tool obeys the same snapping settings the user configured for the built-in tools.",[14,1520,1521],{},[44,1522,1525,1528,1531,1534,1537,1541,1545,1550,1553,1556,1559,1563,1567,1571,1573,1577,1580,1586,1589,1592,1595],{"viewBox":1523,"role":47,"ariaLabel":1524,"xmlns":49},"0 0 760 246","A raw click landing slightly off an existing vertex, and the same click resolved by the snapping utility onto the vertex, with the snap tolerance circle drawn around the cursor",[51,1526,1527],{},"Raw click versus a snapped click",[55,1529,1530],{},"On the left, a cursor clicks near but not on the corner of a polygon, and the recorded point sits a short distance away, creating a visible gap. On the right, the same click is shown with a dashed tolerance circle around it; the snapping utility resolves it onto the existing vertex so the new point coincides exactly with the polygon corner.",[59,1532],{"x":61,"y":61,"width":62,"height":1533,"fill":64},"246",[83,1535,1536],{"x":85,"y":86,"style":87,"fill":88,"textAnchor":89},"Same click, with and without the snapping utility",[59,1538],{"x":689,"y":1539,"width":1540,"height":211,"rx":97,"fill":98,"stroke":180,"style":100},"46","340",[83,1542,1544],{"x":1543,"y":127,"style":135,"fill":180,"textAnchor":89},"190","toMapCoordinates() alone",[78,1546],{"d":1547,"fill":115,"fillOpacity":1548,"stroke":115,"style":1549},"M70 194 L70 106 L172 96 L214 148 L160 194 Z",0.14,"stroke-width:2.5;stroke-linejoin:round",[706,1551],{"cx":1552,"cy":134,"r":74,"fill":115},"172",[706,1554],{"cx":1555,"cy":205,"r":1375,"fill":180},"192",[191,1557],{"x1":1552,"y1":134,"x2":1555,"y2":205,"stroke":180,"style":1558},"stroke-width:2;stroke-dasharray:3 3",[83,1560,1562],{"x":1561,"y":196,"style":732,"fill":81},"248","recorded here",[83,1564,1566],{"x":1561,"y":1565,"style":732,"fill":180},"122","a sliver is born",[83,1568,1570],{"x":1543,"y":1569,"style":110,"fill":220,"textAnchor":89},"216","the point is wherever the pixel was",[59,1572],{"x":204,"y":1539,"width":1540,"height":211,"rx":97,"fill":98,"stroke":168,"style":100},[83,1574,1576],{"x":1575,"y":127,"style":135,"fill":168,"textAnchor":89},"570","snapUtils.snapToMap()",[78,1578],{"d":1579,"fill":115,"fillOpacity":1548,"stroke":115,"style":1549},"M450 194 L450 106 L552 96 L594 148 L540 194 Z",[706,1581],{"cx":1582,"cy":205,"r":1583,"fill":1584,"stroke":168,"style":1585},"572","24","none","stroke-width:1.8;stroke-dasharray:5 4",[706,1587],{"cx":1588,"cy":134,"r":75,"fill":168},"552",[83,1590,1591],{"x":177,"y":196,"style":732,"fill":81},"snapped to the",[83,1593,1594],{"x":177,"y":1565,"style":732,"fill":168},"existing vertex",[83,1596,1597],{"x":1575,"y":1569,"style":110,"fill":220,"textAnchor":89},"tolerance comes from project settings",[271,1599,1601],{"className":273,"code":1600,"language":275,"meta":276,"style":276},"def canvasReleaseEvent(self, event):\n    match = self.canvas.snappingUtils().snapToMap(event.pos())\n    point = match.point() if match.isValid() else self.toMapCoordinates(event.pos())\n    self.pointPicked.emit(point)\n",[23,1602,1603,1611,1623,1643],{"__ignoreMap":276},[280,1604,1605,1607,1609],{"class":191,"line":282},[280,1606,793],{"class":285},[280,1608,474],{"class":343},[280,1610,477],{"class":289},[280,1612,1613,1616,1618,1620],{"class":191,"line":299},[280,1614,1615],{"class":289},"    match ",[280,1617,373],{"class":285},[280,1619,509],{"class":379},[280,1621,1622],{"class":289},".canvas.snappingUtils().snapToMap(event.pos())\n",[280,1624,1625,1627,1629,1632,1634,1637,1639,1641],{"class":191,"line":312},[280,1626,803],{"class":289},[280,1628,373],{"class":285},[280,1630,1631],{"class":289}," match.point() ",[280,1633,1235],{"class":285},[280,1635,1636],{"class":289}," match.isValid() ",[280,1638,1250],{"class":285},[280,1640,509],{"class":379},[280,1642,512],{"class":289},[280,1644,1645,1647],{"class":191,"line":325},[280,1646,826],{"class":379},[280,1648,520],{"class":289},[14,1650,1651,557,1653,1656,1657,1660,1661,645,1664,1667,1668,568,1671,1674,1675,1678],{},[234,1652,556],{},[23,1654,1655],{},"snappingUtils()"," is owned by the canvas and already configured from the project's snapping options, so your tool inherits the user's tolerance, mode and target layers for free. ",[23,1658,1659],{},"snapToMap()"," takes the raw pixel position and returns a ",[23,1662,1663],{},"QgsPointLocator.Match",[23,1665,1666],{},"isValid()"," is false when nothing was within tolerance, which is why the fall-back to the unsnapped coordinate matters — without it, clicking in empty space would return a null point. The match also carries ",[23,1669,1670],{},"match.layer()",[23,1672,1673],{},"match.featureId()",", so a tool can report ",[18,1676,1677],{},"what"," it snapped to, not just where.",[223,1680,1682],{"id":1681},"keyboard-modifiers-and-cancelling","Keyboard modifiers and cancelling",[14,1684,1685],{},"Users expect Escape to abort and modifier keys to change behaviour. Both arrive through methods you override, and handling them is what separates a tool that feels native from one that feels like a prototype.",[271,1687,1689],{"className":273,"code":1688,"language":275,"meta":276,"style":276},"from qgis.PyQt.QtCore import Qt\n\n\ndef keyPressEvent(self, event):\n    if event.key() == Qt.Key_Escape:\n        self.origin = None\n        self.band.reset(QgsWkbTypes.PolygonGeometry)\n        event.ignore()\n\n\ndef canvasReleaseEvent(self, event):\n    additive = bool(event.modifiers() & Qt.ShiftModifier)\n    self.selectionRequested.emit(self.current_rect(), additive)\n",[23,1690,1691,1702,1706,1710,1719,1733,1743,1749,1754,1758,1762,1770,1789],{"__ignoreMap":276},[280,1692,1693,1695,1697,1699],{"class":191,"line":282},[280,1694,286],{"class":285},[280,1696,304],{"class":289},[280,1698,293],{"class":285},[280,1700,1701],{"class":289}," Qt\n",[280,1703,1704],{"class":191,"line":299},[280,1705,329],{"emptyLinePlaceholder":328},[280,1707,1708],{"class":191,"line":312},[280,1709,329],{"emptyLinePlaceholder":328},[280,1711,1712,1714,1717],{"class":191,"line":325},[280,1713,793],{"class":285},[280,1715,1716],{"class":343}," keyPressEvent",[280,1718,477],{"class":289},[280,1720,1721,1724,1727,1730],{"class":191,"line":332},[280,1722,1723],{"class":285},"    if",[280,1725,1726],{"class":289}," event.key() ",[280,1728,1729],{"class":285},"==",[280,1731,1732],{"class":289}," Qt.Key_Escape:\n",[280,1734,1735,1737,1739,1741],{"class":191,"line":337},[280,1736,421],{"class":379},[280,1738,1007],{"class":289},[280,1740,373],{"class":285},[280,1742,1012],{"class":379},[280,1744,1745,1747],{"class":191,"line":355},[280,1746,421],{"class":379},[280,1748,1113],{"class":289},[280,1750,1751],{"class":191,"line":362},[280,1752,1753],{"class":289},"        event.ignore()\n",[280,1755,1756],{"class":191,"line":367},[280,1757,329],{"emptyLinePlaceholder":328},[280,1759,1760],{"class":191,"line":386},[280,1761,329],{"emptyLinePlaceholder":328},[280,1763,1764,1766,1768],{"class":191,"line":391},[280,1765,793],{"class":285},[280,1767,474],{"class":343},[280,1769,477],{"class":289},[280,1771,1772,1775,1777,1780,1783,1786],{"class":191,"line":403},[280,1773,1774],{"class":289},"    additive ",[280,1776,373],{"class":285},[280,1778,1779],{"class":379}," bool",[280,1781,1782],{"class":289},"(event.modifiers() ",[280,1784,1785],{"class":285},"&",[280,1787,1788],{"class":289}," Qt.ShiftModifier)\n",[280,1790,1791,1793,1796,1798],{"class":191,"line":418},[280,1792,826],{"class":379},[280,1794,1795],{"class":289},".selectionRequested.emit(",[280,1797,592],{"class":379},[280,1799,1800],{"class":289},".current_rect(), additive)\n",[14,1802,1803,1805,1806,1809,1810,1813,1814,1816],{},[234,1804,556],{}," Calling ",[23,1807,1808],{},"event.ignore()"," after handling Escape lets the event continue propagating, so QGIS's own \"stop rendering\" behaviour still works — swallowing it would break a shortcut the user relies on elsewhere. ",[23,1811,1812],{},"event.modifiers()"," is a bit flag tested with ",[23,1815,1785],{},", and Shift-to-add is the selection convention across the whole application, so following it costs one line and saves users from having to learn your tool's own rules.",[223,1818,1820],{"id":1819},"reacting-to-signals","Reacting to signals",[14,1822,1823],{},"A map tool is only half of the interaction story. The other half is knowing when the project changed — a layer was removed, the selection changed, the user finished an edit — so your plugin does not act on a stale reference.",[271,1825,1827],{"className":273,"code":1826,"language":275,"meta":276,"style":276},"from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nproject.layersAdded.connect(self.on_layers_added)\nproject.layerWillBeRemoved.connect(self.on_layer_removed)\n\nlayer.selectionChanged.connect(self.on_selection_changed)\nlayer.editingStopped.connect(self.on_edits_committed)\n",[23,1828,1829,1840,1844,1854,1864,1874,1878,1888],{"__ignoreMap":276},[280,1830,1831,1833,1835,1837],{"class":191,"line":282},[280,1832,286],{"class":285},[280,1834,924],{"class":289},[280,1836,293],{"class":285},[280,1838,1839],{"class":289}," QgsProject\n",[280,1841,1842],{"class":191,"line":299},[280,1843,329],{"emptyLinePlaceholder":328},[280,1845,1846,1849,1851],{"class":191,"line":312},[280,1847,1848],{"class":289},"project ",[280,1850,373],{"class":285},[280,1852,1853],{"class":289}," QgsProject.instance()\n",[280,1855,1856,1859,1861],{"class":191,"line":325},[280,1857,1858],{"class":289},"project.layersAdded.connect(",[280,1860,592],{"class":379},[280,1862,1863],{"class":289},".on_layers_added)\n",[280,1865,1866,1869,1871],{"class":191,"line":332},[280,1867,1868],{"class":289},"project.layerWillBeRemoved.connect(",[280,1870,592],{"class":379},[280,1872,1873],{"class":289},".on_layer_removed)\n",[280,1875,1876],{"class":191,"line":337},[280,1877,329],{"emptyLinePlaceholder":328},[280,1879,1880,1883,1885],{"class":191,"line":355},[280,1881,1882],{"class":289},"layer.selectionChanged.connect(",[280,1884,592],{"class":379},[280,1886,1887],{"class":289},".on_selection_changed)\n",[280,1889,1890,1893,1895],{"class":191,"line":362},[280,1891,1892],{"class":289},"layer.editingStopped.connect(",[280,1894,592],{"class":379},[280,1896,1897],{"class":289},".on_edits_committed)\n",[14,1899,1900,557,1902,1905,1906,1909,1910,1913,1914,1917,1918,1921,1922,1925],{},[234,1901,556],{},[23,1903,1904],{},"layerWillBeRemoved"," fires ",[18,1907,1908],{},"before"," the layer is destroyed, which is the only safe moment to drop your reference to it — acting on ",[23,1911,1912],{},"layersRemoved"," and then touching the layer crashes. ",[23,1915,1916],{},"selectionChanged"," carries the added and removed feature IDs, so a handler does not need to re-read the whole selection. ",[23,1919,1920],{},"editingStopped"," fires on both commit and rollback, so check ",[23,1923,1924],{},"layer.isEditable()"," if you need to distinguish them.",[14,1927,1928,1929,1931],{},"Every one of these connections must be undone in the plugin's ",[23,1930,644],{},", or a reloaded plugin will have two handlers attached and every action will run twice:",[271,1933,1935],{"className":273,"code":1934,"language":275,"meta":276,"style":276},"def unload(self):\n    QgsProject.instance().layersAdded.disconnect(self.on_layers_added)\n    self.iface.mapCanvas().unsetMapTool(self.tool)\n",[23,1936,1937,1946,1955],{"__ignoreMap":276},[280,1938,1939,1941,1944],{"class":191,"line":282},[280,1940,793],{"class":285},[280,1942,1943],{"class":343}," unload",[280,1945,445],{"class":289},[280,1947,1948,1951,1953],{"class":191,"line":299},[280,1949,1950],{"class":289},"    QgsProject.instance().layersAdded.disconnect(",[280,1952,592],{"class":379},[280,1954,1863],{"class":289},[280,1956,1957,1959,1962,1964],{"class":191,"line":312},[280,1958,826],{"class":379},[280,1960,1961],{"class":289},".iface.mapCanvas().unsetMapTool(",[280,1963,592],{"class":379},[280,1965,629],{"class":289},[14,1967,1968,1970,1971,1974,1975,1977,1978,1982,1983,1987],{},[234,1969,556],{}," Disconnecting mirrors the connection exactly. ",[23,1972,1973],{},"unsetMapTool()"," restores whatever tool was active before, leaving the canvas usable rather than stuck with a dangling tool object. This is the same discipline described in ",[31,1976,39],{"href":38},", and the reason ",[31,1979,1981],{"href":1980},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting"," is worth setting up early — duplicate-handler bugs only appear after a reload. ",[31,1984,1986],{"href":1985},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","Connect to Layer and Project Signals in PyQGIS"," lists the signals worth knowing.",[223,1989,1991],{"id":1990},"a-complete-measuring-tool","A complete measuring tool",[14,1993,1994],{},"Putting the pieces together, here is a tool small enough to read in one sitting that nonetheless behaves properly: it snaps, draws live feedback, reports a running distance, respects Escape, and cleans up after itself.",[271,1996,1998],{"className":273,"code":1997,"language":275,"meta":276,"style":276},"from qgis.core import QgsDistanceArea, QgsProject, QgsWkbTypes\nfrom qgis.gui import QgsMapTool, QgsRubberBand\nfrom qgis.PyQt.QtCore import Qt, pyqtSignal\nfrom qgis.PyQt.QtGui import QColor, QCursor\n\n\nclass MeasureTool(QgsMapTool):\n    \"\"\"Click to add vertices; reports the running ellipsoidal length.\"\"\"\n\n    lengthChanged = pyqtSignal(float)\n\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.canvas = canvas\n        self.points = []\n        self.band = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry)\n        self.band.setColor(QColor(180, 83, 9))\n        self.band.setWidth(3)\n\n        self.calculator = QgsDistanceArea()\n        self.calculator.setSourceCrs(\n            canvas.mapSettings().destinationCrs(),\n            QgsProject.instance().transformContext(),\n        )\n        self.calculator.setEllipsoid(QgsProject.instance().ellipsoid())\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.RightButton:\n            self._reset()\n            return\n        match = self.canvas.snappingUtils().snapToMap(event.pos())\n        point = match.point() if match.isValid() else self.toMapCoordinates(event.pos())\n        self.points.append(point)\n        self.band.addPoint(point, True)\n        self.lengthChanged.emit(self._length())\n\n    def keyPressEvent(self, event):\n        if event.key() == Qt.Key_Escape:\n            self._reset()\n        event.ignore()\n\n    def _length(self):\n        total = 0.0\n        for start, end in zip(self.points, self.points[1:]):\n            total += self.calculator.measureLine(start, end)\n        return total\n\n    def _reset(self):\n        self.points.clear()\n        self.band.reset(QgsWkbTypes.LineGeometry)\n        self.lengthChanged.emit(0.0)\n\n    def deactivate(self):\n        self._reset()\n        super().deactivate()\n",[23,1999,2000,2011,2022,2032,2043,2047,2051,2064,2069,2073,2087,2091,2099,2109,2119,2131,2142,2162,2173,2177,2189,2196,2201,2206,2211,2218,2222,2230,2236,2242,2246,2254,2265,2272,2276,2287,2305,2313,2326,2339,2344,2353,2364,2371,2376,2381,2391,2402,2433,2447,2456,2461,2471,2479,2487,2499,2504,2513,2520],{"__ignoreMap":276},[280,2001,2002,2004,2006,2008],{"class":191,"line":282},[280,2003,286],{"class":285},[280,2005,924],{"class":289},[280,2007,293],{"class":285},[280,2009,2010],{"class":289}," QgsDistanceArea, QgsProject, QgsWkbTypes\n",[280,2012,2013,2015,2017,2019],{"class":191,"line":299},[280,2014,286],{"class":285},[280,2016,290],{"class":289},[280,2018,293],{"class":285},[280,2020,2021],{"class":289}," QgsMapTool, QgsRubberBand\n",[280,2023,2024,2026,2028,2030],{"class":191,"line":312},[280,2025,286],{"class":285},[280,2027,304],{"class":289},[280,2029,293],{"class":285},[280,2031,309],{"class":289},[280,2033,2034,2036,2038,2040],{"class":191,"line":325},[280,2035,286],{"class":285},[280,2037,317],{"class":289},[280,2039,293],{"class":285},[280,2041,2042],{"class":289}," QColor, QCursor\n",[280,2044,2045],{"class":191,"line":332},[280,2046,329],{"emptyLinePlaceholder":328},[280,2048,2049],{"class":191,"line":337},[280,2050,329],{"emptyLinePlaceholder":328},[280,2052,2053,2055,2058,2060,2062],{"class":191,"line":355},[280,2054,340],{"class":285},[280,2056,2057],{"class":343}," MeasureTool",[280,2059,347],{"class":289},[280,2061,25],{"class":343},[280,2063,352],{"class":289},[280,2065,2066],{"class":191,"line":362},[280,2067,2068],{"class":358},"    \"\"\"Click to add vertices; reports the running ellipsoidal length.\"\"\"\n",[280,2070,2071],{"class":191,"line":367},[280,2072,329],{"emptyLinePlaceholder":328},[280,2074,2075,2078,2080,2082,2085],{"class":191,"line":386},[280,2076,2077],{"class":289},"    lengthChanged ",[280,2079,373],{"class":285},[280,2081,376],{"class":289},[280,2083,2084],{"class":379},"float",[280,2086,383],{"class":289},[280,2088,2089],{"class":191,"line":391},[280,2090,329],{"emptyLinePlaceholder":328},[280,2092,2093,2095,2097],{"class":191,"line":403},[280,2094,394],{"class":285},[280,2096,397],{"class":379},[280,2098,400],{"class":289},[280,2100,2101,2103,2105,2107],{"class":191,"line":418},[280,2102,406],{"class":379},[280,2104,409],{"class":289},[280,2106,412],{"class":379},[280,2108,415],{"class":289},[280,2110,2111,2113,2115,2117],{"class":191,"line":432},[280,2112,421],{"class":379},[280,2114,424],{"class":289},[280,2116,373],{"class":285},[280,2118,429],{"class":289},[280,2120,2121,2123,2126,2128],{"class":191,"line":437},[280,2122,421],{"class":379},[280,2124,2125],{"class":289},".points ",[280,2127,373],{"class":285},[280,2129,2130],{"class":289}," []\n",[280,2132,2133,2135,2137,2139],{"class":191,"line":448},[280,2134,421],{"class":379},[280,2136,1019],{"class":289},[280,2138,373],{"class":285},[280,2140,2141],{"class":289}," QgsRubberBand(canvas, QgsWkbTypes.LineGeometry)\n",[280,2143,2144,2146,2148,2150,2152,2155,2157,2160],{"class":191,"line":456},[280,2145,421],{"class":379},[280,2147,1031],{"class":289},[280,2149,211],{"class":379},[280,2151,852],{"class":289},[280,2153,2154],{"class":379},"83",[280,2156,852],{"class":289},[280,2158,2159],{"class":379},"9",[280,2161,1050],{"class":289},[280,2163,2164,2166,2168,2171],{"class":191,"line":464},[280,2165,421],{"class":379},[280,2167,1076],{"class":289},[280,2169,2170],{"class":379},"3",[280,2172,383],{"class":289},[280,2174,2175],{"class":191,"line":469},[280,2176,329],{"emptyLinePlaceholder":328},[280,2178,2179,2181,2184,2186],{"class":191,"line":480},[280,2180,421],{"class":379},[280,2182,2183],{"class":289},".calculator ",[280,2185,373],{"class":285},[280,2187,2188],{"class":289}," QgsDistanceArea()\n",[280,2190,2191,2193],{"class":191,"line":495},[280,2192,421],{"class":379},[280,2194,2195],{"class":289},".calculator.setSourceCrs(\n",[280,2197,2198],{"class":191,"line":501},[280,2199,2200],{"class":289},"            canvas.mapSettings().destinationCrs(),\n",[280,2202,2203],{"class":191,"line":515},[280,2204,2205],{"class":289},"            QgsProject.instance().transformContext(),\n",[280,2207,2208],{"class":191,"line":523},[280,2209,2210],{"class":289},"        )\n",[280,2212,2213,2215],{"class":191,"line":528},[280,2214,421],{"class":379},[280,2216,2217],{"class":289},".calculator.setEllipsoid(QgsProject.instance().ellipsoid())\n",[280,2219,2220],{"class":191,"line":538},[280,2221,329],{"emptyLinePlaceholder":328},[280,2223,2224,2226,2228],{"class":191,"line":546},[280,2225,394],{"class":285},[280,2227,442],{"class":343},[280,2229,445],{"class":289},[280,2231,2232,2234],{"class":191,"line":1198},[280,2233,406],{"class":379},[280,2235,453],{"class":289},[280,2237,2238,2240],{"class":191,"line":1204},[280,2239,421],{"class":379},[280,2241,461],{"class":289},[280,2243,2244],{"class":191,"line":1215},[280,2245,329],{"emptyLinePlaceholder":328},[280,2247,2248,2250,2252],{"class":191,"line":1221},[280,2249,394],{"class":285},[280,2251,474],{"class":343},[280,2253,477],{"class":289},[280,2255,2256,2258,2260,2262],{"class":191,"line":1261},[280,2257,483],{"class":285},[280,2259,486],{"class":289},[280,2261,1729],{"class":285},[280,2263,2264],{"class":289}," Qt.RightButton:\n",[280,2266,2267,2269],{"class":191,"line":1269},[280,2268,1182],{"class":379},[280,2270,2271],{"class":289},"._reset()\n",[280,2273,2274],{"class":191,"line":1274},[280,2275,498],{"class":285},[280,2277,2278,2281,2283,2285],{"class":191,"line":1283},[280,2279,2280],{"class":289},"        match ",[280,2282,373],{"class":285},[280,2284,509],{"class":379},[280,2286,1622],{"class":289},[280,2288,2289,2291,2293,2295,2297,2299,2301,2303],{"class":191,"line":1290},[280,2290,504],{"class":289},[280,2292,373],{"class":285},[280,2294,1631],{"class":289},[280,2296,1235],{"class":285},[280,2298,1636],{"class":289},[280,2300,1250],{"class":285},[280,2302,509],{"class":379},[280,2304,512],{"class":289},[280,2306,2308,2310],{"class":191,"line":2307},37,[280,2309,421],{"class":379},[280,2311,2312],{"class":289},".points.append(point)\n",[280,2314,2316,2318,2321,2324],{"class":191,"line":2315},38,[280,2317,421],{"class":379},[280,2319,2320],{"class":289},".band.addPoint(point, ",[280,2322,2323],{"class":379},"True",[280,2325,383],{"class":289},[280,2327,2329,2331,2334,2336],{"class":191,"line":2328},39,[280,2330,421],{"class":379},[280,2332,2333],{"class":289},".lengthChanged.emit(",[280,2335,592],{"class":379},[280,2337,2338],{"class":289},"._length())\n",[280,2340,2342],{"class":191,"line":2341},40,[280,2343,329],{"emptyLinePlaceholder":328},[280,2345,2347,2349,2351],{"class":191,"line":2346},41,[280,2348,394],{"class":285},[280,2350,1716],{"class":343},[280,2352,477],{"class":289},[280,2354,2356,2358,2360,2362],{"class":191,"line":2355},42,[280,2357,483],{"class":285},[280,2359,1726],{"class":289},[280,2361,1729],{"class":285},[280,2363,1732],{"class":289},[280,2365,2367,2369],{"class":191,"line":2366},43,[280,2368,1182],{"class":379},[280,2370,2271],{"class":289},[280,2372,2374],{"class":191,"line":2373},44,[280,2375,1753],{"class":289},[280,2377,2379],{"class":191,"line":2378},45,[280,2380,329],{"emptyLinePlaceholder":328},[280,2382,2384,2386,2389],{"class":191,"line":2383},46,[280,2385,394],{"class":285},[280,2387,2388],{"class":343}," _length",[280,2390,445],{"class":289},[280,2392,2394,2397,2399],{"class":191,"line":2393},47,[280,2395,2396],{"class":289},"        total ",[280,2398,373],{"class":285},[280,2400,2401],{"class":379}," 0.0\n",[280,2403,2405,2407,2410,2412,2415,2417,2419,2422,2424,2427,2430],{"class":191,"line":2404},48,[280,2406,1168],{"class":285},[280,2408,2409],{"class":289}," start, end ",[280,2411,1174],{"class":285},[280,2413,2414],{"class":379}," zip",[280,2416,347],{"class":289},[280,2418,592],{"class":379},[280,2420,2421],{"class":289},".points, ",[280,2423,592],{"class":379},[280,2425,2426],{"class":289},".points[",[280,2428,2429],{"class":379},"1",[280,2431,2432],{"class":289},":]):\n",[280,2434,2436,2439,2442,2444],{"class":191,"line":2435},49,[280,2437,2438],{"class":289},"            total ",[280,2440,2441],{"class":285},"+=",[280,2443,509],{"class":379},[280,2445,2446],{"class":289},".calculator.measureLine(start, end)\n",[280,2448,2450,2453],{"class":191,"line":2449},50,[280,2451,2452],{"class":285},"        return",[280,2454,2455],{"class":289}," total\n",[280,2457,2459],{"class":191,"line":2458},51,[280,2460,329],{"emptyLinePlaceholder":328},[280,2462,2464,2466,2469],{"class":191,"line":2463},52,[280,2465,394],{"class":285},[280,2467,2468],{"class":343}," _reset",[280,2470,445],{"class":289},[280,2472,2474,2476],{"class":191,"line":2473},53,[280,2475,421],{"class":379},[280,2477,2478],{"class":289},".points.clear()\n",[280,2480,2482,2484],{"class":191,"line":2481},54,[280,2483,421],{"class":379},[280,2485,2486],{"class":289},".band.reset(QgsWkbTypes.LineGeometry)\n",[280,2488,2490,2492,2494,2497],{"class":191,"line":2489},55,[280,2491,421],{"class":379},[280,2493,2333],{"class":289},[280,2495,2496],{"class":379},"0.0",[280,2498,383],{"class":289},[280,2500,2502],{"class":191,"line":2501},56,[280,2503,329],{"emptyLinePlaceholder":328},[280,2505,2507,2509,2511],{"class":191,"line":2506},57,[280,2508,394],{"class":285},[280,2510,533],{"class":343},[280,2512,445],{"class":289},[280,2514,2516,2518],{"class":191,"line":2515},58,[280,2517,421],{"class":379},[280,2519,2271],{"class":289},[280,2521,2523,2525],{"class":191,"line":2522},59,[280,2524,406],{"class":379},[280,2526,543],{"class":289},[14,2528,2529,2531,2532,2535,2536,2539,2540,645,2542,2545,2546,2548,2549,2552,2553,2556],{},[234,2530,556],{}," The ",[23,2533,2534],{},"QgsDistanceArea"," is built once in the constructor rather than per click, and configured with both the canvas CRS and the project ellipsoid so ",[23,2537,2538],{},"measureLine()"," returns real metres rather than map units — the same distinction covered in ",[31,2541,1498],{"href":1497},[23,2543,2544],{},"addPoint(point, True)"," repaints immediately, which is right here because clicks are infrequent; the ",[23,2547,1256],{}," form used earlier is for the high-frequency move handler. Right-click resets rather than opening a menu, matching the built-in measure tool. ",[23,2550,2551],{},"_reset()"," is called from three places — right click, Escape and deactivation — which is exactly why it is a method rather than three copies of the same three lines. The tool emits ",[23,2554,2555],{},"lengthChanged"," instead of touching a label directly, so a dock widget or a status bar can consume it without the tool knowing which.",[14,2558,2559],{},"Connecting it from the plugin is unchanged from the simple case, and the signal makes the display trivial:",[271,2561,2563],{"className":273,"code":2562,"language":275,"meta":276,"style":276},"self.measure = MeasureTool(self.iface.mapCanvas())\nself.measure.lengthChanged.connect(\n    lambda metres: self.iface.mainWindow().statusBar().showMessage(f\"{metres:,.1f} m\")\n)\nself.iface.mapCanvas().setMapTool(self.measure)\n",[23,2564,2565,2581,2588,2621,2625],{"__ignoreMap":276},[280,2566,2567,2569,2572,2574,2577,2579],{"class":191,"line":282},[280,2568,592],{"class":379},[280,2570,2571],{"class":289},".measure ",[280,2573,373],{"class":285},[280,2575,2576],{"class":289}," MeasureTool(",[280,2578,592],{"class":379},[280,2580,605],{"class":289},[280,2582,2583,2585],{"class":191,"line":299},[280,2584,592],{"class":379},[280,2586,2587],{"class":289},".measure.lengthChanged.connect(\n",[280,2589,2590,2593,2596,2598,2601,2604,2606,2608,2611,2614,2616,2619],{"class":191,"line":312},[280,2591,2592],{"class":285},"    lambda",[280,2594,2595],{"class":289}," metres: ",[280,2597,592],{"class":379},[280,2599,2600],{"class":289},".iface.mainWindow().statusBar().showMessage(",[280,2602,2603],{"class":285},"f",[280,2605,837],{"class":358},[280,2607,840],{"class":379},[280,2609,2610],{"class":289},"metres",[280,2612,2613],{"class":285},":,.1f",[280,2615,849],{"class":379},[280,2617,2618],{"class":358}," m\"",[280,2620,383],{"class":289},[280,2622,2623],{"class":191,"line":325},[280,2624,383],{"class":289},[280,2626,2627,2629,2631,2633],{"class":191,"line":332},[280,2628,592],{"class":379},[280,2630,624],{"class":289},[280,2632,592],{"class":379},[280,2634,2635],{"class":289},".measure)\n",[14,2637,2638,2640],{},[234,2639,556],{}," The lambda keeps formatting out of the tool, which is the right split — a tool that returns a number is reusable, one that writes a formatted string into a specific widget is not.",[223,2642,2644],{"id":2643},"troubleshooting-interaction-bugs","Troubleshooting interaction bugs",[14,2646,2647],{},"Interaction bugs are harder to diagnose than logic bugs because there is no stack trace — the tool simply does nothing, or does something once and then stops. These are the recurring shapes.",[228,2649,2650,2664,2674,2689,2702,2708],{},[231,2651,2652,2655,2656,2659,2660,2663],{},[234,2653,2654],{},"Nothing happens when you click."," The tool was never installed, or another tool replaced it. Print ",[23,2657,2658],{},"canvas.mapTool()"," and confirm it is your instance; a checkable toolbar action that was not connected to ",[23,2661,2662],{},"setMapTool()"," looks active while doing nothing.",[231,2665,2666,2669,2670,40],{},[234,2667,2668],{},"The first click works, then nothing."," Something inside the handler raised. Exceptions in a Qt event handler are printed to the QGIS log rather than surfacing, and the tool keeps running in a half-initialised state. Wrap the handler body while developing and check the Log Messages panel — see ",[31,2671,2673],{"href":2672},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002F","Debugging PyQGIS Scripts",[231,2675,2676,2679,2680,2683,2684,1306,2686,2688],{},[234,2677,2678],{},"Coordinates are enormous or negative."," A pixel position was used where a map coordinate was expected. ",[23,2681,2682],{},"event.pos()"," is never a map coordinate; it must go through ",[23,2685,162],{},[23,2687,1481],{}," first.",[231,2690,2691,2694,2695,2698,2699,2701],{},[234,2692,2693],{},"The rubber band flickers or lags on drag."," Every ",[23,2696,2697],{},"addPoint()"," is repainting. Pass ",[23,2700,1256],{}," for the update argument on intermediate vertices and let the final call repaint once.",[231,2703,2704,2707],{},[234,2705,2706],{},"The tool works in one project and not another."," Almost always a CRS assumption. A tool that compares canvas coordinates against layer geometry works perfectly while both happen to share a CRS and fails silently the moment they do not.",[231,2709,2710,557,2713,2715,2716,2719],{},[234,2711,2712],{},"Move events never arrive.",[23,2714,25],{}," only receives move events while a button is held unless you opt in. Set ",[23,2717,2718],{},"self.setCursor(...)"," and ensure the canvas has mouse tracking enabled, or track state from press and release instead.",[14,2721,2722],{},"A quick way to confirm events are reaching the tool at all is to log the raw position at the top of each handler before any conversion. If nothing is logged the problem is installation; if positions appear but the results are wrong, the problem is conversion.",[223,2724,2726],{"id":2725},"key-takeaways","Key takeaways",[228,2728,2729,2735,2741,2753,2761,2770],{},[231,2730,2731,2734],{},[234,2732,2733],{},"Keep a reference to your tool."," The canvas holds a non-owning pointer; letting Python collect the tool crashes QGIS rather than raising.",[231,2736,2737,2740],{},[234,2738,2739],{},"Handle release, not press."," It gives the user a chance to abort a click by dragging away, which is the behaviour every built-in tool has.",[231,2742,2743,2748,2749,2752],{},[234,2744,2745,2747],{},[23,2746,162],{}," returns canvas-CRS coordinates."," Use ",[23,2750,2751],{},"toLayerCoordinates(layer, pos)"," whenever you are about to compare against layer geometry.",[231,2754,2755,2760],{},[234,2756,2757,2758,40],{},"Size hit tolerances in pixels, converted through ",[23,2759,1489],{}," A fixed map-unit tolerance behaves completely differently at different zooms.",[231,2762,2763,2766,2767,2769],{},[234,2764,2765],{},"Rubber bands are overlays, not layers."," They cost nothing, but they must be reset in ",[23,2768,185],{}," or they linger.",[231,2771,2772,2777],{},[234,2773,2774,2775,40],{},"Disconnect every signal in ",[23,2776,644],{}," Duplicate handlers after a reload are the most confusing plugin bug there is.",[223,2779,2781],{"id":2780},"frequently-asked-questions","Frequently Asked Questions",[14,2783,2784,2787,2788,2791],{},[234,2785,2786],{},"Why does QGIS crash when I activate my map tool?","\nAlmost always because the tool object was garbage-collected. The canvas keeps a raw pointer to it, so the tool must be stored on the plugin instance — ",[23,2789,2790],{},"self.tool = MyTool(canvas)"," — and not created as a local variable inside the function that installs it.",[14,2793,2794,2797,2798,2801,2802,2804,2805,2807],{},[234,2795,2796],{},"My clicks never match any feature. What is wrong?","\nEither the coordinates are in the wrong CRS or the tolerance is zero. Use ",[23,2799,2800],{},"toLayerCoordinates(layer, event.pos())"," rather than ",[23,2803,162],{}," when comparing against a layer, and build a search rectangle sized from ",[23,2806,1489],{}," instead of testing an exact point.",[14,2809,2810,2813,2815,2816,2819,2820,2822],{},[234,2811,2812],{},"Should I subclass QgsMapTool or QgsMapToolEmitPoint?",[23,2814,268],{}," if all you need is a single clicked coordinate — it gives you a ready-made ",[23,2817,2818],{},"canvasClicked"," signal. Subclass ",[23,2821,25],{}," directly when you need drag behaviour, custom feedback, or key handling.",[14,2824,2825,2828,2829,2832,2833,2835,2836,2838],{},[234,2826,2827],{},"How do I stop my rubber band from staying on the canvas?","\nCall ",[23,2830,2831],{},"band.reset(geometry_type)"," in ",[23,2834,185],{},", and again in ",[23,2837,644],{}," for safety. A rubber band is owned by the canvas scene, so nothing removes it automatically when your tool goes out of scope.",[14,2840,2841,2844,2845,2847],{},[234,2842,2843],{},"Why does my plugin react twice to every click after I reload it?","\nThe previous instance's signal connections were never removed. Disconnect every signal you connect in the matching ",[23,2846,644],{}," method; a reload creates a second set of handlers on top of the first otherwise.",[223,2849,2851],{"id":2850},"related","Related",[228,2853,2854,2859,2863,2867,2873,2877,2881,2885,2889,2893],{},[231,2855,2856,2858],{},[31,2857,34],{"href":33}," — the guide this page belongs to",[231,2860,2861],{},[31,2862,39],{"href":38},[231,2864,2865],{},[31,2866,251],{"href":250},[231,2868,2869],{},[31,2870,2872],{"href":2871},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fadd-custom-dock-widget-pyqgis\u002F","Add a Custom Dock Widget with PyQGIS",[231,2874,2875],{},[31,2876,649],{"href":648},[231,2878,2879],{},[31,2880,901],{"href":900},[231,2882,2883],{},[31,2884,1328],{"href":1327},[231,2886,2887],{},[31,2888,1510],{"href":1509},[231,2890,2891],{},[31,2892,1986],{"href":1985},[231,2894,2895],{},[31,2896,2898],{"href":2897},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002F","Map Canvas Control and Image Export in PyQGIS",[2900,2901,2902],"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);}",{"title":276,"searchDepth":299,"depth":299,"links":2904},[2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917],{"id":225,"depth":299,"text":226},{"id":258,"depth":299,"text":259},{"id":653,"depth":299,"text":654},{"id":905,"depth":299,"text":906},{"id":1332,"depth":299,"text":1333},{"id":1514,"depth":299,"text":1515},{"id":1681,"depth":299,"text":1682},{"id":1819,"depth":299,"text":1820},{"id":1990,"depth":299,"text":1991},{"id":2643,"depth":299,"text":2644},{"id":2725,"depth":299,"text":2726},{"id":2780,"depth":299,"text":2781},{"id":2850,"depth":299,"text":2851},"Build custom QGIS map tools with PyQGIS. Handle canvas clicks, convert pixels to map coordinates, draw rubber-band feedback, identify features and connect signals.","md",{"slug":12,"type":2921,"breadcrumb":2922,"datePublished":2923,"dateModified":2923},"guide","Custom Map Tools","2026-08-01","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction",{"title":5,"description":2918},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Findex","A6vA3BxQ7pgOcVnKypgXZarGt9ATxA2ig1o40yjpNwA",1785585990473]