[{"data":1,"prerenderedAt":2301},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-feature-renderer-pyqgis":3},{"id":4,"title":5,"body":6,"description":2290,"extension":2291,"meta":2292,"navigation":272,"path":2297,"seo":2298,"stem":2299,"__hash__":2300},"docs\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-feature-renderer-pyqgis\u002Findex.md","Write a Custom Feature Renderer in PyQGIS",{"type":7,"value":8,"toc":2275},"minimark",[9,13,17,31,178,183,201,205,208,944,974,978,989,1064,1324,1351,1355,1358,1543,1552,1556,1639,1651,1655,1661,1730,1814,1831,1835,1838,2080,2098,2102,2114,2118,2136,2140,2181,2185,2205,2209,2219,2225,2231,2237,2241,2271],[10,11,5],"h1",{"id":12},"write-a-custom-feature-renderer-in-pyqgis",[14,15,16],"p",{},"A renderer decides which symbol each feature gets. QGIS ships renderers for one symbol, categories, graduated classes, rules, point clusters and more, and between them they handle almost everything — especially with expressions and data-defined overrides. Occasionally the choice depends on logic that does not fit an expression: a lookup against an external service, a stateful rule such as \"highlight the five most recently edited features\", or a symbology scheme shared by an organisation and maintained in code. A custom renderer in Python handles those cases while staying part of the normal rendering pipeline.",[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 subclasses ",[27,28,29],"code",{},"QgsFeatureRenderer",", implements the methods the rendering engine calls, makes the renderer survive project save and load, registers it from a plugin, and explains when a built-in renderer is the better choice.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,102,105,109,113,120,128,133,137,141,145,151,155,158,160,163,166,173],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 290","img","The calls a rendering engine makes on a feature renderer: startRender, usedAttributes, symbolForFeature per feature, symbols, stopRender, and save and create for persistence","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"What the engine asks a renderer",[45,46,47],"desc",{},"The rendering engine calls startRender with the render context and fields, then usedAttributes to know which fields to fetch. For each feature it calls symbolForFeature, which returns the symbol to draw or None to skip the feature. symbols returns all symbols the renderer may use, for legends and preparation. stopRender ends the cycle. save and create serialise the renderer to and from XML in project and style files.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","290","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"frCycleArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","The renderer's contract with the engine",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","52","222","84","#eef7f4","#0f766e","stroke-width:2",[73,91,95],{"x":92,"y":93,"style":94,"fill":88,"textAnchor":79},"135","86.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","startRender()",[73,97,101],{"x":92,"y":98,"style":99,"fill":100,"textAnchor":79},"108.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#59645f","context, fields",[49,103],{"x":83,"y":104,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"148",[73,106,108],{"x":92,"y":107,"style":94,"fill":88,"textAnchor":79},"182.78","usedAttributes()",[73,110,112],{"x":92,"y":111,"style":99,"fill":100,"textAnchor":79},"204.78","fields to fetch",[114,115],"line",{"x1":116,"y1":117,"x2":118,"y2":117,"stroke":71,"style":119},"246","140","296","stroke-width:1.8;marker-end:url(#frCycleArrow)",[49,121],{"x":122,"y":123,"width":124,"height":125,"rx":63,"fill":126,"stroke":127,"style":89},"300","92","220","96","#eff3ff","#2563eb",[73,129,132],{"x":130,"y":131,"style":94,"fill":127,"textAnchor":79},"410","119.78","symbolForFeature()",[73,134,136],{"x":130,"y":135,"style":99,"fill":100,"textAnchor":79},"143.78","per feature",[73,138,140],{"x":130,"y":139,"style":99,"fill":100,"textAnchor":79},"167.78","None = skip",[114,142],{"x1":143,"y1":117,"x2":144,"y2":117,"stroke":71,"style":119},"520","562",[49,146],{"x":147,"y":84,"width":148,"height":86,"rx":63,"fill":149,"stroke":150,"style":89},"566","170","#fdf2e2","#b45309",[73,152,154],{"x":153,"y":93,"style":94,"fill":150,"textAnchor":79},"651","symbols()",[73,156,157],{"x":153,"y":98,"style":99,"fill":100,"textAnchor":79},"legend, prepare",[49,159],{"x":147,"y":104,"width":148,"height":86,"rx":63,"fill":149,"stroke":150,"style":89},[73,161,162],{"x":153,"y":107,"style":94,"fill":150,"textAnchor":79},"stopRender()",[73,164,165],{"x":153,"y":111,"style":99,"fill":100,"textAnchor":79},"once",[49,167],{"x":168,"y":169,"width":170,"height":171,"rx":63,"fill":172,"stroke":100,"style":89},"180","250","400","32","#fffdf7",[73,174,177],{"x":75,"y":175,"style":176,"fill":71,"textAnchor":79},"269.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","save() \u002F create(): XML in projects and QML",[179,180,182],"h2",{"id":181},"prerequisites","Prerequisites",[184,185,186,190,193],"ul",{},[187,188,189],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[187,191,192],{},"A plugin to register the renderer type each time QGIS starts.",[187,194,195,196,200],{},"Familiarity with symbols from ",[21,197,199],{"href":198},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","setting vector layer symbol colours",".",[179,202,204],{"id":203},"subclass-qgsfeaturerenderer","Subclass QgsFeatureRenderer",[14,206,207],{},"The example renderer highlights features edited in the last N days: recent edits get a bold orange symbol, older ones a quiet grey, and features without an edit date are skipped. It is the kind of rule people often want for review maps.",[209,210,215],"pre",{"className":211,"code":212,"language":213,"meta":214,"style":214},"language-python shiki shiki-themes github-dark","from datetime import datetime, timedelta\nfrom qgis.core import (QgsFeatureRenderer, QgsSymbol, QgsMarkerSymbol, QgsFillSymbol,\n                       QgsLineSymbol, QgsWkbTypes)\nfrom qgis.PyQt.QtGui import QColor\n\nclass RecentEditsRenderer(QgsFeatureRenderer):\n    TYPE = \"PyRecentEdits\"\n\n    def __init__(self, days=7, field=\"edited_on\", recent=None, old=None):\n        super().__init__(self.TYPE)\n        self.days = days\n        self.field = field\n        self.recent = recent\n        self.old = old\n        self._cutoff = None\n        self._idx = -1\n\n    def _default_symbols(self, geometry_type):\n        self.recent = self.recent or QgsSymbol.defaultSymbol(geometry_type)\n        self.recent.setColor(QColor(\"#b45309\"))\n        self.old = self.old or QgsSymbol.defaultSymbol(geometry_type)\n        self.old.setColor(QColor(\"#c9c2ac\"))\n\n    def startRender(self, context, fields):\n        super().startRender(context, fields)\n        self._cutoff = datetime.now() - timedelta(days=self.days)\n        self._idx = fields.indexOf(self.field)\n        self.recent.startRender(context, fields)\n        self.old.startRender(context, fields)\n\n    def stopRender(self, context):\n        self.recent.stopRender(context)\n        self.old.stopRender(context)\n        super().stopRender(context)\n\n    def usedAttributes(self, context):\n        return {self.field}\n\n    def symbolForFeature(self, feature, context):\n        value = feature.attribute(self._idx) if self._idx >= 0 else None\n        if value is None or (hasattr(value, \"isNull\") and value.isNull()):\n            return None\n        when = value.toPyDateTime() if hasattr(value, \"toPyDateTime\") else value\n        return self.recent if when >= self._cutoff else self.old\n\n    def symbols(self, context):\n        return [self.recent, self.old]\n\n    def clone(self):\n        r = RecentEditsRenderer(self.days, self.field, self.recent.clone(), self.old.clone())\n        self.copyRendererData(r)\n        return r\n","python","",[27,216,217,235,248,254,267,274,292,306,311,353,378,392,405,418,431,444,460,465,476,496,510,527,540,545,556,564,593,610,618,626,631,642,650,658,666,671,681,695,700,711,745,784,792,821,848,853,863,881,886,897,928,936],{"__ignoreMap":214},[218,219,221,225,229,232],"span",{"class":114,"line":220},1,[218,222,224],{"class":223},"snl16","from",[218,226,228],{"class":227},"s95oV"," datetime ",[218,230,231],{"class":223},"import",[218,233,234],{"class":227}," datetime, timedelta\n",[218,236,238,240,243,245],{"class":114,"line":237},2,[218,239,224],{"class":223},[218,241,242],{"class":227}," qgis.core ",[218,244,231],{"class":223},[218,246,247],{"class":227}," (QgsFeatureRenderer, QgsSymbol, QgsMarkerSymbol, QgsFillSymbol,\n",[218,249,251],{"class":114,"line":250},3,[218,252,253],{"class":227},"                       QgsLineSymbol, QgsWkbTypes)\n",[218,255,257,259,262,264],{"class":114,"line":256},4,[218,258,224],{"class":223},[218,260,261],{"class":227}," qgis.PyQt.QtGui ",[218,263,231],{"class":223},[218,265,266],{"class":227}," QColor\n",[218,268,270],{"class":114,"line":269},5,[218,271,273],{"emptyLinePlaceholder":272},true,"\n",[218,275,277,280,284,287,289],{"class":114,"line":276},6,[218,278,279],{"class":223},"class",[218,281,283],{"class":282},"svObZ"," RecentEditsRenderer",[218,285,286],{"class":227},"(",[218,288,29],{"class":282},[218,290,291],{"class":227},"):\n",[218,293,295,299,302],{"class":114,"line":294},7,[218,296,298],{"class":297},"sDLfK","    TYPE",[218,300,301],{"class":223}," =",[218,303,305],{"class":304},"sU2Wk"," \"PyRecentEdits\"\n",[218,307,309],{"class":114,"line":308},8,[218,310,273],{"emptyLinePlaceholder":272},[218,312,314,317,320,323,326,328,331,333,336,339,341,344,347,349,351],{"class":114,"line":313},9,[218,315,316],{"class":223},"    def",[218,318,319],{"class":297}," __init__",[218,321,322],{"class":227},"(self, days",[218,324,325],{"class":223},"=",[218,327,65],{"class":297},[218,329,330],{"class":227},", field",[218,332,325],{"class":223},[218,334,335],{"class":304},"\"edited_on\"",[218,337,338],{"class":227},", recent",[218,340,325],{"class":223},[218,342,343],{"class":297},"None",[218,345,346],{"class":227},", old",[218,348,325],{"class":223},[218,350,343],{"class":297},[218,352,291],{"class":227},[218,354,356,359,362,365,367,370,372,375],{"class":114,"line":355},10,[218,357,358],{"class":297},"        super",[218,360,361],{"class":227},"().",[218,363,364],{"class":297},"__init__",[218,366,286],{"class":227},[218,368,369],{"class":297},"self",[218,371,200],{"class":227},[218,373,374],{"class":297},"TYPE",[218,376,377],{"class":227},")\n",[218,379,381,384,387,389],{"class":114,"line":380},11,[218,382,383],{"class":297},"        self",[218,385,386],{"class":227},".days ",[218,388,325],{"class":223},[218,390,391],{"class":227}," days\n",[218,393,395,397,400,402],{"class":114,"line":394},12,[218,396,383],{"class":297},[218,398,399],{"class":227},".field ",[218,401,325],{"class":223},[218,403,404],{"class":227}," field\n",[218,406,408,410,413,415],{"class":114,"line":407},13,[218,409,383],{"class":297},[218,411,412],{"class":227},".recent ",[218,414,325],{"class":223},[218,416,417],{"class":227}," recent\n",[218,419,421,423,426,428],{"class":114,"line":420},14,[218,422,383],{"class":297},[218,424,425],{"class":227},".old ",[218,427,325],{"class":223},[218,429,430],{"class":227}," old\n",[218,432,434,436,439,441],{"class":114,"line":433},15,[218,435,383],{"class":297},[218,437,438],{"class":227},"._cutoff ",[218,440,325],{"class":223},[218,442,443],{"class":297}," None\n",[218,445,447,449,452,454,457],{"class":114,"line":446},16,[218,448,383],{"class":297},[218,450,451],{"class":227},"._idx ",[218,453,325],{"class":223},[218,455,456],{"class":223}," -",[218,458,459],{"class":297},"1\n",[218,461,463],{"class":114,"line":462},17,[218,464,273],{"emptyLinePlaceholder":272},[218,466,468,470,473],{"class":114,"line":467},18,[218,469,316],{"class":223},[218,471,472],{"class":282}," _default_symbols",[218,474,475],{"class":227},"(self, geometry_type):\n",[218,477,479,481,483,485,488,490,493],{"class":114,"line":478},19,[218,480,383],{"class":297},[218,482,412],{"class":227},[218,484,325],{"class":223},[218,486,487],{"class":297}," self",[218,489,412],{"class":227},[218,491,492],{"class":223},"or",[218,494,495],{"class":227}," QgsSymbol.defaultSymbol(geometry_type)\n",[218,497,499,501,504,507],{"class":114,"line":498},20,[218,500,383],{"class":297},[218,502,503],{"class":227},".recent.setColor(QColor(",[218,505,506],{"class":304},"\"#b45309\"",[218,508,509],{"class":227},"))\n",[218,511,513,515,517,519,521,523,525],{"class":114,"line":512},21,[218,514,383],{"class":297},[218,516,425],{"class":227},[218,518,325],{"class":223},[218,520,487],{"class":297},[218,522,425],{"class":227},[218,524,492],{"class":223},[218,526,495],{"class":227},[218,528,530,532,535,538],{"class":114,"line":529},22,[218,531,383],{"class":297},[218,533,534],{"class":227},".old.setColor(QColor(",[218,536,537],{"class":304},"\"#c9c2ac\"",[218,539,509],{"class":227},[218,541,543],{"class":114,"line":542},23,[218,544,273],{"emptyLinePlaceholder":272},[218,546,548,550,553],{"class":114,"line":547},24,[218,549,316],{"class":223},[218,551,552],{"class":282}," startRender",[218,554,555],{"class":227},"(self, context, fields):\n",[218,557,559,561],{"class":114,"line":558},25,[218,560,358],{"class":297},[218,562,563],{"class":227},"().startRender(context, fields)\n",[218,565,567,569,571,573,576,579,582,586,588,590],{"class":114,"line":566},26,[218,568,383],{"class":297},[218,570,438],{"class":227},[218,572,325],{"class":223},[218,574,575],{"class":227}," datetime.now() ",[218,577,578],{"class":223},"-",[218,580,581],{"class":227}," timedelta(",[218,583,585],{"class":584},"s9osk","days",[218,587,325],{"class":223},[218,589,369],{"class":297},[218,591,592],{"class":227},".days)\n",[218,594,596,598,600,602,605,607],{"class":114,"line":595},27,[218,597,383],{"class":297},[218,599,451],{"class":227},[218,601,325],{"class":223},[218,603,604],{"class":227}," fields.indexOf(",[218,606,369],{"class":297},[218,608,609],{"class":227},".field)\n",[218,611,613,615],{"class":114,"line":612},28,[218,614,383],{"class":297},[218,616,617],{"class":227},".recent.startRender(context, fields)\n",[218,619,621,623],{"class":114,"line":620},29,[218,622,383],{"class":297},[218,624,625],{"class":227},".old.startRender(context, fields)\n",[218,627,629],{"class":114,"line":628},30,[218,630,273],{"emptyLinePlaceholder":272},[218,632,634,636,639],{"class":114,"line":633},31,[218,635,316],{"class":223},[218,637,638],{"class":282}," stopRender",[218,640,641],{"class":227},"(self, context):\n",[218,643,645,647],{"class":114,"line":644},32,[218,646,383],{"class":297},[218,648,649],{"class":227},".recent.stopRender(context)\n",[218,651,653,655],{"class":114,"line":652},33,[218,654,383],{"class":297},[218,656,657],{"class":227},".old.stopRender(context)\n",[218,659,661,663],{"class":114,"line":660},34,[218,662,358],{"class":297},[218,664,665],{"class":227},"().stopRender(context)\n",[218,667,669],{"class":114,"line":668},35,[218,670,273],{"emptyLinePlaceholder":272},[218,672,674,676,679],{"class":114,"line":673},36,[218,675,316],{"class":223},[218,677,678],{"class":282}," usedAttributes",[218,680,641],{"class":227},[218,682,684,687,690,692],{"class":114,"line":683},37,[218,685,686],{"class":223},"        return",[218,688,689],{"class":227}," {",[218,691,369],{"class":297},[218,693,694],{"class":227},".field}\n",[218,696,698],{"class":114,"line":697},38,[218,699,273],{"emptyLinePlaceholder":272},[218,701,703,705,708],{"class":114,"line":702},39,[218,704,316],{"class":223},[218,706,707],{"class":282}," symbolForFeature",[218,709,710],{"class":227},"(self, feature, context):\n",[218,712,714,717,719,722,724,727,730,732,734,737,740,743],{"class":114,"line":713},40,[218,715,716],{"class":227},"        value ",[218,718,325],{"class":223},[218,720,721],{"class":227}," feature.attribute(",[218,723,369],{"class":297},[218,725,726],{"class":227},"._idx) ",[218,728,729],{"class":223},"if",[218,731,487],{"class":297},[218,733,451],{"class":227},[218,735,736],{"class":223},">=",[218,738,739],{"class":297}," 0",[218,741,742],{"class":223}," else",[218,744,443],{"class":297},[218,746,748,751,754,757,760,763,766,769,772,775,778,781],{"class":114,"line":747},41,[218,749,750],{"class":223},"        if",[218,752,753],{"class":227}," value ",[218,755,756],{"class":223},"is",[218,758,759],{"class":297}," None",[218,761,762],{"class":223}," or",[218,764,765],{"class":227}," (",[218,767,768],{"class":297},"hasattr",[218,770,771],{"class":227},"(value, ",[218,773,774],{"class":304},"\"isNull\"",[218,776,777],{"class":227},") ",[218,779,780],{"class":223},"and",[218,782,783],{"class":227}," value.isNull()):\n",[218,785,787,790],{"class":114,"line":786},42,[218,788,789],{"class":223},"            return",[218,791,443],{"class":297},[218,793,795,798,800,803,805,808,810,813,815,818],{"class":114,"line":794},43,[218,796,797],{"class":227},"        when ",[218,799,325],{"class":223},[218,801,802],{"class":227}," value.toPyDateTime() ",[218,804,729],{"class":223},[218,806,807],{"class":297}," hasattr",[218,809,771],{"class":227},[218,811,812],{"class":304},"\"toPyDateTime\"",[218,814,777],{"class":227},[218,816,817],{"class":223},"else",[218,819,820],{"class":227}," value\n",[218,822,824,826,828,830,832,835,837,839,841,843,845],{"class":114,"line":823},44,[218,825,686],{"class":223},[218,827,487],{"class":297},[218,829,412],{"class":227},[218,831,729],{"class":223},[218,833,834],{"class":227}," when ",[218,836,736],{"class":223},[218,838,487],{"class":297},[218,840,438],{"class":227},[218,842,817],{"class":223},[218,844,487],{"class":297},[218,846,847],{"class":227},".old\n",[218,849,851],{"class":114,"line":850},45,[218,852,273],{"emptyLinePlaceholder":272},[218,854,856,858,861],{"class":114,"line":855},46,[218,857,316],{"class":223},[218,859,860],{"class":282}," symbols",[218,862,641],{"class":227},[218,864,866,868,871,873,876,878],{"class":114,"line":865},47,[218,867,686],{"class":223},[218,869,870],{"class":227}," [",[218,872,369],{"class":297},[218,874,875],{"class":227},".recent, ",[218,877,369],{"class":297},[218,879,880],{"class":227},".old]\n",[218,882,884],{"class":114,"line":883},48,[218,885,273],{"emptyLinePlaceholder":272},[218,887,889,891,894],{"class":114,"line":888},49,[218,890,316],{"class":223},[218,892,893],{"class":282}," clone",[218,895,896],{"class":227},"(self):\n",[218,898,900,903,905,908,910,913,915,918,920,923,925],{"class":114,"line":899},50,[218,901,902],{"class":227},"        r ",[218,904,325],{"class":223},[218,906,907],{"class":227}," RecentEditsRenderer(",[218,909,369],{"class":297},[218,911,912],{"class":227},".days, ",[218,914,369],{"class":297},[218,916,917],{"class":227},".field, ",[218,919,369],{"class":297},[218,921,922],{"class":227},".recent.clone(), ",[218,924,369],{"class":297},[218,926,927],{"class":227},".old.clone())\n",[218,929,931,933],{"class":114,"line":930},51,[218,932,383],{"class":297},[218,934,935],{"class":227},".copyRendererData(r)\n",[218,937,939,941],{"class":114,"line":938},52,[218,940,686],{"class":223},[218,942,943],{"class":227}," r\n",[14,945,946,950,951,954,955,958,959,961,962,965,966,969,970,973],{},[947,948,949],"strong",{},"Breakdown:"," The constructor passes the type name to the base class; it must match the registered name. ",[27,952,953],{},"startRender"," resolves the field index and cutoff once, and starts the symbols — a renderer is responsible for starting and stopping the symbols it hands out. ",[27,956,957],{},"usedAttributes"," tells the provider which fields to fetch, so the renderer works even when the layer's request is trimmed. Returning ",[27,960,343],{}," from ",[27,963,964],{},"symbolForFeature"," skips the feature. ",[27,967,968],{},"copyRendererData"," in ",[27,971,972],{},"clone"," carries over properties common to all renderers, such as paint effects and ordering.",[179,975,977],{"id":976},"make-it-survive-saving","Make it survive saving",[14,979,980,981,984,985,988],{},"A renderer that cannot be written to XML disappears when the project is saved and reopened. ",[27,982,983],{},"save"," writes the renderer's state into a DOM element; a static ",[27,986,987],{},"create"," rebuilds it.",[14,990,991],{},[34,992,995,998,1001,1004,1011,1014,1018,1023,1027,1031,1036,1039,1043,1047,1051,1054,1058,1061],{"viewBox":993,"role":37,"ariaLabel":994,"xmlns":39},"0 0 760 206","A renderer writing its type, attributes and symbols to XML on save, and the metadata recreating it from that element on load",[41,996,997],{},"Saving and restoring",[45,999,1000],{},"On save, the renderer writes a renderer-v2 element with its type, its own attributes such as days and field, and its symbols using QgsSymbolLayerUtils.saveSymbols. On load, the registry finds the type, calls the metadata's createRenderer with the element, and the static create method reads the attributes and loads the symbols back.",[49,1002],{"x":51,"y":51,"width":52,"height":1003,"fill":54},"206",[56,1005,1006],{},[59,1007,1009],{"id":1008,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"frSaveArrow",[68,1010],{"d":70,"fill":71},[73,1012,1013],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"XML out, XML in",[49,1015],{"x":83,"y":1016,"width":124,"height":1017,"rx":63,"fill":87,"stroke":88,"style":89},"60","130",[73,1019,1022],{"x":1020,"y":1021,"style":94,"fill":88,"textAnchor":79},"134","102.78","save()",[73,1024,1026],{"x":1020,"y":1025,"style":99,"fill":71,"textAnchor":79},"128.78","type, days, field",[73,1028,1030],{"x":1020,"y":1029,"style":99,"fill":71,"textAnchor":79},"154.78","symbols element",[114,1032],{"x1":1033,"y1":1034,"x2":118,"y2":1034,"stroke":71,"style":1035},"244","125","stroke-width:1.8;marker-end:url(#frSaveArrow)",[49,1037],{"x":122,"y":1016,"width":1038,"height":1017,"rx":63,"fill":172,"stroke":100,"style":89},"200",[73,1040,1042],{"x":170,"y":1041,"style":94,"fill":78,"textAnchor":79},"115.78","renderer-v2",[73,1044,1046],{"x":170,"y":1045,"style":99,"fill":100,"textAnchor":79},"141.78","in .qgs \u002F .qml",[114,1048],{"x1":1049,"y1":1034,"x2":1050,"y2":1034,"stroke":71,"style":1035},"500","552",[49,1052],{"x":1053,"y":1016,"width":168,"height":1017,"rx":63,"fill":126,"stroke":127,"style":89},"556",[73,1055,1057],{"x":1056,"y":1021,"style":94,"fill":127,"textAnchor":79},"646","create()",[73,1059,1060],{"x":1056,"y":1025,"style":99,"fill":100,"textAnchor":79},"read attributes",[73,1062,1063],{"x":1056,"y":1029,"style":99,"fill":100,"textAnchor":79},"load symbols",[209,1065,1067],{"className":211,"code":1066,"language":213,"meta":214,"style":214},"from qgis.core import QgsSymbolLayerUtils, QgsReadWriteContext\nfrom qgis.PyQt.QtXml import QDomDocument\n\n    def save(self, doc, context):\n        elem = doc.createElement(\"renderer-v2\")\n        elem.setAttribute(\"type\", self.TYPE)\n        elem.setAttribute(\"days\", str(self.days))\n        elem.setAttribute(\"field\", self.field)\n        symbols = QgsSymbolLayerUtils.saveSymbols({\"recent\": self.recent, \"old\": self.old},\n                                                  \"symbols\", doc, context)\n        elem.appendChild(symbols)\n        self.saveRendererData(doc, elem, context)\n        return elem\n\n    @staticmethod\n    def create(element, context):\n        symbols = QgsSymbolLayerUtils.loadSymbols(element.firstChildElement(\"symbols\"), context)\n        r = RecentEditsRenderer(int(element.attribute(\"days\", \"7\")),\n                                element.attribute(\"field\", \"edited_on\"),\n                                symbols.get(\"recent\"), symbols.get(\"old\"))\n        return r\n",[27,1068,1069,1080,1092,1096,1106,1121,1140,1159,1172,1202,1210,1215,1222,1229,1233,1241,1251,1266,1290,1304,1318],{"__ignoreMap":214},[218,1070,1071,1073,1075,1077],{"class":114,"line":220},[218,1072,224],{"class":223},[218,1074,242],{"class":227},[218,1076,231],{"class":223},[218,1078,1079],{"class":227}," QgsSymbolLayerUtils, QgsReadWriteContext\n",[218,1081,1082,1084,1087,1089],{"class":114,"line":237},[218,1083,224],{"class":223},[218,1085,1086],{"class":227}," qgis.PyQt.QtXml ",[218,1088,231],{"class":223},[218,1090,1091],{"class":227}," QDomDocument\n",[218,1093,1094],{"class":114,"line":250},[218,1095,273],{"emptyLinePlaceholder":272},[218,1097,1098,1100,1103],{"class":114,"line":256},[218,1099,316],{"class":223},[218,1101,1102],{"class":282}," save",[218,1104,1105],{"class":227},"(self, doc, context):\n",[218,1107,1108,1111,1113,1116,1119],{"class":114,"line":269},[218,1109,1110],{"class":227},"        elem ",[218,1112,325],{"class":223},[218,1114,1115],{"class":227}," doc.createElement(",[218,1117,1118],{"class":304},"\"renderer-v2\"",[218,1120,377],{"class":227},[218,1122,1123,1126,1129,1132,1134,1136,1138],{"class":114,"line":276},[218,1124,1125],{"class":227},"        elem.setAttribute(",[218,1127,1128],{"class":304},"\"type\"",[218,1130,1131],{"class":227},", ",[218,1133,369],{"class":297},[218,1135,200],{"class":227},[218,1137,374],{"class":297},[218,1139,377],{"class":227},[218,1141,1142,1144,1147,1149,1152,1154,1156],{"class":114,"line":294},[218,1143,1125],{"class":227},[218,1145,1146],{"class":304},"\"days\"",[218,1148,1131],{"class":227},[218,1150,1151],{"class":297},"str",[218,1153,286],{"class":227},[218,1155,369],{"class":297},[218,1157,1158],{"class":227},".days))\n",[218,1160,1161,1163,1166,1168,1170],{"class":114,"line":308},[218,1162,1125],{"class":227},[218,1164,1165],{"class":304},"\"field\"",[218,1167,1131],{"class":227},[218,1169,369],{"class":297},[218,1171,609],{"class":227},[218,1173,1174,1177,1179,1182,1185,1188,1190,1192,1195,1197,1199],{"class":114,"line":313},[218,1175,1176],{"class":227},"        symbols ",[218,1178,325],{"class":223},[218,1180,1181],{"class":227}," QgsSymbolLayerUtils.saveSymbols({",[218,1183,1184],{"class":304},"\"recent\"",[218,1186,1187],{"class":227},": ",[218,1189,369],{"class":297},[218,1191,875],{"class":227},[218,1193,1194],{"class":304},"\"old\"",[218,1196,1187],{"class":227},[218,1198,369],{"class":297},[218,1200,1201],{"class":227},".old},\n",[218,1203,1204,1207],{"class":114,"line":355},[218,1205,1206],{"class":304},"                                                  \"symbols\"",[218,1208,1209],{"class":227},", doc, context)\n",[218,1211,1212],{"class":114,"line":380},[218,1213,1214],{"class":227},"        elem.appendChild(symbols)\n",[218,1216,1217,1219],{"class":114,"line":394},[218,1218,383],{"class":297},[218,1220,1221],{"class":227},".saveRendererData(doc, elem, context)\n",[218,1223,1224,1226],{"class":114,"line":407},[218,1225,686],{"class":223},[218,1227,1228],{"class":227}," elem\n",[218,1230,1231],{"class":114,"line":420},[218,1232,273],{"emptyLinePlaceholder":272},[218,1234,1235,1238],{"class":114,"line":433},[218,1236,1237],{"class":282},"    @",[218,1239,1240],{"class":297},"staticmethod\n",[218,1242,1243,1245,1248],{"class":114,"line":446},[218,1244,316],{"class":223},[218,1246,1247],{"class":282}," create",[218,1249,1250],{"class":227},"(element, context):\n",[218,1252,1253,1255,1257,1260,1263],{"class":114,"line":462},[218,1254,1176],{"class":227},[218,1256,325],{"class":223},[218,1258,1259],{"class":227}," QgsSymbolLayerUtils.loadSymbols(element.firstChildElement(",[218,1261,1262],{"class":304},"\"symbols\"",[218,1264,1265],{"class":227},"), context)\n",[218,1267,1268,1270,1272,1274,1277,1280,1282,1284,1287],{"class":114,"line":467},[218,1269,902],{"class":227},[218,1271,325],{"class":223},[218,1273,907],{"class":227},[218,1275,1276],{"class":297},"int",[218,1278,1279],{"class":227},"(element.attribute(",[218,1281,1146],{"class":304},[218,1283,1131],{"class":227},[218,1285,1286],{"class":304},"\"7\"",[218,1288,1289],{"class":227},")),\n",[218,1291,1292,1295,1297,1299,1301],{"class":114,"line":478},[218,1293,1294],{"class":227},"                                element.attribute(",[218,1296,1165],{"class":304},[218,1298,1131],{"class":227},[218,1300,335],{"class":304},[218,1302,1303],{"class":227},"),\n",[218,1305,1306,1309,1311,1314,1316],{"class":114,"line":498},[218,1307,1308],{"class":227},"                                symbols.get(",[218,1310,1184],{"class":304},[218,1312,1313],{"class":227},"), symbols.get(",[218,1315,1194],{"class":304},[218,1317,509],{"class":227},[218,1319,1320,1322],{"class":114,"line":512},[218,1321,686],{"class":223},[218,1323,943],{"class":227},[14,1325,1326,1328,1329,1331,1332,1335,1336,1339,1340,1343,1344,1347,1348,1350],{},[947,1327,949],{}," These two methods belong inside the class above. The element name must be the one QGIS uses for renderers — ",[27,1330,1042],{}," on all current releases — and the ",[27,1333,1334],{},"type"," attribute is how the registry finds your metadata on load. ",[27,1337,1338],{},"saveSymbols"," and ",[27,1341,1342],{},"loadSymbols"," write and read the symbols in QGIS's standard format, so they keep every symbol layer, colour and data-defined property. ",[27,1345,1346],{},"saveRendererData"," stores the common renderer properties that ",[27,1349,968],{}," copies. Without this pair the renderer works in the session but the project reopens with a default single-symbol renderer.",[179,1352,1354],{"id":1353},"register-the-renderer-type","Register the renderer type",[14,1356,1357],{},"As with symbol layers, a metadata object tells the renderer registry the type name, a display name and how to create instances from XML.",[209,1359,1361],{"className":211,"code":1360,"language":213,"meta":214,"style":214},"from qgis.core import QgsApplication, QgsRendererAbstractMetadata\n\nclass RecentEditsMetadata(QgsRendererAbstractMetadata):\n    def __init__(self):\n        super().__init__(RecentEditsRenderer.TYPE, \"Recent edits (Python)\")\n\n    def createRenderer(self, element, context):\n        return RecentEditsRenderer.create(element, context)\n\nclass RecentEditsPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.meta = None\n\n    def initGui(self):\n        self.meta = RecentEditsMetadata()\n        QgsApplication.rendererRegistry().addRenderer(self.meta)\n\n    def unload(self):\n        QgsApplication.rendererRegistry().removeRenderer(RecentEditsRenderer.TYPE)\n",[27,1362,1363,1374,1378,1392,1400,1420,1424,1434,1441,1445,1455,1464,1476,1487,1491,1500,1511,1521,1525,1534],{"__ignoreMap":214},[218,1364,1365,1367,1369,1371],{"class":114,"line":220},[218,1366,224],{"class":223},[218,1368,242],{"class":227},[218,1370,231],{"class":223},[218,1372,1373],{"class":227}," QgsApplication, QgsRendererAbstractMetadata\n",[218,1375,1376],{"class":114,"line":237},[218,1377,273],{"emptyLinePlaceholder":272},[218,1379,1380,1382,1385,1387,1390],{"class":114,"line":250},[218,1381,279],{"class":223},[218,1383,1384],{"class":282}," RecentEditsMetadata",[218,1386,286],{"class":227},[218,1388,1389],{"class":282},"QgsRendererAbstractMetadata",[218,1391,291],{"class":227},[218,1393,1394,1396,1398],{"class":114,"line":256},[218,1395,316],{"class":223},[218,1397,319],{"class":297},[218,1399,896],{"class":227},[218,1401,1402,1404,1406,1408,1411,1413,1415,1418],{"class":114,"line":269},[218,1403,358],{"class":297},[218,1405,361],{"class":227},[218,1407,364],{"class":297},[218,1409,1410],{"class":227},"(RecentEditsRenderer.",[218,1412,374],{"class":297},[218,1414,1131],{"class":227},[218,1416,1417],{"class":304},"\"Recent edits (Python)\"",[218,1419,377],{"class":227},[218,1421,1422],{"class":114,"line":276},[218,1423,273],{"emptyLinePlaceholder":272},[218,1425,1426,1428,1431],{"class":114,"line":294},[218,1427,316],{"class":223},[218,1429,1430],{"class":282}," createRenderer",[218,1432,1433],{"class":227},"(self, element, context):\n",[218,1435,1436,1438],{"class":114,"line":308},[218,1437,686],{"class":223},[218,1439,1440],{"class":227}," RecentEditsRenderer.create(element, context)\n",[218,1442,1443],{"class":114,"line":313},[218,1444,273],{"emptyLinePlaceholder":272},[218,1446,1447,1449,1452],{"class":114,"line":355},[218,1448,279],{"class":223},[218,1450,1451],{"class":282}," RecentEditsPlugin",[218,1453,1454],{"class":227},":\n",[218,1456,1457,1459,1461],{"class":114,"line":380},[218,1458,316],{"class":223},[218,1460,319],{"class":297},[218,1462,1463],{"class":227},"(self, iface):\n",[218,1465,1466,1468,1471,1473],{"class":114,"line":394},[218,1467,383],{"class":297},[218,1469,1470],{"class":227},".iface ",[218,1472,325],{"class":223},[218,1474,1475],{"class":227}," iface\n",[218,1477,1478,1480,1483,1485],{"class":114,"line":407},[218,1479,383],{"class":297},[218,1481,1482],{"class":227},".meta ",[218,1484,325],{"class":223},[218,1486,443],{"class":297},[218,1488,1489],{"class":114,"line":420},[218,1490,273],{"emptyLinePlaceholder":272},[218,1492,1493,1495,1498],{"class":114,"line":433},[218,1494,316],{"class":223},[218,1496,1497],{"class":282}," initGui",[218,1499,896],{"class":227},[218,1501,1502,1504,1506,1508],{"class":114,"line":446},[218,1503,383],{"class":297},[218,1505,1482],{"class":227},[218,1507,325],{"class":223},[218,1509,1510],{"class":227}," RecentEditsMetadata()\n",[218,1512,1513,1516,1518],{"class":114,"line":462},[218,1514,1515],{"class":227},"        QgsApplication.rendererRegistry().addRenderer(",[218,1517,369],{"class":297},[218,1519,1520],{"class":227},".meta)\n",[218,1522,1523],{"class":114,"line":467},[218,1524,273],{"emptyLinePlaceholder":272},[218,1526,1527,1529,1532],{"class":114,"line":478},[218,1528,316],{"class":223},[218,1530,1531],{"class":282}," unload",[218,1533,896],{"class":227},[218,1535,1536,1539,1541],{"class":114,"line":498},[218,1537,1538],{"class":227},"        QgsApplication.rendererRegistry().removeRenderer(RecentEditsRenderer.",[218,1540,374],{"class":297},[218,1542,377],{"class":227},[14,1544,1545,1547,1548,1551],{},[947,1546,949],{}," After registration, the renderer type is known to QGIS and projects using it load correctly as long as the plugin is enabled before they open. Keeping the metadata on the plugin instance prevents garbage collection. Without a ",[27,1549,1550],{},"createRendererWidget"," implementation the renderer does not appear as a choice in the Layer Styling panel, but it can be applied from Python and survives saving — for many organisational renderers that is exactly right, since they are applied by a tool rather than chosen by hand.",[179,1553,1555],{"id":1554},"apply-it-to-a-layer","Apply it to a layer",[209,1557,1559],{"className":211,"code":1558,"language":213,"meta":214,"style":214},"from qgis.core import QgsProject\n\nedits = QgsProject.instance().mapLayersByName(\"inspections\")[0]\nrenderer = RecentEditsRenderer(days=14, field=\"edited_on\")\nrenderer._default_symbols(edits.geometryType())\nedits.setRenderer(renderer)\nedits.triggerRepaint()\n",[27,1560,1561,1572,1576,1597,1624,1629,1634],{"__ignoreMap":214},[218,1562,1563,1565,1567,1569],{"class":114,"line":220},[218,1564,224],{"class":223},[218,1566,242],{"class":227},[218,1568,231],{"class":223},[218,1570,1571],{"class":227}," QgsProject\n",[218,1573,1574],{"class":114,"line":237},[218,1575,273],{"emptyLinePlaceholder":272},[218,1577,1578,1581,1583,1586,1589,1592,1594],{"class":114,"line":250},[218,1579,1580],{"class":227},"edits ",[218,1582,325],{"class":223},[218,1584,1585],{"class":227}," QgsProject.instance().mapLayersByName(",[218,1587,1588],{"class":304},"\"inspections\"",[218,1590,1591],{"class":227},")[",[218,1593,51],{"class":297},[218,1595,1596],{"class":227},"]\n",[218,1598,1599,1602,1604,1606,1608,1610,1613,1615,1618,1620,1622],{"class":114,"line":256},[218,1600,1601],{"class":227},"renderer ",[218,1603,325],{"class":223},[218,1605,907],{"class":227},[218,1607,585],{"class":584},[218,1609,325],{"class":223},[218,1611,1612],{"class":297},"14",[218,1614,1131],{"class":227},[218,1616,1617],{"class":584},"field",[218,1619,325],{"class":223},[218,1621,335],{"class":304},[218,1623,377],{"class":227},[218,1625,1626],{"class":114,"line":269},[218,1627,1628],{"class":227},"renderer._default_symbols(edits.geometryType())\n",[218,1630,1631],{"class":114,"line":276},[218,1632,1633],{"class":227},"edits.setRenderer(renderer)\n",[218,1635,1636],{"class":114,"line":294},[218,1637,1638],{"class":227},"edits.triggerRepaint()\n",[14,1640,1641,1643,1644,1646,1647,1650],{},[947,1642,949],{}," Default symbols are created for the layer's geometry type, so the same renderer works on points, lines and polygons. Setting the renderer transfers ownership to the layer. From this point the layer behaves normally: it draws in the canvas, in layouts and in exports, and the legend shows the two symbols returned by ",[27,1645,154],{}," — add ",[27,1648,1649],{},"legendSymbolItems"," to give them readable labels such as \"edited in last 14 days\".",[179,1652,1654],{"id":1653},"legend-entries","Legend entries",[14,1656,1657,1658,1660],{},"The default legend shows unlabelled symbols. Overriding ",[27,1659,1649],{}," gives each symbol a label and lets users toggle them like categories.",[14,1662,1663],{},[34,1664,1667,1670,1673,1676,1683,1687,1691,1696,1700,1706,1712,1718,1722,1726],{"viewBox":1665,"role":37,"ariaLabel":1666,"xmlns":39},"0 0 501 220","A renderer returning labelled legend symbol items that appear in the Layers panel and layout legends",[41,1668,1669],{},"Legend items from a renderer",[45,1671,1672],{},"legendSymbolItems returns a list of QgsLegendSymbolItem objects, each with a symbol, a label and a rule key. The Layers panel and layout legends display them. A stable rule key per item lets the legend remember which items are checked.",[49,1674],{"x":51,"y":51,"width":1675,"height":124,"fill":54},"501",[56,1677,1678],{},[59,1679,1681],{"id":1680,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"frLegendArrow",[68,1682],{"d":70,"fill":71},[73,1684,1686],{"x":1685,"y":76,"style":77,"fill":78,"textAnchor":79},"250.5","Labelled legend entries",[49,1688],{"x":83,"y":1016,"width":1689,"height":1690,"rx":63,"fill":172,"stroke":100,"style":89},"260","110",[73,1692,1695],{"x":1693,"y":1694,"style":94,"fill":78,"textAnchor":79},"154","105.78","legendSymbolItems()",[73,1697,1699],{"x":1693,"y":1698,"style":99,"fill":100,"textAnchor":79},"131.78","symbol · label · key",[114,1701],{"x1":1702,"y1":1703,"x2":1704,"y2":1703,"stroke":71,"style":1705},"284","115","326","stroke-width:1.8;marker-end:url(#frLegendArrow)",[49,1707],{"x":1708,"y":1709,"width":1710,"height":1612,"rx":51,"fill":150,"stroke":150,"style":1711},"340","80","18","stroke-width:1.5",[73,1713,1717],{"x":1714,"y":123,"style":1715,"fill":71,"textAnchor":1716},"370","text-anchor:start;font-size:10.5px;font-family:sans-serif","start","edited in last 14 days",[49,1719],{"x":1708,"y":1720,"width":1710,"height":1612,"rx":51,"fill":1721,"stroke":100,"style":1711},"120","#c9c2ac",[73,1723,1725],{"x":1714,"y":1724,"style":1715,"fill":71,"textAnchor":1716},"132","older",[73,1727,1729],{"x":122,"y":1728,"style":99,"fill":100,"textAnchor":79},"196","stable keys let the legend remember check states",[209,1731,1733],{"className":211,"code":1732,"language":213,"meta":214,"style":214},"from qgis.core import QgsLegendSymbolItem\n\n    def legendSymbolItems(self):\n        return [QgsLegendSymbolItem(self.recent, f\"edited in last {self.days} days\", \"recent\"),\n                QgsLegendSymbolItem(self.old, \"older\", \"old\")]\n",[27,1734,1735,1746,1750,1759,1794],{"__ignoreMap":214},[218,1736,1737,1739,1741,1743],{"class":114,"line":220},[218,1738,224],{"class":223},[218,1740,242],{"class":227},[218,1742,231],{"class":223},[218,1744,1745],{"class":227}," QgsLegendSymbolItem\n",[218,1747,1748],{"class":114,"line":237},[218,1749,273],{"emptyLinePlaceholder":272},[218,1751,1752,1754,1757],{"class":114,"line":250},[218,1753,316],{"class":223},[218,1755,1756],{"class":282}," legendSymbolItems",[218,1758,896],{"class":227},[218,1760,1761,1763,1766,1768,1770,1773,1776,1779,1782,1785,1788,1790,1792],{"class":114,"line":256},[218,1762,686],{"class":223},[218,1764,1765],{"class":227}," [QgsLegendSymbolItem(",[218,1767,369],{"class":297},[218,1769,875],{"class":227},[218,1771,1772],{"class":223},"f",[218,1774,1775],{"class":304},"\"edited in last ",[218,1777,1778],{"class":297},"{self",[218,1780,1781],{"class":227},".days",[218,1783,1784],{"class":297},"}",[218,1786,1787],{"class":304}," days\"",[218,1789,1131],{"class":227},[218,1791,1184],{"class":304},[218,1793,1303],{"class":227},[218,1795,1796,1799,1801,1804,1807,1809,1811],{"class":114,"line":269},[218,1797,1798],{"class":227},"                QgsLegendSymbolItem(",[218,1800,369],{"class":297},[218,1802,1803],{"class":227},".old, ",[218,1805,1806],{"class":304},"\"older\"",[218,1808,1131],{"class":227},[218,1810,1194],{"class":304},[218,1812,1813],{"class":227},")]\n",[14,1815,1816,1818,1819,1131,1822,1339,1825,1828,1829,200],{},[947,1817,949],{}," Each item carries a symbol, a label and a rule key; the key must be stable across sessions so the legend can remember which entries a user unchecked. To honour those check states while rendering, also implement ",[27,1820,1821],{},"legendSymbolItemsCheckable",[27,1823,1824],{},"legendSymbolItemChecked",[27,1826,1827],{},"checkLegendSymbolItem",", and skip symbols whose item is unchecked in ",[27,1830,964],{},[179,1832,1834],{"id":1833},"test-the-renderer-headless","Test the renderer headless",[14,1836,1837],{},"Renderers are easy to break in ways that only show on screen. A small test that renders the layer to an image with the custom renderer, and checks the symbol choice directly, catches regressions before users see them.",[209,1839,1841],{"className":211,"code":1840,"language":213,"meta":214,"style":214},"from qgis.core import (QgsMapSettings, QgsMapRendererSequentialJob, QgsRenderContext,\n                       QgsFeature, QgsFields, QgsField)\nfrom qgis.PyQt.QtCore import QSize, QVariant, QDateTime\n\ndef test_symbol_choice():\n    fields = QgsFields()\n    fields.append(QgsField(\"edited_on\", QVariant.DateTime))\n    r = RecentEditsRenderer(days=7)\n    r._default_symbols(QgsWkbTypes.PointGeometry)\n    ctx = QgsRenderContext()\n    r.startRender(ctx, fields)\n    f = QgsFeature(fields)\n    f[\"edited_on\"] = QDateTime.currentDateTime()\n    assert r.symbolForFeature(f, ctx) is r.recent\n    f[\"edited_on\"] = QDateTime.currentDateTime().addDays(-30)\n    assert r.symbolForFeature(f, ctx) is r.old\n    r.stopRender(ctx)\n\nsettings = QgsMapSettings()\nsettings.setLayers([edits])\nsettings.setExtent(edits.extent())\nsettings.setOutputSize(QSize(600, 400))\njob = QgsMapRendererSequentialJob(settings)\njob.start(); job.waitForFinished()\njob.renderedImage().save(\"\u002Ftmp\u002Frecent_edits_check.png\")\n",[27,1842,1843,1854,1859,1871,1875,1886,1896,1906,1923,1928,1938,1943,1953,1968,1981,2001,2012,2017,2021,2031,2036,2041,2055,2065,2070],{"__ignoreMap":214},[218,1844,1845,1847,1849,1851],{"class":114,"line":220},[218,1846,224],{"class":223},[218,1848,242],{"class":227},[218,1850,231],{"class":223},[218,1852,1853],{"class":227}," (QgsMapSettings, QgsMapRendererSequentialJob, QgsRenderContext,\n",[218,1855,1856],{"class":114,"line":237},[218,1857,1858],{"class":227},"                       QgsFeature, QgsFields, QgsField)\n",[218,1860,1861,1863,1866,1868],{"class":114,"line":250},[218,1862,224],{"class":223},[218,1864,1865],{"class":227}," qgis.PyQt.QtCore ",[218,1867,231],{"class":223},[218,1869,1870],{"class":227}," QSize, QVariant, QDateTime\n",[218,1872,1873],{"class":114,"line":256},[218,1874,273],{"emptyLinePlaceholder":272},[218,1876,1877,1880,1883],{"class":114,"line":269},[218,1878,1879],{"class":223},"def",[218,1881,1882],{"class":282}," test_symbol_choice",[218,1884,1885],{"class":227},"():\n",[218,1887,1888,1891,1893],{"class":114,"line":276},[218,1889,1890],{"class":227},"    fields ",[218,1892,325],{"class":223},[218,1894,1895],{"class":227}," QgsFields()\n",[218,1897,1898,1901,1903],{"class":114,"line":294},[218,1899,1900],{"class":227},"    fields.append(QgsField(",[218,1902,335],{"class":304},[218,1904,1905],{"class":227},", QVariant.DateTime))\n",[218,1907,1908,1911,1913,1915,1917,1919,1921],{"class":114,"line":308},[218,1909,1910],{"class":227},"    r ",[218,1912,325],{"class":223},[218,1914,907],{"class":227},[218,1916,585],{"class":584},[218,1918,325],{"class":223},[218,1920,65],{"class":297},[218,1922,377],{"class":227},[218,1924,1925],{"class":114,"line":313},[218,1926,1927],{"class":227},"    r._default_symbols(QgsWkbTypes.PointGeometry)\n",[218,1929,1930,1933,1935],{"class":114,"line":355},[218,1931,1932],{"class":227},"    ctx ",[218,1934,325],{"class":223},[218,1936,1937],{"class":227}," QgsRenderContext()\n",[218,1939,1940],{"class":114,"line":380},[218,1941,1942],{"class":227},"    r.startRender(ctx, fields)\n",[218,1944,1945,1948,1950],{"class":114,"line":394},[218,1946,1947],{"class":227},"    f ",[218,1949,325],{"class":223},[218,1951,1952],{"class":227}," QgsFeature(fields)\n",[218,1954,1955,1958,1960,1963,1965],{"class":114,"line":407},[218,1956,1957],{"class":227},"    f[",[218,1959,335],{"class":304},[218,1961,1962],{"class":227},"] ",[218,1964,325],{"class":223},[218,1966,1967],{"class":227}," QDateTime.currentDateTime()\n",[218,1969,1970,1973,1976,1978],{"class":114,"line":420},[218,1971,1972],{"class":223},"    assert",[218,1974,1975],{"class":227}," r.symbolForFeature(f, ctx) ",[218,1977,756],{"class":223},[218,1979,1980],{"class":227}," r.recent\n",[218,1982,1983,1985,1987,1989,1991,1994,1996,1999],{"class":114,"line":433},[218,1984,1957],{"class":227},[218,1986,335],{"class":304},[218,1988,1962],{"class":227},[218,1990,325],{"class":223},[218,1992,1993],{"class":227}," QDateTime.currentDateTime().addDays(",[218,1995,578],{"class":223},[218,1997,1998],{"class":297},"30",[218,2000,377],{"class":227},[218,2002,2003,2005,2007,2009],{"class":114,"line":446},[218,2004,1972],{"class":223},[218,2006,1975],{"class":227},[218,2008,756],{"class":223},[218,2010,2011],{"class":227}," r.old\n",[218,2013,2014],{"class":114,"line":462},[218,2015,2016],{"class":227},"    r.stopRender(ctx)\n",[218,2018,2019],{"class":114,"line":467},[218,2020,273],{"emptyLinePlaceholder":272},[218,2022,2023,2026,2028],{"class":114,"line":478},[218,2024,2025],{"class":227},"settings ",[218,2027,325],{"class":223},[218,2029,2030],{"class":227}," QgsMapSettings()\n",[218,2032,2033],{"class":114,"line":498},[218,2034,2035],{"class":227},"settings.setLayers([edits])\n",[218,2037,2038],{"class":114,"line":512},[218,2039,2040],{"class":227},"settings.setExtent(edits.extent())\n",[218,2042,2043,2046,2049,2051,2053],{"class":114,"line":529},[218,2044,2045],{"class":227},"settings.setOutputSize(QSize(",[218,2047,2048],{"class":297},"600",[218,2050,1131],{"class":227},[218,2052,170],{"class":297},[218,2054,509],{"class":227},[218,2056,2057,2060,2062],{"class":114,"line":542},[218,2058,2059],{"class":227},"job ",[218,2061,325],{"class":223},[218,2063,2064],{"class":227}," QgsMapRendererSequentialJob(settings)\n",[218,2066,2067],{"class":114,"line":547},[218,2068,2069],{"class":227},"job.start(); job.waitForFinished()\n",[218,2071,2072,2075,2078],{"class":114,"line":558},[218,2073,2074],{"class":227},"job.renderedImage().save(",[218,2076,2077],{"class":304},"\"\u002Ftmp\u002Frecent_edits_check.png\"",[218,2079,377],{"class":227},[14,2081,2082,2084,2085,1131,2087,1339,2089,2092,2093,2097],{},[947,2083,949],{}," The unit test calls ",[27,2086,953],{},[27,2088,964],{},[27,2090,2091],{},"stopRender"," directly with a synthetic feature, which pins down the renderer's logic without any canvas. The image render exercises the full pipeline — threads, symbol preparation, legend-independent drawing — and a saved PNG can be compared against a reference image in CI, as described in ",[21,2094,2096],{"href":2095},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","unit testing a QGIS plugin with pytest",". Run both after every change to the renderer.",[179,2099,2101],{"id":2100},"when-not-to-write-a-renderer","When not to write a renderer",[14,2103,2104,2105,2108,2109,2113],{},"Most \"custom logic\" fits a rule-based renderer with expressions — including the example here, which could be two rules on ",[27,2106,2107],{},"\"edited_on\" >= now() - '14 days'",". Rule-based renderers need no plugin, are portable to any QGIS, and are configured in the GUI. Choose a Python renderer when the logic genuinely needs Python: external lookups, state across features, or a scheme your organisation wants maintained as code and versioned in a plugin. ",[21,2110,2112],{"href":2111},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","Rule-based renderers"," are the first thing to try.",[179,2115,2117],{"id":2116},"qgis-version-compatibility","QGIS version compatibility",[14,2119,2120,2121,2123,2124,2126,2127,2129,2130,2132,2133,200],{},"Subclassing ",[27,2122,29],{}," and registering with ",[27,2125,1389],{}," work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The rendering engine may call ",[27,2128,964],{}," from worker threads, so the method must be thread-safe: read only state prepared in ",[27,2131,953],{},". On QGIS 4, geometry type enums are ",[27,2134,2135],{},"Qgis.GeometryType",[179,2137,2139],{"id":2138},"troubleshooting","Troubleshooting",[184,2141,2142,2154,2162,2172],{},[187,2143,2144,2147,2148,2150,2151,2153],{},[947,2145,2146],{},"The layer draws nothing."," ",[27,2149,964],{}," returns ",[27,2152,343],{}," for every feature — check the field index and NULL handling.",[187,2155,2156,2159,2160,200],{},[947,2157,2158],{},"Symbols draw incorrectly or crash."," They were not started in ",[27,2161,953],{},[187,2163,2164,2147,2167,1339,2169,2171],{},[947,2165,2166],{},"The project reopens with a default renderer.",[27,2168,983],{},[27,2170,987],{}," are missing, or the plugin loads after the project.",[187,2173,2174,2177,2178,2180],{},[947,2175,2176],{},"Attributes are always NULL."," The field is not in ",[27,2179,957],{},", so the provider did not fetch it.",[179,2182,2184],{"id":2183},"conclusion","Conclusion",[14,2186,2187,2188,2190,2191,2193,2194,2196,2197,2199,2200,1339,2202,2204],{},"Subclass ",[27,2189,29],{}," with a unique type, prepare state and start symbols in ",[27,2192,953],{},", declare fields in ",[27,2195,957],{},", choose symbols in a thread-safe ",[27,2198,964],{},", serialise with ",[27,2201,983],{},[27,2203,987],{},", register metadata from a plugin, label the legend, and prefer rule-based renderers whenever an expression can express the logic.",[179,2206,2208],{"id":2207},"frequently-asked-questions","Frequently Asked Questions",[14,2210,2211,2214,2215,2218],{},[947,2212,2213],{},"Can a custom renderer draw things that are not symbols?","\nIt can override ",[27,2216,2217],{},"renderFeature"," for full control, but drawing through symbols keeps legends, exports and effects working.",[14,2220,2221,2224],{},[947,2222,2223],{},"Does the renderer work in print layouts?","\nYes; layouts use the same rendering engine.",[14,2226,2227,2230],{},[947,2228,2229],{},"Can I wrap an existing renderer and modify its output?","\nYes — hold an embedded renderer and delegate to it, adjusting symbols as needed. QGIS's own inverted polygon and point displacement renderers work this way.",[14,2232,2233,2236],{},[947,2234,2235],{},"Will QGIS Server use it?","\nOnly if the plugin is installed and loaded as a server plugin.",[179,2238,2240],{"id":2239},"related","Related",[184,2242,2243,2248,2254,2259,2265],{},[187,2244,2245,2247],{},[21,2246,24],{"href":23}," — the guide this recipe belongs to",[187,2249,2250],{},[21,2251,2253],{"href":2252},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-symbol-layer-pyqgis\u002F","Write a Custom Symbol Layer in PyQGIS",[187,2255,2256],{},[21,2257,2258],{"href":2111},"Create a Rule-Based Renderer in PyQGIS",[187,2260,2261],{},[21,2262,2264],{"href":2263},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-categorized-renderer-pyqgis\u002F","Create a Categorized Renderer in PyQGIS",[187,2266,2267],{},[21,2268,2270],{"href":2269},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis\u002F","Add a Custom Layer Properties Page in PyQGIS",[2272,2273,2274],"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 pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":214,"searchDepth":237,"depth":237,"links":2276},[2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289],{"id":181,"depth":237,"text":182},{"id":203,"depth":237,"text":204},{"id":976,"depth":237,"text":977},{"id":1353,"depth":237,"text":1354},{"id":1554,"depth":237,"text":1555},{"id":1653,"depth":237,"text":1654},{"id":1833,"depth":237,"text":1834},{"id":2100,"depth":237,"text":2101},{"id":2116,"depth":237,"text":2117},{"id":2138,"depth":237,"text":2139},{"id":2183,"depth":237,"text":2184},{"id":2207,"depth":237,"text":2208},{"id":2239,"depth":237,"text":2240},"Subclass QgsFeatureRenderer to choose symbols by your own logic — symbolForFeature, startRender and usedAttributes, saving and restoring the renderer in projects, registering it from a plugin, and knowing when a rule-based or data-defined renderer is the better tool.","md",{"slug":2293,"type":2294,"breadcrumb":2295,"datePublished":2296,"dateModified":2296},"write-custom-feature-renderer-pyqgis","article","Write a Custom Feature Renderer","2026-10-02","\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-feature-renderer-pyqgis",{"title":5,"description":2290},"qgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-feature-renderer-pyqgis\u002Findex","6funx-eY2uPZkEgZlt7RnDVZm4qexZJzwtclM9HExww",1790966257292]