Connecting QGIS Form Widgets to Vector Layer Attributes

Connecting QGIS form widgets to vector layer attributes requires binding Qt UI controls to QgsVectorLayer fields through the QgsEditorWidgetSetup API and the…

Connecting QGIS form widgets to vector layer attributes requires binding Qt UI controls to QgsVectorLayer fields through the QgsEditorWidgetSetup API and the layer’s edit buffer. In modern QGIS 3.x, you configure the widget type and configuration dictionary directly on the target field, then synchronize user input via the attributeValueChanged signal or by calling changeAttributeValue() within an active editing session. This approach ensures changes respect QGIS’s undo/redo stack, validation rules, and transaction management without manual state tracking.

For developers building custom interfaces, understanding how Designing Qt Dialogs and Form Widgets integrates with QGIS’s data model is essential. The framework abstracts low-level Qt event loops while preserving direct access to feature attributes, constraint expressions, and provider-level transactions.

Native Form Configuration with QgsEditorWidgetSetup

When attaching a specific input control (e.g., dropdown, calendar, code editor, or multiline text box) to an attribute, QGIS provides QgsEditorWidgetSetup. This replaces legacy QgsVectorLayer.setFieldWidget() calls and integrates directly with the attribute form engine. Configuration is applied at the layer schema level, meaning it persists across sessions and does not require an active edit session to commit.

python
from qgis.core import QgsProject, QgsVectorLayer, QgsEditorWidgetSetup

def configure_field_widget(layer_id: str, field_name: str, widget_type: str = "TextEdit"):
    layer = QgsProject.instance().mapLayer(layer_id)
    if not layer or not layer.isValid():
        raise RuntimeError(f"Layer '{layer_id}' is invalid or not loaded.")

    field_idx = layer.fields().indexFromName(field_name)
    if field_idx == -1:
        raise KeyError(f"Field '{field_name}' not found in layer schema.")

    # Widget configuration varies by type. See QGIS API docs for full schema.
    config = {
        "IsMultiline": True,
        "UseHtml": False,
        "FieldLength": 255
    }

    setup = QgsEditorWidgetSetup(widget_type, config)
    layer.setEditorWidgetSetup(field_idx, setup)
    print(f"Applied {widget_type} widget to '{field_name}' (index {field_idx})")

Supported widget_type strings include TextEdit, ValueMap, Range, DateTime, FileName, UuidGenerator, RelationReference, and Hidden. Each accepts a specific config dictionary structure. For authoritative type definitions and configuration keys, reference the official QgsEditorWidgetSetup documentation.

Common Configuration Patterns:

  • ValueMap: Requires {"map": [{"key1": "value1"}, {"key2": "value2"}]}
  • Range: Requires {"Min": 0, "Max": 100, "Step": 1, "Style": "SpinBox"}
  • RelationReference: Requires {"AllowAddFeatures": False, "ShowOpenFormButton": True, "Relation": "relation_id"}

Custom Qt Dialog Binding Pattern

If you are building a standalone plugin dialog rather than relying on QGIS’s default attribute form, you must manually bridge the Qt widget to the layer’s edit buffer. The pattern below demonstrates a production-ready synchronization loop that respects QGIS transaction states and prevents race conditions during bulk edits.

flowchart LR
    W["Qt widget: QLineEdit"] -->|"edit calls changeAttributeValue()"| BUF["Layer edit buffer"]
    BUF -->|"attributeValueChanged signal"| W
    BUF --> CR["commitChanges() or rollBack()"]
python
from qgis.core import QgsVectorLayer, QgsFeature
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QLabel, QPushButton
from qgis.PyQt.QtCore import pyqtSlot

class AttributeBindingDialog(QWidget):
    def __init__(self, layer: QgsVectorLayer, feature_id: int, field_name: str, parent=None):
        super().__init__(parent)
        self.layer = layer
        self.feature_id = feature_id
        self.field_name = field_name
        self.field_idx = self.layer.fields().indexFromName(field_name)
        
        if self.field_idx == -1:
            raise KeyError(f"Field '{field_name}' missing from layer schema.")

        self._init_ui()
        self._load_initial_value()
        self._connect_signals()

    def _init_ui(self):
        layout = QVBoxLayout(self)
        self.label = QLabel(f"Edit {self.field_name}:")
        self.input_widget = QLineEdit()
        self.save_btn = QPushButton("Save to Layer")
        self.save_btn.clicked.connect(self._commit_to_layer)
        
        layout.addWidget(self.label)
        layout.addWidget(self.input_widget)
        layout.addWidget(self.save_btn)

    def _load_initial_value(self):
        feat = self.layer.getFeature(self.feature_id)
        self.input_widget.setText(str(feat[self.field_idx]) if feat.isValid() else "")

    @pyqtSlot()
    def _commit_to_layer(self):
        if not self.layer.isEditable():
            self.layer.startEditing()
            
        new_value = self.input_widget.text()
        self.layer.changeAttributeValue(self.feature_id, self.field_idx, new_value)
        # Optional: auto-commit or leave in buffer for undo/redo
        # self.layer.commitChanges()

    def _connect_signals(self):
        # Sync external edits back to the widget
        self.layer.attributeValueChanged.connect(self._on_external_change)

    @pyqtSlot(int, int, object)
    def _on_external_change(self, fid: int, idx: int, value):
        if fid == self.feature_id and idx == self.field_idx:
            current_text = self.input_widget.text()
            if current_text != str(value):
                self.input_widget.blockSignals(True)
                self.input_widget.setText(str(value))
                self.input_widget.blockSignals(False)

This architecture ensures bidirectional synchronization. When users modify the QLineEdit, the _commit_to_layer method writes to the layer’s edit buffer. Conversely, the attributeValueChanged signal catches edits from other dialogs, the attribute table, or Python scripts, keeping the UI state consistent. For broader UI architecture patterns, consult the Plugin Development & UI Integration guide.

Managing Edit Sessions & Undo/Redo State

QGIS wraps attribute modifications in an edit buffer rather than writing directly to the underlying data source. This design enables robust undo/redo functionality, constraint validation, and batched I/O operations. When connecting custom widgets, you must explicitly manage the editing lifecycle:

  1. Start Editing: Call layer.startEditing() before the first changeAttributeValue() call. If the layer is already editable, subsequent calls are no-ops.
  2. Buffer Changes: Each changeAttributeValue() pushes a delta to the undo stack. QGIS tracks old vs. new values automatically.
  3. Commit or Rollback: Use layer.commitChanges() to flush the buffer to the provider, or layer.rollBack() to discard changes. Both operations trigger editingStopped() signals.

Never bypass the edit buffer by calling provider-level changeAttributeValues() directly unless you are implementing a custom data source. Doing so breaks QGIS’s constraint engine, skips validation rules, and corrupts the undo stack. For complete signal documentation and transaction handling, review the QgsVectorLayer API reference.

Validation & Production Best Practices

Connecting widgets is only half the implementation. Production-grade plugins must enforce data integrity at the UI and layer levels:

  • Constraint Expressions: Define layer.setConstraintExpression(field_idx, "length(@value) <= 50", "Max 50 characters") to trigger native validation errors in both custom dialogs and default forms.
  • Widget Blocking: Always call widget.blockSignals(True) when programmatically updating UI values from external layer changes. This prevents recursive signal loops and unnecessary buffer writes.
  • Type Coercion: Qt widgets return strings. Cast values to match the field’s QVariant type before calling changeAttributeValue(). Use QgsVectorLayerUtils.convertValue() for safe coercion.
  • Feature Locking: In multi-user environments, check layer.isModified() and handle QgsVectorDataProvider::capabilities() flags before committing. Some providers (e.g., GeoPackage, PostgreSQL) support row-level locking.
  • Memory Management: Disconnect signals in the dialog’s closeEvent() to prevent dangling references to deleted layers or features.

By combining QgsEditorWidgetSetup for declarative form configuration with explicit signal binding for custom dialogs, you maintain full control over the user experience while leveraging QGIS’s native data integrity guarantees. This dual approach scales cleanly from simple attribute editors to complex, multi-layer editing workflows.