[{"data":1,"prerenderedAt":2328},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fdraw-custom-map-canvas-item-pyqgis":3},{"id":4,"title":5,"body":6,"description":2317,"extension":2318,"meta":2319,"navigation":205,"path":2324,"seo":2325,"stem":2326,"__hash__":2327},"docs\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fdraw-custom-map-canvas-item-pyqgis\u002Findex.md","Draw a Custom Map Canvas Item in PyQGIS",{"type":7,"value":8,"toc":2303},"minimark",[9,13,17,31,107,112,126,130,133,394,417,421,434,537,1092,1111,1115,1118,1213,1226,1230,1233,1293,1567,1579,1583,1589,1882,1897,1901,1904,2132,2148,2152,2180,2184,2222,2226,2238,2242,2248,2254,2260,2266,2270,2299],[10,11,5],"h1",{"id":12},"draw-a-custom-map-canvas-item-in-pyqgis",[14,15,16],"p",{},"Some graphics belong on the map canvas but not in any layer: a crosshair showing a GPS position, a range ring around a selected site, a compass rose that follows the view, a measurement readout next to the cursor. They are temporary, interactive and specific to a tool. QGIS draws them as canvas items — Qt graphics items layered over the rendered map, positioned in map coordinates and repainted instantly without re-rendering any layer.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002F","Extending QGIS with Custom Classes",". It starts with the built-in canvas items, then subclasses ",[27,28,29],"code",{},"QgsMapCanvasItem"," to draw a range ring with distance labels, keeps it positioned through pans and zooms, and removes it cleanly.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,87,92,98,102],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 280","img","Canvas items such as markers, rubber bands and custom items drawn in a graphics scene above the rendered map image","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Canvas items above the rendered map",[45,46,47],"desc",{},"The map canvas shows a rendered map image produced by the layer renderers. Above it, canvas items are Qt graphics items in the same scene: vertex markers, rubber bands, annotations and custom items. They are positioned from map coordinates, drawn in pixels, and repainted without re-rendering layers, which makes them suitable for fast, temporary graphics.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","280","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Graphics above the map, not in it",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"80","170","600","70","6","#e8efe6","#15803d","stroke-width:1.5",[56,75,79],{"x":58,"y":76,"style":77,"fill":78,"textAnchor":62},"210","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","#166534","rendered map image (layers)",[49,81],{"x":82,"y":82,"width":83,"height":84,"rx":70,"fill":85,"stroke":86,"style":73},"120","520","40","#eff3ff","#2563eb",[56,88,91],{"x":58,"y":89,"style":90,"fill":86,"textAnchor":62},"145","text-anchor:middle;font-size:10.5px;font-family:sans-serif","canvas items: markers · rubber bands · annotations",[49,93],{"x":94,"y":69,"width":95,"height":84,"rx":70,"fill":96,"stroke":97,"style":73},"200","360","#fdf2e2","#b45309",[56,99,101],{"x":58,"y":100,"style":90,"fill":97,"textAnchor":62},"95","your QgsMapCanvasItem subclass",[56,103,106],{"x":58,"y":104,"style":90,"fill":105,"textAnchor":62},"264","#59645f","items repaint instantly; layers re-render only when needed",[108,109,111],"h2",{"id":110},"prerequisites","Prerequisites",[113,114,115,119],"ul",{},[116,117,118],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with the code running inside QGIS Desktop (the canvas exists only there).",[116,120,121,122,125],{},"Access to ",[27,123,124],{},"iface.mapCanvas()",", from the Python console or a plugin.",[108,127,129],{"id":128},"start-with-the-built-in-items","Start with the built-in items",[14,131,132],{},"Before writing a class, check whether a built-in item already does the job. Three cover most needs.",[134,135,140],"pre",{"className":136,"code":137,"language":138,"meta":139,"style":139},"language-python shiki shiki-themes github-dark","from qgis.core import QgsPointXY, QgsGeometry, QgsWkbTypes\nfrom qgis.gui import QgsVertexMarker, QgsRubberBand\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.utils import iface\n\ncanvas = iface.mapCanvas()\n\nmarker = QgsVertexMarker(canvas)\nmarker.setCenter(QgsPointXY(571244, 5934019))\nmarker.setIconType(QgsVertexMarker.ICON_CROSS)\nmarker.setIconSize(16)\nmarker.setPenWidth(2)\nmarker.setColor(QColor(\"#b91c1c\"))\n\nband = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)\nband.setToGeometry(QgsGeometry.fromPointXY(QgsPointXY(571244, 5934019)).buffer(250, 36), None)\nband.setColor(QColor(37, 99, 235, 60))\nband.setStrokeColor(QColor(\"#2563eb\"))\nband.setWidth(2)\n","python","",[27,141,142,161,174,187,200,207,219,224,235,254,266,277,288,300,305,316,347,373,384],{"__ignoreMap":139},[143,144,147,151,155,158],"span",{"class":145,"line":146},"line",1,[143,148,150],{"class":149},"snl16","from",[143,152,154],{"class":153},"s95oV"," qgis.core ",[143,156,157],{"class":149},"import",[143,159,160],{"class":153}," QgsPointXY, QgsGeometry, QgsWkbTypes\n",[143,162,164,166,169,171],{"class":145,"line":163},2,[143,165,150],{"class":149},[143,167,168],{"class":153}," qgis.gui ",[143,170,157],{"class":149},[143,172,173],{"class":153}," QgsVertexMarker, QgsRubberBand\n",[143,175,177,179,182,184],{"class":145,"line":176},3,[143,178,150],{"class":149},[143,180,181],{"class":153}," qgis.PyQt.QtGui ",[143,183,157],{"class":149},[143,185,186],{"class":153}," QColor\n",[143,188,190,192,195,197],{"class":145,"line":189},4,[143,191,150],{"class":149},[143,193,194],{"class":153}," qgis.utils ",[143,196,157],{"class":149},[143,198,199],{"class":153}," iface\n",[143,201,203],{"class":145,"line":202},5,[143,204,206],{"emptyLinePlaceholder":205},true,"\n",[143,208,210,213,216],{"class":145,"line":209},6,[143,211,212],{"class":153},"canvas ",[143,214,215],{"class":149},"=",[143,217,218],{"class":153}," iface.mapCanvas()\n",[143,220,222],{"class":145,"line":221},7,[143,223,206],{"emptyLinePlaceholder":205},[143,225,227,230,232],{"class":145,"line":226},8,[143,228,229],{"class":153},"marker ",[143,231,215],{"class":149},[143,233,234],{"class":153}," QgsVertexMarker(canvas)\n",[143,236,238,241,245,248,251],{"class":145,"line":237},9,[143,239,240],{"class":153},"marker.setCenter(QgsPointXY(",[143,242,244],{"class":243},"sDLfK","571244",[143,246,247],{"class":153},", ",[143,249,250],{"class":243},"5934019",[143,252,253],{"class":153},"))\n",[143,255,257,260,263],{"class":145,"line":256},10,[143,258,259],{"class":153},"marker.setIconType(QgsVertexMarker.",[143,261,262],{"class":243},"ICON_CROSS",[143,264,265],{"class":153},")\n",[143,267,269,272,275],{"class":145,"line":268},11,[143,270,271],{"class":153},"marker.setIconSize(",[143,273,274],{"class":243},"16",[143,276,265],{"class":153},[143,278,280,283,286],{"class":145,"line":279},12,[143,281,282],{"class":153},"marker.setPenWidth(",[143,284,285],{"class":243},"2",[143,287,265],{"class":153},[143,289,291,294,298],{"class":145,"line":290},13,[143,292,293],{"class":153},"marker.setColor(QColor(",[143,295,297],{"class":296},"sU2Wk","\"#b91c1c\"",[143,299,253],{"class":153},[143,301,303],{"class":145,"line":302},14,[143,304,206],{"emptyLinePlaceholder":205},[143,306,308,311,313],{"class":145,"line":307},15,[143,309,310],{"class":153},"band ",[143,312,215],{"class":149},[143,314,315],{"class":153}," QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)\n",[143,317,319,322,324,326,328,331,334,336,339,342,345],{"class":145,"line":318},16,[143,320,321],{"class":153},"band.setToGeometry(QgsGeometry.fromPointXY(QgsPointXY(",[143,323,244],{"class":243},[143,325,247],{"class":153},[143,327,250],{"class":243},[143,329,330],{"class":153},")).buffer(",[143,332,333],{"class":243},"250",[143,335,247],{"class":153},[143,337,338],{"class":243},"36",[143,340,341],{"class":153},"), ",[143,343,344],{"class":243},"None",[143,346,265],{"class":153},[143,348,350,353,356,358,361,363,366,368,371],{"class":145,"line":349},17,[143,351,352],{"class":153},"band.setColor(QColor(",[143,354,355],{"class":243},"37",[143,357,247],{"class":153},[143,359,360],{"class":243},"99",[143,362,247],{"class":153},[143,364,365],{"class":243},"235",[143,367,247],{"class":153},[143,369,370],{"class":243},"60",[143,372,253],{"class":153},[143,374,376,379,382],{"class":145,"line":375},18,[143,377,378],{"class":153},"band.setStrokeColor(QColor(",[143,380,381],{"class":296},"\"#2563eb\"",[143,383,253],{"class":153},[143,385,387,390,392],{"class":145,"line":386},19,[143,388,389],{"class":153},"band.setWidth(",[143,391,285],{"class":243},[143,393,265],{"class":153},[14,395,396,400,401,404,405,408,409,413,414,416],{},[397,398,399],"strong",{},"Breakdown:"," ",[27,402,403],{},"QgsVertexMarker"," draws a fixed-size icon at a map point — a cross, box, circle or X — and is the quickest way to mark a location. ",[27,406,407],{},"QgsRubberBand"," draws any geometry with fill and outline, in map units, so a 250 m buffer stays 250 m as you zoom; it is the workhorse of map tools, as in ",[21,410,412],{"href":411},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fhighlight-feature-with-rubber-band-pyqgis\u002F","highlighting a feature with a rubber band",". Passing ",[27,415,344],{}," as the layer argument means the geometry is already in the canvas CRS. For text and HTML notes that users can move and save with the project, annotations are the built-in route.",[108,418,420],{"id":419},"subclass-qgsmapcanvasitem","Subclass QgsMapCanvasItem",[14,422,423,424,426,427,247,430,433],{},"When none of those fits — mixed graphics, text that must stay a fixed pixel size next to a map-unit shape, custom interaction — subclass ",[27,425,29],{},". It needs ",[27,428,429],{},"paint",[27,431,432],{},"boundingRect"," and a way to update its position when the view changes.",[14,435,436],{},[34,437,440,443,446,448,465,468,473,479,483,488,493,498,503,507,511,515,518,522,526,530,533],{"viewBox":438,"role":37,"ariaLabel":439,"xmlns":39},"0 0 760 250","A canvas item's methods: store map coordinates, updatePosition on view changes, boundingRect for redraw area, and paint in pixels",[41,441,442],{},"The methods a canvas item implements",[45,444,445],{},"updatePosition recomputes the item's screen position and size from its map coordinates whenever the canvas extent changes. boundingRect returns the area the item paints in, in item coordinates, so Qt knows what to redraw. paint draws with the QPainter in pixels. setCenter or similar methods store map coordinates and trigger an update.",[49,447],{"x":51,"y":51,"width":52,"height":333,"fill":54},[449,450,451],"defs",{},[452,453,460],"marker",{"id":454,"viewBox":455,"refX":456,"refY":457,"markerWidth":458,"markerHeight":458,"orient":459},"ciMethodsArrow","0 0 10 10","8","5","7","auto-start-reverse",[461,462],"path",{"d":463,"fill":464},"M0 0 L10 5 L0 10 z","#2f3b35",[56,466,467],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Map coordinates in, pixels out",[49,469],{"x":470,"y":370,"width":67,"height":82,"rx":456,"fill":471,"stroke":105,"style":472},"24","#fffdf7","stroke-width:2",[56,474,478],{"x":475,"y":476,"style":477,"fill":61,"textAnchor":62},"109","111.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","map point",[56,480,482],{"x":475,"y":481,"style":90,"fill":105,"textAnchor":62},"135.78","stored by you",[145,484],{"x1":485,"y1":82,"x2":486,"y2":82,"stroke":464,"style":487},"194","230","stroke-width:1.8;marker-end:url(#ciMethodsArrow)",[49,489],{"x":490,"y":370,"width":67,"height":82,"rx":456,"fill":491,"stroke":492,"style":472},"234","#eef7f4","#0f766e",[56,494,497],{"x":495,"y":496,"style":77,"fill":492,"textAnchor":62},"319","111.6","updatePosition()",[56,499,502],{"x":495,"y":500,"style":501,"fill":105,"textAnchor":62},"135.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","toCanvasCoordinates",[145,504],{"x1":505,"y1":82,"x2":506,"y2":82,"stroke":464,"style":487},"404","440",[49,508],{"x":509,"y":370,"width":510,"height":82,"rx":456,"fill":85,"stroke":86,"style":472},"444","140",[56,512,514],{"x":513,"y":496,"style":77,"fill":86,"textAnchor":62},"514","boundingRect()",[56,516,517],{"x":513,"y":500,"style":501,"fill":105,"textAnchor":62},"redraw area",[145,519],{"x1":520,"y1":82,"x2":521,"y2":82,"stroke":464,"style":487},"584","620",[49,523],{"x":524,"y":370,"width":525,"height":82,"rx":456,"fill":96,"stroke":97,"style":472},"624","112",[56,527,529],{"x":528,"y":496,"style":77,"fill":97,"textAnchor":62},"680","paint()",[56,531,532],{"x":528,"y":500,"style":501,"fill":105,"textAnchor":62},"QPainter",[56,534,536],{"x":58,"y":535,"style":90,"fill":105,"textAnchor":62},"220","the canvas calls updatePosition after every pan and zoom",[134,538,540],{"className":136,"code":539,"language":138,"meta":139,"style":139},"from qgis.gui import QgsMapCanvasItem\nfrom qgis.PyQt.QtCore import QRectF, QPointF, Qt\nfrom qgis.PyQt.QtGui import QPen, QFont\n\nclass RangeRingItem(QgsMapCanvasItem):\n    def __init__(self, canvas, center, radii_m=(250, 500, 1000)):\n        super().__init__(canvas)\n        self.canvas = canvas\n        self.center = center              # QgsPointXY in canvas CRS\n        self.radii = radii_m\n        self._px = []\n        self.setZValue(50)\n        self.updatePosition()\n\n    def setCenter(self, center):\n        self.center = center\n        self.updatePosition()\n\n    def updatePosition(self):\n        mupp = self.canvas.mapUnitsPerPixel()\n        self._px = [r \u002F mupp for r in self.radii]\n        pos = self.toCanvasCoordinates(self.center)\n        self.prepareGeometryChange()\n        self.setPos(pos)\n        self.update()\n\n    def boundingRect(self):\n        r = (max(self._px) if self._px else 0) + 40\n        return QRectF(-r, -r, 2 * r, 2 * r)\n\n    def paint(self, painter, option=None, widget=None):\n        painter.setRenderHint(painter.Antialiasing)\n        painter.setPen(QPen(QColor(\"#2563eb\"), 1.5, Qt.DashLine))\n        painter.setFont(QFont(\"Sans\", 8))\n        for r_m, r_px in zip(self.radii, self._px):\n            painter.drawEllipse(QPointF(0, 0), r_px, r_px)\n            painter.drawText(QPointF(4, -r_px - 3), f\"{r_m:,} m\")\n",[27,541,542,553,565,576,580,597,627,641,654,670,682,694,706,713,717,727,738,744,748,758,772,804,823,831,839,847,852,862,905,939,944,968,974,990,1005,1031,1046],{"__ignoreMap":139},[143,543,544,546,548,550],{"class":145,"line":146},[143,545,150],{"class":149},[143,547,168],{"class":153},[143,549,157],{"class":149},[143,551,552],{"class":153}," QgsMapCanvasItem\n",[143,554,555,557,560,562],{"class":145,"line":163},[143,556,150],{"class":149},[143,558,559],{"class":153}," qgis.PyQt.QtCore ",[143,561,157],{"class":149},[143,563,564],{"class":153}," QRectF, QPointF, Qt\n",[143,566,567,569,571,573],{"class":145,"line":176},[143,568,150],{"class":149},[143,570,181],{"class":153},[143,572,157],{"class":149},[143,574,575],{"class":153}," QPen, QFont\n",[143,577,578],{"class":145,"line":189},[143,579,206],{"emptyLinePlaceholder":205},[143,581,582,585,589,592,594],{"class":145,"line":202},[143,583,584],{"class":149},"class",[143,586,588],{"class":587},"svObZ"," RangeRingItem",[143,590,591],{"class":153},"(",[143,593,29],{"class":587},[143,595,596],{"class":153},"):\n",[143,598,599,602,605,608,610,612,614,616,619,621,624],{"class":145,"line":209},[143,600,601],{"class":149},"    def",[143,603,604],{"class":243}," __init__",[143,606,607],{"class":153},"(self, canvas, center, radii_m",[143,609,215],{"class":149},[143,611,591],{"class":153},[143,613,333],{"class":243},[143,615,247],{"class":153},[143,617,618],{"class":243},"500",[143,620,247],{"class":153},[143,622,623],{"class":243},"1000",[143,625,626],{"class":153},")):\n",[143,628,629,632,635,638],{"class":145,"line":221},[143,630,631],{"class":243},"        super",[143,633,634],{"class":153},"().",[143,636,637],{"class":243},"__init__",[143,639,640],{"class":153},"(canvas)\n",[143,642,643,646,649,651],{"class":145,"line":226},[143,644,645],{"class":243},"        self",[143,647,648],{"class":153},".canvas ",[143,650,215],{"class":149},[143,652,653],{"class":153}," canvas\n",[143,655,656,658,661,663,666],{"class":145,"line":237},[143,657,645],{"class":243},[143,659,660],{"class":153},".center ",[143,662,215],{"class":149},[143,664,665],{"class":153}," center              ",[143,667,669],{"class":668},"sjoCn","# QgsPointXY in canvas CRS\n",[143,671,672,674,677,679],{"class":145,"line":256},[143,673,645],{"class":243},[143,675,676],{"class":153},".radii ",[143,678,215],{"class":149},[143,680,681],{"class":153}," radii_m\n",[143,683,684,686,689,691],{"class":145,"line":268},[143,685,645],{"class":243},[143,687,688],{"class":153},"._px ",[143,690,215],{"class":149},[143,692,693],{"class":153}," []\n",[143,695,696,698,701,704],{"class":145,"line":279},[143,697,645],{"class":243},[143,699,700],{"class":153},".setZValue(",[143,702,703],{"class":243},"50",[143,705,265],{"class":153},[143,707,708,710],{"class":145,"line":290},[143,709,645],{"class":243},[143,711,712],{"class":153},".updatePosition()\n",[143,714,715],{"class":145,"line":302},[143,716,206],{"emptyLinePlaceholder":205},[143,718,719,721,724],{"class":145,"line":307},[143,720,601],{"class":149},[143,722,723],{"class":587}," setCenter",[143,725,726],{"class":153},"(self, center):\n",[143,728,729,731,733,735],{"class":145,"line":318},[143,730,645],{"class":243},[143,732,660],{"class":153},[143,734,215],{"class":149},[143,736,737],{"class":153}," center\n",[143,739,740,742],{"class":145,"line":349},[143,741,645],{"class":243},[143,743,712],{"class":153},[143,745,746],{"class":145,"line":375},[143,747,206],{"emptyLinePlaceholder":205},[143,749,750,752,755],{"class":145,"line":386},[143,751,601],{"class":149},[143,753,754],{"class":587}," updatePosition",[143,756,757],{"class":153},"(self):\n",[143,759,761,764,766,769],{"class":145,"line":760},20,[143,762,763],{"class":153},"        mupp ",[143,765,215],{"class":149},[143,767,768],{"class":243}," self",[143,770,771],{"class":153},".canvas.mapUnitsPerPixel()\n",[143,773,775,777,779,781,784,787,790,793,796,799,801],{"class":145,"line":774},21,[143,776,645],{"class":243},[143,778,688],{"class":153},[143,780,215],{"class":149},[143,782,783],{"class":153}," [r ",[143,785,786],{"class":149},"\u002F",[143,788,789],{"class":153}," mupp ",[143,791,792],{"class":149},"for",[143,794,795],{"class":153}," r ",[143,797,798],{"class":149},"in",[143,800,768],{"class":243},[143,802,803],{"class":153},".radii]\n",[143,805,807,810,812,814,817,820],{"class":145,"line":806},22,[143,808,809],{"class":153},"        pos ",[143,811,215],{"class":149},[143,813,768],{"class":243},[143,815,816],{"class":153},".toCanvasCoordinates(",[143,818,819],{"class":243},"self",[143,821,822],{"class":153},".center)\n",[143,824,826,828],{"class":145,"line":825},23,[143,827,645],{"class":243},[143,829,830],{"class":153},".prepareGeometryChange()\n",[143,832,834,836],{"class":145,"line":833},24,[143,835,645],{"class":243},[143,837,838],{"class":153},".setPos(pos)\n",[143,840,842,844],{"class":145,"line":841},25,[143,843,645],{"class":243},[143,845,846],{"class":153},".update()\n",[143,848,850],{"class":145,"line":849},26,[143,851,206],{"emptyLinePlaceholder":205},[143,853,855,857,860],{"class":145,"line":854},27,[143,856,601],{"class":149},[143,858,859],{"class":587}," boundingRect",[143,861,757],{"class":153},[143,863,865,868,870,873,876,878,880,883,886,888,890,893,896,899,902],{"class":145,"line":864},28,[143,866,867],{"class":153},"        r ",[143,869,215],{"class":149},[143,871,872],{"class":153}," (",[143,874,875],{"class":243},"max",[143,877,591],{"class":153},[143,879,819],{"class":243},[143,881,882],{"class":153},"._px) ",[143,884,885],{"class":149},"if",[143,887,768],{"class":243},[143,889,688],{"class":153},[143,891,892],{"class":149},"else",[143,894,895],{"class":243}," 0",[143,897,898],{"class":153},") ",[143,900,901],{"class":149},"+",[143,903,904],{"class":243}," 40\n",[143,906,908,911,914,917,920,922,924,926,929,932,934,936],{"class":145,"line":907},29,[143,909,910],{"class":149},"        return",[143,912,913],{"class":153}," QRectF(",[143,915,916],{"class":149},"-",[143,918,919],{"class":153},"r, ",[143,921,916],{"class":149},[143,923,919],{"class":153},[143,925,285],{"class":243},[143,927,928],{"class":149}," *",[143,930,931],{"class":153}," r, ",[143,933,285],{"class":243},[143,935,928],{"class":149},[143,937,938],{"class":153}," r)\n",[143,940,942],{"class":145,"line":941},30,[143,943,206],{"emptyLinePlaceholder":205},[143,945,947,949,952,955,957,959,962,964,966],{"class":145,"line":946},31,[143,948,601],{"class":149},[143,950,951],{"class":587}," paint",[143,953,954],{"class":153},"(self, painter, option",[143,956,215],{"class":149},[143,958,344],{"class":243},[143,960,961],{"class":153},", widget",[143,963,215],{"class":149},[143,965,344],{"class":243},[143,967,596],{"class":153},[143,969,971],{"class":145,"line":970},32,[143,972,973],{"class":153},"        painter.setRenderHint(painter.Antialiasing)\n",[143,975,977,980,982,984,987],{"class":145,"line":976},33,[143,978,979],{"class":153},"        painter.setPen(QPen(QColor(",[143,981,381],{"class":296},[143,983,341],{"class":153},[143,985,986],{"class":243},"1.5",[143,988,989],{"class":153},", Qt.DashLine))\n",[143,991,993,996,999,1001,1003],{"class":145,"line":992},34,[143,994,995],{"class":153},"        painter.setFont(QFont(",[143,997,998],{"class":296},"\"Sans\"",[143,1000,247],{"class":153},[143,1002,456],{"class":243},[143,1004,253],{"class":153},[143,1006,1008,1011,1014,1016,1019,1021,1023,1026,1028],{"class":145,"line":1007},35,[143,1009,1010],{"class":149},"        for",[143,1012,1013],{"class":153}," r_m, r_px ",[143,1015,798],{"class":149},[143,1017,1018],{"class":243}," zip",[143,1020,591],{"class":153},[143,1022,819],{"class":243},[143,1024,1025],{"class":153},".radii, ",[143,1027,819],{"class":243},[143,1029,1030],{"class":153},"._px):\n",[143,1032,1034,1037,1039,1041,1043],{"class":145,"line":1033},36,[143,1035,1036],{"class":153},"            painter.drawEllipse(QPointF(",[143,1038,51],{"class":243},[143,1040,247],{"class":153},[143,1042,51],{"class":243},[143,1044,1045],{"class":153},"), r_px, r_px)\n",[143,1047,1049,1052,1055,1057,1059,1062,1064,1067,1069,1072,1075,1078,1081,1084,1087,1090],{"class":145,"line":1048},37,[143,1050,1051],{"class":153},"            painter.drawText(QPointF(",[143,1053,1054],{"class":243},"4",[143,1056,247],{"class":153},[143,1058,916],{"class":149},[143,1060,1061],{"class":153},"r_px ",[143,1063,916],{"class":149},[143,1065,1066],{"class":243}," 3",[143,1068,341],{"class":153},[143,1070,1071],{"class":149},"f",[143,1073,1074],{"class":296},"\"",[143,1076,1077],{"class":243},"{",[143,1079,1080],{"class":153},"r_m",[143,1082,1083],{"class":149},":,",[143,1085,1086],{"class":243},"}",[143,1088,1089],{"class":296}," m\"",[143,1091,265],{"class":153},[14,1093,1094,1096,1097,1100,1101,1103,1104,1106,1107,1110],{},[397,1095,399],{}," The item stores its anchor in map coordinates and converts to screen coordinates in ",[27,1098,1099],{},"updatePosition",", which the canvas calls whenever the extent changes. ",[27,1102,502],{}," gives the pixel position of the map point; the item is placed there, so ",[27,1105,429],{}," can draw around (0, 0). Radii are converted from metres to pixels using the canvas's map units per pixel, which assumes a projected canvas CRS in metres. ",[27,1108,1109],{},"prepareGeometryChange()"," must be called before the bounding rectangle changes, or Qt leaves stale pixels behind. Labels are drawn in a fixed font size, so they stay readable at every zoom while the rings scale with the map.",[108,1112,1114],{"id":1113},"show-move-and-remove-the-item","Show, move and remove the item",[14,1116,1117],{},"Canvas items live as long as something holds them and the canvas scene owns them. Removing them explicitly is part of every tool's cleanup.",[134,1119,1121],{"className":136,"code":1120,"language":138,"meta":139,"style":139},"rings = RangeRingItem(canvas, QgsPointXY(571244, 5934019), radii_m=(500, 1000, 2000))\n\n# follow a moving position, e.g. from a GPS feed or a map click\nrings.setCenter(QgsPointXY(571600, 5934400))\n\n# remove when the tool is deactivated or the plugin unloads\ncanvas.scene().removeItem(rings)\ndel rings\ncanvas.refresh()\n",[27,1122,1123,1162,1166,1171,1186,1190,1195,1200,1208],{"__ignoreMap":139},[143,1124,1125,1128,1130,1133,1135,1137,1139,1141,1145,1147,1149,1151,1153,1155,1157,1160],{"class":145,"line":146},[143,1126,1127],{"class":153},"rings ",[143,1129,215],{"class":149},[143,1131,1132],{"class":153}," RangeRingItem(canvas, QgsPointXY(",[143,1134,244],{"class":243},[143,1136,247],{"class":153},[143,1138,250],{"class":243},[143,1140,341],{"class":153},[143,1142,1144],{"class":1143},"s9osk","radii_m",[143,1146,215],{"class":149},[143,1148,591],{"class":153},[143,1150,618],{"class":243},[143,1152,247],{"class":153},[143,1154,623],{"class":243},[143,1156,247],{"class":153},[143,1158,1159],{"class":243},"2000",[143,1161,253],{"class":153},[143,1163,1164],{"class":145,"line":163},[143,1165,206],{"emptyLinePlaceholder":205},[143,1167,1168],{"class":145,"line":176},[143,1169,1170],{"class":668},"# follow a moving position, e.g. from a GPS feed or a map click\n",[143,1172,1173,1176,1179,1181,1184],{"class":145,"line":189},[143,1174,1175],{"class":153},"rings.setCenter(QgsPointXY(",[143,1177,1178],{"class":243},"571600",[143,1180,247],{"class":153},[143,1182,1183],{"class":243},"5934400",[143,1185,253],{"class":153},[143,1187,1188],{"class":145,"line":202},[143,1189,206],{"emptyLinePlaceholder":205},[143,1191,1192],{"class":145,"line":209},[143,1193,1194],{"class":668},"# remove when the tool is deactivated or the plugin unloads\n",[143,1196,1197],{"class":145,"line":221},[143,1198,1199],{"class":153},"canvas.scene().removeItem(rings)\n",[143,1201,1202,1205],{"class":145,"line":226},[143,1203,1204],{"class":149},"del",[143,1206,1207],{"class":153}," rings\n",[143,1209,1210],{"class":145,"line":237},[143,1211,1212],{"class":153},"canvas.refresh()\n",[14,1214,1215,1217,1218,1221,1222,1225],{},[397,1216,399],{}," Creating the item with the canvas adds it to the canvas's graphics scene; it appears immediately. ",[27,1219,1220],{},"setCenter"," updates the anchor and repositions without touching any layer, which is why canvas items suit live positions. Removing the item from the scene before dropping the Python reference avoids crashes from a C++ object being deleted while still in the scene. In a plugin, track every item you create and remove them all in ",[27,1223,1224],{},"unload()"," and when the relevant map tool is deactivated.",[108,1227,1229],{"id":1228},"keep-it-correct-when-the-crs-changes","Keep it correct when the CRS changes",[14,1231,1232],{},"The example assumes the canvas CRS is projected in metres. When users switch the project to a geographic CRS, metres per pixel no longer means anything. Measuring ellipsoidally and transforming keeps the rings correct.",[14,1234,1235],{},[34,1236,1239,1242,1245,1248,1251,1256,1261,1265,1269,1272,1275,1278,1281,1283,1287,1290],{"viewBox":1237,"role":37,"ariaLabel":1238,"xmlns":39},"0 0 760 202","Ring radii computed with ellipsoidal measurement so they stay correct in projected and geographic canvas CRSs",[41,1240,1241],{},"Distances in any canvas CRS",[45,1243,1244],{},"The ring radius in metres is converted to canvas units by measuring with QgsDistanceArea from the centre to a point the required distance away along a bearing, in the canvas CRS. In a projected CRS this matches map units per pixel; in a geographic CRS it gives the correct, latitude-dependent size. The item listens to destinationCrsChanged to recompute.",[49,1246],{"x":51,"y":51,"width":52,"height":1247,"fill":54},"202",[56,1249,1250],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Metres, whatever the canvas CRS",[49,1252],{"x":470,"y":1253,"width":1254,"height":1255,"rx":456,"fill":491,"stroke":492,"style":472},"56","222","130",[56,1257,1260],{"x":1258,"y":1259,"style":477,"fill":492,"textAnchor":62},"135","98.78","projected CRS",[56,1262,1264],{"x":1258,"y":1263,"style":90,"fill":464,"textAnchor":62},"124.78","map units = metres",[56,1266,1268],{"x":1258,"y":1267,"style":90,"fill":105,"textAnchor":62},"150.78","mapUnitsPerPixel",[49,1270],{"x":1271,"y":1253,"width":1254,"height":1255,"rx":456,"fill":96,"stroke":97,"style":472},"269",[56,1273,1274],{"x":58,"y":1259,"style":477,"fill":97,"textAnchor":62},"geographic CRS",[56,1276,1277],{"x":58,"y":1263,"style":90,"fill":464,"textAnchor":62},"degrees ≠ metres",[56,1279,1280],{"x":58,"y":1267,"style":90,"fill":105,"textAnchor":62},"measure on ellipsoid",[49,1282],{"x":513,"y":1253,"width":1254,"height":1255,"rx":456,"fill":85,"stroke":86,"style":472},[56,1284,1286],{"x":1285,"y":1259,"style":477,"fill":86,"textAnchor":62},"625","listen",[56,1288,1289],{"x":1285,"y":1263,"style":90,"fill":464,"textAnchor":62},"destinationCrsChanged",[56,1291,1292],{"x":1285,"y":1267,"style":90,"fill":105,"textAnchor":62},"recompute radii",[134,1294,1296],{"className":136,"code":1295,"language":138,"meta":139,"style":139},"from qgis.core import QgsDistanceArea, QgsProject\n\nclass GeoRangeRingItem(RangeRingItem):\n    def __init__(self, canvas, center, radii_m=(250, 500, 1000)):\n        self.da = QgsDistanceArea()\n        super().__init__(canvas, center, radii_m)\n        canvas.destinationCrsChanged.connect(self.updatePosition)\n\n    def updatePosition(self):\n        crs = self.canvas.mapSettings().destinationCrs()\n        self.da.setSourceCrs(crs, QgsProject.instance().transformContext())\n        self.da.setEllipsoid(QgsProject.instance().ellipsoid() or \"EPSG:7030\")\n        mupp = self.canvas.mapUnitsPerPixel()\n        self._px = []\n        for r in self.radii:\n            east = self.da.computeSpheroidProject(self.center, r, 1.5708) \\\n                if crs.isGeographic() else QgsPointXY(self.center.x() + r, self.center.y())\n            self._px.append(abs(east.x() - self.center.x()) \u002F mupp)\n        self.prepareGeometryChange()\n        self.setPos(self.toCanvasCoordinates(self.center))\n        self.update()\n",[27,1297,1298,1309,1313,1327,1351,1363,1374,1384,1388,1396,1408,1415,1430,1440,1450,1463,1486,1513,1539,1545,1561],{"__ignoreMap":139},[143,1299,1300,1302,1304,1306],{"class":145,"line":146},[143,1301,150],{"class":149},[143,1303,154],{"class":153},[143,1305,157],{"class":149},[143,1307,1308],{"class":153}," QgsDistanceArea, QgsProject\n",[143,1310,1311],{"class":145,"line":163},[143,1312,206],{"emptyLinePlaceholder":205},[143,1314,1315,1317,1320,1322,1325],{"class":145,"line":176},[143,1316,584],{"class":149},[143,1318,1319],{"class":587}," GeoRangeRingItem",[143,1321,591],{"class":153},[143,1323,1324],{"class":587},"RangeRingItem",[143,1326,596],{"class":153},[143,1328,1329,1331,1333,1335,1337,1339,1341,1343,1345,1347,1349],{"class":145,"line":189},[143,1330,601],{"class":149},[143,1332,604],{"class":243},[143,1334,607],{"class":153},[143,1336,215],{"class":149},[143,1338,591],{"class":153},[143,1340,333],{"class":243},[143,1342,247],{"class":153},[143,1344,618],{"class":243},[143,1346,247],{"class":153},[143,1348,623],{"class":243},[143,1350,626],{"class":153},[143,1352,1353,1355,1358,1360],{"class":145,"line":202},[143,1354,645],{"class":243},[143,1356,1357],{"class":153},".da ",[143,1359,215],{"class":149},[143,1361,1362],{"class":153}," QgsDistanceArea()\n",[143,1364,1365,1367,1369,1371],{"class":145,"line":209},[143,1366,631],{"class":243},[143,1368,634],{"class":153},[143,1370,637],{"class":243},[143,1372,1373],{"class":153},"(canvas, center, radii_m)\n",[143,1375,1376,1379,1381],{"class":145,"line":221},[143,1377,1378],{"class":153},"        canvas.destinationCrsChanged.connect(",[143,1380,819],{"class":243},[143,1382,1383],{"class":153},".updatePosition)\n",[143,1385,1386],{"class":145,"line":226},[143,1387,206],{"emptyLinePlaceholder":205},[143,1389,1390,1392,1394],{"class":145,"line":237},[143,1391,601],{"class":149},[143,1393,754],{"class":587},[143,1395,757],{"class":153},[143,1397,1398,1401,1403,1405],{"class":145,"line":256},[143,1399,1400],{"class":153},"        crs ",[143,1402,215],{"class":149},[143,1404,768],{"class":243},[143,1406,1407],{"class":153},".canvas.mapSettings().destinationCrs()\n",[143,1409,1410,1412],{"class":145,"line":268},[143,1411,645],{"class":243},[143,1413,1414],{"class":153},".da.setSourceCrs(crs, QgsProject.instance().transformContext())\n",[143,1416,1417,1419,1422,1425,1428],{"class":145,"line":279},[143,1418,645],{"class":243},[143,1420,1421],{"class":153},".da.setEllipsoid(QgsProject.instance().ellipsoid() ",[143,1423,1424],{"class":149},"or",[143,1426,1427],{"class":296}," \"EPSG:7030\"",[143,1429,265],{"class":153},[143,1431,1432,1434,1436,1438],{"class":145,"line":290},[143,1433,763],{"class":153},[143,1435,215],{"class":149},[143,1437,768],{"class":243},[143,1439,771],{"class":153},[143,1441,1442,1444,1446,1448],{"class":145,"line":302},[143,1443,645],{"class":243},[143,1445,688],{"class":153},[143,1447,215],{"class":149},[143,1449,693],{"class":153},[143,1451,1452,1454,1456,1458,1460],{"class":145,"line":307},[143,1453,1010],{"class":149},[143,1455,795],{"class":153},[143,1457,798],{"class":149},[143,1459,768],{"class":243},[143,1461,1462],{"class":153},".radii:\n",[143,1464,1465,1468,1470,1472,1475,1477,1480,1483],{"class":145,"line":318},[143,1466,1467],{"class":153},"            east ",[143,1469,215],{"class":149},[143,1471,768],{"class":243},[143,1473,1474],{"class":153},".da.computeSpheroidProject(",[143,1476,819],{"class":243},[143,1478,1479],{"class":153},".center, r, ",[143,1481,1482],{"class":243},"1.5708",[143,1484,1485],{"class":153},") \\\n",[143,1487,1488,1491,1494,1496,1499,1501,1504,1506,1508,1510],{"class":145,"line":349},[143,1489,1490],{"class":149},"                if",[143,1492,1493],{"class":153}," crs.isGeographic() ",[143,1495,892],{"class":149},[143,1497,1498],{"class":153}," QgsPointXY(",[143,1500,819],{"class":243},[143,1502,1503],{"class":153},".center.x() ",[143,1505,901],{"class":149},[143,1507,931],{"class":153},[143,1509,819],{"class":243},[143,1511,1512],{"class":153},".center.y())\n",[143,1514,1515,1518,1521,1524,1527,1529,1531,1534,1536],{"class":145,"line":375},[143,1516,1517],{"class":243},"            self",[143,1519,1520],{"class":153},"._px.append(",[143,1522,1523],{"class":243},"abs",[143,1525,1526],{"class":153},"(east.x() ",[143,1528,916],{"class":149},[143,1530,768],{"class":243},[143,1532,1533],{"class":153},".center.x()) ",[143,1535,786],{"class":149},[143,1537,1538],{"class":153}," mupp)\n",[143,1540,1541,1543],{"class":145,"line":386},[143,1542,645],{"class":243},[143,1544,830],{"class":153},[143,1546,1547,1549,1552,1554,1556,1558],{"class":145,"line":760},[143,1548,645],{"class":243},[143,1550,1551],{"class":153},".setPos(",[143,1553,819],{"class":243},[143,1555,816],{"class":153},[143,1557,819],{"class":243},[143,1559,1560],{"class":153},".center))\n",[143,1562,1563,1565],{"class":145,"line":774},[143,1564,645],{"class":243},[143,1566,846],{"class":153},[14,1568,1569,1571,1572,1575,1576,1578],{},[397,1570,399],{}," In a geographic CRS, ",[27,1573,1574],{},"computeSpheroidProject"," finds the point a given distance east of the centre on the ellipsoid, and the difference in longitude, divided by degrees per pixel, gives the radius in pixels at that latitude. In a projected CRS the original arithmetic is used. Connecting to ",[27,1577,1289],{}," recomputes when the user changes the project CRS. Rings drawn this way are circles on screen; on large areas in a geographic CRS, true geodesic circles would be ellipses — use a rubber band with a geodesic buffer if that accuracy matters.",[108,1580,1582],{"id":1581},"draw-many-marks-in-one-item","Draw many marks in one item",[14,1584,1585,1586,1588],{},"A separate canvas item per point is fine for a handful; for a few hundred live positions — vehicles, sensors, search results — one item that draws them all is far lighter. The pattern is the same: store map coordinates, convert in ",[27,1587,1099],{},", paint in pixels.",[134,1590,1592],{"className":136,"code":1591,"language":138,"meta":139,"style":139},"class PositionsItem(QgsMapCanvasItem):\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.canvas = canvas\n        self.points = []            # QgsPointXY list in canvas CRS\n        self._screen = []\n        self.setZValue(60)\n\n    def setPoints(self, points):\n        self.points = list(points)\n        self.updatePosition()\n\n    def updatePosition(self):\n        self.prepareGeometryChange()\n        self.setPos(0, 0)\n        self._screen = [self.toCanvasCoordinates(p) for p in self.points]\n        self.update()\n\n    def boundingRect(self):\n        return QRectF(0, 0, self.canvas.width(), self.canvas.height())\n\n    def paint(self, painter, option=None, widget=None):\n        painter.setPen(QPen(QColor(\"#17211d\"), 1))\n        painter.setBrush(QColor(\"#b45309\"))\n        for p in self._screen:\n            painter.drawEllipse(p, 4, 4)\n",[27,1593,1594,1607,1616,1626,1636,1651,1662,1672,1676,1686,1700,1706,1710,1718,1724,1738,1766,1772,1776,1784,1808,1812,1832,1846,1856,1869],{"__ignoreMap":139},[143,1595,1596,1598,1601,1603,1605],{"class":145,"line":146},[143,1597,584],{"class":149},[143,1599,1600],{"class":587}," PositionsItem",[143,1602,591],{"class":153},[143,1604,29],{"class":587},[143,1606,596],{"class":153},[143,1608,1609,1611,1613],{"class":145,"line":163},[143,1610,601],{"class":149},[143,1612,604],{"class":243},[143,1614,1615],{"class":153},"(self, canvas):\n",[143,1617,1618,1620,1622,1624],{"class":145,"line":176},[143,1619,631],{"class":243},[143,1621,634],{"class":153},[143,1623,637],{"class":243},[143,1625,640],{"class":153},[143,1627,1628,1630,1632,1634],{"class":145,"line":189},[143,1629,645],{"class":243},[143,1631,648],{"class":153},[143,1633,215],{"class":149},[143,1635,653],{"class":153},[143,1637,1638,1640,1643,1645,1648],{"class":145,"line":202},[143,1639,645],{"class":243},[143,1641,1642],{"class":153},".points ",[143,1644,215],{"class":149},[143,1646,1647],{"class":153}," []            ",[143,1649,1650],{"class":668},"# QgsPointXY list in canvas CRS\n",[143,1652,1653,1655,1658,1660],{"class":145,"line":209},[143,1654,645],{"class":243},[143,1656,1657],{"class":153},"._screen ",[143,1659,215],{"class":149},[143,1661,693],{"class":153},[143,1663,1664,1666,1668,1670],{"class":145,"line":221},[143,1665,645],{"class":243},[143,1667,700],{"class":153},[143,1669,370],{"class":243},[143,1671,265],{"class":153},[143,1673,1674],{"class":145,"line":226},[143,1675,206],{"emptyLinePlaceholder":205},[143,1677,1678,1680,1683],{"class":145,"line":237},[143,1679,601],{"class":149},[143,1681,1682],{"class":587}," setPoints",[143,1684,1685],{"class":153},"(self, points):\n",[143,1687,1688,1690,1692,1694,1697],{"class":145,"line":256},[143,1689,645],{"class":243},[143,1691,1642],{"class":153},[143,1693,215],{"class":149},[143,1695,1696],{"class":243}," list",[143,1698,1699],{"class":153},"(points)\n",[143,1701,1702,1704],{"class":145,"line":268},[143,1703,645],{"class":243},[143,1705,712],{"class":153},[143,1707,1708],{"class":145,"line":279},[143,1709,206],{"emptyLinePlaceholder":205},[143,1711,1712,1714,1716],{"class":145,"line":290},[143,1713,601],{"class":149},[143,1715,754],{"class":587},[143,1717,757],{"class":153},[143,1719,1720,1722],{"class":145,"line":302},[143,1721,645],{"class":243},[143,1723,830],{"class":153},[143,1725,1726,1728,1730,1732,1734,1736],{"class":145,"line":307},[143,1727,645],{"class":243},[143,1729,1551],{"class":153},[143,1731,51],{"class":243},[143,1733,247],{"class":153},[143,1735,51],{"class":243},[143,1737,265],{"class":153},[143,1739,1740,1742,1744,1746,1749,1751,1754,1756,1759,1761,1763],{"class":145,"line":318},[143,1741,645],{"class":243},[143,1743,1657],{"class":153},[143,1745,215],{"class":149},[143,1747,1748],{"class":153}," [",[143,1750,819],{"class":243},[143,1752,1753],{"class":153},".toCanvasCoordinates(p) ",[143,1755,792],{"class":149},[143,1757,1758],{"class":153}," p ",[143,1760,798],{"class":149},[143,1762,768],{"class":243},[143,1764,1765],{"class":153},".points]\n",[143,1767,1768,1770],{"class":145,"line":349},[143,1769,645],{"class":243},[143,1771,846],{"class":153},[143,1773,1774],{"class":145,"line":375},[143,1775,206],{"emptyLinePlaceholder":205},[143,1777,1778,1780,1782],{"class":145,"line":386},[143,1779,601],{"class":149},[143,1781,859],{"class":587},[143,1783,757],{"class":153},[143,1785,1786,1788,1790,1792,1794,1796,1798,1800,1803,1805],{"class":145,"line":760},[143,1787,910],{"class":149},[143,1789,913],{"class":153},[143,1791,51],{"class":243},[143,1793,247],{"class":153},[143,1795,51],{"class":243},[143,1797,247],{"class":153},[143,1799,819],{"class":243},[143,1801,1802],{"class":153},".canvas.width(), ",[143,1804,819],{"class":243},[143,1806,1807],{"class":153},".canvas.height())\n",[143,1809,1810],{"class":145,"line":774},[143,1811,206],{"emptyLinePlaceholder":205},[143,1813,1814,1816,1818,1820,1822,1824,1826,1828,1830],{"class":145,"line":806},[143,1815,601],{"class":149},[143,1817,951],{"class":587},[143,1819,954],{"class":153},[143,1821,215],{"class":149},[143,1823,344],{"class":243},[143,1825,961],{"class":153},[143,1827,215],{"class":149},[143,1829,344],{"class":243},[143,1831,596],{"class":153},[143,1833,1834,1836,1839,1841,1844],{"class":145,"line":825},[143,1835,979],{"class":153},[143,1837,1838],{"class":296},"\"#17211d\"",[143,1840,341],{"class":153},[143,1842,1843],{"class":243},"1",[143,1845,253],{"class":153},[143,1847,1848,1851,1854],{"class":145,"line":833},[143,1849,1850],{"class":153},"        painter.setBrush(QColor(",[143,1852,1853],{"class":296},"\"#b45309\"",[143,1855,253],{"class":153},[143,1857,1858,1860,1862,1864,1866],{"class":145,"line":841},[143,1859,1010],{"class":149},[143,1861,1758],{"class":153},[143,1863,798],{"class":149},[143,1865,768],{"class":243},[143,1867,1868],{"class":153},"._screen:\n",[143,1870,1871,1874,1876,1878,1880],{"class":145,"line":849},[143,1872,1873],{"class":153},"            painter.drawEllipse(p, ",[143,1875,1054],{"class":243},[143,1877,247],{"class":153},[143,1879,1054],{"class":243},[143,1881,265],{"class":153},[14,1883,1884,1886,1887,1889,1890,1892,1893,1896],{},[397,1885,399],{}," Placing the item at the scene origin and covering the whole canvas lets ",[27,1888,429],{}," draw at absolute screen positions computed in ",[27,1891,1099],{},". Replacing the point list and calling ",[27,1894,1895],{},"setPoints"," from a timer or a network callback animates hundreds of positions with a single repaint. Beyond a few thousand points, or when positions must be queried, styled by attribute or saved, a memory layer with a fast refresh is the better tool.",[108,1898,1900],{"id":1899},"interact-with-the-item","Interact with the item",[14,1902,1903],{},"Canvas items can respond to the mouse, but in QGIS, interaction normally belongs to a map tool. Let the tool receive clicks and drags and update the item.",[134,1905,1907],{"className":136,"code":1906,"language":138,"meta":139,"style":139},"from qgis.gui import QgsMapToolEmitPoint\n\nclass PlaceRingsTool(QgsMapToolEmitPoint):\n    def __init__(self, canvas):\n        super().__init__(canvas)\n        self.item = None\n        self.canvasClicked.connect(self.on_click)\n\n    def on_click(self, point, button):\n        if self.item is None:\n            self.item = GeoRangeRingItem(self.canvas(), point, (500, 1000, 2000))\n        else:\n            self.item.setCenter(point)\n\n    def deactivate(self):\n        if self.item is not None:\n            self.canvas().scene().removeItem(self.item)\n            self.item = None\n        super().deactivate()\n\ntool = PlaceRingsTool(canvas)\ncanvas.setMapTool(tool)\n",[27,1908,1909,1920,1924,1938,1946,1956,1968,1980,1984,1994,2012,2040,2047,2054,2058,2067,2084,2096,2106,2113,2117,2127],{"__ignoreMap":139},[143,1910,1911,1913,1915,1917],{"class":145,"line":146},[143,1912,150],{"class":149},[143,1914,168],{"class":153},[143,1916,157],{"class":149},[143,1918,1919],{"class":153}," QgsMapToolEmitPoint\n",[143,1921,1922],{"class":145,"line":163},[143,1923,206],{"emptyLinePlaceholder":205},[143,1925,1926,1928,1931,1933,1936],{"class":145,"line":176},[143,1927,584],{"class":149},[143,1929,1930],{"class":587}," PlaceRingsTool",[143,1932,591],{"class":153},[143,1934,1935],{"class":587},"QgsMapToolEmitPoint",[143,1937,596],{"class":153},[143,1939,1940,1942,1944],{"class":145,"line":189},[143,1941,601],{"class":149},[143,1943,604],{"class":243},[143,1945,1615],{"class":153},[143,1947,1948,1950,1952,1954],{"class":145,"line":202},[143,1949,631],{"class":243},[143,1951,634],{"class":153},[143,1953,637],{"class":243},[143,1955,640],{"class":153},[143,1957,1958,1960,1963,1965],{"class":145,"line":209},[143,1959,645],{"class":243},[143,1961,1962],{"class":153},".item ",[143,1964,215],{"class":149},[143,1966,1967],{"class":243}," None\n",[143,1969,1970,1972,1975,1977],{"class":145,"line":221},[143,1971,645],{"class":243},[143,1973,1974],{"class":153},".canvasClicked.connect(",[143,1976,819],{"class":243},[143,1978,1979],{"class":153},".on_click)\n",[143,1981,1982],{"class":145,"line":226},[143,1983,206],{"emptyLinePlaceholder":205},[143,1985,1986,1988,1991],{"class":145,"line":237},[143,1987,601],{"class":149},[143,1989,1990],{"class":587}," on_click",[143,1992,1993],{"class":153},"(self, point, button):\n",[143,1995,1996,1999,2001,2003,2006,2009],{"class":145,"line":256},[143,1997,1998],{"class":149},"        if",[143,2000,768],{"class":243},[143,2002,1962],{"class":153},[143,2004,2005],{"class":149},"is",[143,2007,2008],{"class":243}," None",[143,2010,2011],{"class":153},":\n",[143,2013,2014,2016,2018,2020,2023,2025,2028,2030,2032,2034,2036,2038],{"class":145,"line":268},[143,2015,1517],{"class":243},[143,2017,1962],{"class":153},[143,2019,215],{"class":149},[143,2021,2022],{"class":153}," GeoRangeRingItem(",[143,2024,819],{"class":243},[143,2026,2027],{"class":153},".canvas(), point, (",[143,2029,618],{"class":243},[143,2031,247],{"class":153},[143,2033,623],{"class":243},[143,2035,247],{"class":153},[143,2037,1159],{"class":243},[143,2039,253],{"class":153},[143,2041,2042,2045],{"class":145,"line":279},[143,2043,2044],{"class":149},"        else",[143,2046,2011],{"class":153},[143,2048,2049,2051],{"class":145,"line":290},[143,2050,1517],{"class":243},[143,2052,2053],{"class":153},".item.setCenter(point)\n",[143,2055,2056],{"class":145,"line":302},[143,2057,206],{"emptyLinePlaceholder":205},[143,2059,2060,2062,2065],{"class":145,"line":307},[143,2061,601],{"class":149},[143,2063,2064],{"class":587}," deactivate",[143,2066,757],{"class":153},[143,2068,2069,2071,2073,2075,2077,2080,2082],{"class":145,"line":318},[143,2070,1998],{"class":149},[143,2072,768],{"class":243},[143,2074,1962],{"class":153},[143,2076,2005],{"class":149},[143,2078,2079],{"class":149}," not",[143,2081,2008],{"class":243},[143,2083,2011],{"class":153},[143,2085,2086,2088,2091,2093],{"class":145,"line":349},[143,2087,1517],{"class":243},[143,2089,2090],{"class":153},".canvas().scene().removeItem(",[143,2092,819],{"class":243},[143,2094,2095],{"class":153},".item)\n",[143,2097,2098,2100,2102,2104],{"class":145,"line":375},[143,2099,1517],{"class":243},[143,2101,1962],{"class":153},[143,2103,215],{"class":149},[143,2105,1967],{"class":243},[143,2107,2108,2110],{"class":145,"line":386},[143,2109,631],{"class":243},[143,2111,2112],{"class":153},"().deactivate()\n",[143,2114,2115],{"class":145,"line":760},[143,2116,206],{"emptyLinePlaceholder":205},[143,2118,2119,2122,2124],{"class":145,"line":774},[143,2120,2121],{"class":153},"tool ",[143,2123,215],{"class":149},[143,2125,2126],{"class":153}," PlaceRingsTool(canvas)\n",[143,2128,2129],{"class":145,"line":806},[143,2130,2131],{"class":153},"canvas.setMapTool(tool)\n",[14,2133,2134,400,2136,2138,2139,2142,2143,2147],{},[397,2135,399],{},[27,2137,1935],{}," emits the clicked map point already in canvas coordinates, so the tool only has to create or move the item. Removing the item in ",[27,2140,2141],{},"deactivate"," means switching to another tool cleans up automatically. This split — the tool handles input, the item handles drawing — matches how QGIS's own tools are built and keeps both simple; ",[21,2144,2146],{"href":2145},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","creating a custom map tool"," covers the tool side.",[108,2149,2151],{"id":2150},"qgis-version-compatibility","QGIS version compatibility",[14,2153,2154,247,2156,2158,2159,2161,2162,247,2165,247,2168,2171,2172,2175,2176,2179],{},[27,2155,29],{},[27,2157,403],{}," and ",[27,2160,407],{}," work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. On QGIS 4, Qt enums are scoped: ",[27,2163,2164],{},"Qt.PenStyle.DashLine",[27,2166,2167],{},"QPainter.RenderHint.Antialiasing",[27,2169,2170],{},"QgsVertexMarker.IconType.ICON_CROSS","; geometry types are ",[27,2173,2174],{},"Qgis.GeometryType.Polygon",". Writing the scoped forms works on Qt5-based 3.x releases too when using ",[27,2177,2178],{},"qgis.PyQt",".",[108,2181,2183],{"id":2182},"troubleshooting","Troubleshooting",[113,2185,2186,2200,2208,2216],{},[116,2187,2188,400,2191,2193,2194,2196,2197,2199],{},[397,2189,2190],{},"Trails of old drawings remain.",[27,2192,1109],{}," was not called before the bounding rectangle changed, or ",[27,2195,432],{}," is too small for what ",[27,2198,429],{}," draws.",[116,2201,2202,400,2205,2207],{},[397,2203,2204],{},"The item does not move when panning.",[27,2206,1099],{}," is not converting from map coordinates.",[116,2209,2210,2213,2214,2179],{},[397,2211,2212],{},"QGIS crashes on plugin reload."," Items were not removed from the scene in ",[27,2215,1224],{},[116,2217,2218,2221],{},[397,2219,2220],{},"Rings have the wrong size."," The canvas CRS is geographic; measure on the ellipsoid.",[108,2223,2225],{"id":2224},"conclusion","Conclusion",[14,2227,2228,2229,2231,2232,2234,2235,2237],{},"Use vertex markers and rubber bands where they suffice; otherwise subclass ",[27,2230,29],{},", store map coordinates, convert to pixels in ",[27,2233,1099],{},", declare the redraw area in ",[27,2236,432],{},", paint in pixels, handle CRS changes, let a map tool drive interaction, and remove every item from the scene when finished.",[108,2239,2241],{"id":2240},"frequently-asked-questions","Frequently Asked Questions",[14,2243,2244,2247],{},[397,2245,2246],{},"Do canvas items appear in print layouts?","\nNo. They exist only on the canvas. For anything that must be printed, use a layer, annotation or layout item.",[14,2249,2250,2253],{},[397,2251,2252],{},"Are canvas items saved in the project?","\nNo. Annotations are, which is the difference that matters for users.",[14,2255,2256,2259],{},[397,2257,2258],{},"Can I draw on the canvas from a background thread?","\nNo. Create and update items on the main thread, for example via a signal from a task.",[14,2261,2262,2265],{},[397,2263,2264],{},"How many items can the canvas handle?","\nHundreds comfortably; for thousands, draw them in one item or use a memory layer instead.",[108,2267,2269],{"id":2268},"related","Related",[113,2271,2272,2277,2282,2287,2293],{},[116,2273,2274,2276],{},[21,2275,24],{"href":23}," — the guide this recipe belongs to",[116,2278,2279],{},[21,2280,2281],{"href":411},"Highlight a Feature with a Rubber Band in PyQGIS",[116,2283,2284],{},[21,2285,2286],{"href":2145},"Create a Custom Map Tool in PyQGIS",[116,2288,2289],{},[21,2290,2292],{"href":2291},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fbuild-measure-tool-with-qgsdistancearea-pyqgis\u002F","Build a Measure Tool with QgsDistanceArea",[116,2294,2295],{},[21,2296,2298],{"href":2297},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-layer-tree-indicator-pyqgis\u002F","Add a Layer Tree Indicator in PyQGIS",[2300,2301,2302],"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 pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":139,"searchDepth":163,"depth":163,"links":2304},[2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316],{"id":110,"depth":163,"text":111},{"id":128,"depth":163,"text":129},{"id":419,"depth":163,"text":420},{"id":1113,"depth":163,"text":1114},{"id":1228,"depth":163,"text":1229},{"id":1581,"depth":163,"text":1582},{"id":1899,"depth":163,"text":1900},{"id":2150,"depth":163,"text":2151},{"id":2182,"depth":163,"text":2183},{"id":2224,"depth":163,"text":2225},{"id":2240,"depth":163,"text":2241},{"id":2268,"depth":163,"text":2269},"Draw temporary graphics on the QGIS map canvas by subclassing QgsMapCanvasItem — anchoring to map coordinates, painting in pixels, updating on pan and zoom, removing items cleanly — and when built-in vertex markers, rubber bands or annotations are enough.","md",{"slug":2320,"type":2321,"breadcrumb":2322,"datePublished":2323,"dateModified":2323},"draw-custom-map-canvas-item-pyqgis","article","Draw a Custom Map Canvas Item","2026-10-02","\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fdraw-custom-map-canvas-item-pyqgis",{"title":5,"description":2317},"qgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fdraw-custom-map-canvas-item-pyqgis\u002Findex","uqwgazOOaHy7dZKzjqxbZ2Kugyyo5cgQp4F9EeuXUE4",1790966257281]