[{"data":1,"prerenderedAt":1703},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis":3},{"id":4,"title":5,"body":6,"description":1692,"extension":1693,"meta":1694,"navigation":234,"path":1699,"seo":1700,"stem":1701,"__hash__":1702},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis\u002Findex.md","Snap to Features with QgsSnappingUtils in PyQGIS",{"type":7,"value":8,"toc":1679},"minimark",[9,13,17,26,168,173,197,201,334,357,360,446,459,463,733,772,783,806,817,821,899,1136,1156,1160,1255,1260,1264,1267,1474,1496,1499,1503,1530,1534,1591,1595,1607,1611,1617,1629,1635,1641,1645,1675],[10,11,5],"h1",{"id":12},"snap-to-features-with-qgssnappingutils-in-pyqgis",[14,15,16],"p",{},"A custom map tool that takes the raw click position produces coordinates that are almost right, which in GIS is the same as wrong — a vertex placed 40 cm from the one it should coincide with breaks topology, routing and every subsequent geometric test. The built-in editing tools snap, and the machinery that lets them do it is available to any tool you write.",[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 & Canvas Interaction",". It covers configuring snapping, reading a match, telling the user what they hit, and restricting snapping to particular layers.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,87,93,97,103,107,110,113,117,120,124,127,131,135,138,141,143,147,150,159,164],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 326","img","Three snapping outcomes for the same click: no match returning the raw position, a vertex match returning an exact existing vertex, and a segment match returning the nearest point on an edge","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What the click turns into",[40,41,42],"desc",{},"Clicking away from any feature returns the raw map position. Clicking near an existing vertex within the tolerance returns that vertex exactly. Clicking near an edge but away from its vertices returns the perpendicular foot on that edge, which is a new position lying exactly on the line.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","326","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Same click, three different answers",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"20","46","228","192","10","#fffdf7","#59645f","stroke-width:2.5",[51,70,74],{"x":71,"y":72,"style":73,"fill":56,"textAnchor":57},"134","72","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","no match",[76,77],"path",{"d":78,"fill":79,"stroke":67,"style":80},"M44 176 L152 128 L224 160","none","stroke-width:2.4",[82,83],"circle",{"cx":84,"cy":85,"r":86,"fill":67},"152","128","4",[82,88],{"cx":89,"cy":90,"r":91,"fill":92},"90","94","5.5","#b45309",[82,94],{"cx":89,"cy":90,"r":95,"fill":79,"stroke":92,"style":96},"16","stroke-width:1.6;stroke-dasharray:4 3",[51,98,102],{"x":71,"y":99,"style":100,"fill":101,"textAnchor":57},"212","text-anchor:middle;font-size:10px;font-family:sans-serif","#2f3b35","raw position returned",[44,104],{"x":105,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":106,"style":68},"266","#15803d",[51,108,109],{"x":53,"y":72,"style":73,"fill":106,"textAnchor":57},"vertex match",[76,111],{"d":112,"fill":79,"stroke":67,"style":80},"M290 176 L398 128 L470 160",[82,114],{"cx":115,"cy":85,"r":116,"fill":106},"398","6",[82,118],{"cx":115,"cy":85,"r":95,"fill":79,"stroke":106,"style":119},"stroke-width:1.8",[82,121],{"cx":122,"cy":123,"r":86,"fill":92},"386","116",[51,125,126],{"x":53,"y":99,"style":100,"fill":101,"textAnchor":57},"the existing vertex, exactly",[44,128],{"x":129,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":130,"style":68},"512","#2563eb",[51,132,134],{"x":133,"y":72,"style":73,"fill":130,"textAnchor":57},"626","segment match",[76,136],{"d":137,"fill":79,"stroke":67,"style":80},"M536 176 L644 128 L716 160",[82,139],{"cx":140,"cy":84,"r":116,"fill":130},"590",[82,142],{"cx":140,"cy":84,"r":95,"fill":79,"stroke":130,"style":119},[82,144],{"cx":145,"cy":146,"r":86,"fill":92},"578","140",[51,148,149],{"x":133,"y":99,"style":100,"fill":101,"textAnchor":57},"on the edge, a new point",[44,151],{"x":152,"y":153,"width":154,"height":155,"rx":156,"fill":157,"stroke":92,"style":158},"80","262","600","52","8","#fdf2e2","stroke-width:2",[51,160,163],{"x":53,"y":161,"style":162,"fill":101,"textAnchor":57},"286","text-anchor:middle;font-size:11px;font-family:sans-serif","the orange dot is where the cursor was; the ring is the tolerance",[51,165,167],{"x":53,"y":166,"style":100,"fill":67,"textAnchor":57},"306","vertex snapping wins over segment snapping when both are in range",[169,170,172],"h2",{"id":171},"prerequisites","Prerequisites",[174,175,176,184],"ul",{},[177,178,179,183],"li",{},[180,181,182],"strong",{},"QGIS 3.34 LTR"," or newer, with a GUI — snapping is a canvas facility.",[177,185,186,187,191,192,196],{},"A custom map tool, or a subclass of ",[188,189,190],"code",{},"QgsMapTool"," — see ",[21,193,195],{"href":194},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","creating a custom map tool",".",[169,198,200],{"id":199},"configure-the-snapping","Configure the snapping",[202,203,208],"pre",{"className":204,"code":205,"language":206,"meta":207,"style":207},"language-python shiki shiki-themes github-dark","from qgis.core import QgsSnappingConfig, QgsTolerance, QgsProject, Qgis\n\nutils = iface.mapCanvas().snappingUtils()\n\nconfig = QgsSnappingConfig(QgsProject.instance())\nconfig.setEnabled(True)\nconfig.setMode(Qgis.SnappingMode.AllLayers)\nconfig.setTypeFlag(\n    Qgis.SnappingTypes(Qgis.SnappingType.Vertex | Qgis.SnappingType.Segment)\n)\nconfig.setTolerance(12)\nconfig.setUnits(QgsTolerance.Pixels)\n\nutils.setConfig(config)\n","python","",[188,209,210,229,236,248,253,264,277,283,289,301,306,317,323,328],{"__ignoreMap":207},[211,212,215,219,223,226],"span",{"class":213,"line":214},"line",1,[211,216,218],{"class":217},"snl16","from",[211,220,222],{"class":221},"s95oV"," qgis.core ",[211,224,225],{"class":217},"import",[211,227,228],{"class":221}," QgsSnappingConfig, QgsTolerance, QgsProject, Qgis\n",[211,230,232],{"class":213,"line":231},2,[211,233,235],{"emptyLinePlaceholder":234},true,"\n",[211,237,239,242,245],{"class":213,"line":238},3,[211,240,241],{"class":221},"utils ",[211,243,244],{"class":217},"=",[211,246,247],{"class":221}," iface.mapCanvas().snappingUtils()\n",[211,249,251],{"class":213,"line":250},4,[211,252,235],{"emptyLinePlaceholder":234},[211,254,256,259,261],{"class":213,"line":255},5,[211,257,258],{"class":221},"config ",[211,260,244],{"class":217},[211,262,263],{"class":221}," QgsSnappingConfig(QgsProject.instance())\n",[211,265,267,270,274],{"class":213,"line":266},6,[211,268,269],{"class":221},"config.setEnabled(",[211,271,273],{"class":272},"sDLfK","True",[211,275,276],{"class":221},")\n",[211,278,280],{"class":213,"line":279},7,[211,281,282],{"class":221},"config.setMode(Qgis.SnappingMode.AllLayers)\n",[211,284,286],{"class":213,"line":285},8,[211,287,288],{"class":221},"config.setTypeFlag(\n",[211,290,292,295,298],{"class":213,"line":291},9,[211,293,294],{"class":221},"    Qgis.SnappingTypes(Qgis.SnappingType.Vertex ",[211,296,297],{"class":217},"|",[211,299,300],{"class":221}," Qgis.SnappingType.Segment)\n",[211,302,304],{"class":213,"line":303},10,[211,305,276],{"class":221},[211,307,309,312,315],{"class":213,"line":308},11,[211,310,311],{"class":221},"config.setTolerance(",[211,313,314],{"class":272},"12",[211,316,276],{"class":221},[211,318,320],{"class":213,"line":319},12,[211,321,322],{"class":221},"config.setUnits(QgsTolerance.Pixels)\n",[211,324,326],{"class":213,"line":325},13,[211,327,235],{"emptyLinePlaceholder":234},[211,329,331],{"class":213,"line":330},14,[211,332,333],{"class":221},"utils.setConfig(config)\n",[14,335,336,339,340,343,344,347,348,351,352,356],{},[180,337,338],{},"Breakdown:"," Constructing the config from the project rather than empty inherits the project's own snapping settings as a starting point, so a user who configured snapping in the GUI is not overridden wholesale. The mode chooses which layers participate: ",[188,341,342],{},"AllLayers",", ",[188,345,346],{},"ActiveLayer",", or ",[188,349,350],{},"AdvancedConfiguration"," for per-layer control. Type flags combine with a bitwise OR, and including both vertex and segment gives the behaviour users expect — vertices win where both are in range. A tolerance in ",[353,354,355],"em",{},"pixels"," rather than map units is almost always right for interactive work, because it stays constant on screen as the user zooms; map units make snapping impossible at one zoom and indiscriminate at another.",[14,358,359],{},"Changing the canvas's config affects the whole application, so a well-behaved plugin either restores the previous config on deactivation or applies its own without persisting it:",[202,361,363],{"className":204,"code":362,"language":206,"meta":207,"style":207},"class SnappingTool(QgsMapTool):\n    def activate(self):\n        self.previous = self.canvas().snappingUtils().config()\n        self.canvas().snappingUtils().setConfig(self.config)\n\n    def deactivate(self):\n        self.canvas().snappingUtils().setConfig(self.previous)\n",[188,364,365,382,393,409,422,426,435],{"__ignoreMap":207},[211,366,367,370,374,377,379],{"class":213,"line":214},[211,368,369],{"class":217},"class",[211,371,373],{"class":372},"svObZ"," SnappingTool",[211,375,376],{"class":221},"(",[211,378,190],{"class":372},[211,380,381],{"class":221},"):\n",[211,383,384,387,390],{"class":213,"line":231},[211,385,386],{"class":217},"    def",[211,388,389],{"class":372}," activate",[211,391,392],{"class":221},"(self):\n",[211,394,395,398,401,403,406],{"class":213,"line":238},[211,396,397],{"class":272},"        self",[211,399,400],{"class":221},".previous ",[211,402,244],{"class":217},[211,404,405],{"class":272}," self",[211,407,408],{"class":221},".canvas().snappingUtils().config()\n",[211,410,411,413,416,419],{"class":213,"line":250},[211,412,397],{"class":272},[211,414,415],{"class":221},".canvas().snappingUtils().setConfig(",[211,417,418],{"class":272},"self",[211,420,421],{"class":221},".config)\n",[211,423,424],{"class":213,"line":255},[211,425,235],{"emptyLinePlaceholder":234},[211,427,428,430,433],{"class":213,"line":266},[211,429,386],{"class":217},[211,431,432],{"class":372}," deactivate",[211,434,392],{"class":221},[211,436,437,439,441,443],{"class":213,"line":279},[211,438,397],{"class":272},[211,440,415],{"class":221},[211,442,418],{"class":272},[211,444,445],{"class":221},".previous)\n",[14,447,448,450,451,454,455,458],{},[180,449,338],{}," ",[188,452,453],{},"activate"," and ",[188,456,457],{},"deactivate"," are called by the canvas when the tool is set and unset, which makes them the natural place for this. Saving and restoring means the user's snapping settings survive using your tool, which is the difference between a plugin that feels integrated and one that quietly changes the application's behaviour.",[169,460,462],{"id":461},"read-the-match","Read the match",[202,464,466],{"className":204,"code":465,"language":206,"meta":207,"style":207},"from qgis.gui import QgsMapTool\nfrom qgis.core import QgsPointXY\n\n\nclass PickTool(QgsMapTool):\n    def canvasReleaseEvent(self, event):\n        utils = self.canvas().snappingUtils()\n        match = utils.snapToMap(event.pos())\n\n        if not match.isValid():\n            point = self.toMapCoordinates(event.pos())\n            print(\"no snap:\", point.x(), point.y())\n            return\n\n        point = match.point()\n        layer = match.layer()\n        print(f\"snapped to {layer.name()} feature {match.featureId()} at \"\n              f\"{point.x():.3f}, {point.y():.3f}\")\n        print(\"vertex\" if match.hasVertex() else \"edge\",\n              f\"{match.distance():.2f} map units away\")\n",[188,467,468,480,491,495,499,512,522,534,544,548,559,571,585,590,594,605,616,652,686,711],{"__ignoreMap":207},[211,469,470,472,475,477],{"class":213,"line":214},[211,471,218],{"class":217},[211,473,474],{"class":221}," qgis.gui ",[211,476,225],{"class":217},[211,478,479],{"class":221}," QgsMapTool\n",[211,481,482,484,486,488],{"class":213,"line":231},[211,483,218],{"class":217},[211,485,222],{"class":221},[211,487,225],{"class":217},[211,489,490],{"class":221}," QgsPointXY\n",[211,492,493],{"class":213,"line":238},[211,494,235],{"emptyLinePlaceholder":234},[211,496,497],{"class":213,"line":250},[211,498,235],{"emptyLinePlaceholder":234},[211,500,501,503,506,508,510],{"class":213,"line":255},[211,502,369],{"class":217},[211,504,505],{"class":372}," PickTool",[211,507,376],{"class":221},[211,509,190],{"class":372},[211,511,381],{"class":221},[211,513,514,516,519],{"class":213,"line":266},[211,515,386],{"class":217},[211,517,518],{"class":372}," canvasReleaseEvent",[211,520,521],{"class":221},"(self, event):\n",[211,523,524,527,529,531],{"class":213,"line":279},[211,525,526],{"class":221},"        utils ",[211,528,244],{"class":217},[211,530,405],{"class":272},[211,532,533],{"class":221},".canvas().snappingUtils()\n",[211,535,536,539,541],{"class":213,"line":285},[211,537,538],{"class":221},"        match ",[211,540,244],{"class":217},[211,542,543],{"class":221}," utils.snapToMap(event.pos())\n",[211,545,546],{"class":213,"line":291},[211,547,235],{"emptyLinePlaceholder":234},[211,549,550,553,556],{"class":213,"line":303},[211,551,552],{"class":217},"        if",[211,554,555],{"class":217}," not",[211,557,558],{"class":221}," match.isValid():\n",[211,560,561,564,566,568],{"class":213,"line":308},[211,562,563],{"class":221},"            point ",[211,565,244],{"class":217},[211,567,405],{"class":272},[211,569,570],{"class":221},".toMapCoordinates(event.pos())\n",[211,572,573,576,578,582],{"class":213,"line":319},[211,574,575],{"class":272},"            print",[211,577,376],{"class":221},[211,579,581],{"class":580},"sU2Wk","\"no snap:\"",[211,583,584],{"class":221},", point.x(), point.y())\n",[211,586,587],{"class":213,"line":325},[211,588,589],{"class":217},"            return\n",[211,591,592],{"class":213,"line":330},[211,593,235],{"emptyLinePlaceholder":234},[211,595,597,600,602],{"class":213,"line":596},15,[211,598,599],{"class":221},"        point ",[211,601,244],{"class":217},[211,603,604],{"class":221}," match.point()\n",[211,606,608,611,613],{"class":213,"line":607},16,[211,609,610],{"class":221},"        layer ",[211,612,244],{"class":217},[211,614,615],{"class":221}," match.layer()\n",[211,617,619,622,624,627,630,633,636,639,642,644,647,649],{"class":213,"line":618},17,[211,620,621],{"class":272},"        print",[211,623,376],{"class":221},[211,625,626],{"class":217},"f",[211,628,629],{"class":580},"\"snapped to ",[211,631,632],{"class":272},"{",[211,634,635],{"class":221},"layer.name()",[211,637,638],{"class":272},"}",[211,640,641],{"class":580}," feature ",[211,643,632],{"class":272},[211,645,646],{"class":221},"match.featureId()",[211,648,638],{"class":272},[211,650,651],{"class":580}," at \"\n",[211,653,655,658,661,663,666,669,671,673,675,678,680,682,684],{"class":213,"line":654},18,[211,656,657],{"class":217},"              f",[211,659,660],{"class":580},"\"",[211,662,632],{"class":272},[211,664,665],{"class":221},"point.x()",[211,667,668],{"class":217},":.3f",[211,670,638],{"class":272},[211,672,343],{"class":580},[211,674,632],{"class":272},[211,676,677],{"class":221},"point.y()",[211,679,668],{"class":217},[211,681,638],{"class":272},[211,683,660],{"class":580},[211,685,276],{"class":221},[211,687,689,691,693,696,699,702,705,708],{"class":213,"line":688},19,[211,690,621],{"class":272},[211,692,376],{"class":221},[211,694,695],{"class":580},"\"vertex\"",[211,697,698],{"class":217}," if",[211,700,701],{"class":221}," match.hasVertex() ",[211,703,704],{"class":217},"else",[211,706,707],{"class":580}," \"edge\"",[211,709,710],{"class":221},",\n",[211,712,714,716,718,720,723,726,728,731],{"class":213,"line":713},20,[211,715,657],{"class":217},[211,717,660],{"class":580},[211,719,632],{"class":272},[211,721,722],{"class":221},"match.distance()",[211,724,725],{"class":217},":.2f",[211,727,638],{"class":272},[211,729,730],{"class":580}," map units away\"",[211,732,276],{"class":221},[14,734,735,450,737,740,741,744,745,748,749,752,753,756,757,760,761,454,764,767,768,771],{},[180,736,338],{},[188,738,739],{},"snapToMap"," accepts either a screen position (",[188,742,743],{},"event.pos()",") or a map point, and returns a ",[188,746,747],{},"QgsPointLocator.Match"," that is always non-null but may be invalid — checking ",[188,750,751],{},"isValid()"," rather than testing for ",[188,754,755],{},"None"," is what catches \"nothing was near enough\". ",[188,758,759],{},"match.point()"," is the snapped position, which for a vertex match is the exact stored coordinate and for a segment match is a computed foot on the line. ",[188,762,763],{},"hasVertex()",[188,765,766],{},"hasEdge()"," distinguish the two, and ",[188,769,770],{},"vertexIndex()"," on a vertex match tells you which vertex of the geometry it was — which is what an editing tool needs to move it rather than add to it.",[14,773,774,775,778,779,782],{},"In a tool that extends ",[188,776,777],{},"QgsMapToolEdit"," or uses ",[188,780,781],{},"QgsMapMouseEvent",", the event itself can do the snapping:",[202,784,786],{"className":204,"code":785,"language":206,"meta":207,"style":207},"    def canvasMoveEvent(self, event):\n        point = event.snapPoint()\n",[188,787,788,797],{"__ignoreMap":207},[211,789,790,792,795],{"class":213,"line":214},[211,791,386],{"class":217},[211,793,794],{"class":372}," canvasMoveEvent",[211,796,521],{"class":221},[211,798,799,801,803],{"class":213,"line":231},[211,800,599],{"class":221},[211,802,244],{"class":217},[211,804,805],{"class":221}," event.snapPoint()\n",[14,807,808,450,810,813,814,816],{},[180,809,338],{},[188,811,812],{},"QgsMapMouseEvent.snapPoint()"," runs the canvas's snapping utils and returns the snapped map point in one call, caching the result on the event so repeated calls are free. It is the concise form for a tool that only needs the position and not the layer or feature it came from. Note that it applies the canvas configuration, not any private one, which is another argument for configuring the canvas in ",[188,815,453],{}," rather than keeping a separate config.",[169,818,820],{"id":819},"show-the-user-what-will-happen","Show the user what will happen",[14,822,823],{},[29,824,827,830,833,836,839,845,849,852,856,861,865,868,872,875,878,882,885,888,894],{"viewBox":825,"role":32,"ariaLabel":826,"xmlns":34},"0 0 760 300","Feedback while snapping: a vertex marker drawn at the candidate position tells the user what the click will snap to before they commit",[36,828,829],{},"Feedback before the click, not after",[40,831,832],{},"Without feedback the user clicks and hopes, discovering only afterwards whether the point snapped where they intended. Drawing a marker at the snapped position as the cursor moves shows the candidate before the click, and changing its shape between a vertex and an edge match shows which kind of snap is on offer.",[44,834],{"x":46,"y":46,"width":47,"height":835,"fill":49},"300",[51,837,838],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"A marker on move is the whole of good snapping UX",[44,840],{"x":841,"y":62,"width":842,"height":843,"rx":65,"fill":66,"stroke":844,"style":68},"24","344","188","#b91c1c",[51,846,848],{"x":847,"y":72,"style":73,"fill":844,"textAnchor":57},"196","no feedback",[76,850],{"d":851,"fill":79,"stroke":67,"style":80},"M60 176 L180 118 L330 156",[82,853],{"cx":854,"cy":855,"r":86,"fill":67},"180","118",[82,857],{"cx":858,"cy":859,"r":860,"fill":92},"164","104","4.5",[51,862,864],{"x":847,"y":863,"style":100,"fill":101,"textAnchor":57},"210","click and find out",[44,866],{"x":867,"y":62,"width":842,"height":843,"rx":65,"fill":66,"stroke":106,"style":68},"392",[51,869,871],{"x":870,"y":72,"style":73,"fill":106,"textAnchor":57},"564","vertex marker on move",[76,873],{"d":874,"fill":79,"stroke":67,"style":80},"M428 176 L548 118 L698 156",[82,876],{"cx":877,"cy":855,"r":86,"fill":67},"548",[76,879],{"d":880,"stroke":106,"style":881},"M536 106 L560 130 M560 106 L536 130","stroke-width:2.6",[82,883],{"cx":884,"cy":859,"r":860,"fill":92},"532",[51,886,887],{"x":870,"y":863,"style":100,"fill":101,"textAnchor":57},"the target is visible first",[44,889],{"x":890,"y":891,"width":892,"height":893,"rx":156,"fill":157,"stroke":92,"style":119},"120","252","520","34",[51,895,898],{"x":53,"y":896,"style":897,"fill":101,"textAnchor":57},"274","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a cross for a vertex, a box for an edge — the shape carries the information",[202,900,902],{"className":204,"code":901,"language":206,"meta":207,"style":207},"from qgis.gui import QgsVertexMarker\nfrom qgis.PyQt.QtGui import QColor\n\n\nclass PickTool(QgsMapTool):\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.marker = QgsVertexMarker(canvas)\n        self.marker.setColor(QColor(\"#0f766e\"))\n        self.marker.setPenWidth(3)\n        self.marker.hide()\n\n    def canvasMoveEvent(self, event):\n        match = self.canvas().snappingUtils().snapToMap(event.pos())\n        if match.isValid():\n            self.marker.setIconType(\n                QgsVertexMarker.ICON_X if match.hasVertex() else QgsVertexMarker.ICON_BOX\n            )\n            self.marker.setCenter(match.point())\n            self.marker.show()\n        else:\n            self.marker.hide()\n\n    def deactivate(self):\n        self.marker.hide()\n        super().deactivate()\n",[188,903,904,915,927,931,935,947,957,971,983,996,1008,1015,1019,1027,1038,1044,1052,1072,1077,1084,1091,1100,1107,1112,1121,1128],{"__ignoreMap":207},[211,905,906,908,910,912],{"class":213,"line":214},[211,907,218],{"class":217},[211,909,474],{"class":221},[211,911,225],{"class":217},[211,913,914],{"class":221}," QgsVertexMarker\n",[211,916,917,919,922,924],{"class":213,"line":231},[211,918,218],{"class":217},[211,920,921],{"class":221}," qgis.PyQt.QtGui ",[211,923,225],{"class":217},[211,925,926],{"class":221}," QColor\n",[211,928,929],{"class":213,"line":238},[211,930,235],{"emptyLinePlaceholder":234},[211,932,933],{"class":213,"line":250},[211,934,235],{"emptyLinePlaceholder":234},[211,936,937,939,941,943,945],{"class":213,"line":255},[211,938,369],{"class":217},[211,940,505],{"class":372},[211,942,376],{"class":221},[211,944,190],{"class":372},[211,946,381],{"class":221},[211,948,949,951,954],{"class":213,"line":266},[211,950,386],{"class":217},[211,952,953],{"class":272}," __init__",[211,955,956],{"class":221},"(self, canvas):\n",[211,958,959,962,965,968],{"class":213,"line":279},[211,960,961],{"class":272},"        super",[211,963,964],{"class":221},"().",[211,966,967],{"class":272},"__init__",[211,969,970],{"class":221},"(canvas)\n",[211,972,973,975,978,980],{"class":213,"line":285},[211,974,397],{"class":272},[211,976,977],{"class":221},".marker ",[211,979,244],{"class":217},[211,981,982],{"class":221}," QgsVertexMarker(canvas)\n",[211,984,985,987,990,993],{"class":213,"line":291},[211,986,397],{"class":272},[211,988,989],{"class":221},".marker.setColor(QColor(",[211,991,992],{"class":580},"\"#0f766e\"",[211,994,995],{"class":221},"))\n",[211,997,998,1000,1003,1006],{"class":213,"line":303},[211,999,397],{"class":272},[211,1001,1002],{"class":221},".marker.setPenWidth(",[211,1004,1005],{"class":272},"3",[211,1007,276],{"class":221},[211,1009,1010,1012],{"class":213,"line":308},[211,1011,397],{"class":272},[211,1013,1014],{"class":221},".marker.hide()\n",[211,1016,1017],{"class":213,"line":319},[211,1018,235],{"emptyLinePlaceholder":234},[211,1020,1021,1023,1025],{"class":213,"line":325},[211,1022,386],{"class":217},[211,1024,794],{"class":372},[211,1026,521],{"class":221},[211,1028,1029,1031,1033,1035],{"class":213,"line":330},[211,1030,538],{"class":221},[211,1032,244],{"class":217},[211,1034,405],{"class":272},[211,1036,1037],{"class":221},".canvas().snappingUtils().snapToMap(event.pos())\n",[211,1039,1040,1042],{"class":213,"line":596},[211,1041,552],{"class":217},[211,1043,558],{"class":221},[211,1045,1046,1049],{"class":213,"line":607},[211,1047,1048],{"class":272},"            self",[211,1050,1051],{"class":221},".marker.setIconType(\n",[211,1053,1054,1057,1060,1062,1064,1066,1069],{"class":213,"line":618},[211,1055,1056],{"class":221},"                QgsVertexMarker.",[211,1058,1059],{"class":272},"ICON_X",[211,1061,698],{"class":217},[211,1063,701],{"class":221},[211,1065,704],{"class":217},[211,1067,1068],{"class":221}," QgsVertexMarker.",[211,1070,1071],{"class":272},"ICON_BOX\n",[211,1073,1074],{"class":213,"line":654},[211,1075,1076],{"class":221},"            )\n",[211,1078,1079,1081],{"class":213,"line":688},[211,1080,1048],{"class":272},[211,1082,1083],{"class":221},".marker.setCenter(match.point())\n",[211,1085,1086,1088],{"class":213,"line":713},[211,1087,1048],{"class":272},[211,1089,1090],{"class":221},".marker.show()\n",[211,1092,1094,1097],{"class":213,"line":1093},21,[211,1095,1096],{"class":217},"        else",[211,1098,1099],{"class":221},":\n",[211,1101,1103,1105],{"class":213,"line":1102},22,[211,1104,1048],{"class":272},[211,1106,1014],{"class":221},[211,1108,1110],{"class":213,"line":1109},23,[211,1111,235],{"emptyLinePlaceholder":234},[211,1113,1115,1117,1119],{"class":213,"line":1114},24,[211,1116,386],{"class":217},[211,1118,432],{"class":372},[211,1120,392],{"class":221},[211,1122,1124,1126],{"class":213,"line":1123},25,[211,1125,397],{"class":272},[211,1127,1014],{"class":221},[211,1129,1131,1133],{"class":213,"line":1130},26,[211,1132,961],{"class":272},[211,1134,1135],{"class":221},"().deactivate()\n",[14,1137,1138,1140,1141,1143,1144,1147,1148,1152,1153,1155],{},[180,1139,338],{}," Keeping the marker on ",[188,1142,418],{}," is required — a ",[188,1145,1146],{},"QgsVertexMarker"," created as a local is collected while the canvas still points at it, which crashes rather than merely disappearing, as described in ",[21,1149,1151],{"href":1150},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002F","object ownership and crashes",". Creating one marker and moving it, rather than creating one per mouse move, is what keeps this smooth. Changing the icon between a cross and a box communicates the match type without any text. Hiding it in ",[188,1154,457],{}," stops a stale marker sitting on the canvas after the tool is put away.",[169,1157,1159],{"id":1158},"restricting-which-layers-snap","Restricting which layers snap",[202,1161,1163],{"className":204,"code":1162,"language":206,"meta":207,"style":207},"config.setMode(Qgis.SnappingMode.AdvancedConfiguration)\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\nsettings = QgsSnappingConfig.IndividualLayerSettings(\n    True,\n    Qgis.SnappingTypes(Qgis.SnappingType.Vertex),\n    10,\n    QgsTolerance.Pixels,\n    0.0,\n    0.0,\n)\nconfig.setIndividualLayerSettings(roads, settings)\nutils.setConfig(config)\n",[188,1164,1165,1170,1174,1195,1205,1212,1217,1224,1229,1236,1242,1246,1251],{"__ignoreMap":207},[211,1166,1167],{"class":213,"line":214},[211,1168,1169],{"class":221},"config.setMode(Qgis.SnappingMode.AdvancedConfiguration)\n",[211,1171,1172],{"class":213,"line":231},[211,1173,235],{"emptyLinePlaceholder":234},[211,1175,1176,1179,1181,1184,1187,1190,1192],{"class":213,"line":238},[211,1177,1178],{"class":221},"roads ",[211,1180,244],{"class":217},[211,1182,1183],{"class":221}," QgsProject.instance().mapLayersByName(",[211,1185,1186],{"class":580},"\"roads\"",[211,1188,1189],{"class":221},")[",[211,1191,46],{"class":272},[211,1193,1194],{"class":221},"]\n",[211,1196,1197,1200,1202],{"class":213,"line":250},[211,1198,1199],{"class":221},"settings ",[211,1201,244],{"class":217},[211,1203,1204],{"class":221}," QgsSnappingConfig.IndividualLayerSettings(\n",[211,1206,1207,1210],{"class":213,"line":255},[211,1208,1209],{"class":272},"    True",[211,1211,710],{"class":221},[211,1213,1214],{"class":213,"line":266},[211,1215,1216],{"class":221},"    Qgis.SnappingTypes(Qgis.SnappingType.Vertex),\n",[211,1218,1219,1222],{"class":213,"line":279},[211,1220,1221],{"class":272},"    10",[211,1223,710],{"class":221},[211,1225,1226],{"class":213,"line":285},[211,1227,1228],{"class":221},"    QgsTolerance.Pixels,\n",[211,1230,1231,1234],{"class":213,"line":291},[211,1232,1233],{"class":272},"    0.0",[211,1235,710],{"class":221},[211,1237,1238,1240],{"class":213,"line":303},[211,1239,1233],{"class":272},[211,1241,710],{"class":221},[211,1243,1244],{"class":213,"line":308},[211,1245,276],{"class":221},[211,1247,1248],{"class":213,"line":319},[211,1249,1250],{"class":221},"config.setIndividualLayerSettings(roads, settings)\n",[211,1252,1253],{"class":213,"line":325},[211,1254,333],{"class":221},[14,1256,1257,1259],{},[180,1258,338],{}," Advanced configuration is per layer, so a tool can snap to the road centrelines and ignore the twenty other layers in the project — which is both faster and far less frustrating for the user than snapping to whatever happens to be underneath. The settings constructor's arguments are enabled, type flags, tolerance, units, then the minimum and maximum scale at which the layer participates (0 meaning no limit). Layers not given individual settings do not snap in this mode, which is the behaviour you want.",[169,1261,1263],{"id":1262},"using-the-match-not-just-its-position","Using the match, not just its position",[14,1265,1266],{},"The match carries more than a coordinate, and the extra fields are what let a tool do something useful with what the user pointed at.",[202,1268,1270],{"className":204,"code":1269,"language":206,"meta":207,"style":207},"    def canvasReleaseEvent(self, event):\n        match = self.canvas().snappingUtils().snapToMap(event.pos())\n        if not match.isValid() or match.layer() is None:\n            return\n\n        layer = match.layer()\n        feature = next(layer.getFeatures(\n            QgsFeatureRequest().setFilterFid(match.featureId())\n        ), None)\n        if feature is None:\n            return\n\n        if match.hasVertex():\n            geometry = feature.geometry()\n            print(f\"vertex {match.vertexIndex()} of \"\n                  f\"{len(geometry.asPolyline() or [])} on {layer.name()}\")\n        else:\n            print(f\"on the edge starting at vertex {match.vertexIndex()}\")\n",[188,1271,1272,1280,1290,1313,1317,1321,1329,1342,1347,1356,1368,1372,1376,1383,1393,1414,1447,1453],{"__ignoreMap":207},[211,1273,1274,1276,1278],{"class":213,"line":214},[211,1275,386],{"class":217},[211,1277,518],{"class":372},[211,1279,521],{"class":221},[211,1281,1282,1284,1286,1288],{"class":213,"line":231},[211,1283,538],{"class":221},[211,1285,244],{"class":217},[211,1287,405],{"class":272},[211,1289,1037],{"class":221},[211,1291,1292,1294,1296,1299,1302,1305,1308,1311],{"class":213,"line":238},[211,1293,552],{"class":217},[211,1295,555],{"class":217},[211,1297,1298],{"class":221}," match.isValid() ",[211,1300,1301],{"class":217},"or",[211,1303,1304],{"class":221}," match.layer() ",[211,1306,1307],{"class":217},"is",[211,1309,1310],{"class":272}," None",[211,1312,1099],{"class":221},[211,1314,1315],{"class":213,"line":250},[211,1316,589],{"class":217},[211,1318,1319],{"class":213,"line":255},[211,1320,235],{"emptyLinePlaceholder":234},[211,1322,1323,1325,1327],{"class":213,"line":266},[211,1324,610],{"class":221},[211,1326,244],{"class":217},[211,1328,615],{"class":221},[211,1330,1331,1334,1336,1339],{"class":213,"line":279},[211,1332,1333],{"class":221},"        feature ",[211,1335,244],{"class":217},[211,1337,1338],{"class":272}," next",[211,1340,1341],{"class":221},"(layer.getFeatures(\n",[211,1343,1344],{"class":213,"line":285},[211,1345,1346],{"class":221},"            QgsFeatureRequest().setFilterFid(match.featureId())\n",[211,1348,1349,1352,1354],{"class":213,"line":291},[211,1350,1351],{"class":221},"        ), ",[211,1353,755],{"class":272},[211,1355,276],{"class":221},[211,1357,1358,1360,1362,1364,1366],{"class":213,"line":303},[211,1359,552],{"class":217},[211,1361,641],{"class":221},[211,1363,1307],{"class":217},[211,1365,1310],{"class":272},[211,1367,1099],{"class":221},[211,1369,1370],{"class":213,"line":308},[211,1371,589],{"class":217},[211,1373,1374],{"class":213,"line":319},[211,1375,235],{"emptyLinePlaceholder":234},[211,1377,1378,1380],{"class":213,"line":325},[211,1379,552],{"class":217},[211,1381,1382],{"class":221}," match.hasVertex():\n",[211,1384,1385,1388,1390],{"class":213,"line":330},[211,1386,1387],{"class":221},"            geometry ",[211,1389,244],{"class":217},[211,1391,1392],{"class":221}," feature.geometry()\n",[211,1394,1395,1397,1399,1401,1404,1406,1409,1411],{"class":213,"line":596},[211,1396,575],{"class":272},[211,1398,376],{"class":221},[211,1400,626],{"class":217},[211,1402,1403],{"class":580},"\"vertex ",[211,1405,632],{"class":272},[211,1407,1408],{"class":221},"match.vertexIndex()",[211,1410,638],{"class":272},[211,1412,1413],{"class":580}," of \"\n",[211,1415,1416,1419,1421,1424,1427,1429,1432,1434,1437,1439,1441,1443,1445],{"class":213,"line":607},[211,1417,1418],{"class":217},"                  f",[211,1420,660],{"class":580},[211,1422,1423],{"class":272},"{len",[211,1425,1426],{"class":221},"(geometry.asPolyline() ",[211,1428,1301],{"class":217},[211,1430,1431],{"class":221}," [])",[211,1433,638],{"class":272},[211,1435,1436],{"class":580}," on ",[211,1438,632],{"class":272},[211,1440,635],{"class":221},[211,1442,638],{"class":272},[211,1444,660],{"class":580},[211,1446,276],{"class":221},[211,1448,1449,1451],{"class":213,"line":618},[211,1450,1096],{"class":217},[211,1452,1099],{"class":221},[211,1454,1455,1457,1459,1461,1464,1466,1468,1470,1472],{"class":213,"line":654},[211,1456,575],{"class":272},[211,1458,376],{"class":221},[211,1460,626],{"class":217},[211,1462,1463],{"class":580},"\"on the edge starting at vertex ",[211,1465,632],{"class":272},[211,1467,1408],{"class":221},[211,1469,638],{"class":272},[211,1471,660],{"class":580},[211,1473,276],{"class":221},[14,1475,1476,450,1478,1481,1482,1484,1485,1488,1489,1492,1493,196],{},[180,1477,338],{},[188,1479,1480],{},"setFilterFid"," fetches exactly one feature by id and is far cheaper than iterating and comparing. ",[188,1483,770],{}," means slightly different things for the two match types: for a vertex match it is that vertex's index, and for an edge match it is the index of the segment's ",[353,1486,1487],{},"first"," vertex — which is what you need to insert a new vertex in the right place. Guarding on ",[188,1490,1491],{},"match.layer()"," being present matters because intersection matches, when enabled, have no owning layer and would otherwise raise on ",[188,1494,1495],{},"getFeatures",[14,1497,1498],{},"Having the feature as well as the position is what lets a tool report a road's name as the user hovers, prefill a form with the parcel that was clicked, or refuse a click on a feature that is not a valid target — all of which make a tool feel like it understands the data rather than merely the geometry.",[169,1500,1502],{"id":1501},"qgis-version-compatibility","QGIS version compatibility",[14,1504,1505,454,1508,1511,1512,1515,1516,454,1519,1522,1523,1526,1527,1529],{},[188,1506,1507],{},"QgsSnappingUtils",[188,1509,1510],{},"QgsSnappingConfig"," have been present since QGIS 3.0. The mode and type enums moved into the scoped ",[188,1513,1514],{},"Qgis"," namespace in 3.26 — on earlier releases they are ",[188,1517,1518],{},"QgsSnappingConfig.AllLayers",[188,1520,1521],{},"QgsSnappingConfig.Vertex",", with the combined ",[188,1524,1525],{},"VertexAndSegment"," value now deprecated in favour of the flag OR shown above. ",[188,1528,812],{}," lost an earlier argument in 3.0 and has been stable since.",[169,1531,1533],{"id":1532},"troubleshooting","Troubleshooting",[174,1535,1536,1546,1552,1558,1567,1583],{},[177,1537,1538,1541,1542,1545],{},[180,1539,1540],{},"Nothing ever snaps."," The config was built but not passed to ",[188,1543,1544],{},"setConfig",", or snapping is not enabled on it.",[177,1547,1548,1551],{},[180,1549,1550],{},"Snapping works at one zoom and not another."," The tolerance is in map units rather than pixels.",[177,1553,1554,1557],{},[180,1555,1556],{},"The marker crashes QGIS."," It was not kept referenced on the tool.",[177,1559,1560,1563,1564,1566],{},[180,1561,1562],{},"Snapping picks the wrong layer."," Mode is ",[188,1565,342],{},"; switch to advanced configuration and name the layers.",[177,1568,1569,1574,1575,1578,1579,1582],{},[180,1570,1571,1573],{},[188,1572,1491],{}," is None."," The match came from a source without a layer, such as an intersection match; check ",[188,1576,1577],{},"hasVertex","\u002F",[188,1580,1581],{},"hasEdge"," and guard.",[177,1584,1585,1588,1589,196],{},[180,1586,1587],{},"The user's snapping settings changed after using the tool."," No restore in ",[188,1590,457],{},[169,1592,1594],{"id":1593},"conclusion","Conclusion",[14,1596,1597,1598,1600,1601,1603,1604,1606],{},"Configure snapping in ",[188,1599,453],{}," and restore it in ",[188,1602,457],{},", use pixel tolerance, check ",[188,1605,751],{}," on every match, and draw a marker as the cursor moves so the user sees the candidate before committing. A custom tool that snaps properly is indistinguishable from a built-in one, and the difference is about twenty lines.",[169,1608,1610],{"id":1609},"frequently-asked-questions","Frequently Asked Questions",[14,1612,1613,1616],{},[180,1614,1615],{},"Can I snap to a layer that is not visible?","\nIn advanced configuration, yes — participation is per layer and independent of visibility. In the other modes only visible layers are considered.",[14,1618,1619,1622,1625,1626,1628],{},[180,1620,1621],{},"How do I snap to intersections between layers?",[188,1623,1624],{},"QgsSnappingConfig.setIntersectionSnapping(True)"," adds intersection matches. They have no owning feature, so guard on ",[188,1627,1491],{}," being present.",[14,1630,1631,1634],{},[180,1632,1633],{},"Does snapping work on a raster?","\nNo. It operates on vector geometry through a spatial locator per layer.",[14,1636,1637,1640],{},[180,1638,1639],{},"Is snapping expensive on a large layer?","\nThe locator builds an index per layer on first use, so the first snap after loading is slower and the rest are fast. Restricting to the layers you need keeps that cost small.",[169,1642,1644],{"id":1643},"related","Related",[174,1646,1647,1653,1658,1664,1670],{},[177,1648,1649,1652],{},[21,1650,1651],{"href":23},"Custom Map Tools & Canvas Interaction in PyQGIS"," — the guide this recipe belongs to",[177,1654,1655],{},[21,1656,1657],{"href":194},"Create a Custom Map Tool in PyQGIS",[177,1659,1660],{},[21,1661,1663],{"href":1662},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcapture-map-click-coordinates-pyqgis\u002F","Capture Map Click Coordinates in PyQGIS",[177,1665,1666],{},[21,1667,1669],{"href":1668},"\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",[177,1671,1672],{},[21,1673,1674],{"href":1150},"QGIS Object Ownership and Crashes in PyQGIS",[1676,1677,1678],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":207,"searchDepth":231,"depth":231,"links":1680},[1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691],{"id":171,"depth":231,"text":172},{"id":199,"depth":231,"text":200},{"id":461,"depth":231,"text":462},{"id":819,"depth":231,"text":820},{"id":1158,"depth":231,"text":1159},{"id":1262,"depth":231,"text":1263},{"id":1501,"depth":231,"text":1502},{"id":1532,"depth":231,"text":1533},{"id":1593,"depth":231,"text":1594},{"id":1609,"depth":231,"text":1610},{"id":1643,"depth":231,"text":1644},"Make a custom map tool snap like the built-in editing tools — configure snapping mode, types and tolerance, read the match, and show the user what they are about to snap to.","md",{"slug":1695,"type":1696,"breadcrumb":1697,"datePublished":1698,"dateModified":1698},"snap-to-features-with-qgssnappingutils-pyqgis","article","Snap with QgsSnappingUtils","2026-09-05","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis",{"title":5,"description":1692},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fsnap-to-features-with-qgssnappingutils-pyqgis\u002Findex","t-bQJynI842VlGje-7I375vSeEYi65I7rPK_Up03R4c",1788563851502]