[{"data":1,"prerenderedAt":1559},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fshow-features-in-attribute-table-view-pyqgis":3},{"id":4,"title":5,"body":6,"description":1548,"extension":1549,"meta":1550,"navigation":245,"path":1555,"seo":1556,"stem":1557,"__hash__":1558},"docs\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fshow-features-in-attribute-table-view-pyqgis\u002Findex.md","Show Features in an Attribute Table View in PyQGIS",{"type":7,"value":8,"toc":1534},"minimark",[9,13,22,31,160,165,180,184,187,356,371,375,378,454,554,573,577,580,670,684,688,691,801,814,818,821,1229,1241,1245,1251,1301,1378,1390,1394,1421,1425,1453,1457,1463,1467,1473,1479,1485,1491,1497,1501,1530],[10,11,5],"h1",{"id":12},"show-features-in-an-attribute-table-view-in-pyqgis",[14,15,16,17,21],"p",{},"Plugins often need to show features in a table: the results of a search, the parcels affected by a planned road, the assets due for inspection. A plain ",[18,19,20],"code",{},"QTableWidget"," filled in a loop works for a dozen rows and becomes slow, disconnected from the layer and out of date as soon as anything is edited. QGIS's own attribute table — the one behind the Open Attribute Table dialog — is available as reusable classes: a model over the layer, a filter model that decides which rows show, and a view or dual view that displays them, with editing, sorting, field widgets and map selection sync built in.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces",". It embeds an attribute table in a dock widget, filters it to selected or matching features, links its selection to the map, controls editing and columns, and uses the dual view for a table-and-form layout.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,101,108,114,120,125,129,135,139,142,146,152,157],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 196","img","The attribute table stack of layer cache, table model, filter model and view used to embed QGIS's table in a plugin","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Model, filter, view",[45,46,47],"desc",{},"QgsVectorLayerCache caches features from the layer. QgsAttributeTableModel exposes them as rows and columns. QgsAttributeTableFilterModel decides which rows are shown — all, selected, visible on the map or matching an expression — and handles sorting. QgsAttributeTableView or QgsDualView displays the filtered model with QGIS's editor widgets and selection behaviour.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","196","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"atvStackArrow","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","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","The same classes as QGIS's own table",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","100","150","80","#fffdf7","#59645f","stroke-width:2",[73,91,95],{"x":92,"y":93,"style":94,"fill":78,"textAnchor":79},"99","132.42","text-anchor:middle;font-size:10.5px;font-family:sans-serif;font-weight:bold","layer cache",[73,97,100],{"x":92,"y":98,"style":99,"fill":88,"textAnchor":79},"154.42","text-anchor:middle;font-size:9.5px;font-family:sans-serif","QgsVectorLayerCache",[102,103],"line",{"x1":104,"y1":105,"x2":106,"y2":105,"stroke":71,"style":107},"174","140","204","stroke-width:1.8;marker-end:url(#atvStackArrow)",[49,109],{"x":110,"y":84,"width":111,"height":86,"rx":63,"fill":112,"stroke":113,"style":89},"208","160","#eef7f4","#0f766e",[73,115,119],{"x":116,"y":117,"style":118,"fill":113,"textAnchor":79},"288","132.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","table model",[73,121,124],{"x":116,"y":122,"style":123,"fill":88,"textAnchor":79},"154.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","rows = features",[102,126],{"x1":127,"y1":105,"x2":128,"y2":105,"stroke":71,"style":107},"368","398",[49,130],{"x":131,"y":84,"width":132,"height":86,"rx":63,"fill":133,"stroke":134,"style":89},"402","170","#fdf2e2","#b45309",[73,136,138],{"x":137,"y":117,"style":118,"fill":134,"textAnchor":79},"487","filter model",[73,140,141],{"x":137,"y":122,"style":123,"fill":88,"textAnchor":79},"which rows, order",[102,143],{"x1":144,"y1":105,"x2":145,"y2":105,"stroke":71,"style":107},"572","602",[49,147],{"x":148,"y":84,"width":149,"height":86,"rx":63,"fill":150,"stroke":151,"style":89},"606","130","#e8efe6","#15803d",[73,153,156],{"x":154,"y":117,"style":118,"fill":155,"textAnchor":79},"671","#166534","view",[73,158,159],{"x":154,"y":122,"style":123,"fill":88,"textAnchor":79},"table or dual",[161,162,164],"h2",{"id":163},"prerequisites","Prerequisites",[166,167,168,172],"ul",{},[169,170,171],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with code running inside QGIS Desktop (the classes need the map canvas for some filters).",[169,173,174,175,179],{},"A plugin with a dialog or dock widget, as in ",[26,176,178],{"href":177},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fadd-custom-dock-widget-pyqgis\u002F","adding a custom dock widget",".",[161,181,183],{"id":182},"build-the-model-and-view","Build the model and view",[14,185,186],{},"The stack is assembled in order: a feature cache over the layer, a table model over the cache, a filter model over the table model, and a view showing the filter model.",[188,189,194],"pre",{"className":190,"code":191,"language":192,"meta":193,"style":193},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsVectorLayerCache\nfrom qgis.gui import QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView\nfrom qgis.utils import iface\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\ncanvas = iface.mapCanvas()\n\ncache = QgsVectorLayerCache(layer, 10000)\nmodel = QgsAttributeTableModel(cache)\nmodel.loadLayer()\nfilter_model = QgsAttributeTableFilterModel(canvas, model)\nview = QgsAttributeTableView()\nview.setModel(filter_model)\nview.show()\n","python","",[18,195,196,214,227,240,247,272,283,288,305,316,322,333,344,350],{"__ignoreMap":193},[197,198,200,204,208,211],"span",{"class":102,"line":199},1,[197,201,203],{"class":202},"snl16","from",[197,205,207],{"class":206},"s95oV"," qgis.core ",[197,209,210],{"class":202},"import",[197,212,213],{"class":206}," QgsProject, QgsVectorLayerCache\n",[197,215,217,219,222,224],{"class":102,"line":216},2,[197,218,203],{"class":202},[197,220,221],{"class":206}," qgis.gui ",[197,223,210],{"class":202},[197,225,226],{"class":206}," QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView\n",[197,228,230,232,235,237],{"class":102,"line":229},3,[197,231,203],{"class":202},[197,233,234],{"class":206}," qgis.utils ",[197,236,210],{"class":202},[197,238,239],{"class":206}," iface\n",[197,241,243],{"class":102,"line":242},4,[197,244,246],{"emptyLinePlaceholder":245},true,"\n",[197,248,250,253,256,259,263,266,269],{"class":102,"line":249},5,[197,251,252],{"class":206},"layer ",[197,254,255],{"class":202},"=",[197,257,258],{"class":206}," QgsProject.instance().mapLayersByName(",[197,260,262],{"class":261},"sU2Wk","\"parcels\"",[197,264,265],{"class":206},")[",[197,267,51],{"class":268},"sDLfK",[197,270,271],{"class":206},"]\n",[197,273,275,278,280],{"class":102,"line":274},6,[197,276,277],{"class":206},"canvas ",[197,279,255],{"class":202},[197,281,282],{"class":206}," iface.mapCanvas()\n",[197,284,286],{"class":102,"line":285},7,[197,287,246],{"emptyLinePlaceholder":245},[197,289,291,294,296,299,302],{"class":102,"line":290},8,[197,292,293],{"class":206},"cache ",[197,295,255],{"class":202},[197,297,298],{"class":206}," QgsVectorLayerCache(layer, ",[197,300,301],{"class":268},"10000",[197,303,304],{"class":206},")\n",[197,306,308,311,313],{"class":102,"line":307},9,[197,309,310],{"class":206},"model ",[197,312,255],{"class":202},[197,314,315],{"class":206}," QgsAttributeTableModel(cache)\n",[197,317,319],{"class":102,"line":318},10,[197,320,321],{"class":206},"model.loadLayer()\n",[197,323,325,328,330],{"class":102,"line":324},11,[197,326,327],{"class":206},"filter_model ",[197,329,255],{"class":202},[197,331,332],{"class":206}," QgsAttributeTableFilterModel(canvas, model)\n",[197,334,336,339,341],{"class":102,"line":335},12,[197,337,338],{"class":206},"view ",[197,340,255],{"class":202},[197,342,343],{"class":206}," QgsAttributeTableView()\n",[197,345,347],{"class":102,"line":346},13,[197,348,349],{"class":206},"view.setModel(filter_model)\n",[197,351,353],{"class":102,"line":352},14,[197,354,355],{"class":206},"view.show()\n",[14,357,358,362,363,366,367,370],{},[359,360,361],"strong",{},"Breakdown:"," The cache size is the number of features held in memory; 10,000 covers most layers and larger layers still work, fetching as needed. ",[18,364,365],{},"loadLayer()"," populates the model. The filter model needs the map canvas because some filters depend on the visible extent. The view is a normal Qt widget: add it to a layout in your dock or dialog instead of calling ",[18,368,369],{},"show()",". Keep references to the cache, model and filter model on your widget so Python does not garbage-collect them while the view uses them.",[161,372,374],{"id":373},"show-only-selected-or-matching-features","Show only selected or matching features",[14,376,377],{},"The filter model has modes that mirror the attribute table's toolbar filter: all features, selected features, features visible on the map, and features matching a filter expression.",[14,379,380],{},[34,381,384,387,390,393,396,400,405,409,412,417,421,425,430,434,437,440,444,448,451],{"viewBox":382,"role":37,"ariaLabel":383,"xmlns":39},"0 0 760 240","Attribute table filter modes: all features, selected, visible in the map extent, and a filtered list of ids",[41,385,386],{},"Filter modes",[45,388,389],{},"ShowAll lists every feature. ShowSelected lists the layer's current selection and updates when it changes. ShowVisible lists features in the current map extent and updates on pan and zoom. ShowFilteredList lists a given set of feature ids, used with an expression filter, for example parcels with open applications.",[49,391],{"x":51,"y":51,"width":52,"height":392,"fill":54},"240",[73,394,395],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"What the table shows",[49,397],{"x":83,"y":398,"width":399,"height":111,"rx":63,"fill":87,"stroke":88,"style":89},"56","168",[73,401,404],{"x":402,"y":403,"style":118,"fill":78,"textAnchor":79},"108","125.6","ShowAll",[73,406,408],{"x":402,"y":407,"style":123,"fill":88,"textAnchor":79},"153.6","every feature",[49,410],{"x":411,"y":398,"width":399,"height":111,"rx":63,"fill":112,"stroke":113,"style":89},"206",[73,413,416],{"x":414,"y":415,"style":118,"fill":113,"textAnchor":79},"290","111.6","ShowSelected",[73,418,420],{"x":414,"y":419,"style":123,"fill":88,"textAnchor":79},"139.6","follows",[73,422,424],{"x":414,"y":423,"style":123,"fill":88,"textAnchor":79},"167.6","selection",[49,426],{"x":427,"y":398,"width":399,"height":111,"rx":63,"fill":428,"stroke":429,"style":89},"388","#eff3ff","#2563eb",[73,431,433],{"x":432,"y":415,"style":118,"fill":429,"textAnchor":79},"472","ShowVisible",[73,435,436],{"x":432,"y":419,"style":123,"fill":88,"textAnchor":79},"map extent",[73,438,439],{"x":432,"y":423,"style":123,"fill":88,"textAnchor":79},"follows panning",[49,441],{"x":442,"y":398,"width":443,"height":111,"rx":63,"fill":133,"stroke":134,"style":89},"570","166",[73,445,447],{"x":446,"y":415,"style":118,"fill":134,"textAnchor":79},"653","ShowFilteredList",[73,449,450],{"x":446,"y":419,"style":123,"fill":88,"textAnchor":79},"ids from",[73,452,453],{"x":446,"y":423,"style":123,"fill":88,"textAnchor":79},"an expression",[188,455,457],{"className":190,"code":456,"language":192,"meta":193,"style":193},"from qgis.core import QgsFeatureRequest, QgsExpression\n\nfilter_model.setFilterMode(QgsAttributeTableFilterModel.ShowSelected)\n\n# or: only features matching an expression\nexpr = QgsExpression('\"open_applications\" > 0')\nids = [f.id() for f in layer.getFeatures(QgsFeatureRequest(expr).setNoAttributes()\n                                          .setFlags(QgsFeatureRequest.NoGeometry))]\nfilter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)\nfilter_model.setFilteredFeatures(ids)\nprint(filter_model.rowCount(), \"rows shown\")\n",[18,458,459,470,474,479,483,489,504,526,531,536,541],{"__ignoreMap":193},[197,460,461,463,465,467],{"class":102,"line":199},[197,462,203],{"class":202},[197,464,207],{"class":206},[197,466,210],{"class":202},[197,468,469],{"class":206}," QgsFeatureRequest, QgsExpression\n",[197,471,472],{"class":102,"line":216},[197,473,246],{"emptyLinePlaceholder":245},[197,475,476],{"class":102,"line":229},[197,477,478],{"class":206},"filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowSelected)\n",[197,480,481],{"class":102,"line":242},[197,482,246],{"emptyLinePlaceholder":245},[197,484,485],{"class":102,"line":249},[197,486,488],{"class":487},"sjoCn","# or: only features matching an expression\n",[197,490,491,494,496,499,502],{"class":102,"line":274},[197,492,493],{"class":206},"expr ",[197,495,255],{"class":202},[197,497,498],{"class":206}," QgsExpression(",[197,500,501],{"class":261},"'\"open_applications\" > 0'",[197,503,304],{"class":206},[197,505,506,509,511,514,517,520,523],{"class":102,"line":285},[197,507,508],{"class":206},"ids ",[197,510,255],{"class":202},[197,512,513],{"class":206}," [f.id() ",[197,515,516],{"class":202},"for",[197,518,519],{"class":206}," f ",[197,521,522],{"class":202},"in",[197,524,525],{"class":206}," layer.getFeatures(QgsFeatureRequest(expr).setNoAttributes()\n",[197,527,528],{"class":102,"line":290},[197,529,530],{"class":206},"                                          .setFlags(QgsFeatureRequest.NoGeometry))]\n",[197,532,533],{"class":102,"line":307},[197,534,535],{"class":206},"filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)\n",[197,537,538],{"class":102,"line":318},[197,539,540],{"class":206},"filter_model.setFilteredFeatures(ids)\n",[197,542,543,546,549,552],{"class":102,"line":324},[197,544,545],{"class":268},"print",[197,547,548],{"class":206},"(filter_model.rowCount(), ",[197,550,551],{"class":261},"\"rows shown\"",[197,553,304],{"class":206},[14,555,556,558,559,561,562,565,566,569,570,572],{},[359,557,361],{}," ",[18,560,416],{}," is the natural mode for a results panel that follows what the user selects on the map; it updates automatically. For search results, compute matching ids — with the request trimmed to no attributes and no geometry for speed — and pass them to ",[18,563,564],{},"setFilteredFeatures"," in filtered-list mode. Recent releases also accept a filter expression directly on the filter model, which keeps it current as features change; check ",[18,567,568],{},"setFilterExpression"," on your version. ",[18,571,433],{}," makes the table follow the map extent, useful for \"what is on screen\" panels.",[161,574,576],{"id":575},"link-table-selection-with-the-map","Link table selection with the map",[14,578,579],{},"The attribute table view shares the layer's selection: selecting rows selects features, and selecting on the map selects rows. Adding zoom-to and flash behaviour makes the panel feel native.",[188,581,583],{"className":190,"code":582,"language":192,"meta":193,"style":193},"def zoom_to_selected_rows():\n    ids = layer.selectedFeatureIds()\n    if ids:\n        canvas.zoomToFeatureIds(layer, ids)\n        canvas.flashFeatureIds(layer, ids)\n\nview.doubleClicked.connect(lambda index: zoom_to_selected_rows())\nlayer.selectionChanged.connect(lambda *_: print(len(layer.selectedFeatureIds()), \"selected\"))\n",[18,584,585,597,607,615,620,625,629,640],{"__ignoreMap":193},[197,586,587,590,594],{"class":102,"line":199},[197,588,589],{"class":202},"def",[197,591,593],{"class":592},"svObZ"," zoom_to_selected_rows",[197,595,596],{"class":206},"():\n",[197,598,599,602,604],{"class":102,"line":216},[197,600,601],{"class":206},"    ids ",[197,603,255],{"class":202},[197,605,606],{"class":206}," layer.selectedFeatureIds()\n",[197,608,609,612],{"class":102,"line":229},[197,610,611],{"class":202},"    if",[197,613,614],{"class":206}," ids:\n",[197,616,617],{"class":102,"line":242},[197,618,619],{"class":206},"        canvas.zoomToFeatureIds(layer, ids)\n",[197,621,622],{"class":102,"line":249},[197,623,624],{"class":206},"        canvas.flashFeatureIds(layer, ids)\n",[197,626,627],{"class":102,"line":274},[197,628,246],{"emptyLinePlaceholder":245},[197,630,631,634,637],{"class":102,"line":285},[197,632,633],{"class":206},"view.doubleClicked.connect(",[197,635,636],{"class":202},"lambda",[197,638,639],{"class":206}," index: zoom_to_selected_rows())\n",[197,641,642,645,647,650,653,655,658,661,664,667],{"class":102,"line":290},[197,643,644],{"class":206},"layer.selectionChanged.connect(",[197,646,636],{"class":202},[197,648,649],{"class":202}," *",[197,651,652],{"class":206},"_: ",[197,654,545],{"class":268},[197,656,657],{"class":206},"(",[197,659,660],{"class":268},"len",[197,662,663],{"class":206},"(layer.selectedFeatureIds()), ",[197,665,666],{"class":261},"\"selected\"",[197,668,669],{"class":206},"))\n",[14,671,672,674,675,678,679,683],{},[359,673,361],{}," Because the view works on the layer's own selection, nothing extra is needed for sync — that is a key advantage over a hand-built table. Double-clicking zooms to the selected features and flashes them briefly so the eye finds them. Disconnect signal handlers in the plugin's ",[18,676,677],{},"unload()","; ",[26,680,682],{"href":681},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","connecting layer signals"," explains the pattern.",[161,685,687],{"id":686},"control-columns-and-editing","Control columns and editing",[14,689,690],{},"Results panels usually show a few columns and are read-only; data-entry panels show more and allow edits. The layer's attribute table configuration controls columns, and the layer's edit mode controls editing.",[188,692,694],{"className":190,"code":693,"language":192,"meta":193,"style":193},"config = layer.attributeTableConfig()\nkeep = {\"parcel_id\", \"owner_name\", \"area_ha\", \"open_applications\"}\ncolumns = config.columns()\nfor c in columns:\n    c.hidden = c.name not in keep\nconfig.setColumns(columns)\nfilter_model.setAttributeTableConfig(config)\n\nview.setEditTriggers(view.NoEditTriggers)          # read-only panel\n",[18,695,696,706,738,748,760,779,784,789,793],{"__ignoreMap":193},[197,697,698,701,703],{"class":102,"line":199},[197,699,700],{"class":206},"config ",[197,702,255],{"class":202},[197,704,705],{"class":206}," layer.attributeTableConfig()\n",[197,707,708,711,713,716,719,722,725,727,730,732,735],{"class":102,"line":216},[197,709,710],{"class":206},"keep ",[197,712,255],{"class":202},[197,714,715],{"class":206}," {",[197,717,718],{"class":261},"\"parcel_id\"",[197,720,721],{"class":206},", ",[197,723,724],{"class":261},"\"owner_name\"",[197,726,721],{"class":206},[197,728,729],{"class":261},"\"area_ha\"",[197,731,721],{"class":206},[197,733,734],{"class":261},"\"open_applications\"",[197,736,737],{"class":206},"}\n",[197,739,740,743,745],{"class":102,"line":229},[197,741,742],{"class":206},"columns ",[197,744,255],{"class":202},[197,746,747],{"class":206}," config.columns()\n",[197,749,750,752,755,757],{"class":102,"line":242},[197,751,516],{"class":202},[197,753,754],{"class":206}," c ",[197,756,522],{"class":202},[197,758,759],{"class":206}," columns:\n",[197,761,762,765,767,770,773,776],{"class":102,"line":249},[197,763,764],{"class":206},"    c.hidden ",[197,766,255],{"class":202},[197,768,769],{"class":206}," c.name ",[197,771,772],{"class":202},"not",[197,774,775],{"class":202}," in",[197,777,778],{"class":206}," keep\n",[197,780,781],{"class":102,"line":274},[197,782,783],{"class":206},"config.setColumns(columns)\n",[197,785,786],{"class":102,"line":285},[197,787,788],{"class":206},"filter_model.setAttributeTableConfig(config)\n",[197,790,791],{"class":102,"line":290},[197,792,246],{"emptyLinePlaceholder":245},[197,794,795,798],{"class":102,"line":307},[197,796,797],{"class":206},"view.setEditTriggers(view.NoEditTriggers)          ",[197,799,800],{"class":487},"# read-only panel\n",[14,802,803,805,806,809,810,179],{},[359,804,361],{}," The attribute table config lists columns with visibility, width and order; hiding all but the relevant fields keeps the panel focused. Applying the config to the filter model affects only this view, not the layer's own attribute table dialog — use ",[18,807,808],{},"layer.setAttributeTableConfig(config)"," if the change should apply everywhere. With edit triggers disabled, the view is read-only even when the layer is in edit mode; leave the default triggers for panels where users should edit values, which then use the layer's configured ",[26,811,813],{"href":812},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fconfigure-editor-widgets-pyqgis\u002F","editor widgets",[161,815,817],{"id":816},"package-it-as-a-reusable-results-panel","Package it as a reusable results panel",[14,819,820],{},"Plugins that show results in several places benefit from one widget class that wraps the whole stack, with a single method to show a set of features. It keeps references alive, hides the plumbing and makes the panel easy to drop into any dialog or dock.",[188,822,824],{"className":190,"code":823,"language":192,"meta":193,"style":193},"from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel\n\nclass ResultsTable(QWidget):\n    def __init__(self, canvas, parent=None):\n        super().__init__(parent)\n        self.canvas = canvas\n        self.cache = self.model = self.filter_model = None\n        self.label = QLabel(\"No results\")\n        self.view = QgsAttributeTableView()\n        self.view.setEditTriggers(self.view.NoEditTriggers)\n        lay = QVBoxLayout(self)\n        lay.addWidget(self.label)\n        lay.addWidget(self.view)\n        self.view.doubleClicked.connect(self._zoom)\n\n    def show_features(self, layer, ids):\n        self.layer = layer\n        self.cache = QgsVectorLayerCache(layer, 5000)\n        self.model = QgsAttributeTableModel(self.cache)\n        self.model.loadLayer()\n        self.filter_model = QgsAttributeTableFilterModel(self.canvas, self.model)\n        self.filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)\n        self.filter_model.setFilteredFeatures(list(ids))\n        self.view.setModel(self.filter_model)\n        self.label.setText(f\"{len(ids)} {layer.name()} features\")\n\n    def _zoom(self, index):\n        ids = self.layer.selectedFeatureIds()\n        if ids:\n            self.canvas.zoomToFeatureIds(self.layer, ids)\n",[18,825,826,838,842,858,876,890,903,930,947,958,971,985,995,1004,1016,1021,1032,1045,1061,1078,1086,1108,1116,1130,1143,1178,1183,1194,1207,1215],{"__ignoreMap":193},[197,827,828,830,833,835],{"class":102,"line":199},[197,829,203],{"class":202},[197,831,832],{"class":206}," qgis.PyQt.QtWidgets ",[197,834,210],{"class":202},[197,836,837],{"class":206}," QWidget, QVBoxLayout, QLabel\n",[197,839,840],{"class":102,"line":216},[197,841,246],{"emptyLinePlaceholder":245},[197,843,844,847,850,852,855],{"class":102,"line":229},[197,845,846],{"class":202},"class",[197,848,849],{"class":592}," ResultsTable",[197,851,657],{"class":206},[197,853,854],{"class":592},"QWidget",[197,856,857],{"class":206},"):\n",[197,859,860,863,866,869,871,874],{"class":102,"line":242},[197,861,862],{"class":202},"    def",[197,864,865],{"class":268}," __init__",[197,867,868],{"class":206},"(self, canvas, parent",[197,870,255],{"class":202},[197,872,873],{"class":268},"None",[197,875,857],{"class":206},[197,877,878,881,884,887],{"class":102,"line":249},[197,879,880],{"class":268},"        super",[197,882,883],{"class":206},"().",[197,885,886],{"class":268},"__init__",[197,888,889],{"class":206},"(parent)\n",[197,891,892,895,898,900],{"class":102,"line":274},[197,893,894],{"class":268},"        self",[197,896,897],{"class":206},".canvas ",[197,899,255],{"class":202},[197,901,902],{"class":206}," canvas\n",[197,904,905,907,910,912,915,918,920,922,925,927],{"class":102,"line":285},[197,906,894],{"class":268},[197,908,909],{"class":206},".cache ",[197,911,255],{"class":202},[197,913,914],{"class":268}," self",[197,916,917],{"class":206},".model ",[197,919,255],{"class":202},[197,921,914],{"class":268},[197,923,924],{"class":206},".filter_model ",[197,926,255],{"class":202},[197,928,929],{"class":268}," None\n",[197,931,932,934,937,939,942,945],{"class":102,"line":290},[197,933,894],{"class":268},[197,935,936],{"class":206},".label ",[197,938,255],{"class":202},[197,940,941],{"class":206}," QLabel(",[197,943,944],{"class":261},"\"No results\"",[197,946,304],{"class":206},[197,948,949,951,954,956],{"class":102,"line":307},[197,950,894],{"class":268},[197,952,953],{"class":206},".view ",[197,955,255],{"class":202},[197,957,343],{"class":206},[197,959,960,962,965,968],{"class":102,"line":318},[197,961,894],{"class":268},[197,963,964],{"class":206},".view.setEditTriggers(",[197,966,967],{"class":268},"self",[197,969,970],{"class":206},".view.NoEditTriggers)\n",[197,972,973,976,978,981,983],{"class":102,"line":324},[197,974,975],{"class":206},"        lay ",[197,977,255],{"class":202},[197,979,980],{"class":206}," QVBoxLayout(",[197,982,967],{"class":268},[197,984,304],{"class":206},[197,986,987,990,992],{"class":102,"line":335},[197,988,989],{"class":206},"        lay.addWidget(",[197,991,967],{"class":268},[197,993,994],{"class":206},".label)\n",[197,996,997,999,1001],{"class":102,"line":346},[197,998,989],{"class":206},[197,1000,967],{"class":268},[197,1002,1003],{"class":206},".view)\n",[197,1005,1006,1008,1011,1013],{"class":102,"line":352},[197,1007,894],{"class":268},[197,1009,1010],{"class":206},".view.doubleClicked.connect(",[197,1012,967],{"class":268},[197,1014,1015],{"class":206},"._zoom)\n",[197,1017,1019],{"class":102,"line":1018},15,[197,1020,246],{"emptyLinePlaceholder":245},[197,1022,1024,1026,1029],{"class":102,"line":1023},16,[197,1025,862],{"class":202},[197,1027,1028],{"class":592}," show_features",[197,1030,1031],{"class":206},"(self, layer, ids):\n",[197,1033,1035,1037,1040,1042],{"class":102,"line":1034},17,[197,1036,894],{"class":268},[197,1038,1039],{"class":206},".layer ",[197,1041,255],{"class":202},[197,1043,1044],{"class":206}," layer\n",[197,1046,1048,1050,1052,1054,1056,1059],{"class":102,"line":1047},18,[197,1049,894],{"class":268},[197,1051,909],{"class":206},[197,1053,255],{"class":202},[197,1055,298],{"class":206},[197,1057,1058],{"class":268},"5000",[197,1060,304],{"class":206},[197,1062,1064,1066,1068,1070,1073,1075],{"class":102,"line":1063},19,[197,1065,894],{"class":268},[197,1067,917],{"class":206},[197,1069,255],{"class":202},[197,1071,1072],{"class":206}," QgsAttributeTableModel(",[197,1074,967],{"class":268},[197,1076,1077],{"class":206},".cache)\n",[197,1079,1081,1083],{"class":102,"line":1080},20,[197,1082,894],{"class":268},[197,1084,1085],{"class":206},".model.loadLayer()\n",[197,1087,1089,1091,1093,1095,1098,1100,1103,1105],{"class":102,"line":1088},21,[197,1090,894],{"class":268},[197,1092,924],{"class":206},[197,1094,255],{"class":202},[197,1096,1097],{"class":206}," QgsAttributeTableFilterModel(",[197,1099,967],{"class":268},[197,1101,1102],{"class":206},".canvas, ",[197,1104,967],{"class":268},[197,1106,1107],{"class":206},".model)\n",[197,1109,1111,1113],{"class":102,"line":1110},22,[197,1112,894],{"class":268},[197,1114,1115],{"class":206},".filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)\n",[197,1117,1119,1121,1124,1127],{"class":102,"line":1118},23,[197,1120,894],{"class":268},[197,1122,1123],{"class":206},".filter_model.setFilteredFeatures(",[197,1125,1126],{"class":268},"list",[197,1128,1129],{"class":206},"(ids))\n",[197,1131,1133,1135,1138,1140],{"class":102,"line":1132},24,[197,1134,894],{"class":268},[197,1136,1137],{"class":206},".view.setModel(",[197,1139,967],{"class":268},[197,1141,1142],{"class":206},".filter_model)\n",[197,1144,1146,1148,1151,1154,1157,1160,1163,1166,1168,1171,1173,1176],{"class":102,"line":1145},25,[197,1147,894],{"class":268},[197,1149,1150],{"class":206},".label.setText(",[197,1152,1153],{"class":202},"f",[197,1155,1156],{"class":261},"\"",[197,1158,1159],{"class":268},"{len",[197,1161,1162],{"class":206},"(ids)",[197,1164,1165],{"class":268},"}",[197,1167,715],{"class":268},[197,1169,1170],{"class":206},"layer.name()",[197,1172,1165],{"class":268},[197,1174,1175],{"class":261}," features\"",[197,1177,304],{"class":206},[197,1179,1181],{"class":102,"line":1180},26,[197,1182,246],{"emptyLinePlaceholder":245},[197,1184,1186,1188,1191],{"class":102,"line":1185},27,[197,1187,862],{"class":202},[197,1189,1190],{"class":592}," _zoom",[197,1192,1193],{"class":206},"(self, index):\n",[197,1195,1197,1200,1202,1204],{"class":102,"line":1196},28,[197,1198,1199],{"class":206},"        ids ",[197,1201,255],{"class":202},[197,1203,914],{"class":268},[197,1205,1206],{"class":206},".layer.selectedFeatureIds()\n",[197,1208,1210,1213],{"class":102,"line":1209},29,[197,1211,1212],{"class":202},"        if",[197,1214,614],{"class":206},[197,1216,1218,1221,1224,1226],{"class":102,"line":1217},30,[197,1219,1220],{"class":268},"            self",[197,1222,1223],{"class":206},".canvas.zoomToFeatureIds(",[197,1225,967],{"class":268},[197,1227,1228],{"class":206},".layer, ids)\n",[14,1230,1231,1233,1234,1237,1238,179],{},[359,1232,361],{}," Holding the cache, model and filter model as attributes of the widget ties their lifetime to the panel, which avoids the most common bug with these classes — an empty table because the model was garbage-collected. Rebuilding the stack in ",[18,1235,1236],{},"show_features"," lets the same panel show results from different layers over time. The label gives immediate feedback on how many features matched. Any search or analysis in the plugin can now finish with ",[18,1239,1240],{},"results.show_features(layer, ids)",[161,1242,1244],{"id":1243},"use-the-dual-view-for-table-and-form","Use the dual view for table and form",[14,1246,1247,1250],{},[18,1248,1249],{},"QgsDualView"," combines the table with a form: a list of features on the left and the attribute form for the current one on the right, or the full table — exactly the attribute table dialog's two modes.",[14,1252,1253],{},[34,1254,1257,1260,1263,1266,1269,1272,1278,1283,1286,1291,1294,1297],{"viewBox":1255,"role":37,"ariaLabel":1256,"xmlns":39},"0 0 760 230","QgsDualView switching between a table mode and a form mode with a feature list and the layer's attribute form",[41,1258,1259],{},"The dual view",[45,1261,1262],{},"QgsDualView offers two modes. Table mode shows the attribute table. Form mode shows a feature list on the left and the layer's configured attribute form for the current feature on the right, including tabs, widgets and constraints. It is initialised with the layer, the canvas and a feature request that limits which features are loaded.",[49,1264],{"x":51,"y":51,"width":52,"height":1265,"fill":54},"230",[73,1267,1268],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Table mode, or list plus form",[49,1270],{"x":83,"y":398,"width":1271,"height":85,"rx":63,"fill":428,"stroke":429,"style":89},"340",[73,1273,1277],{"x":1274,"y":1275,"style":1276,"fill":429,"textAnchor":79},"194","120.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","table mode",[73,1279,1282],{"x":1274,"y":1280,"style":1281,"fill":88,"textAnchor":79},"148.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","like the attribute table",[49,1284],{"x":1285,"y":398,"width":1271,"height":85,"rx":63,"fill":112,"stroke":113,"style":89},"396",[73,1287,1290],{"x":1288,"y":1289,"style":1276,"fill":113,"textAnchor":79},"566","92.78","form mode",[73,1292,1293],{"x":1288,"y":1275,"style":1281,"fill":71,"textAnchor":79},"feature list",[73,1295,1296],{"x":1288,"y":1280,"style":1281,"fill":71,"textAnchor":79},"+ configured form",[73,1298,1300],{"x":1288,"y":1299,"style":1281,"fill":88,"textAnchor":79},"176.78","tabs, widgets, constraints",[188,1302,1304],{"className":190,"code":1303,"language":192,"meta":193,"style":193},"from qgis.gui import QgsDualView, QgsAttributeEditorContext\n\ndual = QgsDualView()\ncontext = QgsAttributeEditorContext()\ncontext.setMapCanvas(canvas)\nrequest = QgsFeatureRequest().setFilterExpression('\"open_applications\" > 0')\ndual.init(layer, canvas, request, context)\ndual.setView(QgsDualView.AttributeEditor)          # form mode\ndual.show()\n",[18,1305,1306,1317,1321,1331,1341,1346,1360,1365,1373],{"__ignoreMap":193},[197,1307,1308,1310,1312,1314],{"class":102,"line":199},[197,1309,203],{"class":202},[197,1311,221],{"class":206},[197,1313,210],{"class":202},[197,1315,1316],{"class":206}," QgsDualView, QgsAttributeEditorContext\n",[197,1318,1319],{"class":102,"line":216},[197,1320,246],{"emptyLinePlaceholder":245},[197,1322,1323,1326,1328],{"class":102,"line":229},[197,1324,1325],{"class":206},"dual ",[197,1327,255],{"class":202},[197,1329,1330],{"class":206}," QgsDualView()\n",[197,1332,1333,1336,1338],{"class":102,"line":242},[197,1334,1335],{"class":206},"context ",[197,1337,255],{"class":202},[197,1339,1340],{"class":206}," QgsAttributeEditorContext()\n",[197,1342,1343],{"class":102,"line":249},[197,1344,1345],{"class":206},"context.setMapCanvas(canvas)\n",[197,1347,1348,1351,1353,1356,1358],{"class":102,"line":274},[197,1349,1350],{"class":206},"request ",[197,1352,255],{"class":202},[197,1354,1355],{"class":206}," QgsFeatureRequest().setFilterExpression(",[197,1357,501],{"class":261},[197,1359,304],{"class":206},[197,1361,1362],{"class":102,"line":285},[197,1363,1364],{"class":206},"dual.init(layer, canvas, request, context)\n",[197,1366,1367,1370],{"class":102,"line":290},[197,1368,1369],{"class":206},"dual.setView(QgsDualView.AttributeEditor)          ",[197,1371,1372],{"class":487},"# form mode\n",[197,1374,1375],{"class":102,"line":307},[197,1376,1377],{"class":206},"dual.show()\n",[14,1379,1380,558,1382,1385,1386,1389],{},[359,1381,361],{},[18,1383,1384],{},"init"," takes the layer, canvas, a feature request limiting which features load, and an editor context. The request is the simplest way to show a subset in the dual view. Form mode presents the layer's own form, so all the configuration done for data entry — tabs, drop-downs, constraints, form logic — appears in the plugin without extra work. ",[18,1387,1388],{},"setView(QgsDualView.AttributeTable)"," switches to table mode. The dual view is heavier than a bare table view; use it when users need to edit features one at a time.",[161,1391,1393],{"id":1392},"qgis-version-compatibility","QGIS version compatibility",[14,1395,1396,721,1398,721,1401,721,1404,1407,1408,1410,1411,1414,1415,721,1418,179],{},[18,1397,100],{},[18,1399,1400],{},"QgsAttributeTableModel",[18,1402,1403],{},"QgsAttributeTableFilterModel",[18,1405,1406],{},"QgsAttributeTableView"," and ",[18,1409,1249],{}," are available in ",[18,1412,1413],{},"qgis.gui"," on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Filter expressions on the filter model were added in the 3.x series. On QGIS 4, enums are scoped: ",[18,1416,1417],{},"QgsAttributeTableFilterModel.FilterMode.ShowSelected",[18,1419,1420],{},"QgsDualView.ViewMode.AttributeEditor",[161,1422,1424],{"id":1423},"troubleshooting","Troubleshooting",[166,1426,1427,1435,1441,1447],{},[169,1428,1429,558,1432,1434],{},[359,1430,1431],{},"The view is empty.",[18,1433,365],{}," was not called, or the model objects were garbage-collected; keep references.",[169,1436,1437,1440],{},[359,1438,1439],{},"The table does not follow the selection."," The filter mode is ShowAll; set ShowSelected.",[169,1442,1443,1446],{},[359,1444,1445],{},"Column changes affect the main attribute table."," The config was applied to the layer instead of the filter model.",[169,1448,1449,1452],{},[359,1450,1451],{},"Editing is impossible."," The layer is not in edit mode, or edit triggers were disabled.",[161,1454,1456],{"id":1455},"conclusion","Conclusion",[14,1458,1459,1460,1462],{},"Embed QGIS's attribute table with a layer cache, table model, filter model and view, choose a filter mode — selected, visible or a filtered id list — for the panel's purpose, rely on the shared selection for map sync and add zoom and flash, control columns through the attribute table config on the filter model, make results panels read-only, and use ",[18,1461,1249],{}," when users edit through the layer's form.",[161,1464,1466],{"id":1465},"frequently-asked-questions","Frequently Asked Questions",[14,1468,1469,1472],{},[359,1470,1471],{},"Can I show features from several layers in one table?","\nNot with these classes; one model per layer. Use tabs or a combined memory layer.",[14,1474,1475,1478],{},[359,1476,1477],{},"Does the table update when features are edited elsewhere?","\nYes — the cache and model listen to layer signals.",[14,1480,1481,1484],{},[359,1482,1483],{},"Can I add a column computed in Python?","\nAdd a virtual field with an expression to the layer; it appears like any other column.",[14,1486,1487,1490],{},[359,1488,1489],{},"Can users export the table?","\nSelect rows and copy them with Ctrl+C, which copies attributes as tab-separated text; for files, export the filtered features with Processing.",[14,1492,1493,1496],{},[359,1494,1495],{},"Is it fast for large layers?","\nThe cache loads features on demand, so even large layers open quickly; filtered lists keep panels small.",[161,1498,1500],{"id":1499},"related","Related",[166,1502,1503,1508,1513,1519,1524],{},[169,1504,1505,1507],{},[26,1506,29],{"href":28}," — the guide this recipe belongs to",[169,1509,1510],{},[26,1511,1512],{"href":177},"Add a Custom Dock Widget in PyQGIS",[169,1514,1515],{},[26,1516,1518],{"href":1517},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fuse-qgsmaplayercombobox-in-plugin-dialog-pyqgis\u002F","Use a QgsMapLayerComboBox in a Plugin Dialog",[169,1520,1521],{},[26,1522,1523],{"href":812},"Configure Editor Widgets in PyQGIS",[169,1525,1526],{},[26,1527,1529],{"href":1528},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Faccess-active-layer-and-selection-pyqgis\u002F","Access the Active Layer and Selection in PyQGIS",[1531,1532,1533],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":193,"searchDepth":216,"depth":216,"links":1535},[1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547],{"id":163,"depth":216,"text":164},{"id":182,"depth":216,"text":183},{"id":373,"depth":216,"text":374},{"id":575,"depth":216,"text":576},{"id":686,"depth":216,"text":687},{"id":816,"depth":216,"text":817},{"id":1243,"depth":216,"text":1244},{"id":1392,"depth":216,"text":1393},{"id":1423,"depth":216,"text":1424},{"id":1455,"depth":216,"text":1456},{"id":1465,"depth":216,"text":1466},{"id":1499,"depth":216,"text":1500},"Embed QGIS's own attribute table in a plugin dialog or dock — QgsAttributeTableModel, filter model and view, showing only selected or filtered features, linking selection with the map, editable versus read-only tables, and the lighter QgsDualView for table-plus-form layouts.","md",{"slug":1551,"type":1552,"breadcrumb":1553,"datePublished":1554,"dateModified":1554},"show-features-in-attribute-table-view-pyqgis","article","Show Features in an Attribute Table View","2026-10-02","\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fshow-features-in-attribute-table-view-pyqgis",{"title":5,"description":1548},"qgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fshow-features-in-attribute-table-view-pyqgis\u002Findex","Zy-RBlvSdxVwnhB-vbTtEHaKxKtByv1aba48fHkyQ78",1790966259145]