Show Features in an Attribute Table View in PyQGIS
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 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.
This recipe belongs to 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.
Prerequisites
- 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).
- A plugin with a dialog or dock widget, as in adding a custom dock widget.
Build the model and view
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.
from qgis.core import QgsProject, QgsVectorLayerCache
from qgis.gui import QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView
from qgis.utils import iface
layer = QgsProject.instance().mapLayersByName("parcels")[0]
canvas = iface.mapCanvas()
cache = QgsVectorLayerCache(layer, 10000)
model = QgsAttributeTableModel(cache)
model.loadLayer()
filter_model = QgsAttributeTableFilterModel(canvas, model)
view = QgsAttributeTableView()
view.setModel(filter_model)
view.show()
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. 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 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.
Show only selected or matching features
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.
from qgis.core import QgsFeatureRequest, QgsExpression
filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowSelected)
# or: only features matching an expression
expr = QgsExpression('"open_applications" > 0')
ids = [f.id() for f in layer.getFeatures(QgsFeatureRequest(expr).setNoAttributes()
.setFlags(QgsFeatureRequest.NoGeometry))]
filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)
filter_model.setFilteredFeatures(ids)
print(filter_model.rowCount(), "rows shown")
Breakdown: ShowSelected 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 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 setFilterExpression on your version. ShowVisible makes the table follow the map extent, useful for "what is on screen" panels.
Link table selection with the map
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.
def zoom_to_selected_rows():
ids = layer.selectedFeatureIds()
if ids:
canvas.zoomToFeatureIds(layer, ids)
canvas.flashFeatureIds(layer, ids)
view.doubleClicked.connect(lambda index: zoom_to_selected_rows())
layer.selectionChanged.connect(lambda *_: print(len(layer.selectedFeatureIds()), "selected"))
Breakdown: 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 unload(); connecting layer signals explains the pattern.
Control columns and editing
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.
config = layer.attributeTableConfig()
keep = {"parcel_id", "owner_name", "area_ha", "open_applications"}
columns = config.columns()
for c in columns:
c.hidden = c.name not in keep
config.setColumns(columns)
filter_model.setAttributeTableConfig(config)
view.setEditTriggers(view.NoEditTriggers) # read-only panel
Breakdown: 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 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 editor widgets.
Package it as a reusable results panel
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.
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel
class ResultsTable(QWidget):
def __init__(self, canvas, parent=None):
super().__init__(parent)
self.canvas = canvas
self.cache = self.model = self.filter_model = None
self.label = QLabel("No results")
self.view = QgsAttributeTableView()
self.view.setEditTriggers(self.view.NoEditTriggers)
lay = QVBoxLayout(self)
lay.addWidget(self.label)
lay.addWidget(self.view)
self.view.doubleClicked.connect(self._zoom)
def show_features(self, layer, ids):
self.layer = layer
self.cache = QgsVectorLayerCache(layer, 5000)
self.model = QgsAttributeTableModel(self.cache)
self.model.loadLayer()
self.filter_model = QgsAttributeTableFilterModel(self.canvas, self.model)
self.filter_model.setFilterMode(QgsAttributeTableFilterModel.ShowFilteredList)
self.filter_model.setFilteredFeatures(list(ids))
self.view.setModel(self.filter_model)
self.label.setText(f"{len(ids)} {layer.name()} features")
def _zoom(self, index):
ids = self.layer.selectedFeatureIds()
if ids:
self.canvas.zoomToFeatureIds(self.layer, ids)
Breakdown: 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 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 results.show_features(layer, ids).
Use the dual view for table and form
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.
from qgis.gui import QgsDualView, QgsAttributeEditorContext
dual = QgsDualView()
context = QgsAttributeEditorContext()
context.setMapCanvas(canvas)
request = QgsFeatureRequest().setFilterExpression('"open_applications" > 0')
dual.init(layer, canvas, request, context)
dual.setView(QgsDualView.AttributeEditor) # form mode
dual.show()
Breakdown: 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. 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.
QGIS version compatibility
QgsVectorLayerCache, QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsAttributeTableView and QgsDualView are available in 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: QgsAttributeTableFilterModel.FilterMode.ShowSelected, QgsDualView.ViewMode.AttributeEditor.
Troubleshooting
- The view is empty.
loadLayer()was not called, or the model objects were garbage-collected; keep references. - The table does not follow the selection. The filter mode is ShowAll; set ShowSelected.
- Column changes affect the main attribute table. The config was applied to the layer instead of the filter model.
- Editing is impossible. The layer is not in edit mode, or edit triggers were disabled.
Conclusion
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 QgsDualView when users edit through the layer's form.
Frequently Asked Questions
Can I show features from several layers in one table? Not with these classes; one model per layer. Use tabs or a combined memory layer.
Does the table update when features are edited elsewhere? Yes — the cache and model listen to layer signals.
Can I add a column computed in Python? Add a virtual field with an expression to the layer; it appears like any other column.
Can users export the table? Select rows and copy them with Ctrl+C, which copies attributes as tab-separated text; for files, export the filtered features with Processing.
Is it fast for large layers? The cache loads features on demand, so even large layers open quickly; filtered lists keep panels small.