[{"data":1,"prerenderedAt":1395},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes":3},{"id":4,"title":5,"body":6,"description":1386,"extension":1387,"meta":1388,"navigation":360,"path":1391,"seo":1392,"stem":1393,"__hash__":1394},"docs\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Findex.md","Extending QGIS with Custom Classes",{"type":7,"value":8,"toc":1370},"minimark",[9,13,17,26,156,161,232,236,239,265,278,288,537,546,550,553,587,608,711,718,722,729,748,752,755,761,771,852,861,865,868,944,947,951,954,963,974,1080,1094,1098,1113,1117,1120,1164,1167,1171,1179,1183,1186,1223,1227,1255,1259,1265,1275,1281,1291,1297,1301,1366],[10,11,5],"h1",{"id":12},"extending-qgis-with-custom-classes",[14,15,16],"p",{},"Most plugins add things around QGIS: a toolbar button, a dialog, a dock widget, a Processing algorithm. A smaller but powerful set of plugins extend QGIS from the inside. They add a new kind of symbol to the symbol selector, a new renderer to the styling panel, a new page to the Layer Properties dialog, a new source of layers to the provider registry. Users experience these as part of QGIS itself, not as a plugin — which is exactly the point.",[14,18,19,20,25],{},"This guide belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development",". It is for plugin developers who have built a basic plugin and now need QGIS's own machinery to do something it does not do out of the box. It maps the extension points available to Python, explains the patterns they share — subclass, register, clean up — and the rules about threads and ownership that decide whether an extension is solid or crashes QGIS.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,85,90,95,99,103,107,112,116,119,122,125,133,138,142,146,152],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 330","img","Six extension points grouped into rendering, canvas, interface and data, each following the subclass, register and clean up pattern","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Extension points by area",[40,41,42],"desc",{},"Six extension points grouped by the part of QGIS they extend. Rendering: custom symbol layers and custom feature renderers, registered with the symbol layer and renderer registries. Map canvas: custom canvas items drawn above the map. Interface: layer tree view indicators and layer properties pages registered through iface. Data: Python vector data providers registered with the provider registry. Each follows the pattern subclass, register at plugin load, remove at unload.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","330","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Where Python can plug into QGIS",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","52","222","120","8","#eef7f4","#0f766e","stroke-width:2",[51,70,74],{"x":71,"y":72,"style":73,"fill":67,"textAnchor":57},"135","79.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","rendering",[51,76,80],{"x":71,"y":77,"style":78,"fill":79,"textAnchor":57},"103.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","symbol layers",[51,82,84],{"x":71,"y":83,"style":78,"fill":79,"textAnchor":57},"127.78","feature renderers",[51,86,89],{"x":71,"y":87,"style":78,"fill":88,"textAnchor":57},"151.78","#59645f","registries",[44,91],{"x":92,"y":62,"width":63,"height":64,"rx":65,"fill":93,"stroke":94,"style":68},"269","#eff3ff","#2563eb",[51,96,98],{"x":53,"y":97,"style":73,"fill":94,"textAnchor":57},"91.78","map canvas",[51,100,102],{"x":53,"y":101,"style":78,"fill":79,"textAnchor":57},"115.78","canvas items",[51,104,106],{"x":53,"y":105,"style":78,"fill":88,"textAnchor":57},"139.78","graphics scene",[44,108],{"x":109,"y":62,"width":63,"height":64,"rx":65,"fill":110,"stroke":111,"style":68},"514","#fdf2e2","#b45309",[51,113,115],{"x":114,"y":72,"style":73,"fill":111,"textAnchor":57},"625","interface",[51,117,118],{"x":114,"y":77,"style":78,"fill":79,"textAnchor":57},"layer tree indicators",[51,120,121],{"x":114,"y":83,"style":78,"fill":79,"textAnchor":57},"properties pages",[51,123,124],{"x":114,"y":87,"style":78,"fill":88,"textAnchor":57},"iface",[44,126],{"x":127,"y":128,"width":129,"height":130,"rx":65,"fill":131,"stroke":132,"style":68},"140","190","480","70","#e8efe6","#15803d",[51,134,137],{"x":53,"y":135,"style":73,"fill":136,"textAnchor":57},"206.78","#166534","data",[51,139,141],{"x":53,"y":140,"style":78,"fill":79,"textAnchor":57},"228.78","Python vector data providers",[51,143,145],{"x":53,"y":144,"style":78,"fill":88,"textAnchor":57},"250.78","provider registry",[44,147],{"x":64,"y":148,"width":149,"height":150,"rx":65,"fill":151,"stroke":88,"style":68},"278","520","38","#fffdf7",[51,153,155],{"x":53,"y":154,"style":73,"fill":56,"textAnchor":57},"300.78","subclass → register in initGui → remove in unload",[157,158,160],"h2",{"id":159},"what-this-guide-covers","What this guide covers",[162,163,164,182,192,202,212,222],"ul",{},[165,166,167,171,172,176,177,181],"li",{},[168,169,170],"strong",{},"New symbols"," — ",[21,173,175],{"href":174},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-symbol-layer-pyqgis\u002F","write a custom symbol layer"," drawn with ",[178,179,180],"code",{},"QPainter"," and registered with the symbol layer registry.",[165,183,184,171,187,191],{},[168,185,186],{},"New styling logic",[21,188,190],{"href":189},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-custom-feature-renderer-pyqgis\u002F","write a custom feature renderer"," that chooses symbols by your own rules and survives saving.",[165,193,194,171,197,201],{},[168,195,196],{},"Temporary graphics",[21,198,200],{"href":199},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fdraw-custom-map-canvas-item-pyqgis\u002F","draw a custom map canvas item"," for range rings, live positions and tool feedback.",[165,203,204,171,207,211],{},[168,205,206],{},"Status in the Layers panel",[21,208,210],{"href":209},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-layer-tree-indicator-pyqgis\u002F","add a layer tree indicator"," for stale data, validation failures or sync state.",[165,213,214,171,217,221],{},[168,215,216],{},"New data sources",[21,218,220],{"href":219},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fwrite-python-vector-data-provider-pyqgis\u002F","write a Python vector data provider"," so any source appears as a native layer.",[165,223,224,171,227,231],{},[168,225,226],{},"Per-layer settings",[21,228,230],{"href":229},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis\u002F","add a custom layer properties page"," next to Symbology and Labels.",[157,233,235],{"id":234},"the-shared-pattern-subclass-register-clean-up","The shared pattern: subclass, register, clean up",[14,237,238],{},"Every extension point works the same way at the top level, and getting the pattern right matters more than the details of any one class.",[14,240,241,244,245,248,249,252,253,256,257,260,261,264],{},[168,242,243],{},"Subclass"," a QGIS base class and implement the methods QGIS will call: ",[178,246,247],{},"renderPoint"," for a symbol layer, ",[178,250,251],{},"symbolForFeature"," for a renderer, ",[178,254,255],{},"paint"," for a canvas item, ",[178,258,259],{},"fetchFeature"," for a provider iterator, ",[178,262,263],{},"createWidget"," for a properties page factory. QGIS calls these methods — often many times, sometimes from other threads — so they must do exactly what the contract says and nothing more.",[14,266,267,270,271,273,274,277],{},[168,268,269],{},"Register"," the class with the part of QGIS that needs to know about it: the symbol layer registry, the renderer registry, the provider registry, the layer tree view, or ",[178,272,124],{},". Registration usually goes through a small metadata or factory object that names the type and creates instances. It happens in the plugin's ",[178,275,276],{},"initGui",", every session — QGIS does not remember Python registrations between runs.",[14,279,280,283,284,287],{},[168,281,282],{},"Clean up"," in ",[178,285,286],{},"unload",": remove registered types, unregister factories, remove indicators and canvas items, disconnect signals. A plugin that registers without unregistering leaves duplicates behind when reloaded during development, and can crash QGIS when the Python objects behind a registration are destroyed.",[289,290,295],"pre",{"className":291,"code":292,"language":293,"meta":294,"style":294},"language-python shiki shiki-themes github-dark","class MyExtensionsPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self._cleanup = []\n\n    def initGui(self):\n        from qgis.core import QgsApplication\n        meta = MySymbolLayerMetadata()\n        QgsApplication.symbolLayerRegistry().addSymbolLayerType(meta)\n        self._keep = [meta]                            # keep Python objects alive\n\n        factory = MyPropertiesPageFactory()\n        self.iface.registerMapLayerConfigWidgetFactory(factory)\n        self._keep.append(factory)\n        self._cleanup.append(lambda: self.iface.unregisterMapLayerConfigWidgetFactory(factory))\n\n    def unload(self):\n        for undo in reversed(self._cleanup):\n            undo()\n        self._cleanup.clear()\n        self._keep = []\n","python","",[178,296,297,314,327,342,355,362,373,388,399,405,422,427,438,446,454,474,479,489,512,518,526],{"__ignoreMap":294},[298,299,302,306,310],"span",{"class":300,"line":301},"line",1,[298,303,305],{"class":304},"snl16","class",[298,307,309],{"class":308},"svObZ"," MyExtensionsPlugin",[298,311,313],{"class":312},"s95oV",":\n",[298,315,317,320,324],{"class":300,"line":316},2,[298,318,319],{"class":304},"    def",[298,321,323],{"class":322},"sDLfK"," __init__",[298,325,326],{"class":312},"(self, iface):\n",[298,328,330,333,336,339],{"class":300,"line":329},3,[298,331,332],{"class":322},"        self",[298,334,335],{"class":312},".iface ",[298,337,338],{"class":304},"=",[298,340,341],{"class":312}," iface\n",[298,343,345,347,350,352],{"class":300,"line":344},4,[298,346,332],{"class":322},[298,348,349],{"class":312},"._cleanup ",[298,351,338],{"class":304},[298,353,354],{"class":312}," []\n",[298,356,358],{"class":300,"line":357},5,[298,359,361],{"emptyLinePlaceholder":360},true,"\n",[298,363,365,367,370],{"class":300,"line":364},6,[298,366,319],{"class":304},[298,368,369],{"class":308}," initGui",[298,371,372],{"class":312},"(self):\n",[298,374,376,379,382,385],{"class":300,"line":375},7,[298,377,378],{"class":304},"        from",[298,380,381],{"class":312}," qgis.core ",[298,383,384],{"class":304},"import",[298,386,387],{"class":312}," QgsApplication\n",[298,389,391,394,396],{"class":300,"line":390},8,[298,392,393],{"class":312},"        meta ",[298,395,338],{"class":304},[298,397,398],{"class":312}," MySymbolLayerMetadata()\n",[298,400,402],{"class":300,"line":401},9,[298,403,404],{"class":312},"        QgsApplication.symbolLayerRegistry().addSymbolLayerType(meta)\n",[298,406,408,410,413,415,418],{"class":300,"line":407},10,[298,409,332],{"class":322},[298,411,412],{"class":312},"._keep ",[298,414,338],{"class":304},[298,416,417],{"class":312}," [meta]                            ",[298,419,421],{"class":420},"sjoCn","# keep Python objects alive\n",[298,423,425],{"class":300,"line":424},11,[298,426,361],{"emptyLinePlaceholder":360},[298,428,430,433,435],{"class":300,"line":429},12,[298,431,432],{"class":312},"        factory ",[298,434,338],{"class":304},[298,436,437],{"class":312}," MyPropertiesPageFactory()\n",[298,439,441,443],{"class":300,"line":440},13,[298,442,332],{"class":322},[298,444,445],{"class":312},".iface.registerMapLayerConfigWidgetFactory(factory)\n",[298,447,449,451],{"class":300,"line":448},14,[298,450,332],{"class":322},[298,452,453],{"class":312},"._keep.append(factory)\n",[298,455,457,459,462,465,468,471],{"class":300,"line":456},15,[298,458,332],{"class":322},[298,460,461],{"class":312},"._cleanup.append(",[298,463,464],{"class":304},"lambda",[298,466,467],{"class":312},": ",[298,469,470],{"class":322},"self",[298,472,473],{"class":312},".iface.unregisterMapLayerConfigWidgetFactory(factory))\n",[298,475,477],{"class":300,"line":476},16,[298,478,361],{"emptyLinePlaceholder":360},[298,480,482,484,487],{"class":300,"line":481},17,[298,483,319],{"class":304},[298,485,486],{"class":308}," unload",[298,488,372],{"class":312},[298,490,492,495,498,501,504,507,509],{"class":300,"line":491},18,[298,493,494],{"class":304},"        for",[298,496,497],{"class":312}," undo ",[298,499,500],{"class":304},"in",[298,502,503],{"class":322}," reversed",[298,505,506],{"class":312},"(",[298,508,470],{"class":322},[298,510,511],{"class":312},"._cleanup):\n",[298,513,515],{"class":300,"line":514},19,[298,516,517],{"class":312},"            undo()\n",[298,519,521,523],{"class":300,"line":520},20,[298,522,332],{"class":322},[298,524,525],{"class":312},"._cleanup.clear()\n",[298,527,529,531,533,535],{"class":300,"line":528},21,[298,530,332],{"class":322},[298,532,412],{"class":312},[298,534,338],{"class":304},[298,536,354],{"class":312},[14,538,539,542,543,545],{},[168,540,541],{},"Breakdown:"," Keeping every metadata and factory object in a list on the plugin prevents Python's garbage collector from destroying objects QGIS still points to — the most common cause of mysterious crashes in extension plugins. Recording an undo action for each registration and running them in reverse order in ",[178,544,286],{}," guarantees symmetry: whatever was added is removed, in the opposite order. The same structure scales to plugins that register several extension types at once.",[157,547,549],{"id":548},"rendering-extensions-symbols-and-renderers","Rendering extensions: symbols and renderers",[14,551,552],{},"QGIS's styling system has two levels, and Python can extend both.",[14,554,555,556,559,560,563,564,567,568,571,572,283,574,563,576,567,579,582,583,586],{},"A ",[168,557,558],{},"symbol layer"," draws one mark for one feature: a marker, a line style, a fill pattern. Built-in symbol layers cover most needs, and geometry generators plus data-defined properties cover many more. A custom symbol layer is for marks no combination of those can draw — a wind barb computed from two attributes, a domain-specific glyph, a tiny chart. It subclasses ",[178,561,562],{},"QgsMarkerSymbolLayer",", ",[178,565,566],{},"QgsLineSymbolLayer"," or ",[178,569,570],{},"QgsFillSymbolLayer",", draws with ",[178,573,180],{},[178,575,247],{},[178,577,578],{},"renderPolyline",[178,580,581],{},"renderPolygon",", and serialises its settings through ",[178,584,585],{},"properties()",".",[14,588,555,589,592,593,596,597,599,600,603,604,607],{},[168,590,591],{},"feature renderer"," decides which symbol each feature gets. Built-in renderers — categorized, graduated, rule-based — cover nearly everything when combined with expressions. A Python renderer is for decisions an expression cannot make: lookups in external systems, state across features, or an organisation's symbology scheme maintained as code. It subclasses ",[178,594,595],{},"QgsFeatureRenderer",", implements ",[178,598,251],{},", and must implement ",[178,601,602],{},"save"," and ",[178,605,606],{},"create"," to survive a project round trip.",[14,609,610],{},[29,611,614,617,620,623,638,641,645,650,654,658,664,670,675,678,682,686,691,696,700,704,708],{"viewBox":612,"role":32,"ariaLabel":613,"xmlns":34},"0 0 760 260","A feature renderer choosing a symbol for each feature, and the symbol's stack of symbol layers drawing it, with custom classes possible at both levels",[36,615,616],{},"Symbol layers and renderers",[40,618,619],{},"A feature renderer receives each feature and chooses a symbol. The symbol is a stack of symbol layers, each drawing part of the mark. A custom renderer replaces the choosing; a custom symbol layer replaces part of the drawing. Both run inside the rendering engine, possibly on worker threads, so they must prepare state in startRender and avoid touching the GUI.",[44,621],{"x":46,"y":46,"width":47,"height":622,"fill":49},"260",[624,625,626],"defs",{},[627,628,634],"marker",{"id":629,"viewBox":630,"refX":65,"refY":631,"markerWidth":632,"markerHeight":632,"orient":633},"exRenderArrow","0 0 10 10","5","7","auto-start-reverse",[635,636],"path",{"d":637,"fill":79},"M0 0 L10 5 L0 10 z",[51,639,640],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Choose a symbol, then draw it",[44,642],{"x":61,"y":130,"width":643,"height":644,"rx":65,"fill":151,"stroke":88,"style":68},"160","110",[51,646,649],{"x":647,"y":648,"style":73,"fill":56,"textAnchor":57},"104","104.78","feature",[51,651,653],{"x":647,"y":652,"style":78,"fill":88,"textAnchor":57},"128.78","attributes",[51,655,657],{"x":647,"y":656,"style":78,"fill":88,"textAnchor":57},"152.78","geometry",[300,659],{"x1":660,"y1":661,"x2":662,"y2":661,"stroke":79,"style":663},"184","125","226","stroke-width:1.8;marker-end:url(#exRenderArrow)",[44,665],{"x":666,"y":667,"width":668,"height":669,"rx":65,"fill":66,"stroke":67,"style":68},"230","60","210","130",[51,671,674],{"x":672,"y":673,"style":73,"fill":67,"textAnchor":57},"335","102.78","renderer",[51,676,677],{"x":672,"y":652,"style":78,"fill":79,"textAnchor":57},"symbolForFeature()",[51,679,681],{"x":672,"y":680,"style":78,"fill":88,"textAnchor":57},"154.78","custom: your rules",[300,683],{"x1":684,"y1":661,"x2":685,"y2":661,"stroke":79,"style":663},"440","482",[44,687],{"x":688,"y":62,"width":689,"height":690,"rx":65,"fill":93,"stroke":94,"style":68},"486","250","146",[51,692,695],{"x":693,"y":694,"style":73,"fill":94,"textAnchor":57},"611","83.78","symbol",[51,697,699],{"x":693,"y":698,"style":78,"fill":79,"textAnchor":57},"113.78","simple marker",[51,701,703],{"x":693,"y":702,"style":78,"fill":79,"textAnchor":57},"143.78","custom symbol layer",[51,705,707],{"x":693,"y":706,"style":78,"fill":79,"textAnchor":57},"173.78","outline",[51,709,710],{"x":53,"y":666,"style":78,"fill":88,"textAnchor":57},"both run in the rendering engine — prepare in startRender, no GUI access",[14,712,713,714,717],{},"Both run inside the rendering engine, which renders layers in parallel worker threads. That has one hard consequence: the drawing and choosing code must not touch GUI objects, the project or the layer itself, and should read only state prepared in ",[178,715,716],{},"startRender",". Both are also called once per feature on every redraw, so per-feature code must be lean — Python extensions are comfortable with thousands of features and sluggish with hundreds of thousands.",[157,719,721],{"id":720},"canvas-extensions-items-above-the-map","Canvas extensions: items above the map",[14,723,724,725,728],{},"The map canvas is a Qt graphics scene: the rendered map is one image in it, and canvas items sit on top. QGIS's own vertex markers, rubber bands and annotations are canvas items, and plugins can add their own by subclassing ",[178,726,727],{},"QgsMapCanvasItem",". Canvas items are the right tool for graphics that are temporary, interactive and tied to a tool — a range ring around a click, a live GPS crosshair, a measurement readout — because they repaint instantly without re-rendering any layer.",[14,730,731,732,735,736,739,740,743,744,747],{},"The essential discipline is coordinate handling. An item stores its anchor in map coordinates and converts to pixels in ",[178,733,734],{},"updatePosition",", which the canvas calls after every pan and zoom; it paints in pixels; and it declares the area it paints in ",[178,737,738],{},"boundingRect",", calling ",[178,741,742],{},"prepareGeometryChange()"," before that area changes. Items do not appear in print layouts and are not saved with the project — for that, use layers or annotations. The ",[21,745,746],{"href":199},"canvas item recipe"," also shows how to pair an item with a map tool, which handles input while the item handles drawing.",[157,749,751],{"id":750},"interface-extensions-indicators-and-properties-pages","Interface extensions: indicators and properties pages",[14,753,754],{},"Two extension points put a plugin's information where users already look.",[14,756,757,760],{},[168,758,759],{},"Layer tree indicators"," are the small icons beside layers in the Layers panel. QGIS uses them for edit mode, filters and CRS problems; plugins can add their own for stale data, failed validation, sync state or anything else that should catch the eye. An indicator has an icon, a tooltip and a click signal, and is attached to a layer tree node. The work is in the bookkeeping: evaluating a rule per layer, updating rather than duplicating indicators, re-running on layer and project events, and removing everything on unload.",[14,762,763,766,767,770],{},[168,764,765],{},"Layer properties pages"," add a page to the Layer Properties dialog — and optionally the Layer Styling panel — through a ",[178,768,769],{},"QgsMapLayerConfigWidgetFactory",". They are the natural home for per-layer plugin settings, stored as layer custom properties so they travel with the project. A factory decides which layers get the page, so raster-only settings never clutter vector layers and vice versa.",[289,772,774],{"className":291,"code":773,"language":293,"meta":294,"style":294},"from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nlayer.setCustomProperty(\"assetsync\u002Fenabled\", True)\nlayer.setCustomProperty(\"assetsync\u002Fid_field\", \"parcel_id\")\nprint(layer.customPropertyKeys())\n",[178,775,776,788,792,814,830,844],{"__ignoreMap":294},[298,777,778,781,783,785],{"class":300,"line":301},[298,779,780],{"class":304},"from",[298,782,381],{"class":312},[298,784,384],{"class":304},[298,786,787],{"class":312}," QgsProject\n",[298,789,790],{"class":300,"line":316},[298,791,361],{"emptyLinePlaceholder":360},[298,793,794,797,799,802,806,809,811],{"class":300,"line":329},[298,795,796],{"class":312},"layer ",[298,798,338],{"class":304},[298,800,801],{"class":312}," QgsProject.instance().mapLayersByName(",[298,803,805],{"class":804},"sU2Wk","\"parcels\"",[298,807,808],{"class":312},")[",[298,810,46],{"class":322},[298,812,813],{"class":312},"]\n",[298,815,816,819,822,824,827],{"class":300,"line":344},[298,817,818],{"class":312},"layer.setCustomProperty(",[298,820,821],{"class":804},"\"assetsync\u002Fenabled\"",[298,823,563],{"class":312},[298,825,826],{"class":322},"True",[298,828,829],{"class":312},")\n",[298,831,832,834,837,839,842],{"class":300,"line":357},[298,833,818],{"class":312},[298,835,836],{"class":804},"\"assetsync\u002Fid_field\"",[298,838,563],{"class":312},[298,840,841],{"class":804},"\"parcel_id\"",[298,843,829],{"class":312},[298,845,846,849],{"class":300,"line":364},[298,847,848],{"class":322},"print",[298,850,851],{"class":312},"(layer.customPropertyKeys())\n",[14,853,854,856,857,860],{},[168,855,541],{}," Custom properties are the glue between interface extensions: a properties page writes them, an indicator reads them to decide what to show, and the plugin's logic reads them to decide what to do. A plugin-specific prefix such as ",[178,858,859],{},"assetsync\u002F"," keeps keys from colliding with other plugins and makes them easy to find in the project file.",[157,862,864],{"id":863},"data-extensions-python-providers","Data extensions: Python providers",[14,866,867],{},"The deepest extension point is the data provider. Every vector layer gets its features from a provider, and a Python provider makes any source — an internal REST API, a sensor feed, an object model in another application — appear as a normal layer that styles, labels, filters and runs through Processing like any other.",[14,869,870],{},[29,871,874,877,880,883,890,893,895,898,901,904,909,912,916,919,922,926,930,934,937,940],{"viewBox":872,"role":32,"ariaLabel":873,"xmlns":34},"0 0 760 240","The three classes of a Python data provider — provider, feature source and iterator — and the registration that links a key to them",[36,875,876],{},"Provider, source and iterator",[40,878,879],{},"A Python provider has three cooperating classes. The provider describes the layer: fields, geometry type, CRS, extent and capabilities. The feature source is a thread-safe snapshot handed to rendering threads. The iterator fetches features for one request, honouring fid, rectangle, expression and destination CRS filters. Registration links a provider key to a factory.",[44,881],{"x":46,"y":46,"width":47,"height":882,"fill":49},"240",[624,884,885],{},[627,886,888],{"id":887,"viewBox":630,"refX":65,"refY":631,"markerWidth":632,"markerHeight":632,"orient":633},"exProviderArrow",[635,889],{"d":637,"fill":79},[51,891,892],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Describe, snapshot, fetch",[44,894],{"x":61,"y":667,"width":63,"height":669,"rx":65,"fill":66,"stroke":67,"style":68},[51,896,897],{"x":71,"y":673,"style":73,"fill":67,"textAnchor":57},"provider",[51,899,900],{"x":71,"y":652,"style":78,"fill":79,"textAnchor":57},"fields · wkbType · crs",[51,902,903],{"x":71,"y":680,"style":78,"fill":79,"textAnchor":57},"extent · capabilities",[300,905],{"x1":906,"y1":661,"x2":907,"y2":661,"stroke":79,"style":908},"246","266","stroke-width:1.8;marker-end:url(#exProviderArrow)",[44,910],{"x":911,"y":667,"width":63,"height":669,"rx":65,"fill":93,"stroke":94,"style":68},"270",[51,913,915],{"x":914,"y":673,"style":73,"fill":94,"textAnchor":57},"381","feature source",[51,917,918],{"x":914,"y":652,"style":78,"fill":79,"textAnchor":57},"thread-safe",[51,920,921],{"x":914,"y":680,"style":78,"fill":79,"textAnchor":57},"snapshot",[300,923],{"x1":924,"y1":661,"x2":925,"y2":661,"stroke":79,"style":908},"492","512",[44,927],{"x":928,"y":667,"width":929,"height":669,"rx":65,"fill":110,"stroke":111,"style":68},"516","220",[51,931,933],{"x":932,"y":673,"style":73,"fill":111,"textAnchor":57},"626","iterator",[51,935,936],{"x":932,"y":652,"style":78,"fill":79,"textAnchor":57},"fetchFeature()",[51,938,939],{"x":932,"y":680,"style":78,"fill":88,"textAnchor":57},"honours the request",[51,941,943],{"x":53,"y":63,"style":942,"fill":88,"textAnchor":57},"text-anchor:middle;font-size:10.5px;font-family:monospace","register: QgsProviderMetadata(key, description, factory)",[14,945,946],{},"Providers are the most code of any extension here — three classes and a registration — and the most rewarding when they fit. They fit when data is too large to copy and is viewed in pieces, when it changes and should be read live, or when projects must reopen pointing at the source. They do not fit when the data is small and a snapshot is fine: a memory layer filled from the same client is a fraction of the code. The iterator is where correctness lives: it must honour feature id filters, rectangle filters, expression filters and destination CRS requests, because the canvas, the identify tool, selections and Processing all depend on them.",[157,948,950],{"id":949},"threads-and-ownership-the-two-rules-that-prevent-crashes","Threads and ownership: the two rules that prevent crashes",[14,952,953],{},"Python extensions crash QGIS for two reasons far more often than any other.",[14,955,956,959,960,962],{},[168,957,958],{},"Thread rule."," Rendering happens on worker threads. Symbol layers, renderers and provider iterators may be called from them, so they must not touch GUI objects, the project, the layer object or any shared mutable Python state. Prepare what you need in ",[178,961,716],{}," (or copy it into a feature source) and read only that. Canvas items, indicators and properties pages, by contrast, live on the main thread and must only be touched from it — background work hands results back through signals.",[14,964,965,968,969,973],{},[168,966,967],{},"Ownership rule."," QGIS's C++ objects and Python's objects have separate lifetimes. When QGIS holds a pointer to a Python-created object — a metadata object in a registry, a factory, a canvas item in the scene — Python must keep a reference for as long as QGIS uses it, and must remove it from QGIS before dropping that reference. Conversely, once QGIS deletes an object, any Python reference to it is dead and raises \"wrapped C\u002FC++ object has been deleted\". ",[21,970,972],{"href":971},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002F","Object ownership and crashes"," explains the mechanics in depth.",[289,975,977],{"className":291,"code":976,"language":293,"meta":294,"style":294},"import threading\nfrom qgis.PyQt.QtCore import QThread\nfrom qgis.core import QgsApplication\n\ndef assert_main_thread():\n    if QThread.currentThread() != QgsApplication.instance().thread():\n        raise RuntimeError(\"must run on the main thread\")\n\ndef debug_thread(label):\n    print(label, \"on\", threading.current_thread().name)\n",[178,978,979,986,998,1008,1012,1023,1037,1052,1056,1066],{"__ignoreMap":294},[298,980,981,983],{"class":300,"line":301},[298,982,384],{"class":304},[298,984,985],{"class":312}," threading\n",[298,987,988,990,993,995],{"class":300,"line":316},[298,989,780],{"class":304},[298,991,992],{"class":312}," qgis.PyQt.QtCore ",[298,994,384],{"class":304},[298,996,997],{"class":312}," QThread\n",[298,999,1000,1002,1004,1006],{"class":300,"line":329},[298,1001,780],{"class":304},[298,1003,381],{"class":312},[298,1005,384],{"class":304},[298,1007,387],{"class":312},[298,1009,1010],{"class":300,"line":344},[298,1011,361],{"emptyLinePlaceholder":360},[298,1013,1014,1017,1020],{"class":300,"line":357},[298,1015,1016],{"class":304},"def",[298,1018,1019],{"class":308}," assert_main_thread",[298,1021,1022],{"class":312},"():\n",[298,1024,1025,1028,1031,1034],{"class":300,"line":364},[298,1026,1027],{"class":304},"    if",[298,1029,1030],{"class":312}," QThread.currentThread() ",[298,1032,1033],{"class":304},"!=",[298,1035,1036],{"class":312}," QgsApplication.instance().thread():\n",[298,1038,1039,1042,1045,1047,1050],{"class":300,"line":375},[298,1040,1041],{"class":304},"        raise",[298,1043,1044],{"class":322}," RuntimeError",[298,1046,506],{"class":312},[298,1048,1049],{"class":804},"\"must run on the main thread\"",[298,1051,829],{"class":312},[298,1053,1054],{"class":300,"line":390},[298,1055,361],{"emptyLinePlaceholder":360},[298,1057,1058,1060,1063],{"class":300,"line":401},[298,1059,1016],{"class":304},[298,1061,1062],{"class":308}," debug_thread",[298,1064,1065],{"class":312},"(label):\n",[298,1067,1068,1071,1074,1077],{"class":300,"line":407},[298,1069,1070],{"class":322},"    print",[298,1072,1073],{"class":312},"(label, ",[298,1075,1076],{"class":804},"\"on\"",[298,1078,1079],{"class":312},", threading.current_thread().name)\n",[14,1081,1082,1084,1085,1088,1089,567,1091,1093],{},[168,1083,541],{}," Two small helpers make threading visible during development: an assertion that fails loudly when main-thread-only code runs elsewhere, and a print that shows which thread a callback runs on. Calling ",[178,1086,1087],{},"debug_thread"," inside ",[178,1090,247],{},[178,1092,259],{}," while panning shows worker-thread names, which makes the thread rule concrete. Remove the prints before release; keep the assertion in methods that touch the GUI.",[157,1095,1097],{"id":1096},"testing-extensions","Testing extensions",[14,1099,1100,1101,1103,1104,1107,1108,1112],{},"Extensions are easiest to test at the level of their contract, without the GUI. A renderer's ",[178,1102,251],{}," can be called with synthetic features; a provider can be loaded as a layer and queried with every kind of request; a symbol layer can be rendered into an image with ",[178,1105,1106],{},"QgsMapRendererSequentialJob"," and compared against a reference. These tests run headless in CI with the QGIS Docker images, as described in ",[21,1109,1111],{"href":1110},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Frun-qgis-plugin-tests-in-github-actions\u002F","running plugin tests in GitHub Actions",". Interface extensions — indicators and properties pages — are best tested by exercising their logic (the rule, the settings helper) separately from the widgets.",[157,1114,1116],{"id":1115},"choosing-the-right-extension-point","Choosing the right extension point",[14,1118,1119],{},"Before writing a custom class, check whether configuration can do the job, because configuration needs no plugin and survives in any QGIS.",[162,1121,1122,1131,1137,1146,1155],{},[165,1123,555,1124,567,1127,1130],{},[168,1125,1126],{},"geometry generator",[168,1128,1129],{},"data-defined properties"," often replace a custom symbol layer.",[165,1132,555,1133,1136],{},[168,1134,1135],{},"rule-based renderer"," with expressions replaces most custom renderers.",[165,1138,1139,603,1142,1145],{},[168,1140,1141],{},"Annotations",[168,1143,1144],{},"rubber bands"," cover many canvas graphics.",[165,1147,1148,603,1151,1154],{},[168,1149,1150],{},"Layer notes",[168,1152,1153],{},"metadata"," can carry per-layer information without a properties page.",[165,1156,555,1157,567,1160,1163],{},[168,1158,1159],{},"memory layer",[168,1161,1162],{},"virtual layer"," replaces a provider for small or derived data.",[14,1165,1166],{},"Reach for a custom class when the logic genuinely needs Python, when it must be shared and maintained as code, or when users need it to feel like a native part of QGIS.",[157,1168,1170],{"id":1169},"distribution-considerations","Distribution considerations",[14,1172,1173,1174,1178],{},"Extensions that change how projects render have a compatibility cost: a project using a custom symbol layer or renderer only displays correctly where the plugin is installed and enabled. Document this for users, consider providing a fallback style, and declare the plugin as a dependency in any tooling that opens such projects — ",[21,1175,1177],{"href":1176},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fdeclare-plugin-dependencies-qgis-plugin\u002F","declaring plugin dependencies"," covers the metadata. For QGIS Server, the plugin must be installed as a server plugin too. Extensions that only add interface elements — indicators, pages, canvas items — carry no such cost, because projects do not depend on them.",[157,1180,1182],{"id":1181},"a-development-workflow-for-extensions","A development workflow for extensions",[14,1184,1185],{},"Extension plugins are harder to iterate on than ordinary ones, because registrations persist until removed and some cannot be removed at all on older releases. A workflow that keeps the loop short:",[1187,1188,1189,1195,1205,1211,1217],"ol",{},[165,1190,1191,1194],{},[168,1192,1193],{},"Prototype in the Python console."," Define the class, register it, test it on a layer, and unregister it — all without a plugin. Most bugs show up here in minutes.",[165,1196,1197,1204],{},[168,1198,1199,1200,603,1202,586],{},"Move it into a plugin with symmetric ",[178,1201,276],{},[178,1203,286],{}," Use Plugin Reloader to reload after edits, and check that nothing is duplicated after a reload.",[165,1206,1207,1210],{},[168,1208,1209],{},"Restart QGIS when changing a registered type's name or properties format."," Registries and open projects may hold the old definition.",[165,1212,1213,1216],{},[168,1214,1215],{},"Test the contract headless."," Call the methods QGIS calls, render to images, load providers as layers — in pytest, in CI.",[165,1218,1219,1222],{},[168,1220,1221],{},"Open a saved project in a fresh QGIS."," This is the only reliable check that serialisation works and that the plugin loads before projects need it.",[157,1224,1226],{"id":1225},"key-takeaways","Key takeaways",[162,1228,1229,1237,1240,1243,1246,1249,1252],{},[165,1230,1231,1232,1234,1235,586],{},"Every extension follows subclass, register in ",[178,1233,276],{},", remove in ",[178,1236,286],{},[165,1238,1239],{},"Keep references to every metadata, factory and item QGIS points to; remove them from QGIS before dropping them.",[165,1241,1242],{},"Rendering and provider code runs on worker threads — prepare state up front and never touch the GUI there.",[165,1244,1245],{},"Symbol layers draw marks; renderers choose symbols; both must serialise to survive saving.",[165,1247,1248],{},"Canvas items store map coordinates and paint in pixels; indicators and properties pages put plugin state where users look.",[165,1250,1251],{},"Python providers make any source a native layer — worth it for large or live data, overkill for small snapshots.",[165,1253,1254],{},"Try configuration first; write a custom class when the logic truly needs Python.",[157,1256,1258],{"id":1257},"frequently-asked-questions","Frequently Asked Questions",[14,1260,1261,1264],{},[168,1262,1263],{},"Can these extensions be written in C++ instead?","\nYes, and C++ is faster for rendering and providers. Python is quicker to write and distribute through the plugin repository; most plugin authors start in Python and move hot paths to C++ only if needed.",[14,1266,1267,1270,1271,586],{},[168,1268,1269],{},"Do Python extensions work in QGIS 4?","\nYes. The extension points are unchanged; scripts need the usual QGIS 4 adjustments for scoped enums and PyQt6, noted in each recipe and in ",[21,1272,1274],{"href":1273},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fport-qgis-plugin-to-qgis-4-and-qt6\u002F","porting a plugin to QGIS 4",[14,1276,1277,1280],{},[168,1278,1279],{},"What happens to a project if my plugin is missing?","\nUnknown symbol layer types and renderers fall back to defaults with a warning; layers from a missing provider load as invalid. Interface extensions simply do not appear.",[14,1282,1283,1286,1287,586],{},[168,1284,1285],{},"Can I extend the attribute table or forms the same way?","\nForms have their own extension mechanism — Python init code and custom editor widgets — covered in ",[21,1288,1290],{"href":1289},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002F","attribute forms and layer actions",[14,1292,1293,1296],{},[168,1294,1295],{},"Where else can plugins plug in?","\nProcessing providers, locator filters, expression functions, map tools, options pages, browser data items and source-select dialogs are further extension points, several covered elsewhere in this section.",[157,1298,1300],{"id":1299},"related","Related",[162,1302,1303,1308,1315,1322,1329,1336,1341,1346,1351,1356,1361],{},[165,1304,1305,1307],{},[21,1306,24],{"href":23}," — the section this guide belongs to",[165,1309,1310,1314],{},[21,1311,1313],{"href":1312},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools & Canvas Interaction"," — map tools that pair with canvas items",[165,1316,1317,1321],{},[21,1318,1320],{"href":1319},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002F","Plugin Settings & Localization"," — options pages and stored settings",[165,1323,1324,1328],{},[21,1325,1327],{"href":1326},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002F","QGIS API Architecture"," — ownership, signals and modules behind these classes",[165,1330,1331,1335],{},[21,1332,1334],{"href":1333},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002F","Symbol Layers & Advanced Symbology"," — what built-in symbol layers already do",[165,1337,1338],{},[21,1339,1340],{"href":174},"Write a Custom Symbol Layer in PyQGIS",[165,1342,1343],{},[21,1344,1345],{"href":189},"Write a Custom Feature Renderer in PyQGIS",[165,1347,1348],{},[21,1349,1350],{"href":199},"Draw a Custom Map Canvas Item in PyQGIS",[165,1352,1353],{},[21,1354,1355],{"href":209},"Add a Layer Tree Indicator in PyQGIS",[165,1357,1358],{},[21,1359,1360],{"href":219},"Write a Python Vector Data Provider in PyQGIS",[165,1362,1363],{},[21,1364,1365],{"href":229},"Add a Custom Layer Properties Page in PyQGIS",[1367,1368,1369],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":294,"searchDepth":316,"depth":316,"links":1371},[1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385],{"id":159,"depth":316,"text":160},{"id":234,"depth":316,"text":235},{"id":548,"depth":316,"text":549},{"id":720,"depth":316,"text":721},{"id":750,"depth":316,"text":751},{"id":863,"depth":316,"text":864},{"id":949,"depth":316,"text":950},{"id":1096,"depth":316,"text":1097},{"id":1115,"depth":316,"text":1116},{"id":1169,"depth":316,"text":1170},{"id":1181,"depth":316,"text":1182},{"id":1225,"depth":316,"text":1226},{"id":1257,"depth":316,"text":1258},{"id":1299,"depth":316,"text":1300},"Go beyond buttons and dialogs — subclass QGIS's own extension points from Python to add symbol layers, feature renderers, canvas items, layer tree indicators, data providers and layer properties pages, with the registration, ownership and threading rules each one needs.","md",{"slug":12,"type":1389,"breadcrumb":5,"datePublished":1390,"dateModified":1390},"guide","2026-10-02","\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes",{"title":5,"description":1386},"qgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Findex","PKmQCJT7fQxG8WkIYrsuVCyI9xUgHyAY8jQDAhCmLm8",1790966257225]