[{"data":1,"prerenderedAt":2193},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis":3},{"id":4,"title":5,"body":6,"description":2182,"extension":2183,"meta":2184,"navigation":257,"path":2189,"seo":2190,"stem":2191,"__hash__":2192},"docs\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis\u002Findex.md","Build a Tabbed Attribute Form Layout in PyQGIS",{"type":7,"value":8,"toc":2169},"minimark",[9,13,17,26,186,191,205,209,212,511,542,550,554,557,655,907,919,923,926,1082,1092,1206,1210,1213,1647,1661,1665,1668,1983,2007,2011,2037,2041,2081,2085,2088,2092,2102,2116,2122,2132,2136,2165],[10,11,5],"h1",{"id":12},"build-a-tabbed-attribute-form-layout-in-pyqgis",[14,15,16],"p",{},"The form QGIS generates for a layer is a single column of every field in table order, labelled with raw field names. For a layer with six fields that is fine. For an inspection record with forty — site details, measurements, damage observations, follow-up actions, photos, related repair records — it is a scrolling wall that nobody fills in consistently. The drag-and-drop designer fixes that with tabs, groups and conditional sections, and everything it does can be built from Python, which means one script can lay out the same form on every layer that needs it.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002F","Attribute Forms & Layer Actions in PyQGIS",". It builds a form tree from containers and fields, adds columns and a section that only appears when it is relevant, and embeds related child records.",[14,27,28],{},[29,30,35,39,43,50,59,69,74,82,84,88,94,99,101,104,107,110,115,120,124,128,131,135,139,141,147,152,157,161,165,167,170,172,177,181],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 310","img","The drag-and-drop form model as a tree: an invisible root container holds tabs, tabs hold group boxes and fields, a group box has a visibility expression, and a relation element embeds child records","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"A form is a tree of containers",[40,41,42],"desc",{},"The invisible root container has three tab children: Site, Condition and Follow-up. Site holds fields asset_id, species_code and height_m arranged in two columns. Condition holds the condition field and a Damage group box that is only visible when condition is 2 or less. Follow-up holds action_required and a relation element showing repair records from a child layer.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","310","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Root → tabs → groups → fields",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"290","42","180","36","8","#fffdf7","#59645f","stroke-width:2;stroke-dasharray:6 4",[51,70,73],{"x":53,"y":71,"style":72,"fill":67,"textAnchor":57},"65","text-anchor:middle;font-size:10.5px;font-family:sans-serif","invisible root",[75,76],"line",{"x1":77,"y1":78,"x2":79,"y2":80,"stroke":67,"style":81},"330","78","130","100","stroke-width:1.6",[75,83],{"x1":53,"y1":78,"x2":53,"y2":80,"stroke":67,"style":81},[75,85],{"x1":86,"y1":78,"x2":87,"y2":80,"stroke":67,"style":81},"430","630",[44,89],{"x":90,"y":80,"width":63,"height":64,"rx":65,"fill":91,"stroke":92,"style":93},"40","#eef7f4","#0f766e","stroke-width:2",[51,95,98],{"x":79,"y":96,"style":97,"fill":92,"textAnchor":57},"123","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","tab: Site · 2 cols",[44,100],{"x":61,"y":80,"width":63,"height":64,"rx":65,"fill":91,"stroke":92,"style":93},[51,102,103],{"x":53,"y":96,"style":97,"fill":92,"textAnchor":57},"tab: Condition",[44,105],{"x":106,"y":80,"width":63,"height":64,"rx":65,"fill":91,"stroke":92,"style":93},"540",[51,108,109],{"x":87,"y":96,"style":97,"fill":92,"textAnchor":57},"tab: Follow-up",[44,111],{"x":90,"y":112,"width":63,"height":113,"rx":65,"fill":66,"stroke":92,"style":114},"156","96","stroke-width:1.5",[51,116,119],{"x":79,"y":63,"style":117,"fill":118,"textAnchor":57},"text-anchor:middle;font-size:10px;font-family:monospace","#2f3b35","asset_id",[51,121,123],{"x":79,"y":122,"style":117,"fill":118,"textAnchor":57},"204","species_code",[51,125,127],{"x":79,"y":126,"style":117,"fill":118,"textAnchor":57},"228","height_m",[75,129],{"x1":79,"y1":130,"x2":79,"y2":112,"stroke":67,"style":81},"136",[44,132],{"x":61,"y":112,"width":63,"height":133,"rx":134,"fill":66,"stroke":92,"style":114},"30","6",[51,136,138],{"x":53,"y":137,"style":117,"fill":118,"textAnchor":57},"176","condition",[75,140],{"x1":53,"y1":130,"x2":53,"y2":112,"stroke":67,"style":81},[44,142],{"x":61,"y":143,"width":63,"height":144,"rx":65,"fill":145,"stroke":146,"style":93},"198","92","#fdf2e2","#b45309",[51,148,151],{"x":53,"y":149,"style":150,"fill":146,"textAnchor":57},"220","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","group: Damage",[51,153,156],{"x":53,"y":154,"style":155,"fill":118,"textAnchor":57},"240","text-anchor:middle;font-size:9.5px;font-family:monospace","visible if condition ≤ 2",[51,158,160],{"x":53,"y":159,"style":117,"fill":118,"textAnchor":57},"262","damage_type",[51,162,164],{"x":53,"y":163,"style":117,"fill":118,"textAnchor":57},"280","damage_notes",[44,166],{"x":106,"y":112,"width":63,"height":133,"rx":134,"fill":66,"stroke":92,"style":114},[51,168,169],{"x":87,"y":137,"style":117,"fill":118,"textAnchor":57},"action_required",[75,171],{"x1":87,"y1":130,"x2":87,"y2":112,"stroke":67,"style":81},[44,173],{"x":106,"y":143,"width":63,"height":174,"rx":65,"fill":175,"stroke":176,"style":93},"60","#eff3ff","#2563eb",[51,178,180],{"x":87,"y":179,"style":150,"fill":176,"textAnchor":57},"222","relation: repairs",[51,182,185],{"x":87,"y":183,"style":184,"fill":118,"textAnchor":57},"242","text-anchor:middle;font-size:9.5px;font-family:sans-serif","child records inline",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,194,202],"ul",{},[195,196,197,201],"li",{},[198,199,200],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series. Container types and visibility expressions shown here need 3.32 or later.",[195,203,204],{},"A vector layer with enough fields to justify structure, plus a relation to a child layer if you want to embed related records.",[187,206,208],{"id":207},"switch-to-a-designed-layout-and-add-tabs","Switch to a designed layout and add tabs",[14,210,211],{},"The form configuration has a layout mode. In drag-and-drop mode, the form is a tree hanging off an invisible root container; tabs are containers added directly under the root, and fields are elements inside them.",[213,214,219],"pre",{"className":215,"code":216,"language":217,"meta":218,"style":218},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    Qgis, QgsProject, QgsAttributeEditorContainer, QgsAttributeEditorField,\n)\n\ninspections = QgsProject.instance().mapLayersByName(\"inspections\")[0]\nfields = inspections.fields()\n\ndef field_element(name, parent):\n    idx = fields.indexOf(name)\n    if idx \u003C 0:\n        raise KeyError(name)\n    return QgsAttributeEditorField(name, idx, parent)\n\nconfig = inspections.editFormConfig()\nconfig.setLayout(Qgis.AttributeFormLayout.DragAndDrop)\nconfig.clearTabs()\nroot = config.invisibleRootContainer()\n\nsite = QgsAttributeEditorContainer(\"Site\", root)\nsite.setType(Qgis.AttributeEditorContainerType.Tab)\nsite.setColumnCount(2)\nfor name in (\"asset_id\", \"species_code\", \"height_m\", \"girth_cm\", \"surveyed_on\", \"surveyor\"):\n    site.addChildElement(field_element(name, site))\nroot.addChildElement(site)\n\ninspections.setEditFormConfig(config)\n","python","",[220,221,222,240,246,252,259,284,295,300,313,324,342,354,363,368,379,385,391,402,407,424,430,441,488,494,500,505],"code",{"__ignoreMap":218},[223,224,226,230,234,237],"span",{"class":75,"line":225},1,[223,227,229],{"class":228},"snl16","from",[223,231,233],{"class":232},"s95oV"," qgis.core ",[223,235,236],{"class":228},"import",[223,238,239],{"class":232}," (\n",[223,241,243],{"class":75,"line":242},2,[223,244,245],{"class":232},"    Qgis, QgsProject, QgsAttributeEditorContainer, QgsAttributeEditorField,\n",[223,247,249],{"class":75,"line":248},3,[223,250,251],{"class":232},")\n",[223,253,255],{"class":75,"line":254},4,[223,256,258],{"emptyLinePlaceholder":257},true,"\n",[223,260,262,265,268,271,275,278,281],{"class":75,"line":261},5,[223,263,264],{"class":232},"inspections ",[223,266,267],{"class":228},"=",[223,269,270],{"class":232}," QgsProject.instance().mapLayersByName(",[223,272,274],{"class":273},"sU2Wk","\"inspections\"",[223,276,277],{"class":232},")[",[223,279,46],{"class":280},"sDLfK",[223,282,283],{"class":232},"]\n",[223,285,287,290,292],{"class":75,"line":286},6,[223,288,289],{"class":232},"fields ",[223,291,267],{"class":228},[223,293,294],{"class":232}," inspections.fields()\n",[223,296,298],{"class":75,"line":297},7,[223,299,258],{"emptyLinePlaceholder":257},[223,301,303,306,310],{"class":75,"line":302},8,[223,304,305],{"class":228},"def",[223,307,309],{"class":308},"svObZ"," field_element",[223,311,312],{"class":232},"(name, parent):\n",[223,314,316,319,321],{"class":75,"line":315},9,[223,317,318],{"class":232},"    idx ",[223,320,267],{"class":228},[223,322,323],{"class":232}," fields.indexOf(name)\n",[223,325,327,330,333,336,339],{"class":75,"line":326},10,[223,328,329],{"class":228},"    if",[223,331,332],{"class":232}," idx ",[223,334,335],{"class":228},"\u003C",[223,337,338],{"class":280}," 0",[223,340,341],{"class":232},":\n",[223,343,345,348,351],{"class":75,"line":344},11,[223,346,347],{"class":228},"        raise",[223,349,350],{"class":280}," KeyError",[223,352,353],{"class":232},"(name)\n",[223,355,357,360],{"class":75,"line":356},12,[223,358,359],{"class":228},"    return",[223,361,362],{"class":232}," QgsAttributeEditorField(name, idx, parent)\n",[223,364,366],{"class":75,"line":365},13,[223,367,258],{"emptyLinePlaceholder":257},[223,369,371,374,376],{"class":75,"line":370},14,[223,372,373],{"class":232},"config ",[223,375,267],{"class":228},[223,377,378],{"class":232}," inspections.editFormConfig()\n",[223,380,382],{"class":75,"line":381},15,[223,383,384],{"class":232},"config.setLayout(Qgis.AttributeFormLayout.DragAndDrop)\n",[223,386,388],{"class":75,"line":387},16,[223,389,390],{"class":232},"config.clearTabs()\n",[223,392,394,397,399],{"class":75,"line":393},17,[223,395,396],{"class":232},"root ",[223,398,267],{"class":228},[223,400,401],{"class":232}," config.invisibleRootContainer()\n",[223,403,405],{"class":75,"line":404},18,[223,406,258],{"emptyLinePlaceholder":257},[223,408,410,413,415,418,421],{"class":75,"line":409},19,[223,411,412],{"class":232},"site ",[223,414,267],{"class":228},[223,416,417],{"class":232}," QgsAttributeEditorContainer(",[223,419,420],{"class":273},"\"Site\"",[223,422,423],{"class":232},", root)\n",[223,425,427],{"class":75,"line":426},20,[223,428,429],{"class":232},"site.setType(Qgis.AttributeEditorContainerType.Tab)\n",[223,431,433,436,439],{"class":75,"line":432},21,[223,434,435],{"class":232},"site.setColumnCount(",[223,437,438],{"class":280},"2",[223,440,251],{"class":232},[223,442,444,447,450,453,456,459,462,465,467,470,472,475,477,480,482,485],{"class":75,"line":443},22,[223,445,446],{"class":228},"for",[223,448,449],{"class":232}," name ",[223,451,452],{"class":228},"in",[223,454,455],{"class":232}," (",[223,457,458],{"class":273},"\"asset_id\"",[223,460,461],{"class":232},", ",[223,463,464],{"class":273},"\"species_code\"",[223,466,461],{"class":232},[223,468,469],{"class":273},"\"height_m\"",[223,471,461],{"class":232},[223,473,474],{"class":273},"\"girth_cm\"",[223,476,461],{"class":232},[223,478,479],{"class":273},"\"surveyed_on\"",[223,481,461],{"class":232},[223,483,484],{"class":273},"\"surveyor\"",[223,486,487],{"class":232},"):\n",[223,489,491],{"class":75,"line":490},23,[223,492,493],{"class":232},"    site.addChildElement(field_element(name, site))\n",[223,495,497],{"class":75,"line":496},24,[223,498,499],{"class":232},"root.addChildElement(site)\n",[223,501,503],{"class":75,"line":502},25,[223,504,258],{"emptyLinePlaceholder":257},[223,506,508],{"class":75,"line":507},26,[223,509,510],{"class":232},"inspections.setEditFormConfig(config)\n",[14,512,513,516,517,520,521,525,526,529,530,533,534,537,538,541],{},[198,514,515],{},"Breakdown:"," ",[220,518,519],{},"clearTabs()"," removes any existing designed layout, so running the script twice gives the same form rather than doubling it. Every element takes its parent in the constructor ",[522,523,524],"em",{},"and"," must be added to that parent with ",[220,527,528],{},"addChildElement","; the constructor argument only records the relationship. ",[220,531,532],{},"setColumnCount(2)"," lays the six fields out in two columns of three, which is what makes a tab look designed rather than listed. As with all form configuration, ",[220,535,536],{},"editFormConfig()"," hands you a copy, and nothing changes until ",[220,539,540],{},"setEditFormConfig"," writes it back.",[14,543,544,545,549],{},"Any field not placed in the tree does not appear in the form at all. That is useful for technical columns — ids, audit timestamps filled by ",[21,546,548],{"href":547},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis\u002F","default values"," — but it also means a field added to the table later stays invisible until the layout is updated.",[187,551,553],{"id":552},"groups-visibility-and-readable-labels","Groups, visibility and readable labels",[14,555,556],{},"Inside a tab, group boxes cluster related fields, and any container can carry a visibility expression so it appears only when it applies. Aliases and label positions do the rest of the work of making a form readable.",[14,558,559],{},[29,560,563,566,569,572,575,582,588,594,601,607,611,614,618,621,624,628,633,639,642,648,652],{"viewBox":561,"role":32,"ariaLabel":562,"xmlns":34},"0 0 760 256","A conditional group box in action: with condition set to 4 the Damage group is hidden, with condition set to 2 it appears, driven by the visibility expression on the container",[36,564,565],{},"Sections that appear only when relevant",[40,567,568],{},"Two states of the Condition tab. With condition 4 good, only the condition drop-down is visible and the form is short. With condition 2 poor, the Damage group box appears below it with damage type and notes fields, because its visibility expression condition less than or equal to 2 now evaluates true. The expression is re-evaluated as the user edits.",[44,570],{"x":46,"y":46,"width":47,"height":571,"fill":49},"256",[51,573,574],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"The form grows only when the answer needs it",[44,576],{"x":577,"y":578,"width":579,"height":580,"rx":581,"fill":66,"stroke":67,"style":93},"24","46","344","190","10",[51,583,587],{"x":584,"y":585,"style":586,"fill":56,"textAnchor":57},"196","70","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","condition = 4",[51,589,593],{"x":590,"y":591,"style":592,"fill":118},"48","104","font-size:10.5px;font-family:sans-serif","Condition",[44,595],{"x":596,"y":597,"width":584,"height":577,"rx":598,"fill":66,"stroke":599,"style":600},"150","88","4","#15803d","stroke-width:1.2",[51,602,606],{"x":603,"y":604,"style":605,"fill":118},"160","105","font-size:10px;font-family:sans-serif","4 – good",[51,608,610],{"x":584,"y":609,"style":72,"fill":67,"textAnchor":57},"172","Damage group hidden",[44,612],{"x":613,"y":578,"width":579,"height":580,"rx":581,"fill":66,"stroke":146,"style":93},"392",[51,615,617],{"x":616,"y":585,"style":586,"fill":146,"textAnchor":57},"564","condition = 2",[51,619,593],{"x":620,"y":591,"style":592,"fill":118},"416",[44,622],{"x":623,"y":597,"width":584,"height":577,"rx":598,"fill":66,"stroke":146,"style":600},"518",[51,625,627],{"x":626,"y":604,"style":605,"fill":118},"528","2 – poor",[44,629],{"x":630,"y":631,"width":632,"height":113,"rx":134,"fill":145,"stroke":146,"style":114},"412","126","304",[51,634,638],{"x":635,"y":636,"style":637,"fill":146},"428","146","font-size:10.5px;font-weight:bold;font-family:sans-serif","Damage",[51,640,641],{"x":635,"y":137,"style":605,"fill":118},"Damage type",[44,643],{"x":626,"y":644,"width":137,"height":645,"rx":598,"fill":66,"stroke":646,"style":647},"162","22","#9aa39d","stroke-width:1",[51,649,651],{"x":635,"y":650,"style":605,"fill":118},"206","Notes",[44,653],{"x":626,"y":654,"width":137,"height":645,"rx":598,"fill":66,"stroke":646,"style":647},"192",[213,656,658],{"className":215,"code":657,"language":217,"meta":218,"style":218},"from qgis.core import QgsExpression, QgsOptionalExpression\n\ncondition_tab = QgsAttributeEditorContainer(\"Condition\", root)\ncondition_tab.setType(Qgis.AttributeEditorContainerType.Tab)\ncondition_tab.addChildElement(field_element(\"condition\", condition_tab))\n\ndamage = QgsAttributeEditorContainer(\"Damage\", condition_tab)\ndamage.setType(Qgis.AttributeEditorContainerType.GroupBox)\ndamage.setColumnCount(1)\ndamage.setVisibilityExpression(\n    QgsOptionalExpression(QgsExpression('\"condition\" \u003C= 2')))\nfor name in (\"damage_type\", \"damage_extent\", \"damage_notes\"):\n    damage.addChildElement(field_element(name, damage))\ncondition_tab.addChildElement(damage)\nroot.addChildElement(condition_tab)\n\naliases = {\n    \"asset_id\": \"Asset ID\", \"species_code\": \"Species\", \"height_m\": \"Height (m)\",\n    \"girth_cm\": \"Girth at 1.5 m (cm)\", \"damage_notes\": \"Describe the damage\",\n}\nfor name, alias in aliases.items():\n    inspections.setFieldAlias(fields.indexOf(name), alias)\nconfig.setLabelOnTop(fields.indexOf(\"damage_notes\"), True)\n\ninspections.setEditFormConfig(config)\n",[220,659,660,671,675,689,694,705,709,724,729,739,744,755,780,785,790,795,799,809,841,862,867,879,884,899,903],{"__ignoreMap":218},[223,661,662,664,666,668],{"class":75,"line":225},[223,663,229],{"class":228},[223,665,233],{"class":232},[223,667,236],{"class":228},[223,669,670],{"class":232}," QgsExpression, QgsOptionalExpression\n",[223,672,673],{"class":75,"line":242},[223,674,258],{"emptyLinePlaceholder":257},[223,676,677,680,682,684,687],{"class":75,"line":248},[223,678,679],{"class":232},"condition_tab ",[223,681,267],{"class":228},[223,683,417],{"class":232},[223,685,686],{"class":273},"\"Condition\"",[223,688,423],{"class":232},[223,690,691],{"class":75,"line":254},[223,692,693],{"class":232},"condition_tab.setType(Qgis.AttributeEditorContainerType.Tab)\n",[223,695,696,699,702],{"class":75,"line":261},[223,697,698],{"class":232},"condition_tab.addChildElement(field_element(",[223,700,701],{"class":273},"\"condition\"",[223,703,704],{"class":232},", condition_tab))\n",[223,706,707],{"class":75,"line":286},[223,708,258],{"emptyLinePlaceholder":257},[223,710,711,714,716,718,721],{"class":75,"line":297},[223,712,713],{"class":232},"damage ",[223,715,267],{"class":228},[223,717,417],{"class":232},[223,719,720],{"class":273},"\"Damage\"",[223,722,723],{"class":232},", condition_tab)\n",[223,725,726],{"class":75,"line":302},[223,727,728],{"class":232},"damage.setType(Qgis.AttributeEditorContainerType.GroupBox)\n",[223,730,731,734,737],{"class":75,"line":315},[223,732,733],{"class":232},"damage.setColumnCount(",[223,735,736],{"class":280},"1",[223,738,251],{"class":232},[223,740,741],{"class":75,"line":326},[223,742,743],{"class":232},"damage.setVisibilityExpression(\n",[223,745,746,749,752],{"class":75,"line":344},[223,747,748],{"class":232},"    QgsOptionalExpression(QgsExpression(",[223,750,751],{"class":273},"'\"condition\" \u003C= 2'",[223,753,754],{"class":232},")))\n",[223,756,757,759,761,763,765,768,770,773,775,778],{"class":75,"line":356},[223,758,446],{"class":228},[223,760,449],{"class":232},[223,762,452],{"class":228},[223,764,455],{"class":232},[223,766,767],{"class":273},"\"damage_type\"",[223,769,461],{"class":232},[223,771,772],{"class":273},"\"damage_extent\"",[223,774,461],{"class":232},[223,776,777],{"class":273},"\"damage_notes\"",[223,779,487],{"class":232},[223,781,782],{"class":75,"line":365},[223,783,784],{"class":232},"    damage.addChildElement(field_element(name, damage))\n",[223,786,787],{"class":75,"line":370},[223,788,789],{"class":232},"condition_tab.addChildElement(damage)\n",[223,791,792],{"class":75,"line":381},[223,793,794],{"class":232},"root.addChildElement(condition_tab)\n",[223,796,797],{"class":75,"line":387},[223,798,258],{"emptyLinePlaceholder":257},[223,800,801,804,806],{"class":75,"line":393},[223,802,803],{"class":232},"aliases ",[223,805,267],{"class":228},[223,807,808],{"class":232}," {\n",[223,810,811,814,817,820,822,824,826,829,831,833,835,838],{"class":75,"line":404},[223,812,813],{"class":273},"    \"asset_id\"",[223,815,816],{"class":232},": ",[223,818,819],{"class":273},"\"Asset ID\"",[223,821,461],{"class":232},[223,823,464],{"class":273},[223,825,816],{"class":232},[223,827,828],{"class":273},"\"Species\"",[223,830,461],{"class":232},[223,832,469],{"class":273},[223,834,816],{"class":232},[223,836,837],{"class":273},"\"Height (m)\"",[223,839,840],{"class":232},",\n",[223,842,843,846,848,851,853,855,857,860],{"class":75,"line":409},[223,844,845],{"class":273},"    \"girth_cm\"",[223,847,816],{"class":232},[223,849,850],{"class":273},"\"Girth at 1.5 m (cm)\"",[223,852,461],{"class":232},[223,854,777],{"class":273},[223,856,816],{"class":232},[223,858,859],{"class":273},"\"Describe the damage\"",[223,861,840],{"class":232},[223,863,864],{"class":75,"line":426},[223,865,866],{"class":232},"}\n",[223,868,869,871,874,876],{"class":75,"line":432},[223,870,446],{"class":228},[223,872,873],{"class":232}," name, alias ",[223,875,452],{"class":228},[223,877,878],{"class":232}," aliases.items():\n",[223,880,881],{"class":75,"line":443},[223,882,883],{"class":232},"    inspections.setFieldAlias(fields.indexOf(name), alias)\n",[223,885,886,889,891,894,897],{"class":75,"line":490},[223,887,888],{"class":232},"config.setLabelOnTop(fields.indexOf(",[223,890,777],{"class":273},[223,892,893],{"class":232},"), ",[223,895,896],{"class":280},"True",[223,898,251],{"class":232},[223,900,901],{"class":75,"line":496},[223,902,258],{"emptyLinePlaceholder":257},[223,904,905],{"class":75,"line":502},[223,906,510],{"class":232},[14,908,909,516,911,914,915,918],{},[198,910,515],{},[220,912,913],{},"QgsOptionalExpression"," wraps the expression with an enabled flag, which is what the designer's checkbox toggles; constructing it from an expression enables it. The expression is re-evaluated as values change, so the section appears the moment a poor condition is chosen — no Python in the form. Hidden fields keep their values, so pair a conditional section with a constraint or an ",[220,916,917],{},"applyOnUpdate"," default if stale damage notes on a now-healthy tree would be a problem. Aliases are set on the layer and used everywhere field names are shown to users: the form, the attribute table, identify results. Putting the label on top gives a multi-line notes field the full width of the form.",[187,920,922],{"id":921},"embed-related-records","Embed related records",[14,924,925],{},"When the layer is the parent in a relation — a tree and its repair records, an inspection and its photos — a relation element shows the child features inside the parent's form, with buttons to add, link and open them.",[213,927,929],{"className":215,"code":928,"language":217,"meta":218,"style":218},"from qgis.core import QgsAttributeEditorRelation\n\nrelation = QgsProject.instance().relationManager().relation(\"inspections_repairs\")\nif not relation.isValid():\n    raise RuntimeError(\"relation inspections_repairs is not defined\")\n\nfollow_up = QgsAttributeEditorContainer(\"Follow-up\", root)\nfollow_up.setType(Qgis.AttributeEditorContainerType.Tab)\nfollow_up.addChildElement(field_element(\"action_required\", follow_up))\nfollow_up.addChildElement(field_element(\"priority\", follow_up))\n\nrepairs = QgsAttributeEditorRelation(relation, follow_up)\nrepairs.setLabel(\"Repairs carried out\")\nrepairs.setShowLabel(True)\nfollow_up.addChildElement(repairs)\nroot.addChildElement(follow_up)\n\ninspections.setEditFormConfig(config)\n",[220,930,931,942,946,961,972,988,992,1006,1011,1022,1031,1035,1045,1055,1064,1069,1074,1078],{"__ignoreMap":218},[223,932,933,935,937,939],{"class":75,"line":225},[223,934,229],{"class":228},[223,936,233],{"class":232},[223,938,236],{"class":228},[223,940,941],{"class":232}," QgsAttributeEditorRelation\n",[223,943,944],{"class":75,"line":242},[223,945,258],{"emptyLinePlaceholder":257},[223,947,948,951,953,956,959],{"class":75,"line":248},[223,949,950],{"class":232},"relation ",[223,952,267],{"class":228},[223,954,955],{"class":232}," QgsProject.instance().relationManager().relation(",[223,957,958],{"class":273},"\"inspections_repairs\"",[223,960,251],{"class":232},[223,962,963,966,969],{"class":75,"line":254},[223,964,965],{"class":228},"if",[223,967,968],{"class":228}," not",[223,970,971],{"class":232}," relation.isValid():\n",[223,973,974,977,980,983,986],{"class":75,"line":261},[223,975,976],{"class":228},"    raise",[223,978,979],{"class":280}," RuntimeError",[223,981,982],{"class":232},"(",[223,984,985],{"class":273},"\"relation inspections_repairs is not defined\"",[223,987,251],{"class":232},[223,989,990],{"class":75,"line":286},[223,991,258],{"emptyLinePlaceholder":257},[223,993,994,997,999,1001,1004],{"class":75,"line":297},[223,995,996],{"class":232},"follow_up ",[223,998,267],{"class":228},[223,1000,417],{"class":232},[223,1002,1003],{"class":273},"\"Follow-up\"",[223,1005,423],{"class":232},[223,1007,1008],{"class":75,"line":302},[223,1009,1010],{"class":232},"follow_up.setType(Qgis.AttributeEditorContainerType.Tab)\n",[223,1012,1013,1016,1019],{"class":75,"line":315},[223,1014,1015],{"class":232},"follow_up.addChildElement(field_element(",[223,1017,1018],{"class":273},"\"action_required\"",[223,1020,1021],{"class":232},", follow_up))\n",[223,1023,1024,1026,1029],{"class":75,"line":326},[223,1025,1015],{"class":232},[223,1027,1028],{"class":273},"\"priority\"",[223,1030,1021],{"class":232},[223,1032,1033],{"class":75,"line":344},[223,1034,258],{"emptyLinePlaceholder":257},[223,1036,1037,1040,1042],{"class":75,"line":356},[223,1038,1039],{"class":232},"repairs ",[223,1041,267],{"class":228},[223,1043,1044],{"class":232}," QgsAttributeEditorRelation(relation, follow_up)\n",[223,1046,1047,1050,1053],{"class":75,"line":365},[223,1048,1049],{"class":232},"repairs.setLabel(",[223,1051,1052],{"class":273},"\"Repairs carried out\"",[223,1054,251],{"class":232},[223,1056,1057,1060,1062],{"class":75,"line":370},[223,1058,1059],{"class":232},"repairs.setShowLabel(",[223,1061,896],{"class":280},[223,1063,251],{"class":232},[223,1065,1066],{"class":75,"line":381},[223,1067,1068],{"class":232},"follow_up.addChildElement(repairs)\n",[223,1070,1071],{"class":75,"line":387},[223,1072,1073],{"class":232},"root.addChildElement(follow_up)\n",[223,1075,1076],{"class":75,"line":393},[223,1077,258],{"emptyLinePlaceholder":257},[223,1079,1080],{"class":75,"line":404},[223,1081,510],{"class":232},[14,1083,1084,1086,1087,1091],{},[198,1085,515],{}," The relation must already exist in the project's relation manager, which is covered in ",[21,1088,1090],{"href":1089},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002F","defining layer relations","; the element only refers to it by id. The embedded child table uses the child layer's own form configuration when a record is opened, so the repair records can have their own tabs and widgets. Putting relations on their own tab keeps the parent form fast to open, because child features are only fetched when the tab is shown.",[14,1093,1094],{},[29,1095,1098,1101,1104,1106,1121,1124,1129,1134,1139,1142,1145,1149,1155,1161,1166,1169,1176,1178,1181,1188,1193,1196,1200,1202],{"viewBox":1096,"role":32,"ariaLabel":1097,"xmlns":34},"0 0 760 240","Building the same form layout on every matching layer from one function, with a report of fields each layer lacks",[36,1099,1100],{},"One layout function, many layers",[40,1102,1103],{},"A build_form function takes a layer and a layout specification listing tabs, groups and field names. It is applied to three inspection layers. Fields present in a layer are placed; fields missing from a layer are skipped and reported. Each layer then saves its Forms style category so the layout goes with the data.",[44,1105],{"x":46,"y":46,"width":47,"height":154,"fill":49},[1107,1108,1109],"defs",{},[1110,1111,1117],"marker",{"id":1112,"viewBox":1113,"refX":65,"refY":1114,"markerWidth":1115,"markerHeight":1115,"orient":1116},"tfSpecArrow","0 0 10 10","5","7","auto-start-reverse",[1118,1119],"path",{"d":1120,"fill":118},"M0 0 L10 5 L0 10 z",[51,1122,1123],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Describe the layout as data, build it in a loop",[44,1125],{"x":577,"y":1126,"width":1127,"height":1128,"rx":581,"fill":66,"stroke":67,"style":93},"50","230","166",[51,1130,1133],{"x":1131,"y":1132,"style":97,"fill":56,"textAnchor":57},"139","74","LAYOUT spec",[51,1135,1138],{"x":62,"y":1136,"style":1137,"fill":118},"102","font-size:9.5px;font-family:monospace","Site: asset_id, species…",[51,1140,1141],{"x":62,"y":631,"style":1137,"fill":118},"Condition: condition",[51,1143,1144],{"x":62,"y":596,"style":1137,"fill":118},"  Damage if cond ≤ 2",[51,1146,1148],{"x":62,"y":1147,"style":1137,"fill":118},"174","Follow-up: action…",[75,1150],{"x1":1151,"y1":1152,"x2":1153,"y2":1152,"stroke":118,"style":1154},"254","133","298","stroke-width:1.8;marker-end:url(#tfSpecArrow)",[44,1156],{"x":1157,"y":1158,"width":1159,"height":585,"rx":581,"fill":91,"stroke":92,"style":1160},"306","98","140","stroke-width:2.5",[51,1162,1165],{"x":1163,"y":79,"style":1164,"fill":92,"textAnchor":57},"376","text-anchor:middle;font-size:10.5px;font-family:monospace","build_form()",[51,1167,1168],{"x":1163,"y":596,"style":184,"fill":118,"textAnchor":57},"per layer",[75,1170],{"x1":1171,"y1":1172,"x2":1173,"y2":1174,"stroke":118,"style":1175},"446","120","494","72","stroke-width:1.6;marker-end:url(#tfSpecArrow)",[75,1177],{"x1":1171,"y1":1152,"x2":1173,"y2":1152,"stroke":118,"style":1175},[75,1179],{"x1":1171,"y1":636,"x2":1173,"y2":1180,"stroke":118,"style":1175},"194",[44,1182],{"x":1183,"y":1126,"width":1184,"height":1185,"rx":65,"fill":1186,"stroke":599,"style":1187},"502","234","44","#e8efe6","stroke-width:1.8",[51,1189,1192],{"x":1190,"y":1191,"style":72,"fill":118,"textAnchor":57},"619","77","inspections_north · complete",[44,1194],{"x":1183,"y":1195,"width":1184,"height":1185,"rx":65,"fill":1186,"stroke":599,"style":1187},"111",[51,1197,1199],{"x":1190,"y":1198,"style":72,"fill":118,"textAnchor":57},"138","inspections_south · complete",[44,1201],{"x":1183,"y":609,"width":1184,"height":1185,"rx":65,"fill":145,"stroke":146,"style":1187},[51,1203,1205],{"x":1190,"y":1204,"style":72,"fill":118,"textAnchor":57},"199","inspections_2019 · no girth_cm",[187,1207,1209],{"id":1208},"build-it-from-a-specification","Build it from a specification",[14,1211,1212],{},"Hand-writing the tree for every layer repeats itself quickly. A small specification — tabs, optional groups with visibility expressions, field names — and one function that walks it make the layout reviewable and reusable.",[213,1214,1216],{"className":215,"code":1215,"language":217,"meta":218,"style":218},"LAYOUT = [\n    (\"Site\", 2, [\"asset_id\", \"species_code\", \"height_m\", \"girth_cm\"], []),\n    (\"Condition\", 1, [\"condition\"], [\n        (\"Damage\", '\"condition\" \u003C= 2', [\"damage_type\", \"damage_notes\"]),\n    ]),\n    (\"Follow-up\", 1, [\"action_required\", \"priority\"], []),\n]\n\ndef build_form(layer, layout):\n    config = layer.editFormConfig()\n    config.setLayout(Qgis.AttributeFormLayout.DragAndDrop)\n    config.clearTabs()\n    root = config.invisibleRootContainer()\n    names = set(layer.fields().names())\n    skipped = []\n\n    def add_fields(container, field_names):\n        for name in field_names:\n            if name not in names:\n                skipped.append(name)\n                continue\n            container.addChildElement(QgsAttributeEditorField(\n                name, layer.fields().indexOf(name), container))\n\n    for title, columns, tab_fields, groups in layout:\n        tab = QgsAttributeEditorContainer(title, root)\n        tab.setType(Qgis.AttributeEditorContainerType.Tab)\n        tab.setColumnCount(columns)\n        add_fields(tab, tab_fields)\n        for group_title, visible_if, group_fields in groups:\n            group = QgsAttributeEditorContainer(group_title, tab)\n            group.setType(Qgis.AttributeEditorContainerType.GroupBox)\n            if visible_if:\n                group.setVisibilityExpression(\n                    QgsOptionalExpression(QgsExpression(visible_if)))\n            add_fields(group, group_fields)\n            tab.addChildElement(group)\n        root.addChildElement(tab)\n\n    layer.setEditFormConfig(config)\n    return skipped\n\nfor layer in QgsProject.instance().mapLayers().values():\n    if layer.name().startswith(\"inspections\"):\n        print(layer.name(), \"skipped:\", build_form(layer, LAYOUT) or \"none\")\n",[220,1217,1218,1229,1260,1277,1299,1304,1324,1328,1332,1342,1352,1357,1362,1371,1384,1394,1398,1409,1421,1437,1442,1447,1452,1457,1461,1474,1484,1490,1496,1502,1515,1526,1532,1540,1546,1552,1558,1564,1570,1575,1581,1589,1594,1607,1619],{"__ignoreMap":218},[223,1219,1220,1223,1226],{"class":75,"line":225},[223,1221,1222],{"class":280},"LAYOUT",[223,1224,1225],{"class":228}," =",[223,1227,1228],{"class":232}," [\n",[223,1230,1231,1234,1236,1238,1240,1243,1245,1247,1249,1251,1253,1255,1257],{"class":75,"line":242},[223,1232,1233],{"class":232},"    (",[223,1235,420],{"class":273},[223,1237,461],{"class":232},[223,1239,438],{"class":280},[223,1241,1242],{"class":232},", [",[223,1244,458],{"class":273},[223,1246,461],{"class":232},[223,1248,464],{"class":273},[223,1250,461],{"class":232},[223,1252,469],{"class":273},[223,1254,461],{"class":232},[223,1256,474],{"class":273},[223,1258,1259],{"class":232},"], []),\n",[223,1261,1262,1264,1266,1268,1270,1272,1274],{"class":75,"line":248},[223,1263,1233],{"class":232},[223,1265,686],{"class":273},[223,1267,461],{"class":232},[223,1269,736],{"class":280},[223,1271,1242],{"class":232},[223,1273,701],{"class":273},[223,1275,1276],{"class":232},"], [\n",[223,1278,1279,1282,1284,1286,1288,1290,1292,1294,1296],{"class":75,"line":254},[223,1280,1281],{"class":232},"        (",[223,1283,720],{"class":273},[223,1285,461],{"class":232},[223,1287,751],{"class":273},[223,1289,1242],{"class":232},[223,1291,767],{"class":273},[223,1293,461],{"class":232},[223,1295,777],{"class":273},[223,1297,1298],{"class":232},"]),\n",[223,1300,1301],{"class":75,"line":261},[223,1302,1303],{"class":232},"    ]),\n",[223,1305,1306,1308,1310,1312,1314,1316,1318,1320,1322],{"class":75,"line":286},[223,1307,1233],{"class":232},[223,1309,1003],{"class":273},[223,1311,461],{"class":232},[223,1313,736],{"class":280},[223,1315,1242],{"class":232},[223,1317,1018],{"class":273},[223,1319,461],{"class":232},[223,1321,1028],{"class":273},[223,1323,1259],{"class":232},[223,1325,1326],{"class":75,"line":297},[223,1327,283],{"class":232},[223,1329,1330],{"class":75,"line":302},[223,1331,258],{"emptyLinePlaceholder":257},[223,1333,1334,1336,1339],{"class":75,"line":315},[223,1335,305],{"class":228},[223,1337,1338],{"class":308}," build_form",[223,1340,1341],{"class":232},"(layer, layout):\n",[223,1343,1344,1347,1349],{"class":75,"line":326},[223,1345,1346],{"class":232},"    config ",[223,1348,267],{"class":228},[223,1350,1351],{"class":232}," layer.editFormConfig()\n",[223,1353,1354],{"class":75,"line":344},[223,1355,1356],{"class":232},"    config.setLayout(Qgis.AttributeFormLayout.DragAndDrop)\n",[223,1358,1359],{"class":75,"line":356},[223,1360,1361],{"class":232},"    config.clearTabs()\n",[223,1363,1364,1367,1369],{"class":75,"line":365},[223,1365,1366],{"class":232},"    root ",[223,1368,267],{"class":228},[223,1370,401],{"class":232},[223,1372,1373,1376,1378,1381],{"class":75,"line":370},[223,1374,1375],{"class":232},"    names ",[223,1377,267],{"class":228},[223,1379,1380],{"class":280}," set",[223,1382,1383],{"class":232},"(layer.fields().names())\n",[223,1385,1386,1389,1391],{"class":75,"line":381},[223,1387,1388],{"class":232},"    skipped ",[223,1390,267],{"class":228},[223,1392,1393],{"class":232}," []\n",[223,1395,1396],{"class":75,"line":387},[223,1397,258],{"emptyLinePlaceholder":257},[223,1399,1400,1403,1406],{"class":75,"line":393},[223,1401,1402],{"class":228},"    def",[223,1404,1405],{"class":308}," add_fields",[223,1407,1408],{"class":232},"(container, field_names):\n",[223,1410,1411,1414,1416,1418],{"class":75,"line":404},[223,1412,1413],{"class":228},"        for",[223,1415,449],{"class":232},[223,1417,452],{"class":228},[223,1419,1420],{"class":232}," field_names:\n",[223,1422,1423,1426,1428,1431,1434],{"class":75,"line":409},[223,1424,1425],{"class":228},"            if",[223,1427,449],{"class":232},[223,1429,1430],{"class":228},"not",[223,1432,1433],{"class":228}," in",[223,1435,1436],{"class":232}," names:\n",[223,1438,1439],{"class":75,"line":426},[223,1440,1441],{"class":232},"                skipped.append(name)\n",[223,1443,1444],{"class":75,"line":432},[223,1445,1446],{"class":228},"                continue\n",[223,1448,1449],{"class":75,"line":443},[223,1450,1451],{"class":232},"            container.addChildElement(QgsAttributeEditorField(\n",[223,1453,1454],{"class":75,"line":490},[223,1455,1456],{"class":232},"                name, layer.fields().indexOf(name), container))\n",[223,1458,1459],{"class":75,"line":496},[223,1460,258],{"emptyLinePlaceholder":257},[223,1462,1463,1466,1469,1471],{"class":75,"line":502},[223,1464,1465],{"class":228},"    for",[223,1467,1468],{"class":232}," title, columns, tab_fields, groups ",[223,1470,452],{"class":228},[223,1472,1473],{"class":232}," layout:\n",[223,1475,1476,1479,1481],{"class":75,"line":507},[223,1477,1478],{"class":232},"        tab ",[223,1480,267],{"class":228},[223,1482,1483],{"class":232}," QgsAttributeEditorContainer(title, root)\n",[223,1485,1487],{"class":75,"line":1486},27,[223,1488,1489],{"class":232},"        tab.setType(Qgis.AttributeEditorContainerType.Tab)\n",[223,1491,1493],{"class":75,"line":1492},28,[223,1494,1495],{"class":232},"        tab.setColumnCount(columns)\n",[223,1497,1499],{"class":75,"line":1498},29,[223,1500,1501],{"class":232},"        add_fields(tab, tab_fields)\n",[223,1503,1505,1507,1510,1512],{"class":75,"line":1504},30,[223,1506,1413],{"class":228},[223,1508,1509],{"class":232}," group_title, visible_if, group_fields ",[223,1511,452],{"class":228},[223,1513,1514],{"class":232}," groups:\n",[223,1516,1518,1521,1523],{"class":75,"line":1517},31,[223,1519,1520],{"class":232},"            group ",[223,1522,267],{"class":228},[223,1524,1525],{"class":232}," QgsAttributeEditorContainer(group_title, tab)\n",[223,1527,1529],{"class":75,"line":1528},32,[223,1530,1531],{"class":232},"            group.setType(Qgis.AttributeEditorContainerType.GroupBox)\n",[223,1533,1535,1537],{"class":75,"line":1534},33,[223,1536,1425],{"class":228},[223,1538,1539],{"class":232}," visible_if:\n",[223,1541,1543],{"class":75,"line":1542},34,[223,1544,1545],{"class":232},"                group.setVisibilityExpression(\n",[223,1547,1549],{"class":75,"line":1548},35,[223,1550,1551],{"class":232},"                    QgsOptionalExpression(QgsExpression(visible_if)))\n",[223,1553,1555],{"class":75,"line":1554},36,[223,1556,1557],{"class":232},"            add_fields(group, group_fields)\n",[223,1559,1561],{"class":75,"line":1560},37,[223,1562,1563],{"class":232},"            tab.addChildElement(group)\n",[223,1565,1567],{"class":75,"line":1566},38,[223,1568,1569],{"class":232},"        root.addChildElement(tab)\n",[223,1571,1573],{"class":75,"line":1572},39,[223,1574,258],{"emptyLinePlaceholder":257},[223,1576,1578],{"class":75,"line":1577},40,[223,1579,1580],{"class":232},"    layer.setEditFormConfig(config)\n",[223,1582,1584,1586],{"class":75,"line":1583},41,[223,1585,359],{"class":228},[223,1587,1588],{"class":232}," skipped\n",[223,1590,1592],{"class":75,"line":1591},42,[223,1593,258],{"emptyLinePlaceholder":257},[223,1595,1597,1599,1602,1604],{"class":75,"line":1596},43,[223,1598,446],{"class":228},[223,1600,1601],{"class":232}," layer ",[223,1603,452],{"class":228},[223,1605,1606],{"class":232}," QgsProject.instance().mapLayers().values():\n",[223,1608,1610,1612,1615,1617],{"class":75,"line":1609},44,[223,1611,329],{"class":228},[223,1613,1614],{"class":232}," layer.name().startswith(",[223,1616,274],{"class":273},[223,1618,487],{"class":232},[223,1620,1622,1625,1628,1631,1634,1636,1639,1642,1645],{"class":75,"line":1621},45,[223,1623,1624],{"class":280},"        print",[223,1626,1627],{"class":232},"(layer.name(), ",[223,1629,1630],{"class":273},"\"skipped:\"",[223,1632,1633],{"class":232},", build_form(layer, ",[223,1635,1222],{"class":280},[223,1637,1638],{"class":232},") ",[223,1640,1641],{"class":228},"or",[223,1643,1644],{"class":273}," \"none\"",[223,1646,251],{"class":232},[14,1648,1649,1651,1652,1655,1656,1660],{},[198,1650,515],{}," The specification is plain Python data, so it can live in a plugin module, be diffed in code review, and be loaded from JSON if non-developers maintain it. Skipping missing fields keeps older layers working with a newer layout and reports what they lack. After building, save the ",[220,1653,1654],{},"Forms"," style category to the data source — as in ",[21,1657,1659],{"href":1658},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fconfigure-editor-widgets-pyqgis\u002F","configuring editor widgets"," — so the layout is picked up wherever the layer is loaded.",[187,1662,1664],{"id":1663},"read-an-existing-layout-back","Read an existing layout back",[14,1666,1667],{},"Forms designed by hand in the Layer Properties dialog are often the starting point: someone has already arranged the fields sensibly, and the job is to capture that arrangement as a specification so it can be applied elsewhere. Walking the tree recursively turns any designed form back into data.",[213,1669,1671],{"className":215,"code":1670,"language":217,"meta":218,"style":218},"from qgis.core import QgsAttributeEditorElement\n\ndef describe(element, depth=0):\n    pad = \"  \" * depth\n    kind = element.type()\n    if isinstance(element, QgsAttributeEditorContainer):\n        visible = element.visibilityExpression()\n        condition = f\" if {visible.data().expression()}\" if visible.enabled() else \"\"\n        print(f\"{pad}[{element.name()}] cols={element.columnCount()}{condition}\")\n        for child in element.children():\n            describe(child, depth + 1)\n    elif isinstance(element, QgsAttributeEditorField):\n        print(f\"{pad}- {element.name()}\")\n    else:\n        print(f\"{pad}* {element.name()} ({kind})\")\n\nsource = QgsProject.instance().mapLayersByName(\"inspections_template\")[0]\nfor tab in source.editFormConfig().invisibleRootContainer().children():\n    describe(tab)\n",[220,1672,1673,1684,1688,1704,1720,1730,1740,1750,1787,1834,1846,1859,1869,1898,1905,1944,1948,1966,1978],{"__ignoreMap":218},[223,1674,1675,1677,1679,1681],{"class":75,"line":225},[223,1676,229],{"class":228},[223,1678,233],{"class":232},[223,1680,236],{"class":228},[223,1682,1683],{"class":232}," QgsAttributeEditorElement\n",[223,1685,1686],{"class":75,"line":242},[223,1687,258],{"emptyLinePlaceholder":257},[223,1689,1690,1692,1695,1698,1700,1702],{"class":75,"line":248},[223,1691,305],{"class":228},[223,1693,1694],{"class":308}," describe",[223,1696,1697],{"class":232},"(element, depth",[223,1699,267],{"class":228},[223,1701,46],{"class":280},[223,1703,487],{"class":232},[223,1705,1706,1709,1711,1714,1717],{"class":75,"line":254},[223,1707,1708],{"class":232},"    pad ",[223,1710,267],{"class":228},[223,1712,1713],{"class":273}," \"  \"",[223,1715,1716],{"class":228}," *",[223,1718,1719],{"class":232}," depth\n",[223,1721,1722,1725,1727],{"class":75,"line":261},[223,1723,1724],{"class":232},"    kind ",[223,1726,267],{"class":228},[223,1728,1729],{"class":232}," element.type()\n",[223,1731,1732,1734,1737],{"class":75,"line":286},[223,1733,329],{"class":228},[223,1735,1736],{"class":280}," isinstance",[223,1738,1739],{"class":232},"(element, QgsAttributeEditorContainer):\n",[223,1741,1742,1745,1747],{"class":75,"line":297},[223,1743,1744],{"class":232},"        visible ",[223,1746,267],{"class":228},[223,1748,1749],{"class":232}," element.visibilityExpression()\n",[223,1751,1752,1755,1757,1760,1763,1766,1769,1772,1775,1778,1781,1784],{"class":75,"line":302},[223,1753,1754],{"class":232},"        condition ",[223,1756,267],{"class":228},[223,1758,1759],{"class":228}," f",[223,1761,1762],{"class":273},"\" if ",[223,1764,1765],{"class":280},"{",[223,1767,1768],{"class":232},"visible.data().expression()",[223,1770,1771],{"class":280},"}",[223,1773,1774],{"class":273},"\"",[223,1776,1777],{"class":228}," if",[223,1779,1780],{"class":232}," visible.enabled() ",[223,1782,1783],{"class":228},"else",[223,1785,1786],{"class":273}," \"\"\n",[223,1788,1789,1791,1793,1796,1798,1800,1803,1805,1808,1810,1813,1815,1818,1820,1823,1826,1828,1830,1832],{"class":75,"line":315},[223,1790,1624],{"class":280},[223,1792,982],{"class":232},[223,1794,1795],{"class":228},"f",[223,1797,1774],{"class":273},[223,1799,1765],{"class":280},[223,1801,1802],{"class":232},"pad",[223,1804,1771],{"class":280},[223,1806,1807],{"class":273},"[",[223,1809,1765],{"class":280},[223,1811,1812],{"class":232},"element.name()",[223,1814,1771],{"class":280},[223,1816,1817],{"class":273},"] cols=",[223,1819,1765],{"class":280},[223,1821,1822],{"class":232},"element.columnCount()",[223,1824,1825],{"class":280},"}{",[223,1827,138],{"class":232},[223,1829,1771],{"class":280},[223,1831,1774],{"class":273},[223,1833,251],{"class":232},[223,1835,1836,1838,1841,1843],{"class":75,"line":326},[223,1837,1413],{"class":228},[223,1839,1840],{"class":232}," child ",[223,1842,452],{"class":228},[223,1844,1845],{"class":232}," element.children():\n",[223,1847,1848,1851,1854,1857],{"class":75,"line":344},[223,1849,1850],{"class":232},"            describe(child, depth ",[223,1852,1853],{"class":228},"+",[223,1855,1856],{"class":280}," 1",[223,1858,251],{"class":232},[223,1860,1861,1864,1866],{"class":75,"line":356},[223,1862,1863],{"class":228},"    elif",[223,1865,1736],{"class":280},[223,1867,1868],{"class":232},"(element, QgsAttributeEditorField):\n",[223,1870,1871,1873,1875,1877,1879,1881,1883,1885,1888,1890,1892,1894,1896],{"class":75,"line":365},[223,1872,1624],{"class":280},[223,1874,982],{"class":232},[223,1876,1795],{"class":228},[223,1878,1774],{"class":273},[223,1880,1765],{"class":280},[223,1882,1802],{"class":232},[223,1884,1771],{"class":280},[223,1886,1887],{"class":273},"- ",[223,1889,1765],{"class":280},[223,1891,1812],{"class":232},[223,1893,1771],{"class":280},[223,1895,1774],{"class":273},[223,1897,251],{"class":232},[223,1899,1900,1903],{"class":75,"line":370},[223,1901,1902],{"class":228},"    else",[223,1904,341],{"class":232},[223,1906,1907,1909,1911,1913,1915,1917,1919,1921,1924,1926,1928,1930,1932,1934,1937,1939,1942],{"class":75,"line":381},[223,1908,1624],{"class":280},[223,1910,982],{"class":232},[223,1912,1795],{"class":228},[223,1914,1774],{"class":273},[223,1916,1765],{"class":280},[223,1918,1802],{"class":232},[223,1920,1771],{"class":280},[223,1922,1923],{"class":273},"* ",[223,1925,1765],{"class":280},[223,1927,1812],{"class":232},[223,1929,1771],{"class":280},[223,1931,455],{"class":273},[223,1933,1765],{"class":280},[223,1935,1936],{"class":232},"kind",[223,1938,1771],{"class":280},[223,1940,1941],{"class":273},")\"",[223,1943,251],{"class":232},[223,1945,1946],{"class":75,"line":387},[223,1947,258],{"emptyLinePlaceholder":257},[223,1949,1950,1953,1955,1957,1960,1962,1964],{"class":75,"line":393},[223,1951,1952],{"class":232},"source ",[223,1954,267],{"class":228},[223,1956,270],{"class":232},[223,1958,1959],{"class":273},"\"inspections_template\"",[223,1961,277],{"class":232},[223,1963,46],{"class":280},[223,1965,283],{"class":232},[223,1967,1968,1970,1973,1975],{"class":75,"line":404},[223,1969,446],{"class":228},[223,1971,1972],{"class":232}," tab ",[223,1974,452],{"class":228},[223,1976,1977],{"class":232}," source.editFormConfig().invisibleRootContainer().children():\n",[223,1979,1980],{"class":75,"line":409},[223,1981,1982],{"class":232},"    describe(tab)\n",[14,1984,1985,1987,1988,1991,1992,1995,1996,1998,1999,2002,2003,2006],{},[198,1986,515],{}," Every element in the tree is a subclass of ",[220,1989,1990],{},"QgsAttributeEditorElement",", so ",[220,1993,1994],{},"isinstance"," checks sort containers, fields and everything else — relations, text blocks, HTML — into the right branch. Printing the visibility expression only when it is enabled matches what users actually see, because the designer keeps a disabled expression around. The output reads almost exactly like the ",[220,1997,1222],{}," specification used by ",[220,2000,2001],{},"build_form",", and extending ",[220,2004,2005],{},"describe"," to return tuples instead of printing gives you a converter from a hand-designed form to a reusable one. That round trip — design visually once, capture as data, apply everywhere by script — is usually the most practical way for a team to agree a form and then roll it out.",[187,2008,2010],{"id":2009},"qgis-version-compatibility","QGIS version compatibility",[14,2012,2013,2016,2017,2020,2021,2024,2025,2028,2029,2032,2033,2036],{},[220,2014,2015],{},"setType"," with ",[220,2018,2019],{},"Qgis.AttributeEditorContainerType"," arrived in QGIS 3.32; earlier releases use ",[220,2022,2023],{},"setIsGroupBox(True)"," for group boxes and ",[220,2026,2027],{},"False"," for tabs, and ",[220,2030,2031],{},"QgsEditFormConfig.TabLayout"," instead of ",[220,2034,2035],{},"Qgis.AttributeFormLayout.DragAndDrop",". Visibility expressions on containers have existed since 3.0. The QGIS 4 series accepts only the scoped enum forms. Mobile apps such as QField honour tabs, groups, visibility expressions and relation editors.",[187,2038,2040],{"id":2039},"troubleshooting","Troubleshooting",[192,2042,2043,2049,2058,2066,2072],{},[195,2044,2045,2048],{},[198,2046,2047],{},"The form still shows every field in a list."," The layout mode was not switched, or the config was not written back.",[195,2050,2051,2054,2055,2057],{},[198,2052,2053],{},"A tab is empty."," Elements were created with the tab as parent but never added with ",[220,2056,528],{},".",[195,2059,2060,2063,2064,2057],{},[198,2061,2062],{},"Fields appear twice."," The script ran twice without ",[220,2065,519],{},[195,2067,2068,2071],{},[198,2069,2070],{},"A conditional group never appears."," The expression compares a coded value to a label; compare to the stored value.",[195,2073,2074,2077,2078,2057],{},[198,2075,2076],{},"The relation element shows nothing."," The relation id is wrong or the relation is invalid; check ",[220,2079,2080],{},"relation.isValid()",[187,2082,2084],{"id":2083},"conclusion","Conclusion",[14,2086,2087],{},"Switch the form to the drag-and-drop layout, build tabs and group boxes as containers under the invisible root, and add fields by name. Use columns and aliases to make tabs readable, visibility expressions to keep sections out of the way until they apply, and relation elements to bring child records into the parent's form. Keep the layout as a specification and build it with one function so every matching layer gets the same form.",[187,2089,2091],{"id":2090},"frequently-asked-questions","Frequently Asked Questions",[14,2093,2094,2097,2098,2101],{},[198,2095,2096],{},"Can I read an existing designed form back into Python?","\nYes. Walk ",[220,2099,2100],{},"config.invisibleRootContainer().children()"," recursively; each child is a container, field, relation or other element with its own properties.",[14,2103,2104,2107,2108,2111,2112,2115],{},[198,2105,2106],{},"Can a tab contain plain explanatory text?","\nYes — add a ",[220,2109,2110],{},"QgsAttributeEditorTextElement"," (3.30+) or ",[220,2113,2114],{},"QgsAttributeEditorHtmlElement"," with instructions for the person filling in the form.",[14,2117,2118,2121],{},[198,2119,2120],{},"Do layouts work in the attribute table's form view?","\nYes. The form view in the attribute table uses the same layout.",[14,2123,2124,2127,2128,2131],{},[198,2125,2126],{},"Can I stop the form popping up after digitising?","\nSet ",[220,2129,2130],{},"config.setSuppress(Qgis.AttributeFormSuppression.On)","; defaults are still applied.",[187,2133,2135],{"id":2134},"related","Related",[192,2137,2138,2143,2148,2154,2159],{},[195,2139,2140,2142],{},[21,2141,24],{"href":23}," — the guide this recipe belongs to",[195,2144,2145],{},[21,2146,2147],{"href":1658},"Configure Editor Widgets in PyQGIS",[195,2149,2150],{},[21,2151,2153],{"href":2152},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fadd-python-logic-to-attribute-form-pyqgis\u002F","Add Python Logic to an Attribute Form in PyQGIS",[195,2155,2156],{},[21,2157,2158],{"href":1089},"Define Layer Relations in PyQGIS",[195,2160,2161],{},[21,2162,2164],{"href":2163},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fuse-qgis-custom-widgets-in-qt-designer\u002F","Use QGIS Custom Widgets in Qt Designer",[2166,2167,2168],"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 pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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":218,"searchDepth":242,"depth":242,"links":2170},[2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181],{"id":189,"depth":242,"text":190},{"id":207,"depth":242,"text":208},{"id":552,"depth":242,"text":553},{"id":921,"depth":242,"text":922},{"id":1208,"depth":242,"text":1209},{"id":1663,"depth":242,"text":1664},{"id":2009,"depth":242,"text":2010},{"id":2039,"depth":242,"text":2040},{"id":2083,"depth":242,"text":2084},{"id":2090,"depth":242,"text":2091},{"id":2134,"depth":242,"text":2135},"Replace the auto-generated field list with a drag-and-drop form built in code — tabs, group boxes, columns, conditional sections, aliases, help text and embedded child tables — using QgsAttributeEditorContainer.","md",{"slug":2185,"type":2186,"breadcrumb":2187,"datePublished":2188,"dateModified":2188},"build-tabbed-attribute-form-layout-pyqgis","article","Tabbed Attribute Form Layout","2026-09-17","\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis",{"title":5,"description":2182},"qgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis\u002Findex","uXld6KbqgQC-4brXxmwfYVlpAMi8HtgRTpQGet9Ve0U",1789632907420]