[{"data":1,"prerenderedAt":1765},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis":3},{"id":4,"title":5,"body":6,"description":1754,"extension":1755,"meta":1756,"navigation":256,"path":1761,"seo":1762,"stem":1763,"__hash__":1764},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis\u002Findex.md","Identify the Feature Under the Cursor in PyQGIS",{"type":7,"value":8,"toc":1741},"minimark",[9,13,22,31,162,167,189,193,196,540,566,573,577,580,709,722,726,729,862,1068,1088,1092,1095,1254,1266,1274,1278,1285,1370,1477,1512,1516,1522,1583,1592,1596,1652,1656,1668,1672,1684,1694,1700,1709,1713,1737],[10,11,5],"h1",{"id":12},"identify-the-feature-under-the-cursor-in-pyqgis",[14,15,16,17,21],"p",{},"Nobody clicks precisely. A user aiming at a 1 px road line lands two or three pixels away, and an identify tool that tests the exact clicked point matches nothing perhaps nine times in ten. Every usable identify tool therefore builds a small search rectangle around the click — and sizes it in ",[18,19,20],"em",{},"pixels",", converted to map units at the current zoom, so it behaves the same whether the user is looking at a street or a continent.",[14,23,24,25,30],{},"This page is a focused recipe within ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",". It covers the search-rectangle query, ranking overlapping results, searching several layers, and presenting the answer without blocking the user.",[14,32,33],{},[34,35,40,44,48,55,64,107,135],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 250","img","An exact point test missing a thin line, compared with a pixel-sized search rectangle that catches it, shown at two zoom levels where the rectangle covers different ground distances","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"A tolerance in pixels behaves the same at every zoom",[45,46,47],"desc",{},"On the left, an exact click point falls beside a thin road line and matches nothing. In the middle, a six pixel search box around the same click catches the line. On the right, the same six pixel box at a zoomed-out scale covers a much larger ground distance, which is what keeps the tool feeling identical to the user at any zoom.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","250","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Size the tolerance in pixels, convert to map units at query time",[65,66,67,77,83,90,96,102],"g",{},[49,68],{"x":69,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":75,"style":76},"12","46","236","184","10","#fffdf7","#b91c1c","stroke-width:2.5",[56,78,82],{"x":79,"y":80,"style":81,"fill":75,"textAnchor":62},"130","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","exact point",[84,85],"path",{"d":86,"fill":87,"stroke":88,"style":89},"M40 178 L96 118 L164 148 L220 96","none","#0f766e","stroke-width:2",[91,92],"circle",{"cx":93,"cy":94,"r":95,"fill":75},"122","118","3.5",[56,97,101],{"x":79,"y":98,"style":99,"fill":100,"textAnchor":62},"204","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","3 px away from the line",[56,103,106],{"x":79,"y":104,"style":105,"fill":75,"textAnchor":62},"222","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","no match",[65,108,109,113,116,119,126,129,132],{},[49,110],{"x":111,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":112,"style":76},"262","#15803d",[56,114,115],{"x":58,"y":80,"style":81,"fill":112,"textAnchor":62},"6 px box · zoomed in",[84,117],{"d":118,"fill":87,"stroke":88,"style":89},"M290 178 L346 118 L414 148 L470 96",[49,120],{"x":121,"y":122,"width":123,"height":123,"fill":112,"fillOpacity":124,"stroke":112,"style":125},"352","98","40",0.14,"stroke-width:2;stroke-dasharray:5 4",[91,127],{"cx":128,"cy":94,"r":95,"fill":112},"372",[56,130,131],{"x":58,"y":98,"style":99,"fill":100,"textAnchor":62},"box ≈ 12 m on the ground",[56,133,134],{"x":58,"y":104,"style":105,"fill":112,"textAnchor":62},"match",[65,136,137,140,144,148,152,156,159],{},[49,138],{"x":139,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":112,"style":76},"512",[56,141,143],{"x":142,"y":80,"style":81,"fill":112,"textAnchor":62},"630","6 px box · zoomed out",[84,145],{"d":146,"fill":87,"stroke":88,"style":147},"M540 178 L568 148 L602 162 L630 136 L664 150 L700 118","stroke-width:1.4",[49,149],{"x":150,"y":151,"width":123,"height":123,"fill":112,"fillOpacity":124,"stroke":112,"style":125},"602","142",[91,153],{"cx":154,"cy":155,"r":95,"fill":112},"622","162",[56,157,158],{"x":142,"y":98,"style":99,"fill":100,"textAnchor":62},"box ≈ 900 m on the ground",[56,160,161],{"x":142,"y":104,"style":105,"fill":112,"textAnchor":62},"match, same feel",[163,164,166],"h2",{"id":165},"prerequisites","Prerequisites",[168,169,170,178,186],"ul",{},[171,172,173,177],"li",{},[174,175,176],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, running as a desktop application.",[171,179,180,181,185],{},"A plugin skeleton — see ",[26,182,184],{"href":183},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","Create a Custom Map Tool in PyQGIS"," for the tool wiring this page builds on.",[171,187,188],{},"A vector layer with attributes worth reporting.",[163,190,192],{"id":191},"query-with-a-search-rectangle","Query with a search rectangle",[14,194,195],{},"The rectangle is built in layer coordinates, sized from a pixel tolerance converted through the current zoom.",[197,198,203],"pre",{"className":199,"code":200,"language":201,"meta":202,"style":202},"language-python shiki shiki-themes github-dark","from qgis.core import QgsFeatureRequest, QgsRectangle\nfrom qgis.gui import QgsMapTool\nfrom qgis.PyQt.QtCore import Qt, pyqtSignal\n\n\nclass IdentifyTool(QgsMapTool):\n    featuresFound = pyqtSignal(object, list)\n\n    PIXEL_TOLERANCE = 6\n\n    def __init__(self, canvas, layer):\n        super().__init__(canvas)\n        self.canvas = canvas\n        self.layer = layer\n\n    def canvasReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n        point = self.toLayerCoordinates(self.layer, event.pos())\n        radius = self.canvas.mapUnitsPerPixel() * self.PIXEL_TOLERANCE\n        box = QgsRectangle(\n            point.x() - radius, point.y() - radius,\n            point.x() + radius, point.y() + radius,\n        )\n        request = QgsFeatureRequest().setFilterRect(box)\n        self.featuresFound.emit(point, list(self.layer.getFeatures(request)))\n","python","",[204,205,206,225,238,251,258,263,282,307,312,324,329,341,356,370,383,388,399,414,420,440,464,475,492,506,512,523],"code",{"__ignoreMap":202},[207,208,211,215,219,222],"span",{"class":209,"line":210},"line",1,[207,212,214],{"class":213},"snl16","from",[207,216,218],{"class":217},"s95oV"," qgis.core ",[207,220,221],{"class":213},"import",[207,223,224],{"class":217}," QgsFeatureRequest, QgsRectangle\n",[207,226,228,230,233,235],{"class":209,"line":227},2,[207,229,214],{"class":213},[207,231,232],{"class":217}," qgis.gui ",[207,234,221],{"class":213},[207,236,237],{"class":217}," QgsMapTool\n",[207,239,241,243,246,248],{"class":209,"line":240},3,[207,242,214],{"class":213},[207,244,245],{"class":217}," qgis.PyQt.QtCore ",[207,247,221],{"class":213},[207,249,250],{"class":217}," Qt, pyqtSignal\n",[207,252,254],{"class":209,"line":253},4,[207,255,257],{"emptyLinePlaceholder":256},true,"\n",[207,259,261],{"class":209,"line":260},5,[207,262,257],{"emptyLinePlaceholder":256},[207,264,266,269,273,276,279],{"class":209,"line":265},6,[207,267,268],{"class":213},"class",[207,270,272],{"class":271},"svObZ"," IdentifyTool",[207,274,275],{"class":217},"(",[207,277,278],{"class":271},"QgsMapTool",[207,280,281],{"class":217},"):\n",[207,283,285,288,291,294,298,301,304],{"class":209,"line":284},7,[207,286,287],{"class":217},"    featuresFound ",[207,289,290],{"class":213},"=",[207,292,293],{"class":217}," pyqtSignal(",[207,295,297],{"class":296},"sDLfK","object",[207,299,300],{"class":217},", ",[207,302,303],{"class":296},"list",[207,305,306],{"class":217},")\n",[207,308,310],{"class":209,"line":309},8,[207,311,257],{"emptyLinePlaceholder":256},[207,313,315,318,321],{"class":209,"line":314},9,[207,316,317],{"class":296},"    PIXEL_TOLERANCE",[207,319,320],{"class":213}," =",[207,322,323],{"class":296}," 6\n",[207,325,327],{"class":209,"line":326},10,[207,328,257],{"emptyLinePlaceholder":256},[207,330,332,335,338],{"class":209,"line":331},11,[207,333,334],{"class":213},"    def",[207,336,337],{"class":296}," __init__",[207,339,340],{"class":217},"(self, canvas, layer):\n",[207,342,344,347,350,353],{"class":209,"line":343},12,[207,345,346],{"class":296},"        super",[207,348,349],{"class":217},"().",[207,351,352],{"class":296},"__init__",[207,354,355],{"class":217},"(canvas)\n",[207,357,359,362,365,367],{"class":209,"line":358},13,[207,360,361],{"class":296},"        self",[207,363,364],{"class":217},".canvas ",[207,366,290],{"class":213},[207,368,369],{"class":217}," canvas\n",[207,371,373,375,378,380],{"class":209,"line":372},14,[207,374,361],{"class":296},[207,376,377],{"class":217},".layer ",[207,379,290],{"class":213},[207,381,382],{"class":217}," layer\n",[207,384,386],{"class":209,"line":385},15,[207,387,257],{"emptyLinePlaceholder":256},[207,389,391,393,396],{"class":209,"line":390},16,[207,392,334],{"class":213},[207,394,395],{"class":271}," canvasReleaseEvent",[207,397,398],{"class":217},"(self, event):\n",[207,400,402,405,408,411],{"class":209,"line":401},17,[207,403,404],{"class":213},"        if",[207,406,407],{"class":217}," event.button() ",[207,409,410],{"class":213},"!=",[207,412,413],{"class":217}," Qt.LeftButton:\n",[207,415,417],{"class":209,"line":416},18,[207,418,419],{"class":213},"            return\n",[207,421,423,426,428,431,434,437],{"class":209,"line":422},19,[207,424,425],{"class":217},"        point ",[207,427,290],{"class":213},[207,429,430],{"class":296}," self",[207,432,433],{"class":217},".toLayerCoordinates(",[207,435,436],{"class":296},"self",[207,438,439],{"class":217},".layer, event.pos())\n",[207,441,443,446,448,450,453,456,458,461],{"class":209,"line":442},20,[207,444,445],{"class":217},"        radius ",[207,447,290],{"class":213},[207,449,430],{"class":296},[207,451,452],{"class":217},".canvas.mapUnitsPerPixel() ",[207,454,455],{"class":213},"*",[207,457,430],{"class":296},[207,459,460],{"class":217},".",[207,462,463],{"class":296},"PIXEL_TOLERANCE\n",[207,465,467,470,472],{"class":209,"line":466},21,[207,468,469],{"class":217},"        box ",[207,471,290],{"class":213},[207,473,474],{"class":217}," QgsRectangle(\n",[207,476,478,481,484,487,489],{"class":209,"line":477},22,[207,479,480],{"class":217},"            point.x() ",[207,482,483],{"class":213},"-",[207,485,486],{"class":217}," radius, point.y() ",[207,488,483],{"class":213},[207,490,491],{"class":217}," radius,\n",[207,493,495,497,500,502,504],{"class":209,"line":494},23,[207,496,480],{"class":217},[207,498,499],{"class":213},"+",[207,501,486],{"class":217},[207,503,499],{"class":213},[207,505,491],{"class":217},[207,507,509],{"class":209,"line":508},24,[207,510,511],{"class":217},"        )\n",[207,513,515,518,520],{"class":209,"line":514},25,[207,516,517],{"class":217},"        request ",[207,519,290],{"class":213},[207,521,522],{"class":217}," QgsFeatureRequest().setFilterRect(box)\n",[207,524,526,528,531,533,535,537],{"class":209,"line":525},26,[207,527,361],{"class":296},[207,529,530],{"class":217},".featuresFound.emit(point, ",[207,532,303],{"class":296},[207,534,275],{"class":217},[207,536,436],{"class":296},[207,538,539],{"class":217},".layer.getFeatures(request)))\n",[14,541,542,545,546,549,550,553,554,557,558,561,562,565],{},[174,543,544],{},"Breakdown:"," ",[204,547,548],{},"toLayerCoordinates()"," converts the pixel position ",[18,551,552],{},"and"," transforms into the layer's CRS in one call — using ",[204,555,556],{},"toMapCoordinates()"," here is the mistake that makes an identify tool match nothing on any project whose layer CRS differs from the project's. ",[204,559,560],{},"mapUnitsPerPixel()"," returns the current ground distance of one screen pixel, so multiplying by a pixel tolerance produces a map-unit radius that automatically adapts to the zoom. ",[204,563,564],{},"setFilterRect()"," pushes the box test down to the provider, which on an indexed source is close to free.",[14,567,568,569,572],{},"Note this returns ",[18,570,571],{},"candidates"," from a bounding-box test. For a polygon layer that is usually good enough; for precision, follow with an exact test.",[163,574,576],{"id":575},"rank-overlapping-results","Rank overlapping results",[14,578,579],{},"A click on a city centre can land inside a parcel, a district and a region at once. Returning all of them and letting the caller choose is more useful than picking arbitrarily — but the order should be meaningful.",[197,581,583],{"className":199,"code":582,"language":201,"meta":202,"style":202},"from qgis.core import QgsGeometry, QgsPointXY\n\n\ndef rank(point, features):\n    \"\"\"Smallest-area first, so the most specific feature comes first.\"\"\"\n    click = QgsGeometry.fromPointXY(QgsPointXY(point))\n    exact = [f for f in features if f.geometry().intersects(click)]\n    pool = exact or features\n    return sorted(pool, key=lambda f: f.geometry().area() or float(\"inf\"))\n",[204,584,585,596,600,604,615,621,631,659,675],{"__ignoreMap":202},[207,586,587,589,591,593],{"class":209,"line":210},[207,588,214],{"class":213},[207,590,218],{"class":217},[207,592,221],{"class":213},[207,594,595],{"class":217}," QgsGeometry, QgsPointXY\n",[207,597,598],{"class":209,"line":227},[207,599,257],{"emptyLinePlaceholder":256},[207,601,602],{"class":209,"line":240},[207,603,257],{"emptyLinePlaceholder":256},[207,605,606,609,612],{"class":209,"line":253},[207,607,608],{"class":213},"def",[207,610,611],{"class":271}," rank",[207,613,614],{"class":217},"(point, features):\n",[207,616,617],{"class":209,"line":260},[207,618,620],{"class":619},"sU2Wk","    \"\"\"Smallest-area first, so the most specific feature comes first.\"\"\"\n",[207,622,623,626,628],{"class":209,"line":265},[207,624,625],{"class":217},"    click ",[207,627,290],{"class":213},[207,629,630],{"class":217}," QgsGeometry.fromPointXY(QgsPointXY(point))\n",[207,632,633,636,638,641,644,647,650,653,656],{"class":209,"line":284},[207,634,635],{"class":217},"    exact ",[207,637,290],{"class":213},[207,639,640],{"class":217}," [f ",[207,642,643],{"class":213},"for",[207,645,646],{"class":217}," f ",[207,648,649],{"class":213},"in",[207,651,652],{"class":217}," features ",[207,654,655],{"class":213},"if",[207,657,658],{"class":217}," f.geometry().intersects(click)]\n",[207,660,661,664,666,669,672],{"class":209,"line":309},[207,662,663],{"class":217},"    pool ",[207,665,290],{"class":213},[207,667,668],{"class":217}," exact ",[207,670,671],{"class":213},"or",[207,673,674],{"class":217}," features\n",[207,676,677,680,683,686,690,693,696,698,701,703,706],{"class":209,"line":314},[207,678,679],{"class":213},"    return",[207,681,682],{"class":296}," sorted",[207,684,685],{"class":217},"(pool, ",[207,687,689],{"class":688},"s9osk","key",[207,691,692],{"class":213},"=lambda",[207,694,695],{"class":217}," f: f.geometry().area() ",[207,697,671],{"class":213},[207,699,700],{"class":296}," float",[207,702,275],{"class":217},[207,704,705],{"class":619},"\"inf\"",[207,707,708],{"class":217},"))\n",[14,710,711,713,714,717,718,721],{},[174,712,544],{}," Testing ",[204,715,716],{},"intersects()"," against the exact click point separates genuine hits from box-only candidates. Falling back to the full candidate list when nothing contains the point is deliberate — for a line or point layer nothing ever contains a click, and returning an empty list there would break the tool. Sorting by ascending area puts the smallest, most specific feature first, which matches what a user pointing at something means. ",[204,719,720],{},"or float(\"inf\")"," sends zero-area geometries — lines and points — to the end rather than the front.",[163,723,725],{"id":724},"search-several-layers","Search several layers",[14,727,728],{},"Identifying across the whole project is what users expect from the built-in tool, and it is a loop with two guards.",[14,730,731],{},[34,732,734,737,740,742,757,760,766,772,776,781,787,793,796,800,802,807,811,815,819,823,826,831,835,840,844,849,854,857],{"viewBox":36,"role":37,"ariaLabel":733,"xmlns":39},"One click queried against four project layers, with two returning hits, one skipped as a raster and one skipped because it is not visible at the current scale",[41,735,736],{},"One click, four layers, two answers",[45,738,739],{},"A single click fans out to four layers. The parcels layer returns one hit and the districts layer returns one hit. The hillshade raster is skipped because it has no features to identify. The utilities layer is skipped because scale-based visibility hides it at the current zoom, matching what the user can actually see.",[49,741],{"x":51,"y":51,"width":52,"height":53,"fill":54},[743,744,745],"defs",{},[746,747,754],"marker",{"id":748,"viewBox":749,"refX":750,"refY":751,"markerWidth":752,"markerHeight":752,"orient":753},"idFanArrow","0 0 10 10","8","5","7","auto-start-reverse",[84,755],{"d":756,"fill":100},"M0 0 L10 5 L0 10 z",[56,758,759],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Only identify what the user can actually see",[49,761],{"x":762,"y":122,"width":763,"height":764,"rx":73,"fill":765,"stroke":88,"style":76},"16","150","64","#26322d",[56,767,771],{"x":768,"y":769,"style":81,"fill":770,"textAnchor":62},"91","124","#d9f99d","one click",[56,773,775],{"x":768,"y":774,"style":99,"fill":770,"textAnchor":62},"146","6 px tolerance",[49,777],{"x":778,"y":779,"width":780,"height":123,"rx":750,"fill":74,"stroke":112,"style":89},"280","44","256",[56,782,786],{"x":783,"y":784,"style":785,"fill":100},"300","69","font-size:11.5px;font-family:sans-serif","parcels",[56,788,792],{"x":789,"y":784,"style":790,"fill":112,"textAnchor":791},"516","text-anchor:end;font-size:11.5px;font-weight:bold;font-family:sans-serif","end","1 hit",[49,794],{"x":778,"y":795,"width":780,"height":123,"rx":750,"fill":74,"stroke":112,"style":89},"92",[56,797,799],{"x":783,"y":798,"style":785,"fill":100},"117","districts",[56,801,792],{"x":789,"y":798,"style":790,"fill":112,"textAnchor":791},[49,803],{"x":778,"y":804,"width":780,"height":123,"rx":750,"fill":74,"stroke":805,"style":806},"140","#59645f","stroke-width:1.8;stroke-dasharray:5 4",[56,808,810],{"x":783,"y":809,"style":785,"fill":100},"165","hillshade",[56,812,814],{"x":789,"y":809,"style":813,"fill":805,"textAnchor":791},"text-anchor:end;font-size:11.5px;font-family:sans-serif","raster — skipped",[49,816],{"x":778,"y":817,"width":780,"height":123,"rx":750,"fill":74,"stroke":818,"style":806},"188","#b45309",[56,820,822],{"x":783,"y":821,"style":785,"fill":100},"213","utilities",[56,824,825],{"x":789,"y":821,"style":813,"fill":818,"textAnchor":791},"hidden at this scale",[209,827],{"x1":828,"y1":94,"x2":829,"y2":764,"stroke":112,"style":830},"166","276","stroke-width:2;marker-end:url(#idFanArrow)",[209,832],{"x1":828,"y1":833,"x2":829,"y2":834,"stroke":112,"style":830},"126","112",[209,836],{"x1":828,"y1":837,"x2":829,"y2":838,"stroke":805,"style":839},"136","160","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#idFanArrow)",[209,841],{"x1":828,"y1":842,"x2":829,"y2":843,"stroke":818,"style":839},"144","208",[49,845],{"x":846,"y":795,"width":847,"height":848,"rx":73,"fill":74,"stroke":88,"style":76},"580","164","88",[56,850,853],{"x":851,"y":93,"style":852,"fill":88,"textAnchor":62},"662","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","2 results",[56,855,856],{"x":851,"y":774,"style":99,"fill":100,"textAnchor":62},"smallest area first",[209,858],{"x1":859,"y1":837,"x2":860,"y2":837,"stroke":88,"style":861},"536","576","stroke-width:2.5;marker-end:url(#idFanArrow)",[197,863,865],{"className":199,"code":864,"language":201,"meta":202,"style":202},"from qgis.core import QgsProject, QgsVectorLayer\n\n\ndef identify_all(tool, canvas, pos, pixel_tolerance=6):\n    results = {}\n    scale = canvas.scale()\n    for layer in canvas.layers():\n        if not isinstance(layer, QgsVectorLayer):\n            continue\n        if layer.hasScaleBasedVisibility() and not layer.isInScaleRange(scale):\n            continue\n        point = tool.toLayerCoordinates(layer, pos)\n        radius = canvas.mapUnitsPerPixel() * pixel_tolerance\n        box = QgsRectangle(\n            point.x() - radius, point.y() - radius,\n            point.x() + radius, point.y() + radius,\n        )\n        found = list(layer.getFeatures(QgsFeatureRequest().setFilterRect(box)))\n        if found:\n            results[layer] = rank(point, found)\n    return results\n",[204,866,867,878,882,886,903,913,923,936,949,954,968,972,981,995,1003,1015,1027,1031,1044,1051,1061],{"__ignoreMap":202},[207,868,869,871,873,875],{"class":209,"line":210},[207,870,214],{"class":213},[207,872,218],{"class":217},[207,874,221],{"class":213},[207,876,877],{"class":217}," QgsProject, QgsVectorLayer\n",[207,879,880],{"class":209,"line":227},[207,881,257],{"emptyLinePlaceholder":256},[207,883,884],{"class":209,"line":240},[207,885,257],{"emptyLinePlaceholder":256},[207,887,888,890,893,896,898,901],{"class":209,"line":253},[207,889,608],{"class":213},[207,891,892],{"class":271}," identify_all",[207,894,895],{"class":217},"(tool, canvas, pos, pixel_tolerance",[207,897,290],{"class":213},[207,899,900],{"class":296},"6",[207,902,281],{"class":217},[207,904,905,908,910],{"class":209,"line":260},[207,906,907],{"class":217},"    results ",[207,909,290],{"class":213},[207,911,912],{"class":217}," {}\n",[207,914,915,918,920],{"class":209,"line":265},[207,916,917],{"class":217},"    scale ",[207,919,290],{"class":213},[207,921,922],{"class":217}," canvas.scale()\n",[207,924,925,928,931,933],{"class":209,"line":284},[207,926,927],{"class":213},"    for",[207,929,930],{"class":217}," layer ",[207,932,649],{"class":213},[207,934,935],{"class":217}," canvas.layers():\n",[207,937,938,940,943,946],{"class":209,"line":309},[207,939,404],{"class":213},[207,941,942],{"class":213}," not",[207,944,945],{"class":296}," isinstance",[207,947,948],{"class":217},"(layer, QgsVectorLayer):\n",[207,950,951],{"class":209,"line":314},[207,952,953],{"class":213},"            continue\n",[207,955,956,958,961,963,965],{"class":209,"line":326},[207,957,404],{"class":213},[207,959,960],{"class":217}," layer.hasScaleBasedVisibility() ",[207,962,552],{"class":213},[207,964,942],{"class":213},[207,966,967],{"class":217}," layer.isInScaleRange(scale):\n",[207,969,970],{"class":209,"line":331},[207,971,953],{"class":213},[207,973,974,976,978],{"class":209,"line":343},[207,975,425],{"class":217},[207,977,290],{"class":213},[207,979,980],{"class":217}," tool.toLayerCoordinates(layer, pos)\n",[207,982,983,985,987,990,992],{"class":209,"line":358},[207,984,445],{"class":217},[207,986,290],{"class":213},[207,988,989],{"class":217}," canvas.mapUnitsPerPixel() ",[207,991,455],{"class":213},[207,993,994],{"class":217}," pixel_tolerance\n",[207,996,997,999,1001],{"class":209,"line":372},[207,998,469],{"class":217},[207,1000,290],{"class":213},[207,1002,474],{"class":217},[207,1004,1005,1007,1009,1011,1013],{"class":209,"line":385},[207,1006,480],{"class":217},[207,1008,483],{"class":213},[207,1010,486],{"class":217},[207,1012,483],{"class":213},[207,1014,491],{"class":217},[207,1016,1017,1019,1021,1023,1025],{"class":209,"line":390},[207,1018,480],{"class":217},[207,1020,499],{"class":213},[207,1022,486],{"class":217},[207,1024,499],{"class":213},[207,1026,491],{"class":217},[207,1028,1029],{"class":209,"line":401},[207,1030,511],{"class":217},[207,1032,1033,1036,1038,1041],{"class":209,"line":416},[207,1034,1035],{"class":217},"        found ",[207,1037,290],{"class":213},[207,1039,1040],{"class":296}," list",[207,1042,1043],{"class":217},"(layer.getFeatures(QgsFeatureRequest().setFilterRect(box)))\n",[207,1045,1046,1048],{"class":209,"line":422},[207,1047,404],{"class":213},[207,1049,1050],{"class":217}," found:\n",[207,1052,1053,1056,1058],{"class":209,"line":442},[207,1054,1055],{"class":217},"            results[layer] ",[207,1057,290],{"class":213},[207,1059,1060],{"class":217}," rank(point, found)\n",[207,1062,1063,1065],{"class":209,"line":466},[207,1064,679],{"class":213},[207,1066,1067],{"class":217}," results\n",[14,1069,1070,545,1072,1075,1076,1079,1080,1083,1084,1087],{},[174,1071,544],{},[204,1073,1074],{},"canvas.layers()"," returns only the ",[18,1077,1078],{},"visible"," layers in draw order, so an unticked layer is excluded automatically — which is what the user expects. The ",[204,1081,1082],{},"isinstance"," check skips rasters, which have no features to identify this way. ",[204,1085,1086],{},"isInScaleRange()"," respects a layer's scale-dependent visibility, so a detail layer hidden at the current zoom is not silently identified. Converting the click separately per layer is essential: each layer may be in a different CRS, so one shared point would be wrong for all but one of them.",[163,1089,1091],{"id":1090},"show-the-result-without-blocking","Show the result without blocking",[14,1093,1094],{},"A modal dialog per click makes an identify tool tiring. The message bar and a dock widget are both better.",[197,1096,1098],{"className":199,"code":1097,"language":201,"meta":202,"style":202},"def on_features_found(self, point, features):\n    if not features:\n        self.iface.messageBar().pushInfo(\"Identify\", \"Nothing found here\")\n        return\n    feature = features[0]\n    summary = \", \".join(\n        f\"{field.name()}={feature[field.name()]}\"\n        for field in feature.fields()[:3]\n    )\n    self.iface.messageBar().pushInfo(\n        f\"{len(features)} feature(s)\", summary\n    )\n",[204,1099,1100,1110,1120,1137,1142,1157,1170,1199,1217,1222,1230,1250],{"__ignoreMap":202},[207,1101,1102,1104,1107],{"class":209,"line":210},[207,1103,608],{"class":213},[207,1105,1106],{"class":271}," on_features_found",[207,1108,1109],{"class":217},"(self, point, features):\n",[207,1111,1112,1115,1117],{"class":209,"line":227},[207,1113,1114],{"class":213},"    if",[207,1116,942],{"class":213},[207,1118,1119],{"class":217}," features:\n",[207,1121,1122,1124,1127,1130,1132,1135],{"class":209,"line":240},[207,1123,361],{"class":296},[207,1125,1126],{"class":217},".iface.messageBar().pushInfo(",[207,1128,1129],{"class":619},"\"Identify\"",[207,1131,300],{"class":217},[207,1133,1134],{"class":619},"\"Nothing found here\"",[207,1136,306],{"class":217},[207,1138,1139],{"class":209,"line":253},[207,1140,1141],{"class":213},"        return\n",[207,1143,1144,1147,1149,1152,1154],{"class":209,"line":260},[207,1145,1146],{"class":217},"    feature ",[207,1148,290],{"class":213},[207,1150,1151],{"class":217}," features[",[207,1153,51],{"class":296},[207,1155,1156],{"class":217},"]\n",[207,1158,1159,1162,1164,1167],{"class":209,"line":265},[207,1160,1161],{"class":217},"    summary ",[207,1163,290],{"class":213},[207,1165,1166],{"class":619}," \", \"",[207,1168,1169],{"class":217},".join(\n",[207,1171,1172,1175,1178,1181,1184,1187,1189,1191,1194,1196],{"class":209,"line":284},[207,1173,1174],{"class":213},"        f",[207,1176,1177],{"class":619},"\"",[207,1179,1180],{"class":296},"{",[207,1182,1183],{"class":217},"field.name()",[207,1185,1186],{"class":296},"}",[207,1188,290],{"class":619},[207,1190,1180],{"class":296},[207,1192,1193],{"class":217},"feature[field.name()]",[207,1195,1186],{"class":296},[207,1197,1198],{"class":619},"\"\n",[207,1200,1201,1204,1207,1209,1212,1215],{"class":209,"line":309},[207,1202,1203],{"class":213},"        for",[207,1205,1206],{"class":217}," field ",[207,1208,649],{"class":213},[207,1210,1211],{"class":217}," feature.fields()[:",[207,1213,1214],{"class":296},"3",[207,1216,1156],{"class":217},[207,1218,1219],{"class":209,"line":314},[207,1220,1221],{"class":217},"    )\n",[207,1223,1224,1227],{"class":209,"line":326},[207,1225,1226],{"class":296},"    self",[207,1228,1229],{"class":217},".iface.messageBar().pushInfo(\n",[207,1231,1232,1234,1236,1239,1242,1244,1247],{"class":209,"line":331},[207,1233,1174],{"class":213},[207,1235,1177],{"class":619},[207,1237,1238],{"class":296},"{len",[207,1240,1241],{"class":217},"(features)",[207,1243,1186],{"class":296},[207,1245,1246],{"class":619}," feature(s)\"",[207,1248,1249],{"class":217},", summary\n",[207,1251,1252],{"class":209,"line":343},[207,1253,1221],{"class":217},[14,1255,1256,545,1258,1261,1262,460],{},[174,1257,544],{},[204,1259,1260],{},"pushInfo()"," shows a dismissible bar at the top of the map that disappears on its own, so rapid clicking stays fluid. Reporting the count alongside the first feature's summary tells the user there is more without forcing them to page through it. Slicing to the first three fields keeps the line readable — a full attribute dump belongs in a dock widget, which is where a serious identify tool puts it. See ",[26,1263,1265],{"href":1264},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fadd-custom-dock-widget-pyqgis\u002F","Add a Custom Dock Widget with PyQGIS",[14,1267,1268,1269,1273],{},"Pairing the result with a highlight makes it obvious what was matched — ",[26,1270,1272],{"href":1271},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fhighlight-feature-with-rubber-band-pyqgis\u002F","Highlight a Feature with a Rubber Band"," covers that half.",[163,1275,1277],{"id":1276},"use-the-built-in-identify-tool","Use the built-in identify tool",[14,1279,1280,1281,1284],{},"Before writing any of the above, it is worth knowing that QGIS already ships the whole thing. ",[204,1282,1283],{},"QgsMapToolIdentify"," implements the tolerance, the multi-layer search and the result structure, and subclassing it is often five lines.",[14,1286,1287],{},[34,1288,1291,1294,1297,1303,1306,1312,1317,1321,1324,1345],{"viewBox":1289,"role":37,"ariaLabel":1290,"xmlns":39},"159 -2 601 228","A comparison of writing an identify tool by hand against subclassing QgsMapToolIdentify, listing what each requires you to implement",[41,1292,1293],{},"Hand-written versus QgsMapToolIdentify",[45,1295,1296],{},"A hand-written tool requires you to implement the coordinate conversion, the pixel tolerance, the layer loop, the scale-visibility check and the result structure. Subclassing QgsMapToolIdentify provides all five, leaving only the presentation of the results to you.",[49,1298],{"x":1299,"y":1300,"width":1301,"height":1302,"fill":54},"159","-2","601","228",[56,1304,1305],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Five things you can stop writing",[209,1307],{"x1":1308,"y1":1309,"x2":1310,"y2":1309,"stroke":805,"style":1311},"316","66","744","stroke-width:1.5",[56,1313,1316],{"x":1314,"y":1315,"style":790,"fill":61,"textAnchor":791},"304","58","responsibility",[56,1318,1320],{"x":1319,"y":1315,"style":852,"fill":818,"textAnchor":62},"424","by hand",[56,1322,1283],{"x":1323,"y":1315,"style":852,"fill":112,"textAnchor":62},"620",[65,1325,1326,1331,1334,1337,1341],{"style":785},[56,1327,1330],{"x":1314,"y":1328,"style":1329,"fill":100,"textAnchor":791},"94","text-anchor:end","pixel → layer coordinates",[56,1332,1333],{"x":1314,"y":93,"style":1329,"fill":100,"textAnchor":791},"tolerance in pixels",[56,1335,1336],{"x":1314,"y":763,"style":1329,"fill":100,"textAnchor":791},"loop visible layers",[56,1338,1340],{"x":1314,"y":1339,"style":1329,"fill":100,"textAnchor":791},"178","scale-visibility check",[56,1342,1344],{"x":1314,"y":1343,"style":1329,"fill":100,"textAnchor":791},"206","present the results",[65,1346,1348,1351,1353,1355,1357,1359,1362,1364,1366,1368],{"style":1347},"font-size:14px;font-weight:bold;font-family:sans-serif;text-anchor:middle",[56,1349,1350],{"x":1319,"y":1328,"fill":818},"you",[56,1352,1350],{"x":1319,"y":93,"fill":818},[56,1354,1350],{"x":1319,"y":763,"fill":818},[56,1356,1350],{"x":1319,"y":1339,"fill":818},[56,1358,1350],{"x":1319,"y":1343,"fill":818},[56,1360,1361],{"x":1323,"y":1328,"fill":112},"✓",[56,1363,1361],{"x":1323,"y":93,"fill":112},[56,1365,1361],{"x":1323,"y":763,"fill":112},[56,1367,1361],{"x":1323,"y":1339,"fill":112},[56,1369,1350],{"x":1323,"y":1343,"fill":818},[197,1371,1373],{"className":199,"code":1372,"language":201,"meta":202,"style":202},"from qgis.gui import QgsMapToolIdentify\n\n\nclass QuickIdentify(QgsMapToolIdentify):\n    def canvasReleaseEvent(self, event):\n        results = self.identify(\n            event.x(), event.y(),\n            self.TopDownStopAtFirst,\n            self.VectorLayer,\n        )\n        for result in results:\n            print(result.mLayer.name(), dict(result.mAttributes))\n",[204,1374,1375,1386,1390,1394,1407,1415,1427,1432,1440,1447,1451,1463],{"__ignoreMap":202},[207,1376,1377,1379,1381,1383],{"class":209,"line":210},[207,1378,214],{"class":213},[207,1380,232],{"class":217},[207,1382,221],{"class":213},[207,1384,1385],{"class":217}," QgsMapToolIdentify\n",[207,1387,1388],{"class":209,"line":227},[207,1389,257],{"emptyLinePlaceholder":256},[207,1391,1392],{"class":209,"line":240},[207,1393,257],{"emptyLinePlaceholder":256},[207,1395,1396,1398,1401,1403,1405],{"class":209,"line":253},[207,1397,268],{"class":213},[207,1399,1400],{"class":271}," QuickIdentify",[207,1402,275],{"class":217},[207,1404,1283],{"class":271},[207,1406,281],{"class":217},[207,1408,1409,1411,1413],{"class":209,"line":260},[207,1410,334],{"class":213},[207,1412,395],{"class":271},[207,1414,398],{"class":217},[207,1416,1417,1420,1422,1424],{"class":209,"line":265},[207,1418,1419],{"class":217},"        results ",[207,1421,290],{"class":213},[207,1423,430],{"class":296},[207,1425,1426],{"class":217},".identify(\n",[207,1428,1429],{"class":209,"line":284},[207,1430,1431],{"class":217},"            event.x(), event.y(),\n",[207,1433,1434,1437],{"class":209,"line":309},[207,1435,1436],{"class":296},"            self",[207,1438,1439],{"class":217},".TopDownStopAtFirst,\n",[207,1441,1442,1444],{"class":209,"line":314},[207,1443,1436],{"class":296},[207,1445,1446],{"class":217},".VectorLayer,\n",[207,1448,1449],{"class":209,"line":326},[207,1450,511],{"class":217},[207,1452,1453,1455,1458,1460],{"class":209,"line":331},[207,1454,1203],{"class":213},[207,1456,1457],{"class":217}," result ",[207,1459,649],{"class":213},[207,1461,1462],{"class":217}," results:\n",[207,1464,1465,1468,1471,1474],{"class":209,"line":343},[207,1466,1467],{"class":296},"            print",[207,1469,1470],{"class":217},"(result.mLayer.name(), ",[207,1472,1473],{"class":296},"dict",[207,1475,1476],{"class":217},"(result.mAttributes))\n",[14,1478,1479,545,1481,1484,1485,1488,1489,1492,1493,1496,1497,1500,1501,300,1504,1507,1508,1511],{},[174,1480,544],{},[204,1482,1483],{},"identify()"," takes pixel coordinates directly and handles every conversion internally. ",[204,1486,1487],{},"TopDownStopAtFirst"," returns only the topmost hit, which is what a simple identify wants; ",[204,1490,1491],{},"TopDownAll"," returns every match in draw order, and ",[204,1494,1495],{},"LayerSelection"," restricts to the active layer. ",[204,1498,1499],{},"VectorLayer"," filters out rasters. Each result carries ",[204,1502,1503],{},"mLayer",[204,1505,1506],{},"mFeature"," and ",[204,1509,1510],{},"mAttributes",", so the presentation code is all that remains yours. Write the manual version only when you need something the base class does not offer — a custom tolerance policy, a non-standard layer set, or ranking by area as shown above.",[163,1513,1515],{"id":1514},"qgis-version-compatibility","QGIS version compatibility",[14,1517,1518,1519,1521],{},"The examples target ",[174,1520,176],{}," (Python 3.12).",[1523,1524,1525,1541],"table",{},[1526,1527,1528],"thead",{},[1529,1530,1531,1535,1538],"tr",{},[1532,1533,1534],"th",{},"QGIS version",[1532,1536,1537],{},"Python",[1532,1539,1540],{},"Notes",[1542,1543,1544,1560,1571],"tbody",{},[1529,1545,1546,1550,1553],{},[1547,1548,1549],"td",{},"3.28 LTR",[1547,1551,1552],{},"3.9",[1547,1554,1555,1556,1559],{},"Identical API. ",[204,1557,1558],{},"QgsMapToolIdentifyFeature"," also available as a ready-made alternative.",[1529,1561,1562,1565,1568],{},[1547,1563,1564],{},"3.34 LTR",[1547,1566,1567],{},"3.12",[1547,1569,1570],{},"Baseline for this page.",[1529,1572,1573,1576,1578],{},[1547,1574,1575],{},"3.40 \u002F 3.44",[1547,1577,1567],{},[1547,1579,1580,1582],{},[204,1581,1283],{}," gained additional result formats; the manual approach here is unchanged.",[14,1584,1585,300,1587,1507,1589,1591],{},[204,1586,560],{},[204,1588,548],{},[204,1590,564],{}," are unchanged across the 3.x line.",[163,1593,1595],{"id":1594},"troubleshooting","Troubleshooting",[168,1597,1598,1610,1619,1628,1640,1646],{},[171,1599,1600,1603,1604,1606,1607,1609],{},[174,1601,1602],{},"Clicks never match anything."," Either the tolerance is zero — an exact point test — or ",[204,1605,556],{}," was used where ",[204,1608,548],{}," was needed.",[171,1611,1612,1615,1616,1618],{},[174,1613,1614],{},"The tolerance feels wrong when zoomed."," It is fixed in map units. Multiply a pixel tolerance by ",[204,1617,560],{}," at query time instead.",[171,1620,1621,1624,1625,1627],{},[174,1622,1623],{},"Too many features are returned."," The bounding-box test is coarse. Follow it with an exact ",[204,1626,716],{}," against the click point.",[171,1629,1630,545,1633,1636,1637,1639],{},[174,1631,1632],{},"A hidden layer is being identified.",[204,1634,1635],{},"QgsProject.mapLayers()"," was used instead of ",[204,1638,1074],{},". Only the latter reflects what is visible.",[171,1641,1642,1645],{},[174,1643,1644],{},"The wrong feature is reported first."," No ranking was applied. Sort by ascending area so the most specific feature leads.",[171,1647,1648,1651],{},[174,1649,1650],{},"Rapid clicking becomes unresponsive."," A modal dialog is being opened per click. Use the message bar or a dock widget.",[163,1653,1655],{"id":1654},"conclusion","Conclusion",[14,1657,1658,1659,1661,1662,1664,1665,1667],{},"An identify tool is a search rectangle sized in pixels, a provider query, and a ranking rule. Convert the click with ",[204,1660,548],{}," so the CRS is right, derive the radius from ",[204,1663,560],{}," so the tolerance feels constant at any zoom, iterate ",[204,1666,1074],{}," so only visible layers answer, and report through the message bar so the user can keep clicking.",[163,1669,1671],{"id":1670},"frequently-asked-questions","Frequently Asked Questions",[14,1673,1674,1677,1678,1680,1681,1683],{},[174,1675,1676],{},"Why does my identify tool never find anything?","\nMost often the click was converted with ",[204,1679,556],{}," rather than ",[204,1682,548],{},", so the coordinates are in the wrong CRS. The other cause is testing the exact point rather than a small search rectangle.",[14,1685,1686,1689,1690,1693],{},[174,1687,1688],{},"How big should the search tolerance be?","\nAround six screen pixels, converted to map units with ",[204,1691,1692],{},"canvas.mapUnitsPerPixel()"," at the moment of the query. Fixing it in map units makes the tool feel unusable at one zoom or the other.",[14,1695,1696,1699],{},[174,1697,1698],{},"How do I decide which feature to report when several match?","\nFilter to those whose geometry actually contains the click, then sort by ascending area so the smallest and most specific comes first. Fall back to the raw candidates for line and point layers, which never contain a point.",[14,1701,1702,1705,1706,1708],{},[174,1703,1704],{},"Should I identify every layer in the project?","\nNo — iterate ",[204,1707,1074],{},", which returns only the visible layers in draw order, and skip layers outside their scale-visibility range. Identifying something the user cannot see is confusing.",[163,1710,1712],{"id":1711},"related","Related",[168,1714,1715,1720,1725,1731],{},[171,1716,1717,1719],{},[26,1718,29],{"href":28}," — the guide this recipe belongs to",[171,1721,1722],{},[26,1723,1724],{"href":1271},"Highlight a Feature with a Rubber Band in PyQGIS",[171,1726,1727],{},[26,1728,1730],{"href":1729},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcapture-map-click-coordinates-pyqgis\u002F","Capture Map Click Coordinates in PyQGIS",[171,1732,1733],{},[26,1734,1736],{"href":1735},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build and Use a Spatial Index in PyQGIS",[1738,1739,1740],"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":227,"depth":227,"links":1742},[1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753],{"id":165,"depth":227,"text":166},{"id":191,"depth":227,"text":192},{"id":575,"depth":227,"text":576},{"id":724,"depth":227,"text":725},{"id":1090,"depth":227,"text":1091},{"id":1276,"depth":227,"text":1277},{"id":1514,"depth":227,"text":1515},{"id":1594,"depth":227,"text":1595},{"id":1654,"depth":227,"text":1655},{"id":1670,"depth":227,"text":1671},{"id":1711,"depth":227,"text":1712},"Build an identify tool in PyQGIS. Convert a click to a search rectangle sized in pixels, query the provider, rank overlapping hits, and show the result to the user.","md",{"slug":1757,"type":1758,"breadcrumb":1759,"datePublished":1760,"dateModified":1760},"identify-feature-under-cursor-pyqgis","article","Identify a Feature","2026-08-01","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis",{"title":5,"description":1754},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis\u002Findex","bhd0FMjB9nuTmseyCy8mEPzPZzleyCV1vrayTnKPzZ0",1785585990474]