[{"data":1,"prerenderedAt":1533},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis":3},{"id":4,"title":5,"body":6,"description":1522,"extension":1523,"meta":1524,"navigation":263,"path":1529,"seo":1530,"stem":1531,"__hash__":1532},"docs\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis\u002Findex.md","Toggle Layer Visibility and Legend Entries in PyQGIS",{"type":7,"value":8,"toc":1508},"minimark",[9,13,27,36,197,202,224,228,231,354,383,386,401,406,410,413,471,487,490,588,607,690,694,697,835,860,864,867,953,971,975,978,1058,1070,1073,1077,1080,1227,1245,1249,1255,1346,1350,1410,1414,1423,1427,1437,1454,1464,1470,1474,1504],[10,11,5],"h1",{"id":12},"toggle-layer-visibility-and-legend-entries-in-pyqgis",[14,15,16,17,21,22,26],"p",{},"Turning a layer on and off is the first thing anybody automates in QGIS, and the first thing that surprises them, because ",[18,19,20],"code",{},"QgsVectorLayer"," has no visibility property at all. Visibility belongs to the tree of nodes that represents the project in the ",[23,24,25],"em",{},"Layers"," panel — and once you know that, a whole family of controls opens up: group checkboxes, mutually exclusive groups, per-class legend ticks, and the difference between \"checked\" and \"actually drawn\".",[14,28,29,30,35],{},"This recipe belongs to ",[31,32,34],"a",{"href":33},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002F","Map Themes & Layer Visibility in PyQGIS",". It covers finding a node, toggling layers and groups, filtering a categorized layer through its legend, and batching many changes so the canvas redraws once instead of forty times.",[14,37,38],{},[39,40,45,49,53,60,77,86,96,102,110,116,123,128,132,136,139,143,149,153,158,164,171,175,184,189,193],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 320","img","The layer tree structure showing a root group containing nested groups and layer nodes, with the checkbox state on each node and the resulting drawn state","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Nodes, groups, and what actually draws",[50,51,52],"desc",{},"The tree root contains groups, which contain layer nodes, which contain legend nodes. A layer draws only when its own checkbox and every ancestor group checkbox are ticked. A layer inside an unticked group is shown here as checked but not drawn.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","320","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"tvArrow","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","A ticked layer inside an unticked group is not drawn",[54,87],{"x":88,"y":89,"width":90,"height":91,"rx":92,"fill":93,"stroke":94,"style":95},"30","52","360","238","10","#fffdf7","#59645f","stroke-width:2",[78,97,101],{"x":98,"y":99,"style":100,"fill":83,"textAnchor":84},"210","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","layerTreeRoot()",[54,103],{"x":89,"y":104,"width":105,"height":81,"rx":106,"fill":107,"stroke":108,"style":109},"90","316","4","#dbeafe","#2563eb","stroke-width:1.5",[78,111,115],{"x":112,"y":113,"style":114,"fill":76},"66","109","font-size:11px;font-family:sans-serif","☑ group · Reference",[54,117],{"x":99,"y":118,"width":119,"height":120,"rx":106,"fill":121,"stroke":108,"style":122},"122","292","26","#eff3ff","stroke-width:1.3",[78,124,127],{"x":104,"y":125,"style":126,"fill":76},"140","font-size:10.5px;font-family:sans-serif","☑ basemap — drawn",[54,129],{"x":99,"y":130,"width":119,"height":120,"rx":106,"fill":131,"stroke":94,"style":122},"152","#efeadd",[78,133,135],{"x":104,"y":134,"style":126,"fill":94},"170","☐ boundaries — hidden",[54,137],{"x":89,"y":138,"width":105,"height":81,"rx":106,"fill":131,"stroke":94,"style":109},"186",[78,140,142],{"x":112,"y":141,"style":114,"fill":94},"205","☐ group · Analysis",[54,144],{"x":99,"y":145,"width":119,"height":120,"rx":106,"fill":146,"stroke":147,"style":148},"218","#fdf2e2","#b45309","stroke-width:1.6",[78,150,152],{"x":104,"y":151,"style":126,"fill":76},"236","☑ flood extent — NOT drawn",[78,154,157],{"x":98,"y":155,"style":156,"fill":147,"textAnchor":84},"268","text-anchor:middle;font-size:10.5px;font-family:sans-serif","checked, but its parent group is not",[159,160],"line",{"x1":161,"y1":134,"x2":162,"y2":134,"stroke":76,"style":163},"394","426","stroke-width:2;marker-end:url(#tvArrow)",[54,165],{"x":166,"y":89,"width":167,"height":91,"rx":92,"fill":168,"stroke":169,"style":170},"434","296","#edf8e9","#15803d","stroke-width:2.5",[78,172,174],{"x":173,"y":99,"style":100,"fill":169,"textAnchor":84},"582","what the canvas shows",[54,176],{"x":177,"y":178,"width":179,"height":180,"rx":181,"fill":182,"stroke":169,"style":183},"460","96","244","120","6","#e8efe6","stroke-width:1.8",[78,185,188],{"x":173,"y":186,"style":187,"fill":76,"textAnchor":84},"162","text-anchor:middle;font-size:11px;font-family:sans-serif","basemap only",[78,190,192],{"x":173,"y":179,"style":191,"fill":76,"textAnchor":84},"text-anchor:middle;font-size:11px;font-family:monospace","node.isVisible()",[78,194,196],{"x":173,"y":195,"style":156,"fill":76,"textAnchor":84},"264","answers this, walking the ancestors",[198,199,201],"h2",{"id":200},"prerequisites","Prerequisites",[203,204,205,213,216],"ul",{},[206,207,208,212],"li",{},[209,210,211],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[206,214,215],{},"A project with at least one group, so the ancestor behaviour can be observed.",[206,217,218,219,223],{},"For the legend section, a layer using a categorized or rule-based renderer; see ",[31,220,222],{"href":221},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","graduated and categorized renderers",".",[198,225,227],{"id":226},"find-the-node-then-toggle-it","Find the node, then toggle it",[14,229,230],{},"Every operation starts by turning a layer into a tree node.",[232,233,238],"pre",{"className":234,"code":235,"language":236,"meta":237,"style":237},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nroot = project.layerTreeRoot()\n\nlayer = project.mapLayersByName(\"flood extent\")[0]\nnode = root.findLayer(layer.id())\n\nnode.setItemVisibilityChecked(True)\nprint(node.itemVisibilityChecked(), node.isVisible())\n","python","",[18,239,240,258,265,277,288,293,317,328,333,345],{"__ignoreMap":237},[241,242,244,248,252,255],"span",{"class":159,"line":243},1,[241,245,247],{"class":246},"snl16","from",[241,249,251],{"class":250},"s95oV"," qgis.core ",[241,253,254],{"class":246},"import",[241,256,257],{"class":250}," QgsProject\n",[241,259,261],{"class":159,"line":260},2,[241,262,264],{"emptyLinePlaceholder":263},true,"\n",[241,266,268,271,274],{"class":159,"line":267},3,[241,269,270],{"class":250},"project ",[241,272,273],{"class":246},"=",[241,275,276],{"class":250}," QgsProject.instance()\n",[241,278,280,283,285],{"class":159,"line":279},4,[241,281,282],{"class":250},"root ",[241,284,273],{"class":246},[241,286,287],{"class":250}," project.layerTreeRoot()\n",[241,289,291],{"class":159,"line":290},5,[241,292,264],{"emptyLinePlaceholder":263},[241,294,296,299,301,304,308,311,314],{"class":159,"line":295},6,[241,297,298],{"class":250},"layer ",[241,300,273],{"class":246},[241,302,303],{"class":250}," project.mapLayersByName(",[241,305,307],{"class":306},"sU2Wk","\"flood extent\"",[241,309,310],{"class":250},")[",[241,312,56],{"class":313},"sDLfK",[241,315,316],{"class":250},"]\n",[241,318,320,323,325],{"class":159,"line":319},7,[241,321,322],{"class":250},"node ",[241,324,273],{"class":246},[241,326,327],{"class":250}," root.findLayer(layer.id())\n",[241,329,331],{"class":159,"line":330},8,[241,332,264],{"emptyLinePlaceholder":263},[241,334,336,339,342],{"class":159,"line":335},9,[241,337,338],{"class":250},"node.setItemVisibilityChecked(",[241,340,341],{"class":313},"True",[241,343,344],{"class":250},")\n",[241,346,348,351],{"class":159,"line":347},10,[241,349,350],{"class":313},"print",[241,352,353],{"class":250},"(node.itemVisibilityChecked(), node.isVisible())\n",[14,355,356,359,360,363,364,367,368,371,372,374,375,378,379,382],{},[209,357,358],{},"Breakdown:"," ",[18,361,362],{},"findLayer()"," searches the whole tree recursively, so a layer nested three groups deep is found without walking anything yourself. It returns ",[18,365,366],{},"None"," for a layer that is in the project's registry but not in the tree — which happens when a layer was added with ",[18,369,370],{},"addMapLayer(layer, False)"," — so a ",[18,373,366],{}," check belongs in any function that takes a layer name from outside. The two accessors answer different questions: ",[18,376,377],{},"itemVisibilityChecked()"," reports this node's own box, ",[18,380,381],{},"isVisible()"," walks up through every parent group and reports whether the layer will actually be drawn.",[14,384,385],{},"Toggling rather than setting is a one-liner, and useful for a plugin's toolbar button:",[232,387,389],{"className":234,"code":388,"language":236,"meta":237,"style":237},"node.setItemVisibilityChecked(not node.itemVisibilityChecked())\n",[18,390,391],{"__ignoreMap":237},[241,392,393,395,398],{"class":159,"line":243},[241,394,338],{"class":250},[241,396,397],{"class":246},"not",[241,399,400],{"class":250}," node.itemVisibilityChecked())\n",[14,402,403,405],{},[209,404,358],{}," No repaint call is needed anywhere in this section. The canvas subscribes to the tree's change signals and refreshes itself, which is also why a loop that flips forty layers individually triggers forty redraws.",[198,407,409],{"id":408},"groups-and-exclusive-groups","Groups, and exclusive groups",[14,411,412],{},"A group node has the same visibility API plus one extra behaviour worth knowing.",[232,414,416],{"className":234,"code":415,"language":236,"meta":237,"style":237},"group = root.findGroup(\"Analysis\")\ngroup.setItemVisibilityChecked(True)\ngroup.setItemVisibilityCheckedRecursive(True)\ngroup.setIsMutuallyExclusive(True, initialChildIndex=0)\n",[18,417,418,433,442,451],{"__ignoreMap":237},[241,419,420,423,425,428,431],{"class":159,"line":243},[241,421,422],{"class":250},"group ",[241,424,273],{"class":246},[241,426,427],{"class":250}," root.findGroup(",[241,429,430],{"class":306},"\"Analysis\"",[241,432,344],{"class":250},[241,434,435,438,440],{"class":159,"line":260},[241,436,437],{"class":250},"group.setItemVisibilityChecked(",[241,439,341],{"class":313},[241,441,344],{"class":250},[241,443,444,447,449],{"class":159,"line":267},[241,445,446],{"class":250},"group.setItemVisibilityCheckedRecursive(",[241,448,341],{"class":313},[241,450,344],{"class":250},[241,452,453,456,458,461,465,467,469],{"class":159,"line":279},[241,454,455],{"class":250},"group.setIsMutuallyExclusive(",[241,457,341],{"class":313},[241,459,460],{"class":250},", ",[241,462,464],{"class":463},"s9osk","initialChildIndex",[241,466,273],{"class":246},[241,468,56],{"class":313},[241,470,344],{"class":250},[14,472,473,359,475,478,479,482,483,486],{},[209,474,358],{},[18,476,477],{},"setItemVisibilityChecked()"," on a group ticks the group itself and leaves the children alone; ",[18,480,481],{},"setItemVisibilityCheckedRecursive()"," ticks the group and everything beneath it, which is the call people usually mean. ",[18,484,485],{},"setIsMutuallyExclusive()"," turns the group into a radio-button set: checking one child unchecks its siblings automatically, and QGIS enforces it thereafter. That single flag replaces a lot of hand-written toggle logic for scenario layers — 1-in-100, 1-in-200, 1-in-1000 flood extents in one exclusive group behave correctly no matter how the user clicks.",[14,488,489],{},"Finding a group by name only searches the immediate children unless you ask for recursion:",[232,491,493],{"className":234,"code":492,"language":236,"meta":237,"style":237},"def find_group(root, name):\n    for child in root.children():\n        if child.nodeType() == child.NodeGroup:\n            if child.name() == name:\n                return child\n            found = find_group(child, name)\n            if found:\n                return found\n    return None\n",[18,494,495,507,521,535,548,556,566,573,580],{"__ignoreMap":237},[241,496,497,500,504],{"class":159,"line":243},[241,498,499],{"class":246},"def",[241,501,503],{"class":502},"svObZ"," find_group",[241,505,506],{"class":250},"(root, name):\n",[241,508,509,512,515,518],{"class":159,"line":260},[241,510,511],{"class":246},"    for",[241,513,514],{"class":250}," child ",[241,516,517],{"class":246},"in",[241,519,520],{"class":250}," root.children():\n",[241,522,523,526,529,532],{"class":159,"line":267},[241,524,525],{"class":246},"        if",[241,527,528],{"class":250}," child.nodeType() ",[241,530,531],{"class":246},"==",[241,533,534],{"class":250}," child.NodeGroup:\n",[241,536,537,540,543,545],{"class":159,"line":279},[241,538,539],{"class":246},"            if",[241,541,542],{"class":250}," child.name() ",[241,544,531],{"class":246},[241,546,547],{"class":250}," name:\n",[241,549,550,553],{"class":159,"line":290},[241,551,552],{"class":246},"                return",[241,554,555],{"class":250}," child\n",[241,557,558,561,563],{"class":159,"line":295},[241,559,560],{"class":250},"            found ",[241,562,273],{"class":246},[241,564,565],{"class":250}," find_group(child, name)\n",[241,567,568,570],{"class":159,"line":319},[241,569,539],{"class":246},[241,571,572],{"class":250}," found:\n",[241,574,575,577],{"class":159,"line":330},[241,576,552],{"class":246},[241,578,579],{"class":250}," found\n",[241,581,582,585],{"class":159,"line":335},[241,583,584],{"class":246},"    return",[241,586,587],{"class":313}," None\n",[14,589,590,359,592,595,596,598,599,602,603,606],{},[209,591,358],{},[18,593,594],{},"root.findGroup(name)"," does search recursively in current versions, but writing the walk explicitly is worth it when you need the ",[23,597,73],{}," to a group rather than the group itself, or when duplicate group names make the first match the wrong one. ",[18,600,601],{},"nodeType()"," distinguishes group nodes from layer nodes, and comparing against ",[18,604,605],{},"child.NodeGroup"," avoids importing the enum separately.",[14,608,609],{},[39,610,613,616,619,622,625,630,635,640,645,648,652,655,659,663,666,670,673,677,679,682,684,687],{"viewBox":611,"role":42,"ariaLabel":612,"xmlns":44},"0 0 760 276","A mutually exclusive group behaving like radio buttons: checking one scenario layer automatically unchecks the others in the same group",[46,614,615],{},"A mutually exclusive group enforces one choice",[50,617,618],{},"Three scenario layers sit in one group. Before the flag is set, any combination can be checked, including all three at once. After setIsMutuallyExclusive is enabled, checking one layer unchecks the others automatically and QGIS keeps enforcing it as the user clicks.",[54,620],{"x":56,"y":56,"width":57,"height":621,"fill":59},"276",[78,623,624],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"One flag replaces a page of toggle logic",[54,626],{"x":627,"y":628,"width":58,"height":629,"rx":92,"fill":146,"stroke":147,"style":170},"34","54","180",[78,631,634],{"x":632,"y":633,"style":100,"fill":147,"textAnchor":84},"194","80","ordinary group",[54,636],{"x":637,"y":178,"width":638,"height":81,"rx":106,"fill":93,"stroke":147,"style":639},"58","272","stroke-width:1.4",[78,641,644],{"x":642,"y":643,"style":126,"fill":76},"72","115","☑ 1 in 100",[54,646],{"x":637,"y":647,"width":638,"height":81,"rx":106,"fill":93,"stroke":147,"style":639},"128",[78,649,651],{"x":642,"y":650,"style":126,"fill":76},"147","☑ 1 in 200",[54,653],{"x":637,"y":654,"width":638,"height":81,"rx":106,"fill":93,"stroke":147,"style":639},"160",[78,656,658],{"x":642,"y":657,"style":126,"fill":76},"179","☑ 1 in 1000",[78,660,662],{"x":632,"y":661,"style":156,"fill":147,"textAnchor":84},"214","three extents stacked, none readable",[54,664],{"x":665,"y":628,"width":58,"height":629,"rx":92,"fill":168,"stroke":169,"style":170},"406",[78,667,669],{"x":668,"y":633,"style":100,"fill":169,"textAnchor":84},"566","mutually exclusive",[54,671],{"x":672,"y":178,"width":638,"height":81,"rx":106,"fill":182,"stroke":169,"style":183},"430",[78,674,676],{"x":675,"y":643,"style":126,"fill":76},"444","◉ 1 in 100",[54,678],{"x":672,"y":647,"width":638,"height":81,"rx":106,"fill":93,"stroke":94,"style":639},[78,680,681],{"x":675,"y":650,"style":126,"fill":94},"○ 1 in 200",[54,683],{"x":672,"y":654,"width":638,"height":81,"rx":106,"fill":93,"stroke":94,"style":639},[78,685,686],{"x":675,"y":657,"style":126,"fill":94},"○ 1 in 1000",[78,688,689],{"x":668,"y":661,"style":156,"fill":169,"textAnchor":84},"exactly one scenario, always",[198,691,693],{"id":692},"tick-and-untick-legend-classes","Tick and untick legend classes",[14,695,696],{},"Below each layer node sit legend nodes — one per category, class or rule. Their checkboxes filter the map, not just the legend.",[232,698,700],{"className":234,"code":699,"language":236,"meta":237,"style":237},"from qgis.core import QgsLayerTreeModelLegendNode\nfrom qgis.PyQt.QtCore import Qt\n\nmodel = iface.layerTreeView().layerTreeModel()\nnode = root.findLayer(layer.id())\n\nfor legend_node in model.layerLegendNodes(node):\n    label = legend_node.data(Qt.DisplayRole)\n    keep = label in {\"flooding\", \"subsidence\"}\n    legend_node.setData(\n        Qt.Checked if keep else Qt.Unchecked,\n        Qt.CheckStateRole,\n    )\n",[18,701,702,713,725,729,739,747,751,764,774,800,805,823,829],{"__ignoreMap":237},[241,703,704,706,708,710],{"class":159,"line":243},[241,705,247],{"class":246},[241,707,251],{"class":250},[241,709,254],{"class":246},[241,711,712],{"class":250}," QgsLayerTreeModelLegendNode\n",[241,714,715,717,720,722],{"class":159,"line":260},[241,716,247],{"class":246},[241,718,719],{"class":250}," qgis.PyQt.QtCore ",[241,721,254],{"class":246},[241,723,724],{"class":250}," Qt\n",[241,726,727],{"class":159,"line":267},[241,728,264],{"emptyLinePlaceholder":263},[241,730,731,734,736],{"class":159,"line":279},[241,732,733],{"class":250},"model ",[241,735,273],{"class":246},[241,737,738],{"class":250}," iface.layerTreeView().layerTreeModel()\n",[241,740,741,743,745],{"class":159,"line":290},[241,742,322],{"class":250},[241,744,273],{"class":246},[241,746,327],{"class":250},[241,748,749],{"class":159,"line":295},[241,750,264],{"emptyLinePlaceholder":263},[241,752,753,756,759,761],{"class":159,"line":319},[241,754,755],{"class":246},"for",[241,757,758],{"class":250}," legend_node ",[241,760,517],{"class":246},[241,762,763],{"class":250}," model.layerLegendNodes(node):\n",[241,765,766,769,771],{"class":159,"line":330},[241,767,768],{"class":250},"    label ",[241,770,273],{"class":246},[241,772,773],{"class":250}," legend_node.data(Qt.DisplayRole)\n",[241,775,776,779,781,784,786,789,792,794,797],{"class":159,"line":335},[241,777,778],{"class":250},"    keep ",[241,780,273],{"class":246},[241,782,783],{"class":250}," label ",[241,785,517],{"class":246},[241,787,788],{"class":250}," {",[241,790,791],{"class":306},"\"flooding\"",[241,793,460],{"class":250},[241,795,796],{"class":306},"\"subsidence\"",[241,798,799],{"class":250},"}\n",[241,801,802],{"class":159,"line":347},[241,803,804],{"class":250},"    legend_node.setData(\n",[241,806,808,811,814,817,820],{"class":159,"line":807},11,[241,809,810],{"class":250},"        Qt.Checked ",[241,812,813],{"class":246},"if",[241,815,816],{"class":250}," keep ",[241,818,819],{"class":246},"else",[241,821,822],{"class":250}," Qt.Unchecked,\n",[241,824,826],{"class":159,"line":825},12,[241,827,828],{"class":250},"        Qt.CheckStateRole,\n",[241,830,832],{"class":159,"line":831},13,[241,833,834],{"class":250},"    )\n",[14,836,837,839,840,843,844,847,848,851,852,855,856,223],{},[209,838,358],{}," Legend nodes follow Qt's model conventions rather than having named getters, so the label comes from ",[18,841,842],{},"data(Qt.DisplayRole)"," and the tick from ",[18,845,846],{},"setData(..., Qt.CheckStateRole)",". Only renderers that support legend filtering respond — categorized, graduated and rule-based do; a single-symbol renderer has one node and unticking it hides the layer. Because this is model state rather than tree state, a headless script must build its own ",[18,849,850],{},"QgsLayerTreeModel"," with the ",[18,853,854],{},"ShowLegend"," flag, as described in ",[31,857,859],{"href":858},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis\u002F","creating a map theme",[198,861,863],{"id":862},"batch-changes-without-a-repaint-storm","Batch changes without a repaint storm",[14,865,866],{},"Flipping many nodes one at a time makes the canvas redraw after each change, which is slow and looks broken.",[232,868,870],{"className":234,"code":869,"language":236,"meta":237,"style":237},"canvas = iface.mapCanvas()\ncanvas.setRenderFlag(False)\ntry:\n    for tree_layer in root.findLayers():\n        tree_layer.setItemVisibilityChecked(\n            tree_layer.layer().name() in wanted_names\n        )\nfinally:\n    canvas.setRenderFlag(True)\n    canvas.refresh()\n",[18,871,872,882,892,900,912,917,927,932,939,948],{"__ignoreMap":237},[241,873,874,877,879],{"class":159,"line":243},[241,875,876],{"class":250},"canvas ",[241,878,273],{"class":246},[241,880,881],{"class":250}," iface.mapCanvas()\n",[241,883,884,887,890],{"class":159,"line":260},[241,885,886],{"class":250},"canvas.setRenderFlag(",[241,888,889],{"class":313},"False",[241,891,344],{"class":250},[241,893,894,897],{"class":159,"line":267},[241,895,896],{"class":246},"try",[241,898,899],{"class":250},":\n",[241,901,902,904,907,909],{"class":159,"line":279},[241,903,511],{"class":246},[241,905,906],{"class":250}," tree_layer ",[241,908,517],{"class":246},[241,910,911],{"class":250}," root.findLayers():\n",[241,913,914],{"class":159,"line":290},[241,915,916],{"class":250},"        tree_layer.setItemVisibilityChecked(\n",[241,918,919,922,924],{"class":159,"line":295},[241,920,921],{"class":250},"            tree_layer.layer().name() ",[241,923,517],{"class":246},[241,925,926],{"class":250}," wanted_names\n",[241,928,929],{"class":159,"line":319},[241,930,931],{"class":250},"        )\n",[241,933,934,937],{"class":159,"line":330},[241,935,936],{"class":246},"finally",[241,938,899],{"class":250},[241,940,941,944,946],{"class":159,"line":335},[241,942,943],{"class":250},"    canvas.setRenderFlag(",[241,945,341],{"class":313},[241,947,344],{"class":250},[241,949,950],{"class":159,"line":347},[241,951,952],{"class":250},"    canvas.refresh()\n",[14,954,955,359,957,960,961,963,964,966,967,970],{},[209,956,358],{},[18,958,959],{},"setRenderFlag(False)"," suspends canvas rendering entirely; the tree still updates, the panel still shows the new ticks, and nothing is drawn until the flag returns. The ",[18,962,896],{},"\u002F",[18,965,936],{}," is not defensive padding — an exception raised mid-loop with rendering suspended leaves QGIS looking frozen, and users reasonably conclude it has crashed. ",[18,968,969],{},"findLayers()"," returns every layer node in the tree, so this pattern sets every layer explicitly rather than only turning the wanted ones on, which is what makes it deterministic when run repeatedly.",[198,972,974],{"id":973},"reordering-while-you-are-in-there","Reordering while you are in there",[14,976,977],{},"Draw order and visibility are separate, but scripts that manage one usually end up managing the other, and the tree API for moving nodes has one sharp edge: a node cannot be reparented, only cloned and removed.",[232,979,981],{"className":234,"code":980,"language":236,"meta":237,"style":237},"def move_layer_to_top(root, layer):\n    node = root.findLayer(layer.id())\n    if node is None:\n        return\n    clone = node.clone()\n    parent = node.parent()\n    root.insertChildNode(0, clone)\n    parent.removeChildNode(node)\n",[18,982,983,993,1002,1018,1023,1033,1043,1053],{"__ignoreMap":237},[241,984,985,987,990],{"class":159,"line":243},[241,986,499],{"class":246},[241,988,989],{"class":502}," move_layer_to_top",[241,991,992],{"class":250},"(root, layer):\n",[241,994,995,998,1000],{"class":159,"line":260},[241,996,997],{"class":250},"    node ",[241,999,273],{"class":246},[241,1001,327],{"class":250},[241,1003,1004,1007,1010,1013,1016],{"class":159,"line":267},[241,1005,1006],{"class":246},"    if",[241,1008,1009],{"class":250}," node ",[241,1011,1012],{"class":246},"is",[241,1014,1015],{"class":313}," None",[241,1017,899],{"class":250},[241,1019,1020],{"class":159,"line":279},[241,1021,1022],{"class":246},"        return\n",[241,1024,1025,1028,1030],{"class":159,"line":290},[241,1026,1027],{"class":250},"    clone ",[241,1029,273],{"class":246},[241,1031,1032],{"class":250}," node.clone()\n",[241,1034,1035,1038,1040],{"class":159,"line":295},[241,1036,1037],{"class":250},"    parent ",[241,1039,273],{"class":246},[241,1041,1042],{"class":250}," node.parent()\n",[241,1044,1045,1048,1050],{"class":159,"line":319},[241,1046,1047],{"class":250},"    root.insertChildNode(",[241,1049,56],{"class":313},[241,1051,1052],{"class":250},", clone)\n",[241,1054,1055],{"class":159,"line":330},[241,1056,1057],{"class":250},"    parent.removeChildNode(node)\n",[14,1059,1060,359,1062,1065,1066,1069],{},[209,1061,358],{},[18,1063,1064],{},"clone()"," copies the node including its checkbox state and custom properties, so the layer arrives at the top exactly as it was. Inserting before removing matters: remove first and the layer is briefly absent from the tree, which some panels and plugins notice and react to. ",[18,1067,1068],{},"parent()"," is used rather than assuming the root, because the node may have been sitting inside a group.",[14,1071,1072],{},"The same pattern moves a layer into a group — insert the clone as a child of the group node instead of the root. Nothing about the layer itself changes; only which node points at it.",[198,1074,1076],{"id":1075},"solo-and-other-patterns-worth-having","Solo, and other patterns worth having",[14,1078,1079],{},"Three small helpers cover most of what an interface ends up needing, and all of them are built from the primitives above.",[232,1081,1083],{"className":234,"code":1082,"language":236,"meta":237,"style":237},"def solo(root, layer):\n    \"\"\"Show only this layer, and every group needed to reach it.\"\"\"\n    for node in root.findLayers():\n        node.setItemVisibilityChecked(node.layer().id() == layer.id())\n    node = root.findLayer(layer.id())\n    parent = node.parent()\n    while parent is not None and parent is not root:\n        parent.setItemVisibilityChecked(True)\n        parent = parent.parent()\n\n\ndef visible_layers(root):\n    \"\"\"Layers that will genuinely be drawn, in draw order.\"\"\"\n    return [n.layer() for n in root.findLayers() if n.isVisible()]\n",[18,1084,1085,1094,1099,1109,1119,1127,1135,1162,1171,1181,1185,1189,1199,1204],{"__ignoreMap":237},[241,1086,1087,1089,1092],{"class":159,"line":243},[241,1088,499],{"class":246},[241,1090,1091],{"class":502}," solo",[241,1093,992],{"class":250},[241,1095,1096],{"class":159,"line":260},[241,1097,1098],{"class":306},"    \"\"\"Show only this layer, and every group needed to reach it.\"\"\"\n",[241,1100,1101,1103,1105,1107],{"class":159,"line":267},[241,1102,511],{"class":246},[241,1104,1009],{"class":250},[241,1106,517],{"class":246},[241,1108,911],{"class":250},[241,1110,1111,1114,1116],{"class":159,"line":279},[241,1112,1113],{"class":250},"        node.setItemVisibilityChecked(node.layer().id() ",[241,1115,531],{"class":246},[241,1117,1118],{"class":250}," layer.id())\n",[241,1120,1121,1123,1125],{"class":159,"line":290},[241,1122,997],{"class":250},[241,1124,273],{"class":246},[241,1126,327],{"class":250},[241,1128,1129,1131,1133],{"class":159,"line":295},[241,1130,1037],{"class":250},[241,1132,273],{"class":246},[241,1134,1042],{"class":250},[241,1136,1137,1140,1143,1145,1148,1150,1153,1155,1157,1159],{"class":159,"line":319},[241,1138,1139],{"class":246},"    while",[241,1141,1142],{"class":250}," parent ",[241,1144,1012],{"class":246},[241,1146,1147],{"class":246}," not",[241,1149,1015],{"class":313},[241,1151,1152],{"class":246}," and",[241,1154,1142],{"class":250},[241,1156,1012],{"class":246},[241,1158,1147],{"class":246},[241,1160,1161],{"class":250}," root:\n",[241,1163,1164,1167,1169],{"class":159,"line":330},[241,1165,1166],{"class":250},"        parent.setItemVisibilityChecked(",[241,1168,341],{"class":313},[241,1170,344],{"class":250},[241,1172,1173,1176,1178],{"class":159,"line":335},[241,1174,1175],{"class":250},"        parent ",[241,1177,273],{"class":246},[241,1179,1180],{"class":250}," parent.parent()\n",[241,1182,1183],{"class":159,"line":347},[241,1184,264],{"emptyLinePlaceholder":263},[241,1186,1187],{"class":159,"line":807},[241,1188,264],{"emptyLinePlaceholder":263},[241,1190,1191,1193,1196],{"class":159,"line":825},[241,1192,499],{"class":246},[241,1194,1195],{"class":502}," visible_layers",[241,1197,1198],{"class":250},"(root):\n",[241,1200,1201],{"class":159,"line":831},[241,1202,1203],{"class":306},"    \"\"\"Layers that will genuinely be drawn, in draw order.\"\"\"\n",[241,1205,1207,1209,1212,1214,1217,1219,1222,1224],{"class":159,"line":1206},14,[241,1208,584],{"class":246},[241,1210,1211],{"class":250}," [n.layer() ",[241,1213,755],{"class":246},[241,1215,1216],{"class":250}," n ",[241,1218,517],{"class":246},[241,1220,1221],{"class":250}," root.findLayers() ",[241,1223,813],{"class":246},[241,1225,1226],{"class":250}," n.isVisible()]\n",[14,1228,1229,1231,1232,1235,1236,1239,1240,1244],{},[209,1230,358],{}," The upward walk in ",[18,1233,1234],{},"solo()"," is the part that is easy to forget and the reason naive solo buttons show nothing: ticking the target is useless if it lives in a group somebody unchecked earlier. ",[18,1237,1238],{},"visible_layers()"," returns objects rather than names because that is what the rest of a script usually wants — feeding a ",[31,1241,1243],{"href":1242},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Frender-layer-to-image-without-gui-pyqgis\u002F","map settings object"," for an off-screen render, for instance, where the list must match what the canvas shows.",[198,1246,1248],{"id":1247},"qgis-version-compatibility","QGIS version compatibility",[14,1250,1251,1252,1254],{},"The examples target ",[209,1253,211],{}," (Python 3.12).",[1256,1257,1258,1274],"table",{},[1259,1260,1261],"thead",{},[1262,1263,1264,1268,1271],"tr",{},[1265,1266,1267],"th",{},"QGIS version",[1265,1269,1270],{},"Python",[1265,1272,1273],{},"Notes",[1275,1276,1277,1298,1315,1325,1336],"tbody",{},[1262,1278,1279,1283,1286],{},[1280,1281,1282],"td",{},"3.16 LTR",[1280,1284,1285],{},"3.7",[1280,1287,1288,460,1291,460,1294,1297],{},[18,1289,1290],{},"setItemVisibilityChecked",[18,1292,1293],{},"isVisible",[18,1295,1296],{},"setIsMutuallyExclusive"," all present.",[1262,1299,1300,1303,1306],{},[1280,1301,1302],{},"3.22 LTR",[1280,1304,1305],{},"3.9",[1280,1307,1308,1310,1311,1314],{},[18,1309,969],{}," and ",[18,1312,1313],{},"findGroup()"," search recursively.",[1262,1316,1317,1320,1322],{},[1280,1318,1319],{},"3.28 LTR",[1280,1321,1305],{},[1280,1323,1324],{},"Legend node check state stable across renderer types.",[1262,1326,1327,1330,1333],{},[1280,1328,1329],{},"3.34 LTR",[1280,1331,1332],{},"3.12",[1280,1334,1335],{},"Baseline for this page.",[1262,1337,1338,1341,1343],{},[1280,1339,1340],{},"3.40+",[1280,1342,1332],{},[1280,1344,1345],{},"Layer tree nodes expose per-node custom properties used by newer panel features.",[198,1347,1349],{"id":1348},"troubleshooting","Troubleshooting",[203,1351,1352,1364,1375,1384,1392,1401],{},[206,1353,1354,1359,1360,1363],{},[209,1355,1356,1358],{},[18,1357,362],{}," returns None."," The layer is in the registry but not in the tree. Add it with ",[18,1361,1362],{},"project.addMapLayer(layer)"," or insert a node manually.",[206,1365,1366,1369,1370,1372,1373,223],{},[209,1367,1368],{},"The layer is checked but nothing draws."," An ancestor group is unchecked. Test ",[18,1371,192],{},", not ",[18,1374,377],{},[206,1376,1377,1380,1381,223],{},[209,1378,1379],{},"Ticking a group did not tick its children."," Use ",[18,1382,1383],{},"setItemVisibilityCheckedRecursive(True)",[206,1385,1386,1389,1390,223],{},[209,1387,1388],{},"Legend ticks do nothing."," The renderer does not support legend filtering, or the script is headless and the model was built without ",[18,1391,854],{},[206,1393,1394,1397,1398,1400],{},[209,1395,1396],{},"The canvas flickers through forty redraws."," Wrap the loop in ",[18,1399,959],{}," and refresh once at the end.",[206,1402,1403,1406,1407,1409],{},[209,1404,1405],{},"QGIS appears frozen after an error."," The render flag was left off. Always restore it in a ",[18,1408,936],{}," block.",[198,1411,1413],{"id":1412},"conclusion","Conclusion",[14,1415,1416,1417,1419,1420,1422],{},"Visibility lives on tree nodes: find the node, set its checkbox, and read ",[18,1418,381],{}," when you need to know whether the layer will really be drawn. Use recursive setters on groups, ",[18,1421,485],{}," for scenario sets, legend node check states for per-class filtering, and suspend the render flag around any loop that changes more than a handful of layers.",[198,1424,1426],{"id":1425},"frequently-asked-questions","Frequently Asked Questions",[14,1428,1429,1432,1433,1436],{},[209,1430,1431],{},"How do I hide everything except one layer?","\nIterate ",[18,1434,1435],{},"root.findLayers()"," and set each checkbox to whether it is the one you want. Setting only the target true leaves whatever was checked before still checked, which is the usual cause of a \"solo\" button that does not solo.",[14,1438,1439,1442,1443,1446,1447,1449,1450,223],{},[209,1440,1441],{},"Can I react when the user toggles a layer?","\nYes — connect to ",[18,1444,1445],{},"root.visibilityChanged"," for tree-wide notification, or to a specific node's own signal. That is how a plugin keeps a panel in step with the ",[23,1448,25],{}," panel without polling; see ",[31,1451,1453],{"href":1452},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","connecting layer signals",[14,1455,1456,1459,1460,1463],{},[209,1457,1458],{},"Is visibility saved with the project?","\nYes, the whole tree including checkbox state is written into the project file. It is also what a ",[31,1461,1462],{"href":33},"map theme"," snapshots.",[14,1465,1466,1469],{},[209,1467,1468],{},"Does hiding a layer stop it being queried?","\nNo. Identify, selection by expression and processing algorithms all work on hidden layers, because they read the layer rather than the tree. Only rendering consults the checkbox.",[198,1471,1473],{"id":1472},"related","Related",[203,1475,1476,1481,1486,1492,1498],{},[206,1477,1478,1480],{},[31,1479,34],{"href":33}," — the guide this recipe belongs to",[206,1482,1483],{},[31,1484,1485],{"href":858},"Create a Map Theme in PyQGIS",[206,1487,1488],{},[31,1489,1491],{"href":1490},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis\u002F","Set Scale-Based Visibility in PyQGIS",[206,1493,1494],{},[31,1495,1497],{"href":1496},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002F","Organise Layer Tree Groups in PyQGIS",[206,1499,1500],{},[31,1501,1503],{"href":1502},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fadd-and-remove-layers-from-project-pyqgis\u002F","Add and Remove Layers from a Project in PyQGIS",[1505,1506,1507],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":237,"searchDepth":260,"depth":260,"links":1509},[1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521],{"id":200,"depth":260,"text":201},{"id":226,"depth":260,"text":227},{"id":408,"depth":260,"text":409},{"id":692,"depth":260,"text":693},{"id":862,"depth":260,"text":863},{"id":973,"depth":260,"text":974},{"id":1075,"depth":260,"text":1076},{"id":1247,"depth":260,"text":1248},{"id":1348,"depth":260,"text":1349},{"id":1412,"depth":260,"text":1413},{"id":1425,"depth":260,"text":1426},{"id":1472,"depth":260,"text":1473},"Show and hide layers, groups and individual legend classes from Python — layer tree nodes, mutually exclusive groups, legend node checkboxes, and batching changes without repaint storms.","md",{"slug":1525,"type":1526,"breadcrumb":1527,"datePublished":1528,"dateModified":1528},"toggle-layer-visibility-and-legend-pyqgis","article","Toggle Visibility","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis",{"title":5,"description":1522},"pyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Ftoggle-layer-visibility-and-legend-pyqgis\u002Findex","zIQHOcBxvBeUUl3Oztm_gHJCZ-5-SpX9sr4eI56ECRI",1787823360559]