[{"data":1,"prerenderedAt":1523},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis":3},{"id":4,"title":5,"body":6,"description":1512,"extension":1513,"meta":1514,"navigation":266,"path":1519,"seo":1520,"stem":1521,"__hash__":1522},"docs\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis\u002Findex.md","Add an Attribute Table to a Layout in PyQGIS",{"type":7,"value":8,"toc":1497},"minimark",[9,13,22,31,183,188,210,214,409,439,443,588,617,620,658,669,673,794,890,910,919,923,926,1066,1071,1082,1086,1131,1147,1151,1154,1164,1167,1221,1229,1233,1236,1305,1326,1330,1348,1352,1406,1410,1413,1417,1426,1444,1450,1460,1464,1493],[10,11,5],"h1",{"id":12},"add-an-attribute-table-to-a-layout-in-pyqgis",[14,15,16,17,21],"p",{},"A map with a table beside it answers questions a map alone cannot: which sites these are, how big each one is, when each was last inspected. QGIS's layout attribute table does that, and it is the one layout item with an unusual structure — it is a ",[18,19,20],"em",{},"multiframe",", a content object that flows across one or more frames, which is what lets a long table continue onto a second page.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002F","Automated Map Layout Generation",". It covers creating the table and its frame, choosing and renaming columns, the four content sources, filtering and sorting, and letting a long table paginate.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,101,105,110,118,124,150,153,156,160,169,173,178],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 320","img","The multiframe structure: one attribute table object owning two frames on two pages, with rows flowing from the first into the second","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"One table, many frames",[45,46,47],"desc",{},"An attribute table is a multiframe: a single content object added to the layout, with one or more frames placed on pages. Rows fill the first frame and continue into the next, so a table longer than one page flows onto the second without any manual splitting.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","320","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"latFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The content is one object; the frames are where it lands",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","48","164","72","#eef7f4","#0f766e","stroke-width:2.5",[73,91,95],{"x":92,"y":93,"style":94,"fill":88,"textAnchor":79},"106","74","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","the multiframe",[73,97,100],{"x":92,"y":98,"style":99,"fill":71,"textAnchor":79},"96","text-anchor:middle;font-size:10px;font-family:sans-serif","layer, columns, filter",[73,102,104],{"x":92,"y":103,"style":99,"fill":71,"textAnchor":79},"114","added to the layout",[68,106],{"d":107,"fill":108,"stroke":71,"style":109},"M188 84 L262 84 L262 122","none","stroke-width:2;marker-end:url(#latFlowArrow)",[49,111],{"x":112,"y":113,"width":114,"height":85,"rx":63,"fill":115,"stroke":116,"style":117},"196","128","240","#fffdf7","#59645f","stroke-width:2",[73,119,123],{"x":120,"y":121,"style":122,"fill":78,"textAnchor":79},"316","150","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","page 1 frame",[125,126,129,135,138,141,144,147],"g",{"fill":127,"stroke":88,"style":128},"#e8efe6","stroke-width:1.2",[49,130],{"x":131,"y":132,"width":133,"height":134},"216","162","200","18",[49,136],{"x":131,"y":137,"width":133,"height":134},"182",[49,139],{"x":131,"y":140,"width":133,"height":134},"202",[49,142],{"x":131,"y":143,"width":133,"height":134},"222",[49,145],{"x":131,"y":146,"width":133,"height":134},"242",[49,148],{"x":131,"y":149,"width":133,"height":134},"262",[68,151],{"d":152,"fill":108,"stroke":71,"style":109},"M436 210 L470 210",[49,154],{"x":155,"y":113,"width":114,"height":85,"rx":63,"fill":115,"stroke":116,"style":117},"480",[73,157,159],{"x":158,"y":121,"style":122,"fill":78,"textAnchor":79},"600","page 2 frame",[125,161,162,165,167],{"fill":127,"stroke":88,"style":128},[49,163],{"x":164,"y":132,"width":133,"height":134},"500",[49,166],{"x":164,"y":137,"width":133,"height":134},[49,168],{"x":164,"y":140,"width":133,"height":134},[73,170,172],{"x":158,"y":171,"style":99,"fill":116,"textAnchor":79},"248","frames are created as needed",[49,174],{"x":83,"y":171,"width":121,"height":175,"rx":63,"fill":176,"stroke":177,"style":117},"44","#fdf2e2","#b45309",[73,179,182],{"x":180,"y":181,"style":99,"fill":71,"textAnchor":79},"99","275","rows flow onward",[184,185,187],"h2",{"id":186},"prerequisites","Prerequisites",[189,190,191,199,207],"ul",{},[192,193,194,198],"li",{},[195,196,197],"strong",{},"QGIS 3.34 LTR"," or newer.",[192,200,201,202,206],{},"A layout, existing or created in code — see ",[26,203,205],{"href":204},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-map-item-and-set-extent-pyqgis\u002F","adding a map item and setting the extent",".",[192,208,209],{},"A vector layer with the fields you want to show.",[184,211,213],{"id":212},"create-the-table-and-give-it-a-frame","Create the table and give it a frame",[215,216,221],"pre",{"className":217,"code":218,"language":219,"meta":220,"style":220},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsProject, QgsLayoutItemAttributeTable, QgsLayoutFrame,\n    QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,\n)\n\nproject = QgsProject.instance()\nlayout = project.layoutManager().layoutByName(\"site_report\")\nsites = project.mapLayersByName(\"sites\")[0]\n\ntable = QgsLayoutItemAttributeTable.create(layout)\ntable.setVectorLayer(sites)\nlayout.addMultiFrame(table)\n\nframe = QgsLayoutFrame(layout, table)\nframe.attemptMove(QgsLayoutPoint(15, 150, QgsUnitTypes.LayoutMillimeters))\nframe.attemptResize(QgsLayoutSize(180, 100, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(frame)\ntable.addFrame(frame)\n","python","",[222,223,224,243,249,255,261,268,280,297,320,325,336,342,348,353,364,381,397,403],"code",{"__ignoreMap":220},[225,226,229,233,237,240],"span",{"class":227,"line":228},"line",1,[225,230,232],{"class":231},"snl16","from",[225,234,236],{"class":235},"s95oV"," qgis.core ",[225,238,239],{"class":231},"import",[225,241,242],{"class":235}," (\n",[225,244,246],{"class":227,"line":245},2,[225,247,248],{"class":235},"    QgsProject, QgsLayoutItemAttributeTable, QgsLayoutFrame,\n",[225,250,252],{"class":227,"line":251},3,[225,253,254],{"class":235},"    QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes,\n",[225,256,258],{"class":227,"line":257},4,[225,259,260],{"class":235},")\n",[225,262,264],{"class":227,"line":263},5,[225,265,267],{"emptyLinePlaceholder":266},true,"\n",[225,269,271,274,277],{"class":227,"line":270},6,[225,272,273],{"class":235},"project ",[225,275,276],{"class":231},"=",[225,278,279],{"class":235}," QgsProject.instance()\n",[225,281,283,286,288,291,295],{"class":227,"line":282},7,[225,284,285],{"class":235},"layout ",[225,287,276],{"class":231},[225,289,290],{"class":235}," project.layoutManager().layoutByName(",[225,292,294],{"class":293},"sU2Wk","\"site_report\"",[225,296,260],{"class":235},[225,298,300,303,305,308,311,314,317],{"class":227,"line":299},8,[225,301,302],{"class":235},"sites ",[225,304,276],{"class":231},[225,306,307],{"class":235}," project.mapLayersByName(",[225,309,310],{"class":293},"\"sites\"",[225,312,313],{"class":235},")[",[225,315,51],{"class":316},"sDLfK",[225,318,319],{"class":235},"]\n",[225,321,323],{"class":227,"line":322},9,[225,324,267],{"emptyLinePlaceholder":266},[225,326,328,331,333],{"class":227,"line":327},10,[225,329,330],{"class":235},"table ",[225,332,276],{"class":231},[225,334,335],{"class":235}," QgsLayoutItemAttributeTable.create(layout)\n",[225,337,339],{"class":227,"line":338},11,[225,340,341],{"class":235},"table.setVectorLayer(sites)\n",[225,343,345],{"class":227,"line":344},12,[225,346,347],{"class":235},"layout.addMultiFrame(table)\n",[225,349,351],{"class":227,"line":350},13,[225,352,267],{"emptyLinePlaceholder":266},[225,354,356,359,361],{"class":227,"line":355},14,[225,357,358],{"class":235},"frame ",[225,360,276],{"class":231},[225,362,363],{"class":235}," QgsLayoutFrame(layout, table)\n",[225,365,367,370,373,376,378],{"class":227,"line":366},15,[225,368,369],{"class":235},"frame.attemptMove(QgsLayoutPoint(",[225,371,372],{"class":316},"15",[225,374,375],{"class":235},", ",[225,377,121],{"class":316},[225,379,380],{"class":235},", QgsUnitTypes.LayoutMillimeters))\n",[225,382,384,387,390,392,395],{"class":227,"line":383},16,[225,385,386],{"class":235},"frame.attemptResize(QgsLayoutSize(",[225,388,389],{"class":316},"180",[225,391,375],{"class":235},[225,393,394],{"class":316},"100",[225,396,380],{"class":235},[225,398,400],{"class":227,"line":399},17,[225,401,402],{"class":235},"layout.addLayoutItem(frame)\n",[225,404,406],{"class":227,"line":405},18,[225,407,408],{"class":235},"table.addFrame(frame)\n",[14,410,411,414,415,418,419,422,423,426,427,430,431,434,435,438],{},[195,412,413],{},"Breakdown:"," Four steps, and skipping any of them produces nothing visible. ",[222,416,417],{},"create()"," builds the multiframe, ",[222,420,421],{},"addMultiFrame"," registers it with the layout, the frame is a separate item that must itself be added with ",[222,424,425],{},"addLayoutItem",", and ",[222,428,429],{},"addFrame"," associates the two. A table added without a frame exists and renders nowhere, which is the usual reason a scripted table does not appear. ",[222,432,433],{},"attemptMove"," and ",[222,436,437],{},"attemptResize"," are the layout's position API — they are \"attempt\" because a locked or reference-point-bound item may not end up exactly where asked.",[184,440,442],{"id":441},"choose-and-rename-the-columns","Choose and rename the columns",[215,444,446],{"className":217,"code":445,"language":219,"meta":220,"style":220},"from qgis.core import QgsLayoutTableColumn\n\nwanted = [\n    (\"site_id\", \"Site\"),\n    (\"area_m2\", \"Area (m²)\"),\n    (\"last_visit\", \"Last inspected\"),\n]\n\ncolumns = []\nfor field_name, heading in wanted:\n    column = QgsLayoutTableColumn()\n    column.setAttribute(field_name)\n    column.setHeading(heading)\n    columns.append(column)\n\ntable.setColumns(columns)\ntable.refreshAttributes()\n",[222,447,448,459,463,473,489,503,517,521,525,535,549,559,564,569,574,578,583],{"__ignoreMap":220},[225,449,450,452,454,456],{"class":227,"line":228},[225,451,232],{"class":231},[225,453,236],{"class":235},[225,455,239],{"class":231},[225,457,458],{"class":235}," QgsLayoutTableColumn\n",[225,460,461],{"class":227,"line":245},[225,462,267],{"emptyLinePlaceholder":266},[225,464,465,468,470],{"class":227,"line":251},[225,466,467],{"class":235},"wanted ",[225,469,276],{"class":231},[225,471,472],{"class":235}," [\n",[225,474,475,478,481,483,486],{"class":227,"line":257},[225,476,477],{"class":235},"    (",[225,479,480],{"class":293},"\"site_id\"",[225,482,375],{"class":235},[225,484,485],{"class":293},"\"Site\"",[225,487,488],{"class":235},"),\n",[225,490,491,493,496,498,501],{"class":227,"line":263},[225,492,477],{"class":235},[225,494,495],{"class":293},"\"area_m2\"",[225,497,375],{"class":235},[225,499,500],{"class":293},"\"Area (m²)\"",[225,502,488],{"class":235},[225,504,505,507,510,512,515],{"class":227,"line":270},[225,506,477],{"class":235},[225,508,509],{"class":293},"\"last_visit\"",[225,511,375],{"class":235},[225,513,514],{"class":293},"\"Last inspected\"",[225,516,488],{"class":235},[225,518,519],{"class":227,"line":282},[225,520,319],{"class":235},[225,522,523],{"class":227,"line":299},[225,524,267],{"emptyLinePlaceholder":266},[225,526,527,530,532],{"class":227,"line":322},[225,528,529],{"class":235},"columns ",[225,531,276],{"class":231},[225,533,534],{"class":235}," []\n",[225,536,537,540,543,546],{"class":227,"line":327},[225,538,539],{"class":231},"for",[225,541,542],{"class":235}," field_name, heading ",[225,544,545],{"class":231},"in",[225,547,548],{"class":235}," wanted:\n",[225,550,551,554,556],{"class":227,"line":338},[225,552,553],{"class":235},"    column ",[225,555,276],{"class":231},[225,557,558],{"class":235}," QgsLayoutTableColumn()\n",[225,560,561],{"class":227,"line":344},[225,562,563],{"class":235},"    column.setAttribute(field_name)\n",[225,565,566],{"class":227,"line":350},[225,567,568],{"class":235},"    column.setHeading(heading)\n",[225,570,571],{"class":227,"line":355},[225,572,573],{"class":235},"    columns.append(column)\n",[225,575,576],{"class":227,"line":366},[225,577,267],{"emptyLinePlaceholder":266},[225,579,580],{"class":227,"line":383},[225,581,582],{"class":235},"table.setColumns(columns)\n",[225,584,585],{"class":227,"line":399},[225,586,587],{"class":235},"table.refreshAttributes()\n",[14,589,590,592,593,596,597,600,601,604,605,608,609,612,613,616],{},[195,591,413],{}," ",[222,594,595],{},"setColumns"," replaces the automatic all-fields column set, which is almost always what you want — a raw table of every column including internal keys is rarely publishable. ",[222,598,599],{},"setAttribute"," takes a field name ",[18,602,603],{},"or an expression",", so a computed column like ",[222,606,607],{},"round(\"area_m2\" \u002F 10000, 2)"," works with no virtual field. ",[222,610,611],{},"setHeading"," is the printed label and is where the units belong. ",[222,614,615],{},"refreshAttributes()"," re-reads the source after any change to columns or filters; without it, the table can render its previous contents.",[14,618,619],{},"Sorting is a property of the columns rather than of the table:",[215,621,623],{"className":217,"code":622,"language":219,"meta":220,"style":220},"sort_column = columns[1]\nsort_column.setSortOrder(Qt.DescendingOrder)\nsort_column.setSortByRank(1)\ntable.setColumns(columns)\n",[222,624,625,640,645,654],{"__ignoreMap":220},[225,626,627,630,632,635,638],{"class":227,"line":228},[225,628,629],{"class":235},"sort_column ",[225,631,276],{"class":231},[225,633,634],{"class":235}," columns[",[225,636,637],{"class":316},"1",[225,639,319],{"class":235},[225,641,642],{"class":227,"line":245},[225,643,644],{"class":235},"sort_column.setSortOrder(Qt.DescendingOrder)\n",[225,646,647,650,652],{"class":227,"line":251},[225,648,649],{"class":235},"sort_column.setSortByRank(",[225,651,637],{"class":316},[225,653,260],{"class":235},[225,655,656],{"class":227,"line":257},[225,657,582],{"class":235},[14,659,660,592,662,665,666,668],{},[195,661,413],{},[222,663,664],{},"setSortByRank"," gives the column its place in a multi-column sort, counting from 1; a rank of 0 means the column does not participate. That is how \"sort by district, then by area descending\" is expressed. Setting the columns again after modifying them is required because ",[222,667,595],{}," takes a copy.",[184,670,672],{"id":671},"where-the-rows-come-from","Where the rows come from",[14,674,675],{},[34,676,679,682,685,688,691,698,702,722,726,731,735,745,748,753,757,760,763,766,770,781,784,789],{"viewBox":677,"role":37,"ariaLabel":678,"xmlns":39},"0 0 760 306","The four content sources for a layout table: all features of a layer, only those visible in a map item, the current atlas feature, and the children of the atlas feature through a relation",[41,680,681],{},"Four sources, four different tables",[45,683,684],{},"A layer attributes source lists every feature. Restricting to a map item lists only the features visible in that map. The atlas feature source lists the single feature the atlas is on. The relation children source lists the records related to the atlas feature, which is how a per-site report shows that site's inspections.",[49,686],{"x":51,"y":51,"width":52,"height":687,"fill":54},"306",[73,689,690],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Pick the source before fiddling with filters",[49,692],{"x":693,"y":694,"width":695,"height":696,"rx":697,"fill":87,"stroke":88,"style":89},"20","46","174","176","10",[73,699,701],{"x":700,"y":86,"style":122,"fill":88,"textAnchor":79},"107","LayerAttributes",[125,703,704,710,713,716,719],{"fill":127,"stroke":88,"style":128},[49,705],{"x":706,"y":707,"width":708,"height":709},"40","94","134","14",[49,711],{"x":706,"y":712,"width":708,"height":709},"112",[49,714],{"x":706,"y":715,"width":708,"height":709},"130",[49,717],{"x":706,"y":718,"width":708,"height":709},"148",[49,720],{"x":706,"y":721,"width":708,"height":709},"166",[73,723,725],{"x":700,"y":724,"style":99,"fill":71,"textAnchor":79},"204","every feature",[49,727],{"x":728,"y":694,"width":695,"height":696,"rx":697,"fill":729,"stroke":730,"style":89},"206","#eff3ff","#2563eb",[73,732,734],{"x":733,"y":86,"style":122,"fill":730,"textAnchor":79},"293","visible in a map",[125,736,738,741,743],{"fill":737,"stroke":730,"style":128},"#dbeafe",[49,739],{"x":740,"y":707,"width":708,"height":709},"226",[49,742],{"x":740,"y":712,"width":708,"height":709},[49,744],{"x":740,"y":715,"width":708,"height":709},[73,746,747],{"x":733,"y":724,"style":99,"fill":71,"textAnchor":79},"follows the map extent",[49,749],{"x":750,"y":694,"width":695,"height":696,"rx":697,"fill":751,"stroke":752,"style":89},"392","#edf8e9","#15803d",[73,754,756],{"x":755,"y":86,"style":122,"fill":752,"textAnchor":79},"479","AtlasFeature",[49,758],{"x":759,"y":707,"width":708,"height":709,"fill":127,"stroke":752,"style":128},"412",[73,761,762],{"x":755,"y":724,"style":99,"fill":71,"textAnchor":79},"just this one",[49,764],{"x":765,"y":694,"width":132,"height":696,"rx":697,"fill":176,"stroke":177,"style":89},"578",[73,767,769],{"x":768,"y":86,"style":122,"fill":177,"textAnchor":79},"659","RelationChildren",[125,771,773,777,779],{"fill":772,"stroke":177,"style":128},"#f3e9d8",[49,774],{"x":775,"y":707,"width":776,"height":709},"598","122",[49,778],{"x":775,"y":712,"width":776,"height":709},[49,780],{"x":775,"y":715,"width":776,"height":709},[73,782,783],{"x":768,"y":724,"style":99,"fill":71,"textAnchor":79},"its related records",[49,785],{"x":786,"y":787,"width":788,"height":706,"rx":63,"fill":176,"stroke":177,"style":117},"120","256","520",[73,790,793],{"x":75,"y":791,"style":792,"fill":71,"textAnchor":79},"281","text-anchor:middle;font-size:10.5px;font-family:sans-serif","the last two only mean anything with an atlas enabled on the layout",[215,795,797],{"className":217,"code":796,"language":219,"meta":220,"style":220},"table.setSource(QgsLayoutItemAttributeTable.LayerAttributes)\n\ntable.setFilterToAtlasFeature(False)\ntable.setFilterFeatures(True)\ntable.setFeatureFilter('\"status\" = \\'open\\' AND \"area_m2\" > 500')\n\ntable.setMaximumNumberOfFeatures(40)\ntable.setDisplayOnlyVisibleFeatures(True)\ntable.setMap(layout.itemById(\"main_map\"))\n\ntable.refreshAttributes()\n",[222,798,799,804,808,818,828,849,853,862,871,882,886],{"__ignoreMap":220},[225,800,801],{"class":227,"line":228},[225,802,803],{"class":235},"table.setSource(QgsLayoutItemAttributeTable.LayerAttributes)\n",[225,805,806],{"class":227,"line":245},[225,807,267],{"emptyLinePlaceholder":266},[225,809,810,813,816],{"class":227,"line":251},[225,811,812],{"class":235},"table.setFilterToAtlasFeature(",[225,814,815],{"class":316},"False",[225,817,260],{"class":235},[225,819,820,823,826],{"class":227,"line":257},[225,821,822],{"class":235},"table.setFilterFeatures(",[225,824,825],{"class":316},"True",[225,827,260],{"class":235},[225,829,830,833,836,839,842,844,847],{"class":227,"line":263},[225,831,832],{"class":235},"table.setFeatureFilter(",[225,834,835],{"class":293},"'\"status\" = ",[225,837,838],{"class":316},"\\'",[225,840,841],{"class":293},"open",[225,843,838],{"class":316},[225,845,846],{"class":293}," AND \"area_m2\" > 500'",[225,848,260],{"class":235},[225,850,851],{"class":227,"line":270},[225,852,267],{"emptyLinePlaceholder":266},[225,854,855,858,860],{"class":227,"line":282},[225,856,857],{"class":235},"table.setMaximumNumberOfFeatures(",[225,859,706],{"class":316},[225,861,260],{"class":235},[225,863,864,867,869],{"class":227,"line":299},[225,865,866],{"class":235},"table.setDisplayOnlyVisibleFeatures(",[225,868,825],{"class":316},[225,870,260],{"class":235},[225,872,873,876,879],{"class":227,"line":322},[225,874,875],{"class":235},"table.setMap(layout.itemById(",[225,877,878],{"class":293},"\"main_map\"",[225,880,881],{"class":235},"))\n",[225,883,884],{"class":227,"line":327},[225,885,267],{"emptyLinePlaceholder":266},[225,887,888],{"class":227,"line":338},[225,889,587],{"class":235},[14,891,892,592,894,897,898,901,902,905,906,909],{},[195,893,413],{},[222,895,896],{},"setFilterFeatures(True)"," is the switch; setting an expression without it has no effect, which is the most common table-filter confusion. ",[222,899,900],{},"setDisplayOnlyVisibleFeatures"," restricts rows to those inside the named map item's extent — the combination that makes a table beside a map describe exactly what the map shows, and it needs ",[222,903,904],{},"setMap"," or it has nothing to test against. ",[222,907,908],{},"setMaximumNumberOfFeatures(0)"," means unlimited; leaving it at the default of 30 truncates silently, which is worth knowing before someone reports missing rows.",[14,911,912,914,915,206],{},[222,913,769],{}," is the source that makes per-feature reports work: with an atlas on parcels, it lists that parcel's inspections without any filter expression, using a relation defined as in ",[26,916,918],{"href":917},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002F","defining layer relations",[184,920,922],{"id":921},"making-the-table-readable","Making the table readable",[14,924,925],{},"The defaults are functional and plain. Four settings do most of the work of making a table look like part of the map rather than a spreadsheet paste.",[215,927,929],{"className":217,"code":928,"language":219,"meta":220,"style":220},"from qgis.PyQt.QtGui import QFont, QColor\n\ncontent_font = QFont(\"Inter\", 8)\nheader_font = QFont(\"Inter\", 8, QFont.Bold)\n\ntable.setContentFont(content_font)\ntable.setHeaderFont(header_font)\ntable.setShowGrid(True)\ntable.setGridStrokeWidth(0.15)\ntable.setGridColor(QColor(120, 120, 120))\ntable.setBackgroundColor(QColor(255, 255, 255, 0))\ntable.setCellMargin(1.5)\n",[222,930,931,943,947,966,984,988,993,998,1007,1017,1034,1056],{"__ignoreMap":220},[225,932,933,935,938,940],{"class":227,"line":228},[225,934,232],{"class":231},[225,936,937],{"class":235}," qgis.PyQt.QtGui ",[225,939,239],{"class":231},[225,941,942],{"class":235}," QFont, QColor\n",[225,944,945],{"class":227,"line":245},[225,946,267],{"emptyLinePlaceholder":266},[225,948,949,952,954,957,960,962,964],{"class":227,"line":251},[225,950,951],{"class":235},"content_font ",[225,953,276],{"class":231},[225,955,956],{"class":235}," QFont(",[225,958,959],{"class":293},"\"Inter\"",[225,961,375],{"class":235},[225,963,63],{"class":316},[225,965,260],{"class":235},[225,967,968,971,973,975,977,979,981],{"class":227,"line":257},[225,969,970],{"class":235},"header_font ",[225,972,276],{"class":231},[225,974,956],{"class":235},[225,976,959],{"class":293},[225,978,375],{"class":235},[225,980,63],{"class":316},[225,982,983],{"class":235},", QFont.Bold)\n",[225,985,986],{"class":227,"line":263},[225,987,267],{"emptyLinePlaceholder":266},[225,989,990],{"class":227,"line":270},[225,991,992],{"class":235},"table.setContentFont(content_font)\n",[225,994,995],{"class":227,"line":282},[225,996,997],{"class":235},"table.setHeaderFont(header_font)\n",[225,999,1000,1003,1005],{"class":227,"line":299},[225,1001,1002],{"class":235},"table.setShowGrid(",[225,1004,825],{"class":316},[225,1006,260],{"class":235},[225,1008,1009,1012,1015],{"class":227,"line":322},[225,1010,1011],{"class":235},"table.setGridStrokeWidth(",[225,1013,1014],{"class":316},"0.15",[225,1016,260],{"class":235},[225,1018,1019,1022,1024,1026,1028,1030,1032],{"class":227,"line":327},[225,1020,1021],{"class":235},"table.setGridColor(QColor(",[225,1023,786],{"class":316},[225,1025,375],{"class":235},[225,1027,786],{"class":316},[225,1029,375],{"class":235},[225,1031,786],{"class":316},[225,1033,881],{"class":235},[225,1035,1036,1039,1042,1044,1046,1048,1050,1052,1054],{"class":227,"line":338},[225,1037,1038],{"class":235},"table.setBackgroundColor(QColor(",[225,1040,1041],{"class":316},"255",[225,1043,375],{"class":235},[225,1045,1041],{"class":316},[225,1047,375],{"class":235},[225,1049,1041],{"class":316},[225,1051,375],{"class":235},[225,1053,51],{"class":316},[225,1055,881],{"class":235},[225,1057,1058,1061,1064],{"class":227,"line":344},[225,1059,1060],{"class":235},"table.setCellMargin(",[225,1062,1063],{"class":316},"1.5",[225,1065,260],{"class":235},[14,1067,1068,1070],{},[195,1069,413],{}," A grid stroke of 0.15 mm is about as light as prints reliably; the default is heavier and makes a table dominate a page it should support. A fully transparent background — the zero alpha above — lets the table sit over a tinted panel or a map without a white rectangle around it, which is usually what a designed layout wants. Cell margin is in millimetres and is the difference between cramped and legible; 1.5 to 2 mm suits an 8 pt font.",[14,1072,1073,1074,1077,1078,1081],{},"Column widths are set per column rather than on the table, through ",[222,1075,1076],{},"setWidth"," on each ",[222,1079,1080],{},"QgsLayoutTableColumn",", in millimetres, with 0 meaning automatic. Mixing fixed and automatic widths is legitimate and is how you stop a long text column squeezing the numeric ones — pin the numbers, let the text take what is left.",[184,1083,1085],{"id":1084},"letting-a-long-table-paginate","Letting a long table paginate",[215,1087,1089],{"className":217,"code":1088,"language":219,"meta":220,"style":220},"from qgis.core import QgsLayoutMultiFrame\n\ntable.setResizeMode(QgsLayoutMultiFrame.RepeatUntilFinished)\ntable.setEmptyTableBehavior(QgsLayoutTable.ShowMessage)\ntable.setEmptyTableMessage(\"No open sites in this area\")\ntable.recalculateFrameSizes()\n",[222,1090,1091,1102,1106,1111,1116,1126],{"__ignoreMap":220},[225,1092,1093,1095,1097,1099],{"class":227,"line":228},[225,1094,232],{"class":231},[225,1096,236],{"class":235},[225,1098,239],{"class":231},[225,1100,1101],{"class":235}," QgsLayoutMultiFrame\n",[225,1103,1104],{"class":227,"line":245},[225,1105,267],{"emptyLinePlaceholder":266},[225,1107,1108],{"class":227,"line":251},[225,1109,1110],{"class":235},"table.setResizeMode(QgsLayoutMultiFrame.RepeatUntilFinished)\n",[225,1112,1113],{"class":227,"line":257},[225,1114,1115],{"class":235},"table.setEmptyTableBehavior(QgsLayoutTable.ShowMessage)\n",[225,1117,1118,1121,1124],{"class":227,"line":263},[225,1119,1120],{"class":235},"table.setEmptyTableMessage(",[225,1122,1123],{"class":293},"\"No open sites in this area\"",[225,1125,260],{"class":235},[225,1127,1128],{"class":227,"line":270},[225,1129,1130],{"class":235},"table.recalculateFrameSizes()\n",[14,1132,1133,592,1135,1138,1139,1142,1143,1146],{},[195,1134,413],{},[222,1136,1137],{},"RepeatUntilFinished"," creates new frames — and new pages — until every row is placed, which is what turns a fixed box into a report. ",[222,1140,1141],{},"ExtendToNextPage"," is the variant that only continues onto existing pages. ",[222,1144,1145],{},"RepeatOnEveryPage"," places a copy of the same first rows on each page and is almost never what you want. The empty-table behaviour matters more than it sounds: the default draws headers over an empty box, which reads as a broken layout, while a message reads as a result.",[184,1148,1150],{"id":1149},"keeping-the-table-in-step-with-the-map","Keeping the table in step with the map",[14,1152,1153],{},"A table beside a map is only useful if the two agree, and there are two ways they drift apart.",[14,1155,1156,1157,1159,1160,1163],{},"The first is the extent. ",[222,1158,900],{}," reads the map item's extent at render time, so it stays correct when the map moves — including when an atlas drives it. What it does not account for is a feature that is ",[18,1161,1162],{},"within"," the extent but hidden by scale-based visibility or by a subset string, so the map shows fewer features than the table lists. Where that matters, repeat the visibility condition as a feature filter on the table.",[14,1165,1166],{},"The second is the layer. A table holds a reference to the layer it was given, so removing and re-adding that layer — as a refresh script might — leaves the table pointing at nothing and rendering empty. Re-setting the vector layer after any such operation is the fix, and it is cheap:",[215,1168,1170],{"className":217,"code":1169,"language":219,"meta":220,"style":220},"sites = project.mapLayersByName(\"sites\")[0]\nfor item in layout.multiFrames():\n    if isinstance(item, QgsLayoutItemAttributeTable):\n        item.setVectorLayer(sites)\n        item.refreshAttributes()\n",[222,1171,1172,1188,1200,1211,1216],{"__ignoreMap":220},[225,1173,1174,1176,1178,1180,1182,1184,1186],{"class":227,"line":228},[225,1175,302],{"class":235},[225,1177,276],{"class":231},[225,1179,307],{"class":235},[225,1181,310],{"class":293},[225,1183,313],{"class":235},[225,1185,51],{"class":316},[225,1187,319],{"class":235},[225,1189,1190,1192,1195,1197],{"class":227,"line":245},[225,1191,539],{"class":231},[225,1193,1194],{"class":235}," item ",[225,1196,545],{"class":231},[225,1198,1199],{"class":235}," layout.multiFrames():\n",[225,1201,1202,1205,1208],{"class":227,"line":251},[225,1203,1204],{"class":231},"    if",[225,1206,1207],{"class":316}," isinstance",[225,1209,1210],{"class":235},"(item, QgsLayoutItemAttributeTable):\n",[225,1212,1213],{"class":227,"line":257},[225,1214,1215],{"class":235},"        item.setVectorLayer(sites)\n",[225,1217,1218],{"class":227,"line":263},[225,1219,1220],{"class":235},"        item.refreshAttributes()\n",[14,1222,1223,592,1225,1228],{},[195,1224,413],{},[222,1226,1227],{},"layout.multiFrames()"," returns the content objects rather than the frames, which is the right level to work at here — one call fixes a table however many frames it spans. Testing the type rather than assuming keeps the loop safe on a layout that also contains an HTML multiframe. Running this at the end of any script that reloads layers removes an entire class of \"the table was empty in last night's export\" report.",[184,1230,1232],{"id":1231},"adding-a-totals-row","Adding a totals row",[14,1234,1235],{},"There is no built-in summary row, and the usual approach is a separate label positioned under the frame, carrying an expression that aggregates the same features:",[215,1237,1239],{"className":217,"code":1238,"language":219,"meta":220,"style":220},"from qgis.core import QgsLayoutItemLabel\n\ntotal = QgsLayoutItemLabel(layout)\ntotal.setText(\"[% 'Total area: ' || format_number(sum(\\\"area_m2\\\"), 0) || ' m²' %]\")\ntotal.attemptMove(QgsLayoutPoint(15, 255, QgsUnitTypes.LayoutMillimeters))\nlayout.addLayoutItem(total)\n",[222,1240,1241,1252,1256,1266,1287,1300],{"__ignoreMap":220},[225,1242,1243,1245,1247,1249],{"class":227,"line":228},[225,1244,232],{"class":231},[225,1246,236],{"class":235},[225,1248,239],{"class":231},[225,1250,1251],{"class":235}," QgsLayoutItemLabel\n",[225,1253,1254],{"class":227,"line":245},[225,1255,267],{"emptyLinePlaceholder":266},[225,1257,1258,1261,1263],{"class":227,"line":251},[225,1259,1260],{"class":235},"total ",[225,1262,276],{"class":231},[225,1264,1265],{"class":235}," QgsLayoutItemLabel(layout)\n",[225,1267,1268,1271,1274,1277,1280,1282,1285],{"class":227,"line":257},[225,1269,1270],{"class":235},"total.setText(",[225,1272,1273],{"class":293},"\"[% 'Total area: ' || format_number(sum(",[225,1275,1276],{"class":316},"\\\"",[225,1278,1279],{"class":293},"area_m2",[225,1281,1276],{"class":316},[225,1283,1284],{"class":293},"), 0) || ' m²' %]\"",[225,1286,260],{"class":235},[225,1288,1289,1292,1294,1296,1298],{"class":227,"line":263},[225,1290,1291],{"class":235},"total.attemptMove(QgsLayoutPoint(",[225,1293,372],{"class":316},[225,1295,375],{"class":235},[225,1297,1041],{"class":316},[225,1299,380],{"class":235},[225,1301,1302],{"class":227,"line":270},[225,1303,1304],{"class":235},"layout.addLayoutItem(total)\n",[14,1306,1307,1309,1310,1313,1314,1317,1318,1321,1322,206],{},[195,1308,413],{}," The ",[222,1311,1312],{},"[% ... %]"," delimiters are what make a layout label evaluate an expression rather than print it literally, and forgetting them is why a label sometimes shows its own source. ",[222,1315,1316],{},"sum()"," here aggregates over the label's layer context rather than over the table's filter, so a filtered table and an unfiltered total will disagree — repeating the filter inside the aggregate's ",[222,1319,1320],{},"filter"," argument keeps them consistent. The wider expression mechanics are covered in ",[26,1323,1325],{"href":1324},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fatlas-expressions-and-dynamic-text-pyqgis\u002F","atlas expressions and dynamic text",[184,1327,1329],{"id":1328},"qgis-version-compatibility","QGIS version compatibility",[14,1331,1332,1335,1336,1339,1340,1343,1344,1347],{},[222,1333,1334],{},"QgsLayoutItemAttributeTable"," and the multiframe model arrived with the layout rewrite in QGIS 3.0 and are unchanged in shape since. ",[222,1337,1338],{},"setEmptyTableBehavior"," and the sort-by-rank column API have been present throughout 3.x. The ",[222,1341,1342],{},"Qt.DescendingOrder"," constant comes from ",[222,1345,1346],{},"qgis.PyQt.QtCore",", which is the import you need alongside the layout classes.",[184,1349,1351],{"id":1350},"troubleshooting","Troubleshooting",[189,1353,1354,1368,1374,1382,1390,1398],{},[192,1355,1356,1359,1360,375,1362,1364,1365,1367],{},[195,1357,1358],{},"The table does not appear."," A missing ",[222,1361,421],{},[222,1363,425],{}," or ",[222,1366,429],{}," — all four steps are required.",[192,1369,1370,1373],{},[195,1371,1372],{},"Only thirty rows show."," The default maximum. Set it to 0 for unlimited.",[192,1375,1376,592,1379,1381],{},[195,1377,1378],{},"The filter has no effect.",[222,1380,896],{}," was not called.",[192,1383,1384,1387,1388,206],{},[195,1385,1386],{},"\"Only visible features\" shows everything."," No map item was set with ",[222,1389,904],{},[192,1391,1392,1395,1396,206],{},[195,1393,1394],{},"The table is stale after changing the layer."," Call ",[222,1397,615],{},[192,1399,1400,1403,1404,206],{},[195,1401,1402],{},"A long table is clipped rather than continuing."," Resize mode is the default rather than ",[222,1405,1137],{},[184,1407,1409],{"id":1408},"conclusion","Conclusion",[14,1411,1412],{},"Build the multiframe, add it, add a frame, associate them — then set columns explicitly, turn the filter on before setting it, point the table at a map if it should follow the extent, and choose a resize mode that lets it flow. A table is the item that turns a map into a report, and every part of it is scriptable.",[184,1414,1416],{"id":1415},"frequently-asked-questions","Frequently Asked Questions",[14,1418,1419,1422,1423,1425],{},[195,1420,1421],{},"Can the table show a computed column?","\nYes — ",[222,1424,599],{}," accepts an expression, so unit conversions and concatenations need no field in the data.",[14,1427,1428,1431,375,1434,375,1437,375,1440,1443],{},[195,1429,1430],{},"How do I style the table?",[222,1432,1433],{},"setContentFont",[222,1435,1436],{},"setHeaderFont",[222,1438,1439],{},"setGridStrokeWidth",[222,1441,1442],{},"setShowGrid"," and the background colour setters are all on the table object, and all take effect on the next refresh.",[14,1445,1446,1449],{},[195,1447,1448],{},"Can I put two tables on one layout?","\nYes, each as its own multiframe with its own frames. They are independent, so one can list sites and another their inspections.",[14,1451,1452,1455,1456,206],{},[195,1453,1454],{},"Does the table appear in an exported PDF?","\nYes, as vector text, so it remains selectable and searchable — see ",[26,1457,1459],{"href":1458},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fexporting-multiple-qgis-layouts-to-pdf\u002F","exporting multiple layouts to PDF",[184,1461,1463],{"id":1462},"related","Related",[189,1465,1466,1472,1477,1483,1488],{},[192,1467,1468,1471],{},[26,1469,1470],{"href":28},"Automated Map Layout Generation in PyQGIS"," — the guide this recipe belongs to",[192,1473,1474],{},[26,1475,1476],{"href":204},"Add a Map Item and Set the Extent in PyQGIS",[192,1478,1479],{},[26,1480,1482],{"href":1481},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis\u002F","Add a Legend to a Layout in PyQGIS",[192,1484,1485],{},[26,1486,1487],{"href":1324},"Atlas Expressions and Dynamic Text in PyQGIS",[192,1489,1490],{},[26,1491,1492],{"href":917},"Define Layer Relations in PyQGIS",[1494,1495,1496],"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);}",{"title":220,"searchDepth":245,"depth":245,"links":1498},[1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511],{"id":186,"depth":245,"text":187},{"id":212,"depth":245,"text":213},{"id":441,"depth":245,"text":442},{"id":671,"depth":245,"text":672},{"id":921,"depth":245,"text":922},{"id":1084,"depth":245,"text":1085},{"id":1149,"depth":245,"text":1150},{"id":1231,"depth":245,"text":1232},{"id":1328,"depth":245,"text":1329},{"id":1350,"depth":245,"text":1351},{"id":1408,"depth":245,"text":1409},{"id":1415,"depth":245,"text":1416},{"id":1462,"depth":245,"text":1463},"Put a feature table on a print layout from Python — frames and multiframes, choosing and renaming columns, filtering to the map or the atlas feature, sorting and pagination.","md",{"slug":1515,"type":1516,"breadcrumb":1517,"datePublished":1518,"dateModified":1518},"add-attribute-table-to-layout-pyqgis","article","Attribute Table in a Layout","2026-09-05","\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis",{"title":5,"description":1512},"spatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-attribute-table-to-layout-pyqgis\u002Findex","wZZ51QtQaKu7a0D9fUM1f2Nxw_foB4IBkA_vdTLrzEE",1788563852295]