[{"data":1,"prerenderedAt":1646},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis":3},{"id":4,"title":5,"body":6,"description":1635,"extension":1636,"meta":1637,"navigation":266,"path":1642,"seo":1643,"stem":1644,"__hash__":1645},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis\u002Findex.md","Create a Map Theme in PyQGIS",{"type":7,"value":8,"toc":1621},"minimark",[9,13,22,36,200,205,227,231,234,344,366,370,377,537,558,561,631,644,741,745,753,883,899,903,906,1144,1153,1157,1160,1314,1337,1340,1344,1352,1355,1359,1365,1460,1464,1527,1531,1543,1547,1557,1567,1573,1584,1588,1618],[10,11,5],"h1",{"id":12},"create-a-map-theme-in-pyqgis",[14,15,16,17,21],"p",{},"Creating a theme by hand means arranging the layer tree exactly as you want it and clicking ",[18,19,20],"em",{},"Add theme",". Doing it from Python is the same two steps, but it opens a door the interface does not: a script can rebuild the entire theme collection from a declaration, so the themes in a project become something you version rather than something you remember.",[14,23,24,25,30,31,35],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002F","Map Themes & Layer Visibility in PyQGIS",". It covers capturing the current state, constructing a theme record explicitly without touching the tree, running with no ",[32,33,34],"code",{},"iface"," available, and making the whole thing safe to re-run.",[14,37,38],{},[39,40,45,49,53,60,77,86,96,102,110,116,118,122,125,129,136,140,145,149,154,158,162,166,169,173,177,180,183,191,196],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 304","img","Two routes to a map theme: capturing the current layer tree state, or constructing a theme record layer by layer without disturbing the canvas","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Capture the state, or declare the record",[50,51,52],"desc",{},"The capture route arranges the layer tree first and then snapshots it, which changes what the user sees. The declarative route builds a theme record from a list of layer records and inserts it, leaving the current canvas untouched. Both produce an identical entry in the project's theme collection.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","304","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"cmtArrow","0 0 10 10","8","5","7","auto-start-reverse",[73,74],"path",{"d":75,"fill":76},"M0 0 L10 5 L0 10 z","#2f3b35",[78,79,85],"text",{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One of these disturbs the user's canvas; the other does not",[54,87],{"x":88,"y":89,"width":90,"height":91,"rx":92,"fill":93,"stroke":94,"style":95},"24","54","330","150","10","#fdf2e2","#b45309","stroke-width:2.5",[78,97,101],{"x":98,"y":99,"style":100,"fill":94,"textAnchor":84},"189","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","capture the current state",[54,103],{"x":104,"y":105,"width":106,"height":107,"rx":69,"fill":108,"stroke":94,"style":109},"46","96","88","40","#fffdf7","stroke-width:1.6",[78,111,115],{"x":112,"y":113,"style":114,"fill":76,"textAnchor":84},"90","121","text-anchor:middle;font-size:10px;font-family:sans-serif","set checkboxes",[54,117],{"x":91,"y":105,"width":106,"height":107,"rx":69,"fill":108,"stroke":94,"style":109},[78,119,121],{"x":120,"y":113,"style":114,"fill":76,"textAnchor":84},"194","canvas redraws",[54,123],{"x":124,"y":105,"width":106,"height":107,"rx":69,"fill":108,"stroke":94,"style":109},"254",[78,126,128],{"x":127,"y":113,"style":114,"fill":76,"textAnchor":84},"298","snapshot",[130,131],"line",{"x1":132,"y1":133,"x2":134,"y2":133,"stroke":76,"style":135},"134","116","146","stroke-width:1.8;marker-end:url(#cmtArrow)",[130,137],{"x1":138,"y1":133,"x2":139,"y2":133,"stroke":76,"style":135},"238","250",[78,141,144],{"x":98,"y":142,"style":143,"fill":76,"textAnchor":84},"168","text-anchor:middle;font-size:10.5px;font-family:sans-serif","simple, but the user watches layers flicker",[78,146,148],{"x":98,"y":147,"style":143,"fill":76,"textAnchor":84},"188","and the state must be restored afterwards",[54,150],{"x":151,"y":89,"width":90,"height":91,"rx":92,"fill":152,"stroke":153,"style":95},"406","#edf8e9","#15803d",[78,155,157],{"x":156,"y":99,"style":100,"fill":153,"textAnchor":84},"571","declare the record",[54,159],{"x":160,"y":105,"width":161,"height":107,"rx":69,"fill":108,"stroke":153,"style":109},"428","136",[78,163,165],{"x":164,"y":113,"style":114,"fill":76,"textAnchor":84},"496","build layer records",[54,167],{"x":168,"y":105,"width":161,"height":107,"rx":69,"fill":108,"stroke":153,"style":109},"580",[78,170,172],{"x":171,"y":113,"style":114,"fill":76,"textAnchor":84},"648","insert into collection",[130,174],{"x1":175,"y1":133,"x2":176,"y2":133,"stroke":76,"style":135},"564","576",[78,178,179],{"x":156,"y":142,"style":143,"fill":76,"textAnchor":84},"the canvas never changes,",[78,181,182],{"x":156,"y":147,"style":143,"fill":76,"textAnchor":84},"so it is safe inside a plugin",[54,184],{"x":185,"y":186,"width":187,"height":188,"rx":68,"fill":189,"stroke":190,"style":95},"200","228","360","56","#eef7f4","#0f766e",[78,192,195],{"x":80,"y":193,"style":194,"fill":190,"textAnchor":84},"252","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","project.mapThemeCollection()",[78,197,199],{"x":80,"y":198,"style":143,"fill":76,"textAnchor":84},"272","identical entry either way, saved with the project",[201,202,204],"h2",{"id":203},"prerequisites","Prerequisites",[206,207,208,216,219],"ul",{},[209,210,211,215],"li",{},[212,213,214],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[209,217,218],{},"A project with several layers, ideally organised into groups so the group behaviour is visible.",[209,220,221,222,226],{},"If you intend to record styles as well as visibility, the ",[26,223,225],{"href":224},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fsave-and-load-qml-style-pyqgis\u002F","named styles"," must exist on the layers before the theme is created.",[201,228,230],{"id":229},"capture-what-is-on-screen-now","Capture what is on screen now",[14,232,233],{},"The shortest route takes a snapshot of whatever the tree currently says.",[235,236,241],"pre",{"className":237,"code":238,"language":239,"meta":240,"style":240},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsMapThemeCollection\n\nproject = QgsProject.instance()\nroot = project.layerTreeRoot()\nmodel = iface.layerTreeView().layerTreeModel()\n\nrecord = QgsMapThemeCollection.createThemeFromCurrentState(root, model)\nproject.mapThemeCollection().insert(\"night\", record)\nproject.setDirty(True)\n","python","",[32,242,243,261,268,280,291,302,307,318,331],{"__ignoreMap":240},[244,245,247,251,255,258],"span",{"class":130,"line":246},1,[244,248,250],{"class":249},"snl16","from",[244,252,254],{"class":253},"s95oV"," qgis.core ",[244,256,257],{"class":249},"import",[244,259,260],{"class":253}," QgsProject, QgsMapThemeCollection\n",[244,262,264],{"class":130,"line":263},2,[244,265,267],{"emptyLinePlaceholder":266},true,"\n",[244,269,271,274,277],{"class":130,"line":270},3,[244,272,273],{"class":253},"project ",[244,275,276],{"class":249},"=",[244,278,279],{"class":253}," QgsProject.instance()\n",[244,281,283,286,288],{"class":130,"line":282},4,[244,284,285],{"class":253},"root ",[244,287,276],{"class":249},[244,289,290],{"class":253}," project.layerTreeRoot()\n",[244,292,294,297,299],{"class":130,"line":293},5,[244,295,296],{"class":253},"model ",[244,298,276],{"class":249},[244,300,301],{"class":253}," iface.layerTreeView().layerTreeModel()\n",[244,303,305],{"class":130,"line":304},6,[244,306,267],{"emptyLinePlaceholder":266},[244,308,310,313,315],{"class":130,"line":309},7,[244,311,312],{"class":253},"record ",[244,314,276],{"class":249},[244,316,317],{"class":253}," QgsMapThemeCollection.createThemeFromCurrentState(root, model)\n",[244,319,321,324,328],{"class":130,"line":320},8,[244,322,323],{"class":253},"project.mapThemeCollection().insert(",[244,325,327],{"class":326},"sU2Wk","\"night\"",[244,329,330],{"class":253},", record)\n",[244,332,334,337,341],{"class":130,"line":333},9,[244,335,336],{"class":253},"project.setDirty(",[244,338,340],{"class":339},"sDLfK","True",[244,342,343],{"class":253},")\n",[14,345,346,349,350,353,354,357,358,361,362,365],{},[212,347,348],{},"Breakdown:"," ",[32,351,352],{},"createThemeFromCurrentState()"," is a static method on the collection class, not an instance method, which is why it takes the root and model as arguments rather than reading them from a project it does not have. ",[32,355,356],{},"setDirty(True)"," marks the project as modified so QGIS offers to save it; without that line a theme created by a script can be lost when the user closes the project believing nothing has changed. ",[32,359,360],{},"insert()"," replaces any existing theme of the same name in silence, which is exactly what you want when re-running a build script and exactly what you do not want when a user names a new theme carelessly — check ",[32,363,364],{},"mapThemes()"," first if the name came from a dialog.",[201,367,369],{"id":368},"build-a-theme-without-touching-the-canvas","Build a theme without touching the canvas",[14,371,372,373,376],{},"Inside a plugin, rearranging the user's layer tree to capture a theme is rude and hard to undo cleanly. ",[32,374,375],{},"QgsMapThemeCollection.MapThemeRecord"," can be assembled directly instead.",[235,378,380],{"className":237,"code":379,"language":239,"meta":240,"style":240},"from qgis.core import QgsMapThemeCollection\n\nrecord = QgsMapThemeCollection.MapThemeRecord()\n\nfor name, style in ((\"basemap\", \"\"), (\"buildings\", \"by age\"), (\"flood extent\", \"1 in 100\")):\n    layer = project.mapLayersByName(name)[0]\n    layer_record = QgsMapThemeCollection.MapThemeLayerRecord(layer)\n    layer_record.usingCurrentStyle = bool(style)\n    layer_record.currentStyle = style\n    layer_record.usingLegendItems = False\n    record.addLayerRecord(layer_record)\n\nproject.mapThemeCollection().insert(\"flood risk\", record)\n",[32,381,382,393,397,406,410,457,472,482,495,505,516,522,527],{"__ignoreMap":240},[244,383,384,386,388,390],{"class":130,"line":246},[244,385,250],{"class":249},[244,387,254],{"class":253},[244,389,257],{"class":249},[244,391,392],{"class":253}," QgsMapThemeCollection\n",[244,394,395],{"class":130,"line":263},[244,396,267],{"emptyLinePlaceholder":266},[244,398,399,401,403],{"class":130,"line":270},[244,400,312],{"class":253},[244,402,276],{"class":249},[244,404,405],{"class":253}," QgsMapThemeCollection.MapThemeRecord()\n",[244,407,408],{"class":130,"line":282},[244,409,267],{"emptyLinePlaceholder":266},[244,411,412,415,418,421,424,427,430,433,436,439,441,444,446,449,451,454],{"class":130,"line":293},[244,413,414],{"class":249},"for",[244,416,417],{"class":253}," name, style ",[244,419,420],{"class":249},"in",[244,422,423],{"class":253}," ((",[244,425,426],{"class":326},"\"basemap\"",[244,428,429],{"class":253},", ",[244,431,432],{"class":326},"\"\"",[244,434,435],{"class":253},"), (",[244,437,438],{"class":326},"\"buildings\"",[244,440,429],{"class":253},[244,442,443],{"class":326},"\"by age\"",[244,445,435],{"class":253},[244,447,448],{"class":326},"\"flood extent\"",[244,450,429],{"class":253},[244,452,453],{"class":326},"\"1 in 100\"",[244,455,456],{"class":253},")):\n",[244,458,459,462,464,467,469],{"class":130,"line":304},[244,460,461],{"class":253},"    layer ",[244,463,276],{"class":249},[244,465,466],{"class":253}," project.mapLayersByName(name)[",[244,468,56],{"class":339},[244,470,471],{"class":253},"]\n",[244,473,474,477,479],{"class":130,"line":309},[244,475,476],{"class":253},"    layer_record ",[244,478,276],{"class":249},[244,480,481],{"class":253}," QgsMapThemeCollection.MapThemeLayerRecord(layer)\n",[244,483,484,487,489,492],{"class":130,"line":320},[244,485,486],{"class":253},"    layer_record.usingCurrentStyle ",[244,488,276],{"class":249},[244,490,491],{"class":339}," bool",[244,493,494],{"class":253},"(style)\n",[244,496,497,500,502],{"class":130,"line":333},[244,498,499],{"class":253},"    layer_record.currentStyle ",[244,501,276],{"class":249},[244,503,504],{"class":253}," style\n",[244,506,508,511,513],{"class":130,"line":507},10,[244,509,510],{"class":253},"    layer_record.usingLegendItems ",[244,512,276],{"class":249},[244,514,515],{"class":339}," False\n",[244,517,519],{"class":130,"line":518},11,[244,520,521],{"class":253},"    record.addLayerRecord(layer_record)\n",[244,523,525],{"class":130,"line":524},12,[244,526,267],{"emptyLinePlaceholder":266},[244,528,530,532,535],{"class":130,"line":529},13,[244,531,323],{"class":253},[244,533,534],{"class":326},"\"flood risk\"",[244,536,330],{"class":253},[14,538,539,541,542,545,546,549,550,553,554,557],{},[212,540,348],{}," A ",[32,543,544],{},"MapThemeLayerRecord"," holds the layer plus four flags, and the ones that matter here are ",[32,547,548],{},"usingCurrentStyle"," — meaning \"this theme pins a named style\" — and ",[32,551,552],{},"usingLegendItems",", which when ",[32,555,556],{},"False"," means \"show every legend entry\" rather than a recorded subset. Only layers present in the record are visible when the theme is applied; every layer you omit is hidden, so this list is the complete definition of the map rather than a set of additions. Building the record this way never touches the layer tree, so the canvas the user is looking at is undisturbed until they choose the theme themselves.",[14,559,560],{},"The style name must match an existing entry in the layer's style manager. A name that does not exist is not an error — the layer simply keeps its current style — so validate before inserting:",[235,562,564],{"className":237,"code":563,"language":239,"meta":240,"style":240},"if style and style not in layer.styleManager().styles():\n    raise ValueError(f\"{name} has no style called {style!r}\")\n",[32,565,566,588],{"__ignoreMap":240},[244,567,568,571,574,577,579,582,585],{"class":130,"line":246},[244,569,570],{"class":249},"if",[244,572,573],{"class":253}," style ",[244,575,576],{"class":249},"and",[244,578,573],{"class":253},[244,580,581],{"class":249},"not",[244,583,584],{"class":249}," in",[244,586,587],{"class":253}," layer.styleManager().styles():\n",[244,589,590,593,596,599,602,605,608,611,614,617,619,622,625,627,629],{"class":130,"line":263},[244,591,592],{"class":249},"    raise",[244,594,595],{"class":339}," ValueError",[244,597,598],{"class":253},"(",[244,600,601],{"class":249},"f",[244,603,604],{"class":326},"\"",[244,606,607],{"class":339},"{",[244,609,610],{"class":253},"name",[244,612,613],{"class":339},"}",[244,615,616],{"class":326}," has no style called ",[244,618,607],{"class":339},[244,620,621],{"class":253},"style",[244,623,624],{"class":249},"!r",[244,626,613],{"class":339},[244,628,604],{"class":326},[244,630,343],{"class":253},[14,632,633,635,636,639,640,643],{},[212,634,348],{}," Failing here, at build time, converts a silent wrong-looking map into an immediate and precise complaint. ",[32,637,638],{},"styleManager().styles()"," returns the list of names, with the default one usually called ",[32,641,642],{},"default"," unless it has been renamed.",[14,645,646],{},[39,647,650,653,656,659,662,667,670,679,684,687,690,693,696,699,703,706,709,712,715,720,726,730,734,738],{"viewBox":648,"role":42,"ariaLabel":649,"xmlns":44},"0 0 760 292","The fields of a map theme layer record: the layer itself, whether a named style is pinned, which style, and whether a subset of legend items is recorded",[46,651,652],{},"Inside a map theme layer record",[50,654,655],{},"Each layer record names one layer and carries flags. Using current style pins a named style from the layer's style manager. Using legend items restricts the theme to a recorded subset of legend entries. Any layer with no record at all is hidden when the theme is applied.",[54,657],{"x":56,"y":56,"width":57,"height":658,"fill":59},"292",[78,660,661],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Omission is how a theme hides something",[54,663],{"x":107,"y":664,"width":665,"height":666,"rx":92,"fill":108,"stroke":190,"style":95},"52","680","120",[78,668,544],{"x":80,"y":669,"style":100,"fill":190,"textAnchor":84},"78",[54,671],{"x":672,"y":673,"width":91,"height":674,"rx":675,"fill":676,"stroke":677,"style":678},"64","94","58","6","#eff3ff","#2563eb","stroke-width:1.8",[78,680,683],{"x":681,"y":133,"style":682,"fill":677,"textAnchor":84},"139","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:monospace","layer",[78,685,686],{"x":681,"y":161,"style":114,"fill":76,"textAnchor":84},"which layer this is",[54,688],{"x":689,"y":673,"width":91,"height":674,"rx":675,"fill":93,"stroke":94,"style":678},"230",[78,691,548],{"x":692,"y":133,"style":682,"fill":94,"textAnchor":84},"305",[78,694,695],{"x":692,"y":161,"style":114,"fill":76,"textAnchor":84},"pin a named style?",[54,697],{"x":698,"y":673,"width":91,"height":674,"rx":675,"fill":93,"stroke":94,"style":678},"396",[78,700,702],{"x":701,"y":133,"style":682,"fill":94,"textAnchor":84},"471","currentStyle",[78,704,705],{"x":701,"y":161,"style":114,"fill":76,"textAnchor":84},"the style's name",[54,707],{"x":708,"y":673,"width":91,"height":674,"rx":675,"fill":152,"stroke":153,"style":678},"562",[78,710,552],{"x":711,"y":133,"style":682,"fill":153,"textAnchor":84},"637",[78,713,714],{"x":711,"y":161,"style":114,"fill":76,"textAnchor":84},"a subset of classes?",[54,716],{"x":717,"y":185,"width":689,"height":718,"rx":68,"fill":152,"stroke":153,"style":719},"140","62","stroke-width:2",[78,721,725],{"x":722,"y":723,"style":724,"fill":153,"textAnchor":84},"255","226","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","has a record",[78,727,729],{"x":722,"y":728,"style":143,"fill":76,"textAnchor":84},"248","drawn when the theme applies",[54,731],{"x":732,"y":185,"width":689,"height":718,"rx":68,"fill":108,"stroke":733,"style":719},"392","#b91c1c",[78,735,737],{"x":736,"y":723,"style":724,"fill":733,"textAnchor":84},"507","no record",[78,739,740],{"x":736,"y":728,"style":143,"fill":76,"textAnchor":84},"hidden — there is no third state",[201,742,744],{"id":743},"running-without-iface","Running without iface",[14,746,747,748,752],{},"A ",[26,749,751],{"href":750},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","standalone script"," has no interface object, so the model has to be built.",[235,754,756],{"className":237,"code":755,"language":239,"meta":240,"style":240},"from qgis.core import QgsApplication, QgsProject, QgsLayerTreeModel\n\nqgs = QgsApplication([], False)\nqgs.initQgis()\n\nproject = QgsProject.instance()\nproject.read(\"\u002Fdata\u002Fprojects\u002Fflooding.qgz\")\n\nroot = project.layerTreeRoot()\nmodel = QgsLayerTreeModel(root)\nmodel.setFlag(QgsLayerTreeModel.ShowLegend, True)\n\nrecord = QgsMapThemeCollection.createThemeFromCurrentState(root, model)\nproject.mapThemeCollection().insert(\"automated\", record)\nproject.write()\n\nqgs.exitQgis()\n",[32,757,758,769,773,787,792,796,804,814,818,826,835,844,848,856,866,872,877],{"__ignoreMap":240},[244,759,760,762,764,766],{"class":130,"line":246},[244,761,250],{"class":249},[244,763,254],{"class":253},[244,765,257],{"class":249},[244,767,768],{"class":253}," QgsApplication, QgsProject, QgsLayerTreeModel\n",[244,770,771],{"class":130,"line":263},[244,772,267],{"emptyLinePlaceholder":266},[244,774,775,778,780,783,785],{"class":130,"line":270},[244,776,777],{"class":253},"qgs ",[244,779,276],{"class":249},[244,781,782],{"class":253}," QgsApplication([], ",[244,784,556],{"class":339},[244,786,343],{"class":253},[244,788,789],{"class":130,"line":282},[244,790,791],{"class":253},"qgs.initQgis()\n",[244,793,794],{"class":130,"line":293},[244,795,267],{"emptyLinePlaceholder":266},[244,797,798,800,802],{"class":130,"line":304},[244,799,273],{"class":253},[244,801,276],{"class":249},[244,803,279],{"class":253},[244,805,806,809,812],{"class":130,"line":309},[244,807,808],{"class":253},"project.read(",[244,810,811],{"class":326},"\"\u002Fdata\u002Fprojects\u002Fflooding.qgz\"",[244,813,343],{"class":253},[244,815,816],{"class":130,"line":320},[244,817,267],{"emptyLinePlaceholder":266},[244,819,820,822,824],{"class":130,"line":333},[244,821,285],{"class":253},[244,823,276],{"class":249},[244,825,290],{"class":253},[244,827,828,830,832],{"class":130,"line":507},[244,829,296],{"class":253},[244,831,276],{"class":249},[244,833,834],{"class":253}," QgsLayerTreeModel(root)\n",[244,836,837,840,842],{"class":130,"line":518},[244,838,839],{"class":253},"model.setFlag(QgsLayerTreeModel.ShowLegend, ",[244,841,340],{"class":339},[244,843,343],{"class":253},[244,845,846],{"class":130,"line":524},[244,847,267],{"emptyLinePlaceholder":266},[244,849,850,852,854],{"class":130,"line":529},[244,851,312],{"class":253},[244,853,276],{"class":249},[244,855,317],{"class":253},[244,857,859,861,864],{"class":130,"line":858},14,[244,860,323],{"class":253},[244,862,863],{"class":326},"\"automated\"",[244,865,330],{"class":253},[244,867,869],{"class":130,"line":868},15,[244,870,871],{"class":253},"project.write()\n",[244,873,875],{"class":130,"line":874},16,[244,876,267],{"emptyLinePlaceholder":266},[244,878,880],{"class":130,"line":879},17,[244,881,882],{"class":253},"qgs.exitQgis()\n",[14,884,885,349,887,890,891,894,895,898],{},[212,886,348],{},[32,888,889],{},"QgsLayerTreeModel(root)"," builds the model over the project's existing tree; it must be kept alive while the theme is created, so assigning it to a local that stays in scope matters in a way it does not in the console. ",[32,892,893],{},"setFlag(ShowLegend, True)"," is what causes the model to create legend nodes at all — without it a theme captured headlessly records visibility but no legend-item state, and applying it later ticks every class regardless of what was intended. ",[32,896,897],{},"project.write()"," with no argument saves back to the path it was read from.",[201,900,902],{"id":901},"make-the-script-idempotent","Make the script idempotent",[14,904,905],{},"A build script that is safe to run twice is worth the extra six lines, because it can be wired into project load or a scheduled job.",[235,907,909],{"className":237,"code":908,"language":239,"meta":240,"style":240},"WANTED = {\n    \"base\": [\"basemap\", \"boundaries\"],\n    \"flood risk\": [\"basemap\", \"flood extent\", \"buildings\"],\n}\n\ncollection = project.mapThemeCollection()\nfor existing in list(collection.mapThemes()):\n    if existing not in WANTED:\n        collection.removeMapTheme(existing)\n\nfor theme_name, layer_names in WANTED.items():\n    record = QgsMapThemeCollection.MapThemeRecord()\n    for layer_name in layer_names:\n        matches = project.mapLayersByName(layer_name)\n        if not matches:\n            raise LookupError(f\"theme {theme_name!r} wants missing layer {layer_name!r}\")\n        record.addLayerRecord(\n            QgsMapThemeCollection.MapThemeLayerRecord(matches[0])\n        )\n    collection.insert(theme_name, record)\n",[32,910,911,922,940,959,964,968,978,993,1010,1015,1019,1033,1042,1055,1065,1076,1116,1121,1132,1138],{"__ignoreMap":240},[244,912,913,916,919],{"class":130,"line":246},[244,914,915],{"class":339},"WANTED",[244,917,918],{"class":249}," =",[244,920,921],{"class":253}," {\n",[244,923,924,927,930,932,934,937],{"class":130,"line":263},[244,925,926],{"class":326},"    \"base\"",[244,928,929],{"class":253},": [",[244,931,426],{"class":326},[244,933,429],{"class":253},[244,935,936],{"class":326},"\"boundaries\"",[244,938,939],{"class":253},"],\n",[244,941,942,945,947,949,951,953,955,957],{"class":130,"line":270},[244,943,944],{"class":326},"    \"flood risk\"",[244,946,929],{"class":253},[244,948,426],{"class":326},[244,950,429],{"class":253},[244,952,448],{"class":326},[244,954,429],{"class":253},[244,956,438],{"class":326},[244,958,939],{"class":253},[244,960,961],{"class":130,"line":282},[244,962,963],{"class":253},"}\n",[244,965,966],{"class":130,"line":293},[244,967,267],{"emptyLinePlaceholder":266},[244,969,970,973,975],{"class":130,"line":304},[244,971,972],{"class":253},"collection ",[244,974,276],{"class":249},[244,976,977],{"class":253}," project.mapThemeCollection()\n",[244,979,980,982,985,987,990],{"class":130,"line":309},[244,981,414],{"class":249},[244,983,984],{"class":253}," existing ",[244,986,420],{"class":249},[244,988,989],{"class":339}," list",[244,991,992],{"class":253},"(collection.mapThemes()):\n",[244,994,995,998,1000,1002,1004,1007],{"class":130,"line":320},[244,996,997],{"class":249},"    if",[244,999,984],{"class":253},[244,1001,581],{"class":249},[244,1003,584],{"class":249},[244,1005,1006],{"class":339}," WANTED",[244,1008,1009],{"class":253},":\n",[244,1011,1012],{"class":130,"line":333},[244,1013,1014],{"class":253},"        collection.removeMapTheme(existing)\n",[244,1016,1017],{"class":130,"line":507},[244,1018,267],{"emptyLinePlaceholder":266},[244,1020,1021,1023,1026,1028,1030],{"class":130,"line":518},[244,1022,414],{"class":249},[244,1024,1025],{"class":253}," theme_name, layer_names ",[244,1027,420],{"class":249},[244,1029,1006],{"class":339},[244,1031,1032],{"class":253},".items():\n",[244,1034,1035,1038,1040],{"class":130,"line":524},[244,1036,1037],{"class":253},"    record ",[244,1039,276],{"class":249},[244,1041,405],{"class":253},[244,1043,1044,1047,1050,1052],{"class":130,"line":529},[244,1045,1046],{"class":249},"    for",[244,1048,1049],{"class":253}," layer_name ",[244,1051,420],{"class":249},[244,1053,1054],{"class":253}," layer_names:\n",[244,1056,1057,1060,1062],{"class":130,"line":858},[244,1058,1059],{"class":253},"        matches ",[244,1061,276],{"class":249},[244,1063,1064],{"class":253}," project.mapLayersByName(layer_name)\n",[244,1066,1067,1070,1073],{"class":130,"line":868},[244,1068,1069],{"class":249},"        if",[244,1071,1072],{"class":249}," not",[244,1074,1075],{"class":253}," matches:\n",[244,1077,1078,1081,1084,1086,1088,1091,1093,1096,1098,1100,1103,1105,1108,1110,1112,1114],{"class":130,"line":874},[244,1079,1080],{"class":249},"            raise",[244,1082,1083],{"class":339}," LookupError",[244,1085,598],{"class":253},[244,1087,601],{"class":249},[244,1089,1090],{"class":326},"\"theme ",[244,1092,607],{"class":339},[244,1094,1095],{"class":253},"theme_name",[244,1097,624],{"class":249},[244,1099,613],{"class":339},[244,1101,1102],{"class":326}," wants missing layer ",[244,1104,607],{"class":339},[244,1106,1107],{"class":253},"layer_name",[244,1109,624],{"class":249},[244,1111,613],{"class":339},[244,1113,604],{"class":326},[244,1115,343],{"class":253},[244,1117,1118],{"class":130,"line":879},[244,1119,1120],{"class":253},"        record.addLayerRecord(\n",[244,1122,1124,1127,1129],{"class":130,"line":1123},18,[244,1125,1126],{"class":253},"            QgsMapThemeCollection.MapThemeLayerRecord(matches[",[244,1128,56],{"class":339},[244,1130,1131],{"class":253},"])\n",[244,1133,1135],{"class":130,"line":1134},19,[244,1136,1137],{"class":253},"        )\n",[244,1139,1141],{"class":130,"line":1140},20,[244,1142,1143],{"class":253},"    collection.insert(theme_name, record)\n",[14,1145,1146,1148,1149,1152],{},[212,1147,348],{}," Iterating over ",[32,1150,1151],{},"list(collection.mapThemes())"," rather than the live sequence avoids mutating a collection while walking it. Removing themes that are no longer declared is what makes the script the source of truth rather than an additive process that accumulates stale entries. Raising on a missing layer converts the silent failure mode — a theme that shows less than it should — into a stack trace naming both the theme and the layer.",[201,1154,1156],{"id":1155},"record-only-some-of-a-layers-classes","Record only some of a layer's classes",[14,1158,1159],{},"A theme can show part of a categorized layer. That is the difference between three filtered copies of an incidents layer and one layer appearing three ways.",[235,1161,1163],{"className":237,"code":1162,"language":239,"meta":240,"style":240},"from qgis.core import QgsMapThemeCollection\n\nlayer = project.mapLayersByName(\"incidents\")[0]\nrenderer = layer.renderer()\n\nwanted = {\"flooding\", \"subsidence\"}\nrule_keys = [\n    category.uuid()\n    for category in renderer.categories()\n    if category.value() in wanted\n]\n\nlayer_record = QgsMapThemeCollection.MapThemeLayerRecord(layer)\nlayer_record.usingLegendItems = True\nlayer_record.checkedLegendItems = rule_keys\nrecord.addLayerRecord(layer_record)\n",[32,1164,1165,1175,1179,1199,1209,1213,1233,1243,1248,1260,1272,1276,1280,1289,1299,1309],{"__ignoreMap":240},[244,1166,1167,1169,1171,1173],{"class":130,"line":246},[244,1168,250],{"class":249},[244,1170,254],{"class":253},[244,1172,257],{"class":249},[244,1174,392],{"class":253},[244,1176,1177],{"class":130,"line":263},[244,1178,267],{"emptyLinePlaceholder":266},[244,1180,1181,1184,1186,1189,1192,1195,1197],{"class":130,"line":270},[244,1182,1183],{"class":253},"layer ",[244,1185,276],{"class":249},[244,1187,1188],{"class":253}," project.mapLayersByName(",[244,1190,1191],{"class":326},"\"incidents\"",[244,1193,1194],{"class":253},")[",[244,1196,56],{"class":339},[244,1198,471],{"class":253},[244,1200,1201,1204,1206],{"class":130,"line":282},[244,1202,1203],{"class":253},"renderer ",[244,1205,276],{"class":249},[244,1207,1208],{"class":253}," layer.renderer()\n",[244,1210,1211],{"class":130,"line":293},[244,1212,267],{"emptyLinePlaceholder":266},[244,1214,1215,1218,1220,1223,1226,1228,1231],{"class":130,"line":304},[244,1216,1217],{"class":253},"wanted ",[244,1219,276],{"class":249},[244,1221,1222],{"class":253}," {",[244,1224,1225],{"class":326},"\"flooding\"",[244,1227,429],{"class":253},[244,1229,1230],{"class":326},"\"subsidence\"",[244,1232,963],{"class":253},[244,1234,1235,1238,1240],{"class":130,"line":309},[244,1236,1237],{"class":253},"rule_keys ",[244,1239,276],{"class":249},[244,1241,1242],{"class":253}," [\n",[244,1244,1245],{"class":130,"line":320},[244,1246,1247],{"class":253},"    category.uuid()\n",[244,1249,1250,1252,1255,1257],{"class":130,"line":333},[244,1251,1046],{"class":249},[244,1253,1254],{"class":253}," category ",[244,1256,420],{"class":249},[244,1258,1259],{"class":253}," renderer.categories()\n",[244,1261,1262,1264,1267,1269],{"class":130,"line":507},[244,1263,997],{"class":249},[244,1265,1266],{"class":253}," category.value() ",[244,1268,420],{"class":249},[244,1270,1271],{"class":253}," wanted\n",[244,1273,1274],{"class":130,"line":518},[244,1275,471],{"class":253},[244,1277,1278],{"class":130,"line":524},[244,1279,267],{"emptyLinePlaceholder":266},[244,1281,1282,1285,1287],{"class":130,"line":529},[244,1283,1284],{"class":253},"layer_record ",[244,1286,276],{"class":249},[244,1288,481],{"class":253},[244,1290,1291,1294,1296],{"class":130,"line":858},[244,1292,1293],{"class":253},"layer_record.usingLegendItems ",[244,1295,276],{"class":249},[244,1297,1298],{"class":339}," True\n",[244,1300,1301,1304,1306],{"class":130,"line":868},[244,1302,1303],{"class":253},"layer_record.checkedLegendItems ",[244,1305,276],{"class":249},[244,1307,1308],{"class":253}," rule_keys\n",[244,1310,1311],{"class":130,"line":874},[244,1312,1313],{"class":253},"record.addLayerRecord(layer_record)\n",[14,1315,1316,349,1318,1321,1322,1325,1326,1329,1330,1333,1334,1336],{},[212,1317,348],{},[32,1319,1320],{},"checkedLegendItems"," holds legend node keys, not labels — for a categorized renderer each category's ",[32,1323,1324],{},"uuid()"," is that key, and for a rule-based renderer it is the rule's ",[32,1327,1328],{},"ruleKey()",". Setting ",[32,1331,1332],{},"usingLegendItems = True"," is what makes the list mean anything; leave it ",[32,1335,556],{}," and the keys are stored and ignored, which is the usual reason a legend-filtered theme shows everything. Because the keys are generated when the renderer is built, re-classifying the layer invalidates them, so themes of this kind belong in the same script that defines the renderer.",[14,1338,1339],{},"The reward is a legend that matches the map without extra work. Unticked classes vanish from both the canvas and any legend that follows the theme, so a \"flood incidents only\" deliverable needs no separate layer, no subset string and no second style.",[201,1341,1343],{"id":1342},"when-a-theme-is-the-wrong-tool","When a theme is the wrong tool",[14,1345,1346,1347,1351],{},"Themes describe what is visible, and nothing else. They do not store an extent, a scale, a page size or an output path. If two deliverables differ only in where the map is looking, the difference belongs on the ",[26,1348,1350],{"href":1349},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-map-item-and-set-extent-pyqgis\u002F","layout map item"," or in an atlas, not in a theme.",[14,1353,1354],{},"They are also a poor fit when the difference is really about data rather than presentation. Two themes showing the same layer with different subset strings will fight each other, because the subset string lives on the layer and a theme cannot record it. In that case duplicate the layer — QGIS is happy to hold two layers over one file — give each its own filter and style, and let the themes choose between them. The general rule holds up well in practice: if the thing that differs is a property of the tree or the style, use a theme; if it is a property of the data or the page, use something else.",[201,1356,1358],{"id":1357},"qgis-version-compatibility","QGIS version compatibility",[14,1360,1361,1362,1364],{},"The examples target ",[212,1363,214],{}," (Python 3.12).",[1366,1367,1368,1384],"table",{},[1369,1370,1371],"thead",{},[1372,1373,1374,1378,1381],"tr",{},[1375,1376,1377],"th",{},"QGIS version",[1375,1379,1380],{},"Python",[1375,1382,1383],{},"Notes",[1385,1386,1387,1406,1423,1439,1450],"tbody",{},[1372,1388,1389,1393,1396],{},[1390,1391,1392],"td",{},"3.16 LTR",[1390,1394,1395],{},"3.7",[1390,1397,1398,1401,1402,1405],{},[32,1399,1400],{},"QgsMapThemeCollection"," and ",[32,1403,1404],{},"MapThemeRecord"," present; API stable since 3.0.",[1372,1407,1408,1411,1414],{},[1390,1409,1410],{},"3.22 LTR",[1390,1412,1413],{},"3.9",[1390,1415,1416,1418,1419,1422],{},[32,1417,544],{}," exposes ",[32,1420,1421],{},"expandedLegendItems"," for legend-tree state.",[1372,1424,1425,1428,1430],{},[1390,1426,1427],{},"3.28 LTR",[1390,1429,1413],{},[1390,1431,1432,1401,1435,1438],{},[32,1433,1434],{},"removeMapTheme()",[32,1436,1437],{},"renameMapTheme()"," available.",[1372,1440,1441,1444,1447],{},[1390,1442,1443],{},"3.34 LTR",[1390,1445,1446],{},"3.12",[1390,1448,1449],{},"Baseline for this page.",[1372,1451,1452,1455,1457],{},[1390,1453,1454],{},"3.40+",[1390,1456,1446],{},[1390,1458,1459],{},"Theme records can carry per-theme layer opacity overrides.",[201,1461,1463],{"id":1462},"troubleshooting","Troubleshooting",[206,1465,1466,1478,1488,1499,1505,1514],{},[209,1467,1468,1474,1475,1477],{},[212,1469,1470,1473],{},[32,1471,1472],{},"AttributeError: iface is not defined","."," The script is running standalone. Construct ",[32,1476,889],{}," yourself.",[209,1479,1480,1483,1484,1487],{},[212,1481,1482],{},"The theme records visibility but not legend classes."," The model was created without ",[32,1485,1486],{},"ShowLegend",". Set the flag before capturing.",[209,1489,1490,349,1493,1495,1496,1498],{},[212,1491,1492],{},"The theme silently overwrote another.",[32,1494,360],{}," replaces by name. Check ",[32,1497,364],{}," before inserting a user-supplied name.",[209,1500,1501,1504],{},[212,1502,1503],{},"Applying the theme hides a layer you expected."," That layer has no record. A theme is a complete definition, not a set of changes.",[209,1506,1507,1510,1511,1473],{},[212,1508,1509],{},"The pinned style is ignored."," The style name does not exist in that layer's style manager. Compare against ",[32,1512,1513],{},"layer.styleManager().styles()",[209,1515,1516,1519,1520,1523,1524,1526],{},[212,1517,1518],{},"The theme disappears after closing the project."," The project was never saved. Call ",[32,1521,1522],{},"project.setDirty(True)"," in the GUI, or ",[32,1525,897],{}," headlessly.",[201,1528,1530],{"id":1529},"conclusion","Conclusion",[14,1532,1533,1534,1536,1537,1539,1540,1542],{},"Capture with ",[32,1535,352],{}," when a quick snapshot is fine, and build a ",[32,1538,1404],{}," by hand when the script must not disturb what the user is looking at. Remember that omission means hidden, that the model needs ",[32,1541,1486],{}," to record legend classes, and that a rebuild script which removes undeclared themes is the version that stays correct.",[201,1544,1546],{"id":1545},"frequently-asked-questions","Frequently Asked Questions",[14,1548,1549,1552,1553,1556],{},[212,1550,1551],{},"Can I rename a theme without recreating it?","\nYes — ",[32,1554,1555],{},"collection.renameMapTheme(old, new)"," since QGIS 3.28. Anything referring to the old name, including a layout map item following it, needs updating separately.",[14,1558,1559,1562,1563,1566],{},[212,1560,1561],{},"How do I copy themes between projects?","\nRead both projects into separate ",[32,1564,1565],{},"QgsProject"," instances and copy the records across, matching layers by name rather than id, because ids differ between projects. Layers absent from the target must be resolved or skipped explicitly.",[14,1568,1569,1572],{},[212,1570,1571],{},"Does a theme store layer order?","\nNo. Draw order is a property of the layer tree and is shared by every theme in the project. If two deliverables need different draw orders they need different projects, or a script that reorders the tree before export.",[14,1574,1575,1578,1579,1583],{},[212,1576,1577],{},"Can a theme include a layer that is not in the project?","\nNo. Records hold references to loaded layers, so ",[26,1580,1582],{"href":1581},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fadd-and-remove-layers-from-project-pyqgis\u002F","add the layer to the project"," first, then build the record.",[201,1585,1587],{"id":1586},"related","Related",[206,1589,1590,1595,1601,1607,1612],{},[209,1591,1592,1594],{},[26,1593,29],{"href":28}," — the guide this recipe belongs to",[209,1596,1597],{},[26,1598,1600],{"href":1599},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fapply-map-theme-to-layout-map-pyqgis\u002F","Apply a Map Theme to a Layout Map in PyQGIS",[209,1602,1603],{},[26,1604,1606],{"href":1605},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis\u002F","Toggle Layer Visibility and Legend Entries in PyQGIS",[209,1608,1609],{},[26,1610,1611],{"href":224},"Save and Load a QML Style in PyQGIS",[209,1613,1614],{},[26,1615,1617],{"href":1616},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002F","Save and Load a QGIS Project in PyQGIS",[621,1619,1620],{},"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);}",{"title":240,"searchDepth":263,"depth":263,"links":1622},[1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634],{"id":203,"depth":263,"text":204},{"id":229,"depth":263,"text":230},{"id":368,"depth":263,"text":369},{"id":743,"depth":263,"text":744},{"id":901,"depth":263,"text":902},{"id":1155,"depth":263,"text":1156},{"id":1342,"depth":263,"text":1343},{"id":1357,"depth":263,"text":1358},{"id":1462,"depth":263,"text":1463},{"id":1529,"depth":263,"text":1530},{"id":1545,"depth":263,"text":1546},{"id":1586,"depth":263,"text":1587},"Snapshot layer visibility and styles into a named map theme with QgsMapThemeCollection — including headless projects with no iface, idempotent rebuilds, and theme records built by hand.","md",{"slug":1638,"type":1639,"breadcrumb":1640,"datePublished":1641,"dateModified":1641},"create-map-theme-pyqgis","article","Create a Map Theme","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis",{"title":5,"description":1635},"pyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis\u002Findex","i-UEIpYdiEzQ9bDdAYRPYxQFr35mY7noNMOlV6oq5wE",1787823360559]