[{"data":1,"prerenderedAt":2443},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fdigitize-features-with-custom-map-tool-pyqgis":3},{"id":4,"title":5,"body":6,"description":2432,"extension":2433,"meta":2434,"navigation":268,"path":2439,"seo":2440,"stem":2441,"__hash__":2442},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fdigitize-features-with-custom-map-tool-pyqgis\u002Findex.md","Digitize Features with a Custom Map Tool in PyQGIS",{"type":7,"value":8,"toc":2420},"minimark",[9,13,17,26,152,157,189,193,196,950,981,985,992,1129,1509,1536,1540,1543,1856,1864,1940,1944,1947,2237,2253,2257,2292,2296,2331,2335,2338,2342,2355,2364,2374,2384,2388,2416],[10,11,5],"h1",{"id":12},"digitize-features-with-a-custom-map-tool-in-pyqgis",[14,15,16],"p",{},"QGIS's own digitizing tools are excellent for general editing. Plugins need their own when the capture has rules: a pipe that must start and end on a valve, a survey transect with a fixed number of vertices, a boundary that snaps only to one reference layer, a line whose attributes are filled from what it touches. A custom capture tool gives you every click, so you can enforce those rules as the user draws rather than validating afterwards.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",". It builds a line and polygon capture tool with a live preview, snapping, backspace to remove the last vertex, and a commit step that adds the feature through the layer's edit buffer with defaults and the attribute form — so it behaves like a native tool.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,80,85,89,93,99,106,110,117,122,127,130,134,137,142,145,149],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","The event flow of a capture tool: left clicks add snapped vertices, mouse moves update the rubber band preview to the cursor, backspace removes the last vertex, right click finishes and commits the feature, escape cancels","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Clicks become a geometry",[40,41,42],"desc",{},"A line being digitized with three committed vertices and a dashed segment following the cursor. Left click adds the snapped position as a vertex. Mouse move updates the trailing segment and the snap indicator. Backspace removes the most recent vertex. Right click finishes: the geometry is transformed to the layer CRS, a feature is created with defaults, the attribute form opens, and the feature is added inside an edit command. Escape clears everything.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Five events drive the whole tool",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","48","340","230","10","#fffdf7","#59645f","stroke-width:2",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M60 230 L140 150 L240 180","none","#b91c1c","stroke-width:3",[70,77],{"d":78,"fill":73,"stroke":74,"style":79},"M240 180 L310 100","stroke-width:2;stroke-dasharray:6 4",[81,82],"circle",{"cx":83,"cy":64,"r":84,"fill":74},"60","5",[81,86],{"cx":87,"cy":88,"r":84,"fill":74},"140","150",[81,90],{"cx":91,"cy":92,"r":84,"fill":74},"240","180",[44,94],{"x":95,"y":96,"width":97,"height":97,"fill":73,"stroke":98,"style":68},"302","92","16","#b45309",[51,100,105],{"x":101,"y":102,"style":103,"fill":104,"textAnchor":57},"194","264","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","3 vertices + preview to a snapped cursor",[44,107],{"x":108,"y":62,"width":109,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"388","348",[51,111,116],{"x":112,"y":113,"style":114,"fill":115},"412","84","font-size:10.5px;font-weight:bold;font-family:sans-serif","#166534","left click",[51,118,121],{"x":119,"y":113,"style":120,"fill":104},"512","font-size:10.5px;font-family:sans-serif","add snapped vertex",[51,123,126],{"x":112,"y":124,"style":114,"fill":125},"120","#2563eb","mouse move",[51,128,129],{"x":119,"y":124,"style":120,"fill":104},"update preview, snap marker",[51,131,133],{"x":112,"y":132,"style":114,"fill":98},"156","backspace",[51,135,136],{"x":119,"y":132,"style":120,"fill":104},"remove last vertex",[51,138,141],{"x":112,"y":139,"style":114,"fill":140},"192","#0f766e","right click",[51,143,144],{"x":119,"y":139,"style":120,"fill":104},"finish and commit",[51,146,148],{"x":112,"y":147,"style":114,"fill":74},"228","escape",[51,150,151],{"x":119,"y":147,"style":120,"fill":104},"cancel the capture",[153,154,156],"h2",{"id":155},"prerequisites","Prerequisites",[158,159,160,168,176],"ul",{},[161,162,163,167],"li",{},[164,165,166],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[161,169,170,171,175],{},"A plugin or console session with ",[172,173,174],"code",{},"iface",", and an editable line or polygon layer.",[161,177,178,179,183,184,188],{},"The map tool basics from ",[21,180,182],{"href":181},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","creating a custom map tool"," and rubber bands from ",[21,185,187],{"href":186},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fhighlight-feature-with-rubber-band-pyqgis\u002F","highlighting a feature with a rubber band",".",[153,190,192],{"id":191},"the-capture-tool","The capture tool",[14,194,195],{},"The tool keeps a list of vertices in map canvas coordinates, draws them with a rubber band, and uses the canvas's snapping utilities for every position.",[197,198,203],"pre",{"className":199,"code":200,"language":201,"meta":202,"style":202},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtCore import Qt\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.core import Qgis, QgsGeometry, QgsPointLocator, QgsPointXY\nfrom qgis.gui import QgsMapTool, QgsRubberBand, QgsSnapIndicator\n\n\nclass CaptureTool(QgsMapTool):\n    def __init__(self, canvas, layer, on_finished):\n        super().__init__(canvas)\n        self.layer = layer\n        self.on_finished = on_finished\n        self.is_polygon = layer.geometryType() == Qgis.GeometryType.Polygon\n        self.points = []\n        self.band = QgsRubberBand(\n            canvas, Qgis.GeometryType.Polygon if self.is_polygon else Qgis.GeometryType.Line)\n        self.band.setColor(QColor(185, 28, 28, 200))\n        self.band.setFillColor(QColor(185, 28, 28, 40))\n        self.band.setWidth(2)\n        self.snap_indicator = QgsSnapIndicator(canvas)\n        self.setCursor(Qt.CursorShape.CrossCursor)\n\n    def _snapped(self, event):\n        match = self.canvas().snappingUtils().snapToMap(event.pos())\n        self.snap_indicator.setMatch(match)\n        return QgsPointXY(match.point()) if match.isValid() else event.mapPoint()\n\n    def _redraw(self, cursor=None):\n        pts = self.points + ([cursor] if cursor else [])\n        geometry_type = Qgis.GeometryType.Polygon if self.is_polygon else Qgis.GeometryType.Line\n        self.band.reset(geometry_type)\n        for i, p in enumerate(pts):\n            self.band.addPoint(p, i == len(pts) - 1)\n\n    def canvasMoveEvent(self, event):\n        cursor = self._snapped(event)\n        if self.points:\n            self._redraw(cursor)\n\n    def canvasReleaseEvent(self, event):\n        if event.button() == Qt.MouseButton.LeftButton:\n            self.points.append(self._snapped(event))\n            self._redraw()\n        elif event.button() == Qt.MouseButton.RightButton:\n            self.finish()\n\n    def keyPressEvent(self, event):\n        if event.key() == Qt.Key.Key_Backspace and self.points:\n            self.points.pop()\n            self._redraw()\n            event.ignore()\n        elif event.key() == Qt.Key.Key_Escape:\n            self.clear()\n\n    def clear(self):\n        self.points = []\n        self.band.reset()\n        self.snap_indicator.setMatch(QgsPointLocator.Match())\n\n    def deactivate(self):\n        self.clear()\n        super().deactivate()\n","python","",[172,204,205,224,237,250,263,270,275,294,307,322,337,350,369,382,395,415,443,468,482,495,503,508,519,532,540,559,564,582,610,632,640,658,683,688,698,711,722,730,735,745,758,772,780,793,801,806,816,836,844,851,857,869,877,882,893,904,912,920,925,935,942],{"__ignoreMap":202},[206,207,210,214,218,221],"span",{"class":208,"line":209},"line",1,[206,211,213],{"class":212},"snl16","from",[206,215,217],{"class":216},"s95oV"," qgis.PyQt.QtCore ",[206,219,220],{"class":212},"import",[206,222,223],{"class":216}," Qt\n",[206,225,227,229,232,234],{"class":208,"line":226},2,[206,228,213],{"class":212},[206,230,231],{"class":216}," qgis.PyQt.QtGui ",[206,233,220],{"class":212},[206,235,236],{"class":216}," QColor\n",[206,238,240,242,245,247],{"class":208,"line":239},3,[206,241,213],{"class":212},[206,243,244],{"class":216}," qgis.core ",[206,246,220],{"class":212},[206,248,249],{"class":216}," Qgis, QgsGeometry, QgsPointLocator, QgsPointXY\n",[206,251,253,255,258,260],{"class":208,"line":252},4,[206,254,213],{"class":212},[206,256,257],{"class":216}," qgis.gui ",[206,259,220],{"class":212},[206,261,262],{"class":216}," QgsMapTool, QgsRubberBand, QgsSnapIndicator\n",[206,264,266],{"class":208,"line":265},5,[206,267,269],{"emptyLinePlaceholder":268},true,"\n",[206,271,273],{"class":208,"line":272},6,[206,274,269],{"emptyLinePlaceholder":268},[206,276,278,281,285,288,291],{"class":208,"line":277},7,[206,279,280],{"class":212},"class",[206,282,284],{"class":283},"svObZ"," CaptureTool",[206,286,287],{"class":216},"(",[206,289,290],{"class":283},"QgsMapTool",[206,292,293],{"class":216},"):\n",[206,295,297,300,304],{"class":208,"line":296},8,[206,298,299],{"class":212},"    def",[206,301,303],{"class":302},"sDLfK"," __init__",[206,305,306],{"class":216},"(self, canvas, layer, on_finished):\n",[206,308,310,313,316,319],{"class":208,"line":309},9,[206,311,312],{"class":302},"        super",[206,314,315],{"class":216},"().",[206,317,318],{"class":302},"__init__",[206,320,321],{"class":216},"(canvas)\n",[206,323,325,328,331,334],{"class":208,"line":324},10,[206,326,327],{"class":302},"        self",[206,329,330],{"class":216},".layer ",[206,332,333],{"class":212},"=",[206,335,336],{"class":216}," layer\n",[206,338,340,342,345,347],{"class":208,"line":339},11,[206,341,327],{"class":302},[206,343,344],{"class":216},".on_finished ",[206,346,333],{"class":212},[206,348,349],{"class":216}," on_finished\n",[206,351,353,355,358,360,363,366],{"class":208,"line":352},12,[206,354,327],{"class":302},[206,356,357],{"class":216},".is_polygon ",[206,359,333],{"class":212},[206,361,362],{"class":216}," layer.geometryType() ",[206,364,365],{"class":212},"==",[206,367,368],{"class":216}," Qgis.GeometryType.Polygon\n",[206,370,372,374,377,379],{"class":208,"line":371},13,[206,373,327],{"class":302},[206,375,376],{"class":216},".points ",[206,378,333],{"class":212},[206,380,381],{"class":216}," []\n",[206,383,385,387,390,392],{"class":208,"line":384},14,[206,386,327],{"class":302},[206,388,389],{"class":216},".band ",[206,391,333],{"class":212},[206,393,394],{"class":216}," QgsRubberBand(\n",[206,396,398,401,404,407,409,412],{"class":208,"line":397},15,[206,399,400],{"class":216},"            canvas, Qgis.GeometryType.Polygon ",[206,402,403],{"class":212},"if",[206,405,406],{"class":302}," self",[206,408,357],{"class":216},[206,410,411],{"class":212},"else",[206,413,414],{"class":216}," Qgis.GeometryType.Line)\n",[206,416,418,420,423,426,429,431,433,435,437,440],{"class":208,"line":417},16,[206,419,327],{"class":302},[206,421,422],{"class":216},".band.setColor(QColor(",[206,424,425],{"class":302},"185",[206,427,428],{"class":216},", ",[206,430,54],{"class":302},[206,432,428],{"class":216},[206,434,54],{"class":302},[206,436,428],{"class":216},[206,438,439],{"class":302},"200",[206,441,442],{"class":216},"))\n",[206,444,446,448,451,453,455,457,459,461,463,466],{"class":208,"line":445},17,[206,447,327],{"class":302},[206,449,450],{"class":216},".band.setFillColor(QColor(",[206,452,425],{"class":302},[206,454,428],{"class":216},[206,456,54],{"class":302},[206,458,428],{"class":216},[206,460,54],{"class":302},[206,462,428],{"class":216},[206,464,465],{"class":302},"40",[206,467,442],{"class":216},[206,469,471,473,476,479],{"class":208,"line":470},18,[206,472,327],{"class":302},[206,474,475],{"class":216},".band.setWidth(",[206,477,478],{"class":302},"2",[206,480,481],{"class":216},")\n",[206,483,485,487,490,492],{"class":208,"line":484},19,[206,486,327],{"class":302},[206,488,489],{"class":216},".snap_indicator ",[206,491,333],{"class":212},[206,493,494],{"class":216}," QgsSnapIndicator(canvas)\n",[206,496,498,500],{"class":208,"line":497},20,[206,499,327],{"class":302},[206,501,502],{"class":216},".setCursor(Qt.CursorShape.CrossCursor)\n",[206,504,506],{"class":208,"line":505},21,[206,507,269],{"emptyLinePlaceholder":268},[206,509,511,513,516],{"class":208,"line":510},22,[206,512,299],{"class":212},[206,514,515],{"class":283}," _snapped",[206,517,518],{"class":216},"(self, event):\n",[206,520,522,525,527,529],{"class":208,"line":521},23,[206,523,524],{"class":216},"        match ",[206,526,333],{"class":212},[206,528,406],{"class":302},[206,530,531],{"class":216},".canvas().snappingUtils().snapToMap(event.pos())\n",[206,533,535,537],{"class":208,"line":534},24,[206,536,327],{"class":302},[206,538,539],{"class":216},".snap_indicator.setMatch(match)\n",[206,541,543,546,549,551,554,556],{"class":208,"line":542},25,[206,544,545],{"class":212},"        return",[206,547,548],{"class":216}," QgsPointXY(match.point()) ",[206,550,403],{"class":212},[206,552,553],{"class":216}," match.isValid() ",[206,555,411],{"class":212},[206,557,558],{"class":216}," event.mapPoint()\n",[206,560,562],{"class":208,"line":561},26,[206,563,269],{"emptyLinePlaceholder":268},[206,565,567,569,572,575,577,580],{"class":208,"line":566},27,[206,568,299],{"class":212},[206,570,571],{"class":283}," _redraw",[206,573,574],{"class":216},"(self, cursor",[206,576,333],{"class":212},[206,578,579],{"class":302},"None",[206,581,293],{"class":216},[206,583,585,588,590,592,594,597,600,602,605,607],{"class":208,"line":584},28,[206,586,587],{"class":216},"        pts ",[206,589,333],{"class":212},[206,591,406],{"class":302},[206,593,376],{"class":216},[206,595,596],{"class":212},"+",[206,598,599],{"class":216}," ([cursor] ",[206,601,403],{"class":212},[206,603,604],{"class":216}," cursor ",[206,606,411],{"class":212},[206,608,609],{"class":216}," [])\n",[206,611,613,616,618,621,623,625,627,629],{"class":208,"line":612},29,[206,614,615],{"class":216},"        geometry_type ",[206,617,333],{"class":212},[206,619,620],{"class":216}," Qgis.GeometryType.Polygon ",[206,622,403],{"class":212},[206,624,406],{"class":302},[206,626,357],{"class":216},[206,628,411],{"class":212},[206,630,631],{"class":216}," Qgis.GeometryType.Line\n",[206,633,635,637],{"class":208,"line":634},30,[206,636,327],{"class":302},[206,638,639],{"class":216},".band.reset(geometry_type)\n",[206,641,643,646,649,652,655],{"class":208,"line":642},31,[206,644,645],{"class":212},"        for",[206,647,648],{"class":216}," i, p ",[206,650,651],{"class":212},"in",[206,653,654],{"class":302}," enumerate",[206,656,657],{"class":216},"(pts):\n",[206,659,661,664,667,669,672,675,678,681],{"class":208,"line":660},32,[206,662,663],{"class":302},"            self",[206,665,666],{"class":216},".band.addPoint(p, i ",[206,668,365],{"class":212},[206,670,671],{"class":302}," len",[206,673,674],{"class":216},"(pts) ",[206,676,677],{"class":212},"-",[206,679,680],{"class":302}," 1",[206,682,481],{"class":216},[206,684,686],{"class":208,"line":685},33,[206,687,269],{"emptyLinePlaceholder":268},[206,689,691,693,696],{"class":208,"line":690},34,[206,692,299],{"class":212},[206,694,695],{"class":283}," canvasMoveEvent",[206,697,518],{"class":216},[206,699,701,704,706,708],{"class":208,"line":700},35,[206,702,703],{"class":216},"        cursor ",[206,705,333],{"class":212},[206,707,406],{"class":302},[206,709,710],{"class":216},"._snapped(event)\n",[206,712,714,717,719],{"class":208,"line":713},36,[206,715,716],{"class":212},"        if",[206,718,406],{"class":302},[206,720,721],{"class":216},".points:\n",[206,723,725,727],{"class":208,"line":724},37,[206,726,663],{"class":302},[206,728,729],{"class":216},"._redraw(cursor)\n",[206,731,733],{"class":208,"line":732},38,[206,734,269],{"emptyLinePlaceholder":268},[206,736,738,740,743],{"class":208,"line":737},39,[206,739,299],{"class":212},[206,741,742],{"class":283}," canvasReleaseEvent",[206,744,518],{"class":216},[206,746,748,750,753,755],{"class":208,"line":747},40,[206,749,716],{"class":212},[206,751,752],{"class":216}," event.button() ",[206,754,365],{"class":212},[206,756,757],{"class":216}," Qt.MouseButton.LeftButton:\n",[206,759,761,763,766,769],{"class":208,"line":760},41,[206,762,663],{"class":302},[206,764,765],{"class":216},".points.append(",[206,767,768],{"class":302},"self",[206,770,771],{"class":216},"._snapped(event))\n",[206,773,775,777],{"class":208,"line":774},42,[206,776,663],{"class":302},[206,778,779],{"class":216},"._redraw()\n",[206,781,783,786,788,790],{"class":208,"line":782},43,[206,784,785],{"class":212},"        elif",[206,787,752],{"class":216},[206,789,365],{"class":212},[206,791,792],{"class":216}," Qt.MouseButton.RightButton:\n",[206,794,796,798],{"class":208,"line":795},44,[206,797,663],{"class":302},[206,799,800],{"class":216},".finish()\n",[206,802,804],{"class":208,"line":803},45,[206,805,269],{"emptyLinePlaceholder":268},[206,807,809,811,814],{"class":208,"line":808},46,[206,810,299],{"class":212},[206,812,813],{"class":283}," keyPressEvent",[206,815,518],{"class":216},[206,817,819,821,824,826,829,832,834],{"class":208,"line":818},47,[206,820,716],{"class":212},[206,822,823],{"class":216}," event.key() ",[206,825,365],{"class":212},[206,827,828],{"class":216}," Qt.Key.Key_Backspace ",[206,830,831],{"class":212},"and",[206,833,406],{"class":302},[206,835,721],{"class":216},[206,837,839,841],{"class":208,"line":838},48,[206,840,663],{"class":302},[206,842,843],{"class":216},".points.pop()\n",[206,845,847,849],{"class":208,"line":846},49,[206,848,663],{"class":302},[206,850,779],{"class":216},[206,852,854],{"class":208,"line":853},50,[206,855,856],{"class":216},"            event.ignore()\n",[206,858,860,862,864,866],{"class":208,"line":859},51,[206,861,785],{"class":212},[206,863,823],{"class":216},[206,865,365],{"class":212},[206,867,868],{"class":216}," Qt.Key.Key_Escape:\n",[206,870,872,874],{"class":208,"line":871},52,[206,873,663],{"class":302},[206,875,876],{"class":216},".clear()\n",[206,878,880],{"class":208,"line":879},53,[206,881,269],{"emptyLinePlaceholder":268},[206,883,885,887,890],{"class":208,"line":884},54,[206,886,299],{"class":212},[206,888,889],{"class":283}," clear",[206,891,892],{"class":216},"(self):\n",[206,894,896,898,900,902],{"class":208,"line":895},55,[206,897,327],{"class":302},[206,899,376],{"class":216},[206,901,333],{"class":212},[206,903,381],{"class":216},[206,905,907,909],{"class":208,"line":906},56,[206,908,327],{"class":302},[206,910,911],{"class":216},".band.reset()\n",[206,913,915,917],{"class":208,"line":914},57,[206,916,327],{"class":302},[206,918,919],{"class":216},".snap_indicator.setMatch(QgsPointLocator.Match())\n",[206,921,923],{"class":208,"line":922},58,[206,924,269],{"emptyLinePlaceholder":268},[206,926,928,930,933],{"class":208,"line":927},59,[206,929,299],{"class":212},[206,931,932],{"class":283}," deactivate",[206,934,892],{"class":216},[206,936,938,940],{"class":208,"line":937},60,[206,939,327],{"class":302},[206,941,876],{"class":216},[206,943,945,947],{"class":208,"line":944},61,[206,946,312],{"class":302},[206,948,949],{"class":216},"().deactivate()\n",[14,951,952,955,956,959,960,964,965,968,969,972,973,976,977,980],{},[164,953,954],{},"Breakdown:"," ",[172,957,958],{},"snapToMap"," uses the project's snapping configuration — the same layers, modes and tolerance the user set for native tools — which is what makes a custom tool feel consistent; configuring snapping programmatically is covered in ",[21,961,963],{"href":962},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis\u002F","snapping to features with QgsSnappingUtils",". ",[172,966,967],{},"QgsSnapIndicator"," draws the same snap marker the built-in tools show. The rubber band is rebuilt on each move with the cursor as a temporary last point, which gives the trailing preview segment; passing ",[172,970,971],{},"True"," only for the final point avoids redrawing the canvas for every vertex added. ",[172,974,975],{},"event.ignore()"," after backspace stops QGIS also treating the key as \"delete selected features\". An empty ",[172,978,979],{},"QgsPointLocator.Match()"," hides the snap marker when the capture is cleared.",[153,982,984],{"id":983},"finish-validate-transform-and-commit","Finish: validate, transform and commit",[14,986,987,988,991],{},"Right click hands the vertices to ",[172,989,990],{},"finish",", which checks there are enough of them, converts from canvas CRS to layer CRS, and commits through the edit buffer so the addition can be undone.",[14,993,994],{},[29,995,998,1001,1004,1007,1021,1025,1030,1036,1040,1046,1050,1054,1057,1061,1065,1069,1072,1075,1079,1083,1086,1090,1093,1097,1100,1104,1110,1114,1117,1121,1125],{"viewBox":996,"role":32,"ariaLabel":997,"xmlns":34},"0 0 760 256","The commit sequence for a captured geometry: check vertex count, transform from canvas to layer coordinates, validate the geometry, create a feature with default values, show the attribute form, and add it inside an edit command that appears in the undo stack",[36,999,1000],{},"From rubber band to an undoable feature",[40,1002,1003],{},"Six steps. One: at least two vertices for a line or three for a polygon. Two: toLayerCoordinates converts each point from the canvas CRS to the layer CRS. Three: build the geometry and check isGeosValid. Four: QgsVectorLayerUtils.createFeature fills default values. Five: iface.openFeatureForm lets the user complete attributes, and cancelling abandons the feature. Six: beginEditCommand, addFeature and endEditCommand record one undo step.",[44,1005],{"x":46,"y":46,"width":47,"height":1006,"fill":49},"256",[1008,1009,1010],"defs",{},[1011,1012,1018],"marker",{"id":1013,"viewBox":1014,"refX":1015,"refY":84,"markerWidth":1016,"markerHeight":1016,"orient":1017},"digCommitArrow","0 0 10 10","8","7","auto-start-reverse",[70,1019],{"d":1020,"fill":104},"M0 0 L10 5 L0 10 z",[51,1022,1024],{"x":53,"y":1023,"style":55,"fill":56,"textAnchor":57},"26","Six steps between right click and the undo stack",[44,1026],{"x":97,"y":1027,"width":1028,"height":1029,"rx":65,"fill":66,"stroke":67,"style":68},"56","110","80",[51,1031,1035],{"x":1032,"y":1033,"style":1034,"fill":56,"textAnchor":57},"71","88","text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","1 count",[51,1037,1039],{"x":1032,"y":1028,"style":1038,"fill":104,"textAnchor":57},"text-anchor:middle;font-size:9.5px;font-family:sans-serif","≥2 or ≥3",[208,1041],{"x1":1042,"y1":1043,"x2":1044,"y2":1043,"stroke":104,"style":1045},"126","96","136","stroke-width:1.6;marker-end:url(#digCommitArrow)",[44,1047],{"x":1048,"y":1027,"width":1028,"height":1029,"rx":65,"fill":1049,"stroke":125,"style":68},"142","#eff3ff",[51,1051,1053],{"x":1052,"y":1033,"style":1034,"fill":125,"textAnchor":57},"197","2 transform",[51,1055,1056],{"x":1052,"y":1028,"style":1038,"fill":104,"textAnchor":57},"canvas → layer",[208,1058],{"x1":1059,"y1":1043,"x2":1060,"y2":1043,"stroke":104,"style":1045},"252","262",[44,1062],{"x":1063,"y":1027,"width":1028,"height":1029,"rx":65,"fill":1064,"stroke":98,"style":68},"268","#fdf2e2",[51,1066,1068],{"x":1067,"y":1033,"style":1034,"fill":98,"textAnchor":57},"323","3 validate",[51,1070,1071],{"x":1067,"y":1028,"style":1038,"fill":104,"textAnchor":57},"isGeosValid",[208,1073],{"x1":1074,"y1":1043,"x2":108,"y2":1043,"stroke":104,"style":1045},"378",[44,1076],{"x":1077,"y":1027,"width":1028,"height":1029,"rx":65,"fill":1078,"stroke":140,"style":68},"394","#eef7f4",[51,1080,1082],{"x":1081,"y":1033,"style":1034,"fill":140,"textAnchor":57},"449","4 defaults",[51,1084,1085],{"x":1081,"y":1028,"style":1038,"fill":104,"textAnchor":57},"createFeature",[208,1087],{"x1":1088,"y1":1043,"x2":1089,"y2":1043,"stroke":104,"style":1045},"504","514",[44,1091],{"x":1092,"y":1027,"width":1028,"height":1029,"rx":65,"fill":1078,"stroke":140,"style":68},"520",[51,1094,1096],{"x":1095,"y":1033,"style":1034,"fill":140,"textAnchor":57},"575","5 form",[51,1098,1099],{"x":1095,"y":1028,"style":1038,"fill":104,"textAnchor":57},"user completes",[208,1101],{"x1":1102,"y1":1043,"x2":1103,"y2":1043,"stroke":104,"style":1045},"630","640",[44,1105],{"x":1106,"y":1027,"width":1107,"height":1029,"rx":65,"fill":1108,"stroke":1109,"style":68},"646","98","#e8efe6","#15803d",[51,1111,1113],{"x":1112,"y":1033,"style":1034,"fill":115,"textAnchor":57},"695","6 add",[51,1115,1116],{"x":1112,"y":1028,"style":1038,"fill":104,"textAnchor":57},"edit command",[44,1118],{"x":124,"y":1119,"width":1092,"height":1027,"rx":65,"fill":66,"stroke":67,"style":1120},"170","stroke-width:1.8",[51,1122,1124],{"x":53,"y":101,"style":1123,"fill":104,"textAnchor":57},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","any failure clears the capture and tells the user why",[51,1126,1128],{"x":53,"y":1127,"style":103,"fill":67,"textAnchor":57},"214","nothing is written until step 6",[197,1130,1132],{"className":199,"code":1131,"language":201,"meta":202,"style":202},"from qgis.core import QgsPointLocator, QgsVectorLayerUtils\nfrom qgis.utils import iface\n\ndef finish(self):\n    minimum = 3 if self.is_polygon else 2\n    if len(self.points) \u003C minimum:\n        iface.messageBar().pushWarning(\"Capture\", f\"Need at least {minimum} vertices\")\n        return\n\n    layer_points = [self.toLayerCoordinates(self.layer, p) for p in self.points]\n    if self.is_polygon:\n        geometry = QgsGeometry.fromPolygonXY([layer_points + [layer_points[0]]])\n    else:\n        geometry = QgsGeometry.fromPolylineXY(layer_points)\n\n    if not geometry.isGeosValid():\n        iface.messageBar().pushWarning(\"Capture\", \"Geometry is invalid; not added\")\n        self.clear()\n        return\n\n    if not self.layer.isEditable():\n        self.layer.startEditing()\n\n    feature = QgsVectorLayerUtils.createFeature(\n        self.layer, geometry, {}, self.layer.createExpressionContext())\n\n    self.layer.beginEditCommand(\"Capture feature\")\n    if iface.openFeatureForm(self.layer, feature, False, True):\n        self.layer.endEditCommand()\n        self.on_finished(feature)\n    else:\n        self.layer.destroyEditCommand()\n    self.clear()\n    self.canvas().refresh()\n\nCaptureTool.finish = finish\n",[172,1133,1134,1145,1157,1161,1171,1193,1213,1244,1249,1253,1286,1295,1315,1323,1332,1336,1346,1359,1365,1369,1373,1384,1391,1395,1405,1417,1421,1434,1455,1462,1469,1475,1482,1488,1495,1499],{"__ignoreMap":202},[206,1135,1136,1138,1140,1142],{"class":208,"line":209},[206,1137,213],{"class":212},[206,1139,244],{"class":216},[206,1141,220],{"class":212},[206,1143,1144],{"class":216}," QgsPointLocator, QgsVectorLayerUtils\n",[206,1146,1147,1149,1152,1154],{"class":208,"line":226},[206,1148,213],{"class":212},[206,1150,1151],{"class":216}," qgis.utils ",[206,1153,220],{"class":212},[206,1155,1156],{"class":216}," iface\n",[206,1158,1159],{"class":208,"line":239},[206,1160,269],{"emptyLinePlaceholder":268},[206,1162,1163,1166,1169],{"class":208,"line":252},[206,1164,1165],{"class":212},"def",[206,1167,1168],{"class":283}," finish",[206,1170,892],{"class":216},[206,1172,1173,1176,1178,1181,1184,1186,1188,1190],{"class":208,"line":265},[206,1174,1175],{"class":216},"    minimum ",[206,1177,333],{"class":212},[206,1179,1180],{"class":302}," 3",[206,1182,1183],{"class":212}," if",[206,1185,406],{"class":302},[206,1187,357],{"class":216},[206,1189,411],{"class":212},[206,1191,1192],{"class":302}," 2\n",[206,1194,1195,1198,1200,1202,1204,1207,1210],{"class":208,"line":272},[206,1196,1197],{"class":212},"    if",[206,1199,671],{"class":302},[206,1201,287],{"class":216},[206,1203,768],{"class":302},[206,1205,1206],{"class":216},".points) ",[206,1208,1209],{"class":212},"\u003C",[206,1211,1212],{"class":216}," minimum:\n",[206,1214,1215,1218,1222,1224,1227,1230,1233,1236,1239,1242],{"class":208,"line":277},[206,1216,1217],{"class":216},"        iface.messageBar().pushWarning(",[206,1219,1221],{"class":1220},"sU2Wk","\"Capture\"",[206,1223,428],{"class":216},[206,1225,1226],{"class":212},"f",[206,1228,1229],{"class":1220},"\"Need at least ",[206,1231,1232],{"class":302},"{",[206,1234,1235],{"class":216},"minimum",[206,1237,1238],{"class":302},"}",[206,1240,1241],{"class":1220}," vertices\"",[206,1243,481],{"class":216},[206,1245,1246],{"class":208,"line":296},[206,1247,1248],{"class":212},"        return\n",[206,1250,1251],{"class":208,"line":309},[206,1252,269],{"emptyLinePlaceholder":268},[206,1254,1255,1258,1260,1263,1265,1268,1270,1273,1276,1279,1281,1283],{"class":208,"line":324},[206,1256,1257],{"class":216},"    layer_points ",[206,1259,333],{"class":212},[206,1261,1262],{"class":216}," [",[206,1264,768],{"class":302},[206,1266,1267],{"class":216},".toLayerCoordinates(",[206,1269,768],{"class":302},[206,1271,1272],{"class":216},".layer, p) ",[206,1274,1275],{"class":212},"for",[206,1277,1278],{"class":216}," p ",[206,1280,651],{"class":212},[206,1282,406],{"class":302},[206,1284,1285],{"class":216},".points]\n",[206,1287,1288,1290,1292],{"class":208,"line":339},[206,1289,1197],{"class":212},[206,1291,406],{"class":302},[206,1293,1294],{"class":216},".is_polygon:\n",[206,1296,1297,1300,1302,1305,1307,1310,1312],{"class":208,"line":352},[206,1298,1299],{"class":216},"        geometry ",[206,1301,333],{"class":212},[206,1303,1304],{"class":216}," QgsGeometry.fromPolygonXY([layer_points ",[206,1306,596],{"class":212},[206,1308,1309],{"class":216}," [layer_points[",[206,1311,46],{"class":302},[206,1313,1314],{"class":216},"]]])\n",[206,1316,1317,1320],{"class":208,"line":371},[206,1318,1319],{"class":212},"    else",[206,1321,1322],{"class":216},":\n",[206,1324,1325,1327,1329],{"class":208,"line":384},[206,1326,1299],{"class":216},[206,1328,333],{"class":212},[206,1330,1331],{"class":216}," QgsGeometry.fromPolylineXY(layer_points)\n",[206,1333,1334],{"class":208,"line":397},[206,1335,269],{"emptyLinePlaceholder":268},[206,1337,1338,1340,1343],{"class":208,"line":417},[206,1339,1197],{"class":212},[206,1341,1342],{"class":212}," not",[206,1344,1345],{"class":216}," geometry.isGeosValid():\n",[206,1347,1348,1350,1352,1354,1357],{"class":208,"line":445},[206,1349,1217],{"class":216},[206,1351,1221],{"class":1220},[206,1353,428],{"class":216},[206,1355,1356],{"class":1220},"\"Geometry is invalid; not added\"",[206,1358,481],{"class":216},[206,1360,1361,1363],{"class":208,"line":470},[206,1362,327],{"class":302},[206,1364,876],{"class":216},[206,1366,1367],{"class":208,"line":484},[206,1368,1248],{"class":212},[206,1370,1371],{"class":208,"line":497},[206,1372,269],{"emptyLinePlaceholder":268},[206,1374,1375,1377,1379,1381],{"class":208,"line":505},[206,1376,1197],{"class":212},[206,1378,1342],{"class":212},[206,1380,406],{"class":302},[206,1382,1383],{"class":216},".layer.isEditable():\n",[206,1385,1386,1388],{"class":208,"line":510},[206,1387,327],{"class":302},[206,1389,1390],{"class":216},".layer.startEditing()\n",[206,1392,1393],{"class":208,"line":521},[206,1394,269],{"emptyLinePlaceholder":268},[206,1396,1397,1400,1402],{"class":208,"line":534},[206,1398,1399],{"class":216},"    feature ",[206,1401,333],{"class":212},[206,1403,1404],{"class":216}," QgsVectorLayerUtils.createFeature(\n",[206,1406,1407,1409,1412,1414],{"class":208,"line":542},[206,1408,327],{"class":302},[206,1410,1411],{"class":216},".layer, geometry, {}, ",[206,1413,768],{"class":302},[206,1415,1416],{"class":216},".layer.createExpressionContext())\n",[206,1418,1419],{"class":208,"line":561},[206,1420,269],{"emptyLinePlaceholder":268},[206,1422,1423,1426,1429,1432],{"class":208,"line":566},[206,1424,1425],{"class":302},"    self",[206,1427,1428],{"class":216},".layer.beginEditCommand(",[206,1430,1431],{"class":1220},"\"Capture feature\"",[206,1433,481],{"class":216},[206,1435,1436,1438,1441,1443,1446,1449,1451,1453],{"class":208,"line":584},[206,1437,1197],{"class":212},[206,1439,1440],{"class":216}," iface.openFeatureForm(",[206,1442,768],{"class":302},[206,1444,1445],{"class":216},".layer, feature, ",[206,1447,1448],{"class":302},"False",[206,1450,428],{"class":216},[206,1452,971],{"class":302},[206,1454,293],{"class":216},[206,1456,1457,1459],{"class":208,"line":612},[206,1458,327],{"class":302},[206,1460,1461],{"class":216},".layer.endEditCommand()\n",[206,1463,1464,1466],{"class":208,"line":634},[206,1465,327],{"class":302},[206,1467,1468],{"class":216},".on_finished(feature)\n",[206,1470,1471,1473],{"class":208,"line":642},[206,1472,1319],{"class":212},[206,1474,1322],{"class":216},[206,1476,1477,1479],{"class":208,"line":660},[206,1478,327],{"class":302},[206,1480,1481],{"class":216},".layer.destroyEditCommand()\n",[206,1483,1484,1486],{"class":208,"line":685},[206,1485,1425],{"class":302},[206,1487,876],{"class":216},[206,1489,1490,1492],{"class":208,"line":690},[206,1491,1425],{"class":302},[206,1493,1494],{"class":216},".canvas().refresh()\n",[206,1496,1497],{"class":208,"line":700},[206,1498,269],{"emptyLinePlaceholder":268},[206,1500,1501,1504,1506],{"class":208,"line":713},[206,1502,1503],{"class":216},"CaptureTool.finish ",[206,1505,333],{"class":212},[206,1507,1508],{"class":216}," finish\n",[14,1510,1511,955,1513,1516,1517,1519,1520,964,1524,1527,1528,1531,1532,1535],{},[164,1512,954],{},[172,1514,1515],{},"toLayerCoordinates"," converts from the canvas CRS to the layer CRS using the project's transform context, so a tool drawing on a Web Mercator canvas writes correct coordinates into a British National Grid layer. Polygon rings are closed explicitly by repeating the first point. Validating before touching the layer means an invalid shape never enters the edit buffer. ",[172,1518,1085],{}," evaluates default value expressions, as described in ",[21,1521,1523],{"href":1522},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis\u002F","setting default values and field constraints",[172,1525,1526],{},"openFeatureForm"," with ",[172,1529,1530],{},"updateFeatureOnly=False"," shows the layer's configured form and adds the feature when the user clicks OK; cancelling returns false, and ",[172,1533,1534],{},"destroyEditCommand"," removes the empty undo step. The whole capture is one entry in the undo stack, labelled so the user sees what they would undo.",[153,1537,1539],{"id":1538},"enforce-a-capture-rule","Enforce a capture rule",[14,1541,1542],{},"The reason to write a custom tool is usually a rule. Here the first and last vertices of a pipe must snap to a valve.",[197,1544,1546],{"className":199,"code":1545,"language":201,"meta":202,"style":202},"from qgis.core import QgsProject, QgsPointLocator\n\nvalves = QgsProject.instance().mapLayersByName(\"valves\")[0]\n\ndef _snap_to_valve(self, event):\n    locator = self.canvas().snappingUtils().locatorForLayer(valves)\n    tolerance = self.canvas().mapUnitsPerPixel() * 12\n    point = self.toLayerCoordinates(valves, event.mapPoint())\n    match = locator.nearestVertex(point, tolerance)\n    return match\n\ndef canvasReleaseEvent(self, event):\n    if event.button() == Qt.MouseButton.LeftButton:\n        if not self.points:\n            match = self._snap_to_valve(event)\n            if not match.isValid():\n                iface.messageBar().pushInfo(\"Pipe\", \"Start the pipe on a valve\")\n                return\n            self.points.append(self.toMapCoordinates(valves, match.point()))\n        else:\n            self.points.append(self._snapped(event))\n        self._redraw()\n    elif event.button() == Qt.MouseButton.RightButton:\n        last_valve = self._snap_to_valve(event)\n        if not last_valve.isValid():\n            iface.messageBar().pushInfo(\"Pipe\", \"Finish the pipe on a valve\")\n            return\n        self.points.append(self.toMapCoordinates(valves, last_valve.point()))\n        self.finish()\n\nCaptureTool._snap_to_valve = _snap_to_valve\nCaptureTool.canvasReleaseEvent = canvasReleaseEvent\n",[172,1547,1548,1559,1563,1584,1588,1597,1609,1627,1639,1649,1657,1661,1669,1679,1689,1701,1711,1726,1731,1742,1749,1759,1765,1776,1787,1796,1810,1815,1826,1832,1836,1846],{"__ignoreMap":202},[206,1549,1550,1552,1554,1556],{"class":208,"line":209},[206,1551,213],{"class":212},[206,1553,244],{"class":216},[206,1555,220],{"class":212},[206,1557,1558],{"class":216}," QgsProject, QgsPointLocator\n",[206,1560,1561],{"class":208,"line":226},[206,1562,269],{"emptyLinePlaceholder":268},[206,1564,1565,1568,1570,1573,1576,1579,1581],{"class":208,"line":239},[206,1566,1567],{"class":216},"valves ",[206,1569,333],{"class":212},[206,1571,1572],{"class":216}," QgsProject.instance().mapLayersByName(",[206,1574,1575],{"class":1220},"\"valves\"",[206,1577,1578],{"class":216},")[",[206,1580,46],{"class":302},[206,1582,1583],{"class":216},"]\n",[206,1585,1586],{"class":208,"line":252},[206,1587,269],{"emptyLinePlaceholder":268},[206,1589,1590,1592,1595],{"class":208,"line":265},[206,1591,1165],{"class":212},[206,1593,1594],{"class":283}," _snap_to_valve",[206,1596,518],{"class":216},[206,1598,1599,1602,1604,1606],{"class":208,"line":272},[206,1600,1601],{"class":216},"    locator ",[206,1603,333],{"class":212},[206,1605,406],{"class":302},[206,1607,1608],{"class":216},".canvas().snappingUtils().locatorForLayer(valves)\n",[206,1610,1611,1614,1616,1618,1621,1624],{"class":208,"line":277},[206,1612,1613],{"class":216},"    tolerance ",[206,1615,333],{"class":212},[206,1617,406],{"class":302},[206,1619,1620],{"class":216},".canvas().mapUnitsPerPixel() ",[206,1622,1623],{"class":212},"*",[206,1625,1626],{"class":302}," 12\n",[206,1628,1629,1632,1634,1636],{"class":208,"line":296},[206,1630,1631],{"class":216},"    point ",[206,1633,333],{"class":212},[206,1635,406],{"class":302},[206,1637,1638],{"class":216},".toLayerCoordinates(valves, event.mapPoint())\n",[206,1640,1641,1644,1646],{"class":208,"line":309},[206,1642,1643],{"class":216},"    match ",[206,1645,333],{"class":212},[206,1647,1648],{"class":216}," locator.nearestVertex(point, tolerance)\n",[206,1650,1651,1654],{"class":208,"line":324},[206,1652,1653],{"class":212},"    return",[206,1655,1656],{"class":216}," match\n",[206,1658,1659],{"class":208,"line":339},[206,1660,269],{"emptyLinePlaceholder":268},[206,1662,1663,1665,1667],{"class":208,"line":352},[206,1664,1165],{"class":212},[206,1666,742],{"class":283},[206,1668,518],{"class":216},[206,1670,1671,1673,1675,1677],{"class":208,"line":371},[206,1672,1197],{"class":212},[206,1674,752],{"class":216},[206,1676,365],{"class":212},[206,1678,757],{"class":216},[206,1680,1681,1683,1685,1687],{"class":208,"line":384},[206,1682,716],{"class":212},[206,1684,1342],{"class":212},[206,1686,406],{"class":302},[206,1688,721],{"class":216},[206,1690,1691,1694,1696,1698],{"class":208,"line":397},[206,1692,1693],{"class":216},"            match ",[206,1695,333],{"class":212},[206,1697,406],{"class":302},[206,1699,1700],{"class":216},"._snap_to_valve(event)\n",[206,1702,1703,1706,1708],{"class":208,"line":417},[206,1704,1705],{"class":212},"            if",[206,1707,1342],{"class":212},[206,1709,1710],{"class":216}," match.isValid():\n",[206,1712,1713,1716,1719,1721,1724],{"class":208,"line":445},[206,1714,1715],{"class":216},"                iface.messageBar().pushInfo(",[206,1717,1718],{"class":1220},"\"Pipe\"",[206,1720,428],{"class":216},[206,1722,1723],{"class":1220},"\"Start the pipe on a valve\"",[206,1725,481],{"class":216},[206,1727,1728],{"class":208,"line":470},[206,1729,1730],{"class":212},"                return\n",[206,1732,1733,1735,1737,1739],{"class":208,"line":484},[206,1734,663],{"class":302},[206,1736,765],{"class":216},[206,1738,768],{"class":302},[206,1740,1741],{"class":216},".toMapCoordinates(valves, match.point()))\n",[206,1743,1744,1747],{"class":208,"line":497},[206,1745,1746],{"class":212},"        else",[206,1748,1322],{"class":216},[206,1750,1751,1753,1755,1757],{"class":208,"line":505},[206,1752,663],{"class":302},[206,1754,765],{"class":216},[206,1756,768],{"class":302},[206,1758,771],{"class":216},[206,1760,1761,1763],{"class":208,"line":510},[206,1762,327],{"class":302},[206,1764,779],{"class":216},[206,1766,1767,1770,1772,1774],{"class":208,"line":521},[206,1768,1769],{"class":212},"    elif",[206,1771,752],{"class":216},[206,1773,365],{"class":212},[206,1775,792],{"class":216},[206,1777,1778,1781,1783,1785],{"class":208,"line":534},[206,1779,1780],{"class":216},"        last_valve ",[206,1782,333],{"class":212},[206,1784,406],{"class":302},[206,1786,1700],{"class":216},[206,1788,1789,1791,1793],{"class":208,"line":542},[206,1790,716],{"class":212},[206,1792,1342],{"class":212},[206,1794,1795],{"class":216}," last_valve.isValid():\n",[206,1797,1798,1801,1803,1805,1808],{"class":208,"line":561},[206,1799,1800],{"class":216},"            iface.messageBar().pushInfo(",[206,1802,1718],{"class":1220},[206,1804,428],{"class":216},[206,1806,1807],{"class":1220},"\"Finish the pipe on a valve\"",[206,1809,481],{"class":216},[206,1811,1812],{"class":208,"line":566},[206,1813,1814],{"class":212},"            return\n",[206,1816,1817,1819,1821,1823],{"class":208,"line":584},[206,1818,327],{"class":302},[206,1820,765],{"class":216},[206,1822,768],{"class":302},[206,1824,1825],{"class":216},".toMapCoordinates(valves, last_valve.point()))\n",[206,1827,1828,1830],{"class":208,"line":612},[206,1829,327],{"class":302},[206,1831,800],{"class":216},[206,1833,1834],{"class":208,"line":634},[206,1835,269],{"emptyLinePlaceholder":268},[206,1837,1838,1841,1843],{"class":208,"line":642},[206,1839,1840],{"class":216},"CaptureTool._snap_to_valve ",[206,1842,333],{"class":212},[206,1844,1845],{"class":216}," _snap_to_valve\n",[206,1847,1848,1851,1853],{"class":208,"line":660},[206,1849,1850],{"class":216},"CaptureTool.canvasReleaseEvent ",[206,1852,333],{"class":212},[206,1854,1855],{"class":216}," canvasReleaseEvent\n",[14,1857,1858,955,1860,1863],{},[164,1859,954],{},[172,1861,1862],{},"locatorForLayer"," returns the point locator that the snapping utilities maintain for a layer, so the valve lookup reuses an existing index rather than building one. The tolerance is twelve pixels converted to map units, which keeps the feel constant across zoom levels. Refusing the click with a message is friendlier than letting the user draw a whole pipe and rejecting it at the end. Converting the valve's layer coordinates back to map coordinates keeps the internal vertex list in one CRS. Monkey-patching methods onto the class is only for presenting the rule separately here; in a plugin, write them directly in the class.",[14,1865,1866],{},[29,1867,1870,1873,1876,1879,1886,1889,1893,1899,1903,1909,1913,1916,1920,1924,1927,1930,1933,1937],{"viewBox":1868,"role":32,"ariaLabel":1869,"xmlns":34},"0 0 760 236","Activating and deactivating the capture tool from a plugin action: a checkable toolbar action sets the tool on the canvas, the tool is unset when the layer changes or editing stops, and the action unchecks when another tool is chosen",[36,1871,1872],{},"Wire the tool to a checkable action",[40,1874,1875],{},"A plugin toolbar button, checkable, sets the capture tool on the map canvas when checked. The tool's action is linked with setAction so the button unchecks automatically when the user switches to another tool. When the active layer changes or stops being editable, the plugin unsets the tool so it never writes into the wrong layer.",[44,1877],{"x":46,"y":46,"width":47,"height":1878,"fill":49},"236",[1008,1880,1881],{},[1011,1882,1884],{"id":1883,"viewBox":1014,"refX":1015,"refY":84,"markerWidth":1016,"markerHeight":1016,"orient":1017},"digWireArrow",[70,1885],{"d":1020,"fill":104},[51,1887,1888],{"x":53,"y":1023,"style":55,"fill":56,"textAnchor":57},"Behave like a native tool in the toolbar",[44,1890],{"x":61,"y":1891,"width":92,"height":1892,"rx":65,"fill":66,"stroke":67,"style":68},"66","90",[51,1894,1898],{"x":1895,"y":1896,"style":1897,"fill":56,"textAnchor":57},"114","100","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","toolbar action",[51,1900,1902],{"x":1895,"y":1901,"style":103,"fill":104,"textAnchor":57},"124","checkable",[208,1904],{"x1":1905,"y1":1906,"x2":1907,"y2":1906,"stroke":104,"style":1908},"204","111","266","stroke-width:1.8;marker-end:url(#digWireArrow)",[44,1910],{"x":1911,"y":1891,"width":1912,"height":1892,"rx":65,"fill":1078,"stroke":140,"style":68},"274","212",[51,1914,1915],{"x":53,"y":1896,"style":1897,"fill":140,"textAnchor":57},"canvas.setMapTool",[51,1917,1919],{"x":53,"y":1901,"style":1918,"fill":104,"textAnchor":57},"text-anchor:middle;font-size:10px;font-family:monospace","tool.setAction(action)",[208,1921],{"x1":1922,"y1":1906,"x2":1923,"y2":1906,"stroke":104,"style":1908},"486","548",[44,1925],{"x":1926,"y":1891,"width":92,"height":1892,"rx":65,"fill":1064,"stroke":98,"style":68},"556",[51,1928,1929],{"x":1106,"y":1896,"style":1897,"fill":98,"textAnchor":57},"guard",[51,1931,1932],{"x":1106,"y":1901,"style":103,"fill":104,"textAnchor":57},"layer changed? unset",[44,1934],{"x":124,"y":1935,"width":1092,"height":1936,"rx":1015,"fill":66,"stroke":67,"style":1120},"182","34",[51,1938,1939],{"x":53,"y":1905,"style":1123,"fill":104,"textAnchor":57},"the button unchecks itself when the user picks another tool",[153,1941,1943],{"id":1942},"wire-it-into-a-plugin","Wire it into a plugin",[14,1945,1946],{},"A capture tool belongs behind a checkable toolbar action, tied to the layer that was active when it was switched on.",[197,1948,1950],{"className":199,"code":1949,"language":201,"meta":202,"style":202},"from qgis.PyQt.QtWidgets import QAction\nfrom qgis.core import Qgis\n\ndef init_capture_action(plugin):\n    plugin.capture_action = QAction(\"Capture pipe\", plugin.iface.mainWindow())\n    plugin.capture_action.setCheckable(True)\n    plugin.capture_action.triggered.connect(lambda checked: toggle_capture(plugin, checked))\n    plugin.iface.addToolBarIcon(plugin.capture_action)\n    plugin.iface.currentLayerChanged.connect(lambda _layer: stop_capture(plugin))\n\ndef toggle_capture(plugin, checked):\n    canvas = plugin.iface.mapCanvas()\n    layer = plugin.iface.activeLayer()\n    if not checked:\n        stop_capture(plugin)\n        return\n    if layer is None or layer.type() != Qgis.LayerType.Vector:\n        plugin.capture_action.setChecked(False)\n        return\n    plugin.capture_tool = CaptureTool(canvas, layer, on_finished=lambda f: None)\n    plugin.capture_tool.setAction(plugin.capture_action)\n    canvas.setMapTool(plugin.capture_tool)\n\ndef stop_capture(plugin):\n    tool = getattr(plugin, \"capture_tool\", None)\n    if tool is not None and plugin.iface.mapCanvas().mapTool() is tool:\n        plugin.iface.mapCanvas().unsetMapTool(tool)\n    plugin.capture_tool = None\n",[172,1951,1952,1964,1975,1979,1989,2005,2014,2025,2030,2040,2044,2054,2064,2074,2083,2088,2092,2117,2126,2130,2154,2159,2164,2168,2177,2199,2223,2228],{"__ignoreMap":202},[206,1953,1954,1956,1959,1961],{"class":208,"line":209},[206,1955,213],{"class":212},[206,1957,1958],{"class":216}," qgis.PyQt.QtWidgets ",[206,1960,220],{"class":212},[206,1962,1963],{"class":216}," QAction\n",[206,1965,1966,1968,1970,1972],{"class":208,"line":226},[206,1967,213],{"class":212},[206,1969,244],{"class":216},[206,1971,220],{"class":212},[206,1973,1974],{"class":216}," Qgis\n",[206,1976,1977],{"class":208,"line":239},[206,1978,269],{"emptyLinePlaceholder":268},[206,1980,1981,1983,1986],{"class":208,"line":252},[206,1982,1165],{"class":212},[206,1984,1985],{"class":283}," init_capture_action",[206,1987,1988],{"class":216},"(plugin):\n",[206,1990,1991,1994,1996,1999,2002],{"class":208,"line":265},[206,1992,1993],{"class":216},"    plugin.capture_action ",[206,1995,333],{"class":212},[206,1997,1998],{"class":216}," QAction(",[206,2000,2001],{"class":1220},"\"Capture pipe\"",[206,2003,2004],{"class":216},", plugin.iface.mainWindow())\n",[206,2006,2007,2010,2012],{"class":208,"line":272},[206,2008,2009],{"class":216},"    plugin.capture_action.setCheckable(",[206,2011,971],{"class":302},[206,2013,481],{"class":216},[206,2015,2016,2019,2022],{"class":208,"line":277},[206,2017,2018],{"class":216},"    plugin.capture_action.triggered.connect(",[206,2020,2021],{"class":212},"lambda",[206,2023,2024],{"class":216}," checked: toggle_capture(plugin, checked))\n",[206,2026,2027],{"class":208,"line":296},[206,2028,2029],{"class":216},"    plugin.iface.addToolBarIcon(plugin.capture_action)\n",[206,2031,2032,2035,2037],{"class":208,"line":309},[206,2033,2034],{"class":216},"    plugin.iface.currentLayerChanged.connect(",[206,2036,2021],{"class":212},[206,2038,2039],{"class":216}," _layer: stop_capture(plugin))\n",[206,2041,2042],{"class":208,"line":324},[206,2043,269],{"emptyLinePlaceholder":268},[206,2045,2046,2048,2051],{"class":208,"line":339},[206,2047,1165],{"class":212},[206,2049,2050],{"class":283}," toggle_capture",[206,2052,2053],{"class":216},"(plugin, checked):\n",[206,2055,2056,2059,2061],{"class":208,"line":352},[206,2057,2058],{"class":216},"    canvas ",[206,2060,333],{"class":212},[206,2062,2063],{"class":216}," plugin.iface.mapCanvas()\n",[206,2065,2066,2069,2071],{"class":208,"line":371},[206,2067,2068],{"class":216},"    layer ",[206,2070,333],{"class":212},[206,2072,2073],{"class":216}," plugin.iface.activeLayer()\n",[206,2075,2076,2078,2080],{"class":208,"line":384},[206,2077,1197],{"class":212},[206,2079,1342],{"class":212},[206,2081,2082],{"class":216}," checked:\n",[206,2084,2085],{"class":208,"line":397},[206,2086,2087],{"class":216},"        stop_capture(plugin)\n",[206,2089,2090],{"class":208,"line":417},[206,2091,1248],{"class":212},[206,2093,2094,2096,2099,2102,2105,2108,2111,2114],{"class":208,"line":445},[206,2095,1197],{"class":212},[206,2097,2098],{"class":216}," layer ",[206,2100,2101],{"class":212},"is",[206,2103,2104],{"class":302}," None",[206,2106,2107],{"class":212}," or",[206,2109,2110],{"class":216}," layer.type() ",[206,2112,2113],{"class":212},"!=",[206,2115,2116],{"class":216}," Qgis.LayerType.Vector:\n",[206,2118,2119,2122,2124],{"class":208,"line":470},[206,2120,2121],{"class":216},"        plugin.capture_action.setChecked(",[206,2123,1448],{"class":302},[206,2125,481],{"class":216},[206,2127,2128],{"class":208,"line":484},[206,2129,1248],{"class":212},[206,2131,2132,2135,2137,2140,2144,2147,2150,2152],{"class":208,"line":497},[206,2133,2134],{"class":216},"    plugin.capture_tool ",[206,2136,333],{"class":212},[206,2138,2139],{"class":216}," CaptureTool(canvas, layer, ",[206,2141,2143],{"class":2142},"s9osk","on_finished",[206,2145,2146],{"class":212},"=lambda",[206,2148,2149],{"class":216}," f: ",[206,2151,579],{"class":302},[206,2153,481],{"class":216},[206,2155,2156],{"class":208,"line":505},[206,2157,2158],{"class":216},"    plugin.capture_tool.setAction(plugin.capture_action)\n",[206,2160,2161],{"class":208,"line":510},[206,2162,2163],{"class":216},"    canvas.setMapTool(plugin.capture_tool)\n",[206,2165,2166],{"class":208,"line":521},[206,2167,269],{"emptyLinePlaceholder":268},[206,2169,2170,2172,2175],{"class":208,"line":534},[206,2171,1165],{"class":212},[206,2173,2174],{"class":283}," stop_capture",[206,2176,1988],{"class":216},[206,2178,2179,2182,2184,2187,2190,2193,2195,2197],{"class":208,"line":542},[206,2180,2181],{"class":216},"    tool ",[206,2183,333],{"class":212},[206,2185,2186],{"class":302}," getattr",[206,2188,2189],{"class":216},"(plugin, ",[206,2191,2192],{"class":1220},"\"capture_tool\"",[206,2194,428],{"class":216},[206,2196,579],{"class":302},[206,2198,481],{"class":216},[206,2200,2201,2203,2206,2208,2210,2212,2215,2218,2220],{"class":208,"line":561},[206,2202,1197],{"class":212},[206,2204,2205],{"class":216}," tool ",[206,2207,2101],{"class":212},[206,2209,1342],{"class":212},[206,2211,2104],{"class":302},[206,2213,2214],{"class":212}," and",[206,2216,2217],{"class":216}," plugin.iface.mapCanvas().mapTool() ",[206,2219,2101],{"class":212},[206,2221,2222],{"class":216}," tool:\n",[206,2224,2225],{"class":208,"line":566},[206,2226,2227],{"class":216},"        plugin.iface.mapCanvas().unsetMapTool(tool)\n",[206,2229,2230,2232,2234],{"class":208,"line":584},[206,2231,2134],{"class":216},[206,2233,333],{"class":212},[206,2235,2236],{"class":302}," None\n",[14,2238,2239,955,2241,2244,2245,2248,2249,188],{},[164,2240,954],{},[172,2242,2243],{},"setAction"," links the tool to its button so QGIS unchecks the button when another tool takes over. Stopping capture when the active layer changes prevents the classic bug where a user switches layers mid-session and digitises into the wrong one. Keeping the tool on the plugin object keeps it alive; a tool held only by a local variable is garbage-collected and the canvas is left pointing at nothing. Remove the action and disconnect the signal in the plugin's ",[172,2246,2247],{},"unload",", as covered in ",[21,2250,2252],{"href":2251},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","adding a toolbar button to a QGIS plugin",[153,2254,2256],{"id":2255},"qgis-version-compatibility","QGIS version compatibility",[14,2258,2259,428,2261,428,2264,2266,2267,2269,2270,2273,2274,2277,2278,428,2281,428,2284,2287,2288,2291],{},[172,2260,290],{},[172,2262,2263],{},"QgsRubberBand",[172,2265,967],{}," and ",[172,2268,1526],{}," are available across QGIS 3.x. ",[172,2271,2272],{},"Qgis.GeometryType"," replaced ",[172,2275,2276],{},"QgsWkbTypes.GeometryType"," in 3.30; on the QGIS 4 series only the scoped enums work, including Qt's — ",[172,2279,2280],{},"Qt.MouseButton.LeftButton",[172,2282,2283],{},"Qt.Key.Key_Backspace",[172,2285,2286],{},"Qt.CursorShape.CrossCursor",". QGIS also offers ",[172,2289,2290],{},"QgsMapToolDigitizeFeature",", which integrates the advanced digitizing panel; it suits tools that need CAD constraints and is less flexible for custom rules.",[153,2293,2295],{"id":2294},"troubleshooting","Troubleshooting",[158,2297,2298,2307,2313,2319,2325],{},[161,2299,2300,2303,2304,2306],{},[164,2301,2302],{},"Vertices land in the wrong place."," Points were not converted with ",[172,2305,1515],{}," before building the geometry.",[161,2308,2309,2312],{},[164,2310,2311],{},"Backspace deletes selected features."," The key event was not ignored.",[161,2314,2315,2318],{},[164,2316,2317],{},"Snapping never happens."," Snapping is off in the project, or the target layer is not in the snapping configuration.",[161,2320,2321,2324],{},[164,2322,2323],{},"Nothing appears after OK in the form."," The canvas was not refreshed, or the layer was not in edit mode.",[161,2326,2327,2330],{},[164,2328,2329],{},"The tool stops responding."," It was garbage-collected; keep a reference on the plugin.",[153,2332,2334],{"id":2333},"conclusion","Conclusion",[14,2336,2337],{},"Keep captured vertices in canvas coordinates, snap every position through the canvas's snapping utilities, preview with a rubber band, and support backspace and escape. On finish, convert to layer coordinates, validate, create the feature with defaults, open the form, and add it inside a named edit command. Enforce capture rules while the user draws, and wire the tool to a checkable action that stops when the active layer changes.",[153,2339,2341],{"id":2340},"frequently-asked-questions","Frequently Asked Questions",[14,2343,2344,2347,2348,2351,2352,2354],{},[164,2345,2346],{},"Can the tool capture curves?","\nYes, by building a ",[172,2349,2350],{},"QgsCompoundCurve"," from arc segments, but it needs extra UI. ",[172,2353,2290],{}," handles curves natively.",[14,2356,2357,2360,2361,2363],{},[164,2358,2359],{},"How do I prefill attributes from what the line touches?","\nBefore ",[172,2362,1526],{},", query the relevant layers with the geometry and set attributes on the feature; the form shows them for confirmation.",[14,2365,2366,2369,2370,2373],{},[164,2367,2368],{},"Can I capture without showing the form?","\nCall ",[172,2371,2372],{},"layer.addFeature(feature)"," inside the edit command instead of opening the form, or suppress the form in the layer's form configuration.",[14,2375,2376,2379,2380,2383],{},[164,2377,2378],{},"Does this work with the advanced digitizing panel?","\nNot automatically. Subclass ",[172,2381,2382],{},"QgsMapToolAdvancedDigitizing"," if users need angle and distance constraints.",[153,2385,2387],{"id":2386},"related","Related",[158,2389,2390,2395,2400,2405,2410],{},[161,2391,2392,2394],{},[21,2393,24],{"href":23}," — the guide this recipe belongs to",[161,2396,2397],{},[21,2398,2399],{"href":181},"Create a Custom Map Tool in PyQGIS",[161,2401,2402],{},[21,2403,2404],{"href":962},"Snap to Features with QgsSnappingUtils in PyQGIS",[161,2406,2407],{},[21,2408,2409],{"href":186},"Highlight a Feature with a Rubber Band in PyQGIS",[161,2411,2412],{},[21,2413,2415],{"href":2414},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002F","Add a Layer Context Menu Action in PyQGIS",[2417,2418,2419],"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 .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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":202,"searchDepth":226,"depth":226,"links":2421},[2422,2423,2424,2425,2426,2427,2428,2429,2430,2431],{"id":155,"depth":226,"text":156},{"id":191,"depth":226,"text":192},{"id":983,"depth":226,"text":984},{"id":1538,"depth":226,"text":1539},{"id":1942,"depth":226,"text":1943},{"id":2255,"depth":226,"text":2256},{"id":2294,"depth":226,"text":2295},{"id":2333,"depth":226,"text":2334},{"id":2340,"depth":226,"text":2341},{"id":2386,"depth":226,"text":2387},"Build a map tool that captures line or polygon geometry click by click — rubber band preview, snapping with a snap indicator, undo of the last vertex, CRS-correct coordinates, and committing the feature with default values and the attribute form inside an undoable edit command.","md",{"slug":2435,"type":2436,"breadcrumb":2437,"datePublished":2438,"dateModified":2438},"digitize-features-with-custom-map-tool-pyqgis","article","Digitize Features with a Custom Map Tool","2026-09-17","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fdigitize-features-with-custom-map-tool-pyqgis",{"title":5,"description":2432},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fdigitize-features-with-custom-map-tool-pyqgis\u002Findex","fwu0MJzkMS78apfNlgHvZi-nD2piv1AUJez9-1aQk4w",1789632907483]