[{"data":1,"prerenderedAt":1462},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis":3},{"id":4,"title":5,"body":6,"description":1451,"extension":1452,"meta":1453,"navigation":260,"path":1458,"seo":1459,"stem":1460,"__hash__":1461},"docs\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis\u002Findex.md","Stack Symbol Layers in PyQGIS",{"type":7,"value":8,"toc":1439},"minimark",[9,13,17,26,159,164,195,199,202,409,427,433,487,492,496,499,612,631,634,698,707,828,832,835,1063,1080,1086,1090,1093,1135,1143,1147,1153,1250,1254,1327,1331,1337,1341,1359,1369,1380,1390,1401,1405,1435],[10,11,5],"h1",{"id":12},"stack-symbol-layers-in-pyqgis",[14,15,16],"p",{},"Almost every symbol in a good QGIS map is more than one thing drawn on top of another. A road is a dark wide line with a bright narrow line over it. A boundary is a soft band with a hard edge on its inner side. A building has a fill, an outline, and a slightly offset copy underneath it doing the work of a shadow. In PyQGIS all three are the same operation: append symbol layers to a symbol in the order they should be painted.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002F","Symbol Layers & Advanced Symbology in PyQGIS",". It covers building a stack from scratch, inserting into an existing one, cloning a stack across the classes of a categorized renderer, and the two ordering mistakes that account for most \"it looks nothing like the GUI\" reports.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,93,99,104,108,113,117,121,128,134,140,145,148,151,155],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","A symbol drawn as a stack of three symbol layers, showing that index zero is painted first and each higher index is painted over the ones below it","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Paint order in a symbol layer stack",[40,41,42],"desc",{},"Three symbol layers are listed in index order alongside the picture they combine to produce. Index zero, a wide dark line, is painted first. Index one, a narrower bright line, is painted over it. Index two, a dashed centre line, is painted last and therefore sits on top of both.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"ssArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Index 0 is the bottom of the pile, not the top",[44,77],{"x":71,"y":78,"width":79,"height":80,"rx":81,"fill":82,"stroke":83,"style":84},"54","330","204","10","#fffdf7","#59645f","stroke-width:2",[44,86],{"x":87,"y":88,"width":89,"height":90,"rx":59,"fill":91,"stroke":73,"style":92},"52","76","282","40","#e7e2d4","stroke-width:1.6",[68,94,98],{"x":95,"y":96,"style":97,"fill":73,"textAnchor":74},"193","101","text-anchor:middle;font-size:11px;font-family:sans-serif","index 0 · SimpleLine · width 1.6 · dark",[44,100],{"x":87,"y":101,"width":89,"height":90,"rx":59,"fill":102,"stroke":103,"style":92},"128","#dbeafe","#2563eb",[68,105,107],{"x":95,"y":106,"style":97,"fill":66,"textAnchor":74},"153","index 1 · SimpleLine · width 1.0 · bright",[44,109],{"x":87,"y":110,"width":89,"height":90,"rx":59,"fill":111,"stroke":112,"style":92},"180","#edf8e9","#15803d",[68,114,116],{"x":95,"y":115,"style":97,"fill":66,"textAnchor":74},"205","index 2 · SimpleLine · dashed centre",[68,118,120],{"x":95,"y":119,"style":97,"fill":83,"textAnchor":74},"242","symbol.symbolLayers() in order",[122,123],"line",{"x1":124,"y1":125,"x2":126,"y2":125,"stroke":66,"style":127},"360","156","396","stroke-width:2;marker-end:url(#ssArrow)",[44,129],{"x":130,"y":78,"width":131,"height":80,"rx":81,"fill":82,"stroke":132,"style":133},"410","322","#0f766e","stroke-width:2.5",[68,135,139],{"x":136,"y":137,"style":138,"fill":132,"textAnchor":74},"571","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","what gets painted",[63,141],{"d":142,"fill":143,"stroke":73,"style":144},"M440 200 L520 120 L620 158 L706 108","none","stroke-width:20;stroke-linecap:round;stroke-linejoin:round",[63,146],{"d":142,"fill":143,"stroke":103,"style":147},"stroke-width:12;stroke-linecap:round;stroke-linejoin:round",[63,149],{"d":142,"fill":143,"stroke":111,"style":150},"stroke-width:2;stroke-dasharray:7 6",[68,152,154],{"x":136,"y":153,"style":97,"fill":66,"textAnchor":74},"240","one geometry, three passes over it",[68,156,158],{"x":70,"y":157,"style":97,"fill":83,"textAnchor":74},"288","reverse the list and the casing hides everything above it",[160,161,163],"h2",{"id":162},"prerequisites","Prerequisites",[165,166,167,175,187],"ul",{},[168,169,170,174],"li",{},[171,172,173],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer; the API used here is stable back to 3.16.",[168,176,177,178,182,183,186],{},"A loaded vector layer. The examples use a line layer named ",[179,180,181],"code",{},"roads"," and a polygon layer named ",[179,184,185],{},"zoning",".",[168,188,189,190,194],{},"A single-symbol renderer to start from. If the layer is already categorized, read ",[21,191,193],{"href":192},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","graduated and categorized renderers"," first — the stack is built the same way, but you assign it per category.",[160,196,198],{"id":197},"build-a-stack-from-nothing","Build a stack from nothing",[14,200,201],{},"The most predictable route is to construct an empty symbol and append every layer yourself, so nothing is inherited from a default you did not choose.",[203,204,209],"pre",{"className":205,"code":206,"language":207,"meta":208,"style":208},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsLineSymbol, QgsSimpleLineSymbolLayer\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.PyQt.QtCore import Qt\n\nlayer = QgsProject.instance().mapLayersByName(\"roads\")[0]\n\nsymbol = QgsLineSymbol()\nsymbol.deleteSymbolLayer(0)\n\nfor colour, width in ((\"#3f3f46\", 1.8), (\"#fbbf24\", 1.1)):\n    line = QgsSimpleLineSymbolLayer(QColor(colour))\n    line.setWidth(width)\n    line.setPenCapStyle(Qt.RoundCap)\n    line.setPenJoinStyle(Qt.RoundJoin)\n    symbol.appendSymbolLayer(line)\n\nlayer.renderer().setSymbol(symbol)\nlayer.triggerRepaint()\n","python","",[179,210,211,229,242,255,262,287,292,303,314,319,357,368,374,380,386,392,397,403],{"__ignoreMap":208},[212,213,215,219,223,226],"span",{"class":122,"line":214},1,[212,216,218],{"class":217},"snl16","from",[212,220,222],{"class":221},"s95oV"," qgis.core ",[212,224,225],{"class":217},"import",[212,227,228],{"class":221}," QgsProject, QgsLineSymbol, QgsSimpleLineSymbolLayer\n",[212,230,232,234,237,239],{"class":122,"line":231},2,[212,233,218],{"class":217},[212,235,236],{"class":221}," qgis.PyQt.QtGui ",[212,238,225],{"class":217},[212,240,241],{"class":221}," QColor\n",[212,243,245,247,250,252],{"class":122,"line":244},3,[212,246,218],{"class":217},[212,248,249],{"class":221}," qgis.PyQt.QtCore ",[212,251,225],{"class":217},[212,253,254],{"class":221}," Qt\n",[212,256,258],{"class":122,"line":257},4,[212,259,261],{"emptyLinePlaceholder":260},true,"\n",[212,263,265,268,271,274,278,281,284],{"class":122,"line":264},5,[212,266,267],{"class":221},"layer ",[212,269,270],{"class":217},"=",[212,272,273],{"class":221}," QgsProject.instance().mapLayersByName(",[212,275,277],{"class":276},"sU2Wk","\"roads\"",[212,279,280],{"class":221},")[",[212,282,46],{"class":283},"sDLfK",[212,285,286],{"class":221},"]\n",[212,288,290],{"class":122,"line":289},6,[212,291,261],{"emptyLinePlaceholder":260},[212,293,295,298,300],{"class":122,"line":294},7,[212,296,297],{"class":221},"symbol ",[212,299,270],{"class":217},[212,301,302],{"class":221}," QgsLineSymbol()\n",[212,304,306,309,311],{"class":122,"line":305},8,[212,307,308],{"class":221},"symbol.deleteSymbolLayer(",[212,310,46],{"class":283},[212,312,313],{"class":221},")\n",[212,315,317],{"class":122,"line":316},9,[212,318,261],{"emptyLinePlaceholder":260},[212,320,322,325,328,331,334,337,340,343,346,349,351,354],{"class":122,"line":321},10,[212,323,324],{"class":217},"for",[212,326,327],{"class":221}," colour, width ",[212,329,330],{"class":217},"in",[212,332,333],{"class":221}," ((",[212,335,336],{"class":276},"\"#3f3f46\"",[212,338,339],{"class":221},", ",[212,341,342],{"class":283},"1.8",[212,344,345],{"class":221},"), (",[212,347,348],{"class":276},"\"#fbbf24\"",[212,350,339],{"class":221},[212,352,353],{"class":283},"1.1",[212,355,356],{"class":221},")):\n",[212,358,360,363,365],{"class":122,"line":359},11,[212,361,362],{"class":221},"    line ",[212,364,270],{"class":217},[212,366,367],{"class":221}," QgsSimpleLineSymbolLayer(QColor(colour))\n",[212,369,371],{"class":122,"line":370},12,[212,372,373],{"class":221},"    line.setWidth(width)\n",[212,375,377],{"class":122,"line":376},13,[212,378,379],{"class":221},"    line.setPenCapStyle(Qt.RoundCap)\n",[212,381,383],{"class":122,"line":382},14,[212,384,385],{"class":221},"    line.setPenJoinStyle(Qt.RoundJoin)\n",[212,387,389],{"class":122,"line":388},15,[212,390,391],{"class":221},"    symbol.appendSymbolLayer(line)\n",[212,393,395],{"class":122,"line":394},16,[212,396,261],{"emptyLinePlaceholder":260},[212,398,400],{"class":122,"line":399},17,[212,401,402],{"class":221},"layer.renderer().setSymbol(symbol)\n",[212,404,406],{"class":122,"line":405},18,[212,407,408],{"class":221},"layer.triggerRepaint()\n",[14,410,411,414,415,418,419,422,423,426],{},[171,412,413],{},"Breakdown:"," ",[179,416,417],{},"QgsLineSymbol()"," arrives with one default symbol layer already in it, and ",[179,420,421],{},"deleteSymbolLayer(0)"," is what stops that default blue line showing through at every join. The loop appends in painting order — dark first, bright second — because ",[179,424,425],{},"appendSymbolLayer()"," adds to the end of the list and the end of the list is drawn last. Round caps and joins are set on both layers so the casing wraps the fill at corners; with the default flat caps the bright line pokes out past the dark one at every bend.",[14,428,429,432],{},[179,430,431],{},"setWidth()"," is in millimetres. If the road should keep its real ground width as the map zooms, switch the unit instead of guessing a number:",[203,434,436],{"className":205,"code":435,"language":207,"meta":208,"style":208},"from qgis.core import QgsUnitTypes\n\ncasing = symbol.symbolLayer(0)\ncasing.setOutputUnit(QgsUnitTypes.RenderMetersInMapUnits)\ncasing.setWidth(9.0)                  # nine metres of carriageway\n",[179,437,438,449,453,467,472],{"__ignoreMap":208},[212,439,440,442,444,446],{"class":122,"line":214},[212,441,218],{"class":217},[212,443,222],{"class":221},[212,445,225],{"class":217},[212,447,448],{"class":221}," QgsUnitTypes\n",[212,450,451],{"class":122,"line":231},[212,452,261],{"emptyLinePlaceholder":260},[212,454,455,458,460,463,465],{"class":122,"line":244},[212,456,457],{"class":221},"casing ",[212,459,270],{"class":217},[212,461,462],{"class":221}," symbol.symbolLayer(",[212,464,46],{"class":283},[212,466,313],{"class":221},[212,468,469],{"class":122,"line":257},[212,470,471],{"class":221},"casing.setOutputUnit(QgsUnitTypes.RenderMetersInMapUnits)\n",[212,473,474,477,480,483],{"class":122,"line":264},[212,475,476],{"class":221},"casing.setWidth(",[212,478,479],{"class":283},"9.0",[212,481,482],{"class":221},")                  ",[212,484,486],{"class":485},"sjoCn","# nine metres of carriageway\n",[14,488,489,491],{},[171,490,413],{}," In map-unit mode the width is a distance on the ground, so the line shrinks as you zoom out and eventually disappears — correct for a cadastral plan, wrong for a national overview where a road must stay visible at any scale. Mixing units within one stack is legal and occasionally right: a ground-width casing under a hairline millimetre centre line keeps the road findable when zoomed out.",[160,493,495],{"id":494},"insert-into-a-stack-you-did-not-build","Insert into a stack you did not build",[14,497,498],{},"When the layer already has a symbol you like and you only want to add to it, do not rebuild — insert.",[203,500,502],{"className":205,"code":501,"language":207,"meta":208,"style":208},"from qgis.core import QgsSimpleLineSymbolLayer\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.PyQt.QtCore import Qt\n\nsymbol = layer.renderer().symbol()\n\ncentre = QgsSimpleLineSymbolLayer(QColor(\"#fffdf7\"))\ncentre.setWidth(0.3)\ncentre.setPenStyle(Qt.DashLine)\n\nsymbol.appendSymbolLayer(centre)          # on top of everything\nsymbol.insertSymbolLayer(0, shadow)       # underneath everything\nlayer.triggerRepaint()\n",[179,503,504,515,525,535,539,548,552,568,578,583,587,595,608],{"__ignoreMap":208},[212,505,506,508,510,512],{"class":122,"line":214},[212,507,218],{"class":217},[212,509,222],{"class":221},[212,511,225],{"class":217},[212,513,514],{"class":221}," QgsSimpleLineSymbolLayer\n",[212,516,517,519,521,523],{"class":122,"line":231},[212,518,218],{"class":217},[212,520,236],{"class":221},[212,522,225],{"class":217},[212,524,241],{"class":221},[212,526,527,529,531,533],{"class":122,"line":244},[212,528,218],{"class":217},[212,530,249],{"class":221},[212,532,225],{"class":217},[212,534,254],{"class":221},[212,536,537],{"class":122,"line":257},[212,538,261],{"emptyLinePlaceholder":260},[212,540,541,543,545],{"class":122,"line":264},[212,542,297],{"class":221},[212,544,270],{"class":217},[212,546,547],{"class":221}," layer.renderer().symbol()\n",[212,549,550],{"class":122,"line":289},[212,551,261],{"emptyLinePlaceholder":260},[212,553,554,557,559,562,565],{"class":122,"line":294},[212,555,556],{"class":221},"centre ",[212,558,270],{"class":217},[212,560,561],{"class":221}," QgsSimpleLineSymbolLayer(QColor(",[212,563,564],{"class":276},"\"#fffdf7\"",[212,566,567],{"class":221},"))\n",[212,569,570,573,576],{"class":122,"line":305},[212,571,572],{"class":221},"centre.setWidth(",[212,574,575],{"class":283},"0.3",[212,577,313],{"class":221},[212,579,580],{"class":122,"line":316},[212,581,582],{"class":221},"centre.setPenStyle(Qt.DashLine)\n",[212,584,585],{"class":122,"line":321},[212,586,261],{"emptyLinePlaceholder":260},[212,588,589,592],{"class":122,"line":359},[212,590,591],{"class":221},"symbol.appendSymbolLayer(centre)          ",[212,593,594],{"class":485},"# on top of everything\n",[212,596,597,600,602,605],{"class":122,"line":370},[212,598,599],{"class":221},"symbol.insertSymbolLayer(",[212,601,46],{"class":283},[212,603,604],{"class":221},", shadow)       ",[212,606,607],{"class":485},"# underneath everything\n",[212,609,610],{"class":122,"line":376},[212,611,408],{"class":221},[14,613,614,414,616,618,619,622,623,626,627,630],{},[171,615,413],{},[179,617,425],{}," and ",[179,620,621],{},"insertSymbolLayer(index, layer)"," are the only two ways in; there is no ",[179,624,625],{},"setSymbolLayer"," that takes an arbitrary position. Inserting at 0 is how you slide something under an existing design without touching it, which is the whole trick behind adding a drop shadow to a symbol somebody else authored. Note that the symbol returned by ",[179,628,629],{},"renderer().symbol()"," is the live object, so these calls take effect immediately and there is nothing to assign back.",[14,632,633],{},"Removing works the same way, and the index shifts as you go — deleting layers in ascending order will delete the wrong ones after the first:",[203,635,637],{"className":205,"code":636,"language":207,"meta":208,"style":208},"for index in range(symbol.symbolLayerCount() - 1, -1, -1):\n    if symbol.symbolLayer(index).layerType() == \"SimpleLine\":\n        symbol.deleteSymbolLayer(index)\n",[179,638,639,676,693],{"__ignoreMap":208},[212,640,641,643,646,648,651,654,657,660,662,664,667,669,671,673],{"class":122,"line":214},[212,642,324],{"class":217},[212,644,645],{"class":221}," index ",[212,647,330],{"class":217},[212,649,650],{"class":283}," range",[212,652,653],{"class":221},"(symbol.symbolLayerCount() ",[212,655,656],{"class":217},"-",[212,658,659],{"class":283}," 1",[212,661,339],{"class":221},[212,663,656],{"class":217},[212,665,666],{"class":283},"1",[212,668,339],{"class":221},[212,670,656],{"class":217},[212,672,666],{"class":283},[212,674,675],{"class":221},"):\n",[212,677,678,681,684,687,690],{"class":122,"line":231},[212,679,680],{"class":217},"    if",[212,682,683],{"class":221}," symbol.symbolLayer(index).layerType() ",[212,685,686],{"class":217},"==",[212,688,689],{"class":276}," \"SimpleLine\"",[212,691,692],{"class":221},":\n",[212,694,695],{"class":122,"line":244},[212,696,697],{"class":221},"        symbol.deleteSymbolLayer(index)\n",[14,699,700,702,703,706],{},[171,701,413],{}," Iterating backwards is the standard defence against index shifting. ",[179,704,705],{},"layerType()"," returns the registry name of the class, so this is also how you find \"the SVG marker somewhere in this stack\" without knowing where the previous author put it.",[14,708,709],{},[29,710,713,716,719,721,724,729,734,742,747,750,754,758,761,764,767,770,773,776,779,781,785,788,790,793,798,802,805,808,810,813,815,818,823,826],{"viewBox":711,"role":32,"ariaLabel":712,"xmlns":34},"0 0 760 288","Two ways of adding to an existing symbol: appending puts the new layer on top of the design, while inserting at index zero slides it underneath without disturbing the layers above",[36,714,715],{},"Append versus insert at index zero",[40,717,718],{},"Starting from a two-layer symbol, appending a dashed centre line places it above both existing layers. Inserting a wide soft layer at index zero places it below both, producing a shadow effect while the original design is untouched.",[44,720],{"x":46,"y":46,"width":47,"height":157,"fill":49},[68,722,723],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"The same call, two very different results",[44,725],{"x":726,"y":87,"width":727,"height":728,"rx":81,"fill":82,"stroke":83,"style":84},"24","222","196",[68,730,733],{"x":731,"y":732,"style":138,"fill":73,"textAnchor":74},"135","78","starting symbol",[44,735],{"x":736,"y":737,"width":738,"height":739,"rx":740,"fill":91,"stroke":73,"style":741},"46","96","178","30","4","stroke-width:1.5",[68,743,746],{"x":731,"y":744,"style":745,"fill":73,"textAnchor":74},"116","text-anchor:middle;font-size:10.5px;font-family:sans-serif","0 · casing",[44,748],{"x":736,"y":749,"width":738,"height":739,"rx":740,"fill":102,"stroke":103,"style":741},"134",[68,751,753],{"x":731,"y":752,"style":745,"fill":66,"textAnchor":74},"154","1 · fill",[63,755],{"d":756,"fill":143,"stroke":73,"style":757},"M60 200 L130 186 L212 204","stroke-width:14;stroke-linecap:round",[63,759],{"d":756,"fill":143,"stroke":103,"style":760},"stroke-width:8;stroke-linecap:round",[44,762],{"x":763,"y":87,"width":727,"height":728,"rx":81,"fill":111,"stroke":112,"style":133},"270",[68,765,425],{"x":766,"y":732,"style":138,"fill":112,"textAnchor":74},"381",[44,768],{"x":769,"y":737,"width":738,"height":726,"rx":740,"fill":91,"stroke":73,"style":741},"292",[68,771,746],{"x":766,"y":772,"style":745,"fill":73,"textAnchor":74},"113",[44,774],{"x":769,"y":775,"width":738,"height":726,"rx":740,"fill":102,"stroke":103,"style":741},"126",[68,777,753],{"x":766,"y":778,"style":745,"fill":66,"textAnchor":74},"143",[44,780],{"x":769,"y":125,"width":738,"height":726,"rx":740,"fill":111,"stroke":112,"style":84},[68,782,784],{"x":766,"y":783,"style":745,"fill":66,"textAnchor":74},"173","2 · new dashes — on top",[63,786],{"d":787,"fill":143,"stroke":73,"style":757},"M306 214 L376 200 L458 218",[63,789],{"d":787,"fill":143,"stroke":103,"style":760},[63,791],{"d":787,"fill":143,"stroke":111,"style":792},"stroke-width:2;stroke-dasharray:6 5",[44,794],{"x":795,"y":87,"width":727,"height":728,"rx":81,"fill":796,"stroke":797,"style":133},"516","#fdf2e2","#b45309",[68,799,801],{"x":800,"y":732,"style":138,"fill":797,"textAnchor":74},"627","insertSymbolLayer(0, …)",[44,803],{"x":804,"y":737,"width":738,"height":726,"rx":740,"fill":796,"stroke":797,"style":84},"538",[68,806,807],{"x":800,"y":772,"style":745,"fill":66,"textAnchor":74},"0 · new shadow — below",[44,809],{"x":804,"y":775,"width":738,"height":726,"rx":740,"fill":91,"stroke":73,"style":741},[68,811,812],{"x":800,"y":778,"style":745,"fill":73,"textAnchor":74},"1 · casing",[44,814],{"x":804,"y":125,"width":738,"height":726,"rx":740,"fill":102,"stroke":103,"style":741},[68,816,817],{"x":800,"y":783,"style":745,"fill":66,"textAnchor":74},"2 · fill",[63,819],{"d":820,"fill":143,"stroke":821,"style":822},"M556 219 L626 205 L708 223","#b8b1a0","stroke-width:20;stroke-linecap:round",[63,824],{"d":825,"fill":143,"stroke":73,"style":757},"M552 214 L622 200 L704 218",[63,827],{"d":825,"fill":143,"stroke":103,"style":760},[160,829,831],{"id":830},"a-hatched-fill-under-a-crisp-outline","A hatched fill under a crisp outline",[14,833,834],{},"Polygons show off the stack better than lines, because the fill and the border genuinely want different treatments.",[203,836,838],{"className":205,"code":837,"language":207,"meta":208,"style":208},"from qgis.core import (\n    QgsFillSymbol, QgsLinePatternFillSymbolLayer,\n    QgsSimpleFillSymbolLayer, QgsSimpleLineSymbolLayer, QgsLineSymbol,\n)\nfrom qgis.PyQt.QtGui import QColor\nfrom qgis.PyQt.QtCore import Qt\n\nhatch = QgsLinePatternFillSymbolLayer()\nhatch.setLineAngle(45)\nhatch.setDistance(2.2)\nhatch_line = QgsSimpleLineSymbolLayer(QColor(\"#b45309\"))\nhatch_line.setWidth(0.3)\nhatch.setSubSymbol(QgsLineSymbol([hatch_line]))\n\noutline = QgsSimpleFillSymbolLayer(QColor(\"#000000\"))\noutline.setBrushStyle(Qt.NoBrush)\noutline.setStrokeColor(QColor(\"#7c2d12\"))\noutline.setStrokeWidth(0.6)\n\nsymbol = QgsFillSymbol()\nsymbol.deleteSymbolLayer(0)\nsymbol.appendSymbolLayer(hatch)\nsymbol.appendSymbolLayer(outline)\n\nzoning = QgsProject.instance().mapLayersByName(\"zoning\")[0]\nzoning.renderer().setSymbol(symbol)\nzoning.triggerRepaint()\n",[179,839,840,851,856,861,865,875,885,889,899,909,919,933,942,947,951,966,971,981,991,996,1006,1015,1021,1027,1032,1051,1057],{"__ignoreMap":208},[212,841,842,844,846,848],{"class":122,"line":214},[212,843,218],{"class":217},[212,845,222],{"class":221},[212,847,225],{"class":217},[212,849,850],{"class":221}," (\n",[212,852,853],{"class":122,"line":231},[212,854,855],{"class":221},"    QgsFillSymbol, QgsLinePatternFillSymbolLayer,\n",[212,857,858],{"class":122,"line":244},[212,859,860],{"class":221},"    QgsSimpleFillSymbolLayer, QgsSimpleLineSymbolLayer, QgsLineSymbol,\n",[212,862,863],{"class":122,"line":257},[212,864,313],{"class":221},[212,866,867,869,871,873],{"class":122,"line":264},[212,868,218],{"class":217},[212,870,236],{"class":221},[212,872,225],{"class":217},[212,874,241],{"class":221},[212,876,877,879,881,883],{"class":122,"line":289},[212,878,218],{"class":217},[212,880,249],{"class":221},[212,882,225],{"class":217},[212,884,254],{"class":221},[212,886,887],{"class":122,"line":294},[212,888,261],{"emptyLinePlaceholder":260},[212,890,891,894,896],{"class":122,"line":305},[212,892,893],{"class":221},"hatch ",[212,895,270],{"class":217},[212,897,898],{"class":221}," QgsLinePatternFillSymbolLayer()\n",[212,900,901,904,907],{"class":122,"line":316},[212,902,903],{"class":221},"hatch.setLineAngle(",[212,905,906],{"class":283},"45",[212,908,313],{"class":221},[212,910,911,914,917],{"class":122,"line":321},[212,912,913],{"class":221},"hatch.setDistance(",[212,915,916],{"class":283},"2.2",[212,918,313],{"class":221},[212,920,921,924,926,928,931],{"class":122,"line":359},[212,922,923],{"class":221},"hatch_line ",[212,925,270],{"class":217},[212,927,561],{"class":221},[212,929,930],{"class":276},"\"#b45309\"",[212,932,567],{"class":221},[212,934,935,938,940],{"class":122,"line":370},[212,936,937],{"class":221},"hatch_line.setWidth(",[212,939,575],{"class":283},[212,941,313],{"class":221},[212,943,944],{"class":122,"line":376},[212,945,946],{"class":221},"hatch.setSubSymbol(QgsLineSymbol([hatch_line]))\n",[212,948,949],{"class":122,"line":382},[212,950,261],{"emptyLinePlaceholder":260},[212,952,953,956,958,961,964],{"class":122,"line":388},[212,954,955],{"class":221},"outline ",[212,957,270],{"class":217},[212,959,960],{"class":221}," QgsSimpleFillSymbolLayer(QColor(",[212,962,963],{"class":276},"\"#000000\"",[212,965,567],{"class":221},[212,967,968],{"class":122,"line":394},[212,969,970],{"class":221},"outline.setBrushStyle(Qt.NoBrush)\n",[212,972,973,976,979],{"class":122,"line":399},[212,974,975],{"class":221},"outline.setStrokeColor(QColor(",[212,977,978],{"class":276},"\"#7c2d12\"",[212,980,567],{"class":221},[212,982,983,986,989],{"class":122,"line":405},[212,984,985],{"class":221},"outline.setStrokeWidth(",[212,987,988],{"class":283},"0.6",[212,990,313],{"class":221},[212,992,994],{"class":122,"line":993},19,[212,995,261],{"emptyLinePlaceholder":260},[212,997,999,1001,1003],{"class":122,"line":998},20,[212,1000,297],{"class":221},[212,1002,270],{"class":217},[212,1004,1005],{"class":221}," QgsFillSymbol()\n",[212,1007,1009,1011,1013],{"class":122,"line":1008},21,[212,1010,308],{"class":221},[212,1012,46],{"class":283},[212,1014,313],{"class":221},[212,1016,1018],{"class":122,"line":1017},22,[212,1019,1020],{"class":221},"symbol.appendSymbolLayer(hatch)\n",[212,1022,1024],{"class":122,"line":1023},23,[212,1025,1026],{"class":221},"symbol.appendSymbolLayer(outline)\n",[212,1028,1030],{"class":122,"line":1029},24,[212,1031,261],{"emptyLinePlaceholder":260},[212,1033,1035,1038,1040,1042,1045,1047,1049],{"class":122,"line":1034},25,[212,1036,1037],{"class":221},"zoning ",[212,1039,270],{"class":217},[212,1041,273],{"class":221},[212,1043,1044],{"class":276},"\"zoning\"",[212,1046,280],{"class":221},[212,1048,46],{"class":283},[212,1050,286],{"class":221},[212,1052,1054],{"class":122,"line":1053},26,[212,1055,1056],{"class":221},"zoning.renderer().setSymbol(symbol)\n",[212,1058,1060],{"class":122,"line":1059},27,[212,1061,1062],{"class":221},"zoning.triggerRepaint()\n",[14,1064,1065,1067,1068,1071,1072,1075,1076,1079],{},[171,1066,413],{}," The hatch's own line is set through ",[179,1069,1070],{},"setSubSymbol()",", which takes a whole ",[179,1073,1074],{},"QgsLineSymbol"," rather than a symbol layer — the same nesting rule that applies to marker lines and point-pattern fills. ",[179,1077,1078],{},"Qt.NoBrush"," on the outline layer is what makes it an outline rather than a second fill; without it you have painted an opaque black polygon over your hatch. Putting the outline last is not cosmetic: a hatch drawn over the border leaves ragged ends at the polygon edge, and the crisp line on top hides them.",[14,1081,1082,1085],{},[179,1083,1084],{},"setDistance()"," is the spacing between hatch lines in the layer's output unit. Anything under about 1.5 mm produces a solid-looking wash at print resolution and an enormous number of primitives at draw time, so it is the first thing to raise when a hatched layer makes the canvas crawl.",[160,1087,1089],{"id":1088},"reuse-a-stack-across-renderer-classes","Reuse a stack across renderer classes",[14,1091,1092],{},"Building an elaborate stack once and applying it to every class of a categorized renderer is the normal case, and it has one hazard.",[203,1094,1096],{"className":205,"code":1095,"language":207,"meta":208,"style":208},"for category in renderer.categories():\n    clone = template_symbol.clone()\n    clone.symbolLayer(1).setColor(category_colour[category.value()])\n    renderer.updateCategorySymbol(renderer.categories().index(category), clone)\n",[179,1097,1098,1110,1120,1130],{"__ignoreMap":208},[212,1099,1100,1102,1105,1107],{"class":122,"line":214},[212,1101,324],{"class":217},[212,1103,1104],{"class":221}," category ",[212,1106,330],{"class":217},[212,1108,1109],{"class":221}," renderer.categories():\n",[212,1111,1112,1115,1117],{"class":122,"line":231},[212,1113,1114],{"class":221},"    clone ",[212,1116,270],{"class":217},[212,1118,1119],{"class":221}," template_symbol.clone()\n",[212,1121,1122,1125,1127],{"class":122,"line":244},[212,1123,1124],{"class":221},"    clone.symbolLayer(",[212,1126,666],{"class":283},[212,1128,1129],{"class":221},").setColor(category_colour[category.value()])\n",[212,1131,1132],{"class":122,"line":257},[212,1133,1134],{"class":221},"    renderer.updateCategorySymbol(renderer.categories().index(category), clone)\n",[14,1136,1137,414,1139,1142],{},[171,1138,413],{},[179,1140,1141],{},"clone()"," is mandatory. Symbols are reference types, so assigning the same object to several categories makes them share every subsequent edit — change one colour and all of them change, which is exactly the bug that looks like \"the renderer is ignoring my colours\". Cloning also copies the whole stack including data-defined overrides, so a template built once carries its expressions everywhere.",[160,1144,1146],{"id":1145},"qgis-version-compatibility","QGIS version compatibility",[14,1148,1149,1150,1152],{},"The examples target ",[171,1151,173],{}," (Python 3.12).",[1154,1155,1156,1172],"table",{},[1157,1158,1159],"thead",{},[1160,1161,1162,1166,1169],"tr",{},[1163,1164,1165],"th",{},"QGIS version",[1163,1167,1168],{},"Python",[1163,1170,1171],{},"Notes",[1173,1174,1175,1197,1211,1225,1236],"tbody",{},[1160,1176,1177,1181,1184],{},[1178,1179,1180],"td",{},"3.16 LTR",[1178,1182,1183],{},"3.7",[1178,1185,1186,1189,1190,1189,1193,1196],{},[179,1187,1188],{},"appendSymbolLayer"," \u002F ",[179,1191,1192],{},"insertSymbolLayer",[179,1194,1195],{},"deleteSymbolLayer"," all present and unchanged.",[1160,1198,1199,1202,1205],{},[1178,1200,1201],{},"3.22 LTR",[1178,1203,1204],{},"3.9",[1178,1206,1207,1210],{},[179,1208,1209],{},"QgsUnitTypes.RenderMetersInMapUnits"," available for ground-unit widths.",[1160,1212,1213,1216,1218],{},[1178,1214,1215],{},"3.28 LTR",[1178,1217,1204],{},[1178,1219,1220,1221,1224],{},"Symbol-layer property enums still flat (",[179,1222,1223],{},"QgsSymbolLayer.PropertyStrokeWidth",").",[1160,1226,1227,1230,1233],{},[1178,1228,1229],{},"3.34 LTR",[1178,1231,1232],{},"3.12",[1178,1234,1235],{},"Baseline for this page.",[1160,1237,1238,1241,1243],{},[1178,1239,1240],{},"3.36+",[1178,1242,1232],{},[1178,1244,1245,1246,1249],{},"Property enums gain the scoped ",[179,1247,1248],{},"QgsSymbolLayer.Property.StrokeWidth"," form; flat names remain as aliases.",[160,1251,1253],{"id":1252},"troubleshooting","Troubleshooting",[165,1255,1256,1265,1278,1293,1303,1312,1318],{},[168,1257,1258,1261,1262,1264],{},[171,1259,1260],{},"A stray blue line appears under everything."," The symbol's default layer 0 was never deleted. Call ",[179,1263,421],{}," immediately after constructing the symbol.",[168,1266,1267,1270,1271,618,1274,1277],{},[171,1268,1269],{},"The bright line escapes the casing at corners."," Caps and joins default to flat and mitre. Set ",[179,1272,1273],{},"Qt.RoundCap",[179,1275,1276],{},"Qt.RoundJoin"," on both layers.",[168,1279,1280,414,1283,1286,1287,1289,1290,186],{},[171,1281,1282],{},"The canvas does not change.",[179,1284,1285],{},"triggerRepaint()"," was not called, or the renderer is categorized and ",[179,1288,629],{}," raised on the way past — check ",[179,1291,1292],{},"type(layer.renderer()).__name__",[168,1294,1295,1298,1299,1302],{},[171,1296,1297],{},"Changing one category's colour changes them all."," Symbols were assigned by reference. Use ",[179,1300,1301],{},"symbol.clone()"," per category.",[168,1304,1305,1308,1309,186],{},[171,1306,1307],{},"The legend still shows the old symbol."," Repainting the canvas does not refresh the layer tree. Call ",[179,1310,1311],{},"iface.layerTreeView().refreshLayerSymbology(layer.id())",[168,1313,1314,1317],{},[171,1315,1316],{},"The hatch overruns the polygon edge."," The outline layer is below the pattern fill. Move it to the top of the stack.",[168,1319,1320,1323,1324,1326],{},[171,1321,1322],{},"The layer became slow after adding a pattern fill."," The hatch spacing is too fine. Raise ",[179,1325,1084],{}," or switch to a plain fill at small scales.",[160,1328,1330],{"id":1329},"conclusion","Conclusion",[14,1332,1333,1334,1336],{},"A symbol is a list, and the list is painted from index 0 upward. Construct the symbol, delete the default layer, append in painting order, and repaint. Use ",[179,1335,801],{}," to slide something beneath a design you want to keep, iterate backwards when deleting, and clone before assigning a stack to more than one renderer class.",[160,1338,1340],{"id":1339},"frequently-asked-questions","Frequently Asked Questions",[14,1342,1343,1346,1347,1350,1351,1354,1355,1358],{},[171,1344,1345],{},"How do I copy a stack I built in the GUI into a script?","\nStyle the layer in the interface, then in the Python console run ",[179,1348,1349],{},"[sl.properties() for sl in layer.renderer().symbol().symbolLayers()]",". Each dictionary can be passed straight to ",[179,1352,1353],{},"QgsSymbolLayerRegistry.instance().createSymbolLayer(type_name, props)",", or more simply to the matching ",[179,1356,1357],{},"createSimple()"," helper.",[14,1360,1361,1364,1365,1368],{},[171,1362,1363],{},"Can symbol layers in one stack use different units?","\nYes — each symbol layer carries its own ",[179,1366,1367],{},"outputUnit()",". Mixing a map-unit casing with a millimetre centre line is a deliberate and useful combination, because the casing tracks the real road width while the centre line stays visible at every scale.",[14,1370,1371,1374,1375,1379],{},[171,1372,1373],{},"Is there a limit on stack depth?","\nNo hard limit, but rendering cost is roughly the number of layers multiplied by the number of features. Three or four layers is normal, above six is usually a sign that a ",[21,1376,1378],{"href":1377},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fgeometry-generator-symbol-layer-pyqgis\u002F","geometry generator"," or a second map layer would express the idea more cheaply.",[14,1381,1382,1385,1386,1389],{},[171,1383,1384],{},"How do I turn one layer of the stack off without deleting it?","\nCall ",[179,1387,1388],{},"symbol_layer.setEnabled(False)",". The layer stays in the list with its settings intact, which is far friendlier than deleting and rebuilding when you are experimenting.",[14,1391,1392,1395,1396,1400],{},[171,1393,1394],{},"Does saving the project preserve the stack?","\nYes. The full symbol definition is written into the project file, and ",[21,1397,1399],{"href":1398},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-and-load-qml-style-pyqgis\u002F","saving a QML style"," exports the same definition as a portable sidecar you can apply to another layer.",[160,1402,1404],{"id":1403},"related","Related",[165,1406,1407,1412,1418,1423,1429],{},[168,1408,1409,1411],{},[21,1410,24],{"href":23}," — the guide this recipe belongs to",[168,1413,1414],{},[21,1415,1417],{"href":1416},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fuse-svg-marker-symbol-pyqgis\u002F","Use SVG and Font Markers in PyQGIS",[168,1419,1420],{},[21,1421,1422],{"href":1377},"Draw with a Geometry Generator in PyQGIS",[168,1424,1425],{},[21,1426,1428],{"href":1427},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer Symbol Colour in PyQGIS",[168,1430,1431],{},[21,1432,1434],{"href":1433},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Frule-based-renderer-pyqgis\u002F","Create a Rule-Based Renderer in PyQGIS",[1436,1437,1438],"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":208,"searchDepth":231,"depth":231,"links":1440},[1441,1442,1443,1444,1445,1446,1447,1448,1449,1450],{"id":162,"depth":231,"text":163},{"id":197,"depth":231,"text":198},{"id":494,"depth":231,"text":495},{"id":830,"depth":231,"text":831},{"id":1088,"depth":231,"text":1089},{"id":1145,"depth":231,"text":1146},{"id":1252,"depth":231,"text":1253},{"id":1329,"depth":231,"text":1330},{"id":1339,"depth":231,"text":1340},{"id":1403,"depth":231,"text":1404},"Build layered QGIS symbols from Python — road casings, hatched fills under crisp outlines, offset shadows — using appendSymbolLayer, insertSymbolLayer and the draw order that decides what wins.","md",{"slug":1454,"type":1455,"breadcrumb":1456,"datePublished":1457,"dateModified":1457},"stack-symbol-layers-pyqgis","article","Stack Symbol Layers","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis",{"title":5,"description":1451},"pyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis\u002Findex","4sLFusIBXUv8JNgFq-zCnq2vz0wP9ZT4KbOJcYlZp7k",1787823360561]