1

I want to add an input parameter in a a QGIS processing plugin that let the user choose among all the values in a layer field (or at least provide autocomplete). However, I could not find any QgsProcessingParameterDefinition that can do this.

I am thinking in something like:

        self.addParameter(
            QgsProcessingParameter<value>(
                'wwtp_id',
                self.tr('Id of the node containing the WWTP'),
                parentLayerParameterName=self.NODES,
                allowMultiple=False
            )
        )

Also, it should be type agnostic because the id can be an integer or a string, depending on the NODES layer provided by the user.

1 Answer 1

6

You could create a custom PyQt widget and use it your processing script with a processing widget wrapper.

In the example below, I have created a custom widget class which includes a QgsMapLayerComboBox, a QgsFieldComboBox, plus a generic QComboBox, which displays all the values in the current selected field. You can filter the layer and field combo boxes to show only certain layer/field types.

If you have a potentially very large number of values, you can make the combo box editable and use a QCompleter so that you can filter the items by typing into the editable combo box.

from processing.gui.wrappers import WidgetWrapper

from qgis.PyQt.QtCore import (QCoreApplication,
                                QVariant,
                                Qt)
                                
from qgis.PyQt.QtWidgets import (QLabel,
                                QWidget,
                                QGridLayout,
                                QComboBox,
                                QCompleter)
                                
from qgis.core import (QgsProcessing,
                        QgsProcessingAlgorithm,
                        QgsProcessingParameterMatrix,
                        QgsMapLayerProxyModel,
                        QgsFieldProxyModel)
                        
from qgis.gui import (QgsMapLayerComboBox,
                        QgsFieldComboBox)
                       
class AddLayoutTable(QgsProcessingAlgorithm):
    INPUT_PARAMS = 'INPUT_PARAMS'
 
    def __init__(self):
        super().__init__()
 
    def name(self):
        return "widgetwrapperdemo"
         
    def displayName(self):
        return "Widget Wrapper Demo"
 
    def group(self):
        return "General"
 
    def groupId(self):
        return "general"
 
    def shortHelpString(self):
        return "Example of using a custom widget wrapper."
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return type(self)()
   
    def initAlgorithm(self, config=None):
        test_param = QgsProcessingParameterMatrix(self.INPUT_PARAMS, 'Input Parameters')
        test_param.setMetadata({'widget_wrapper': {'class': CustomParametersWidget}})
        self.addParameter(test_param)
        

    def processAlgorithm(self, parameters, context, feedback):
        # Retrieve the list of parameters returned by the custom widget wrapper
        input_params_list = self.parameterAsMatrix(parameters, 'INPUT_PARAMS', context)
        # Access the list items to retrieve each parameter object
        node_lyr = input_params_list[0]
        node_fld = input_params_list[1]
        # Check whether Type of input field is String or Integer
        if node_lyr.fields()[node_lyr.fields().lookupField(node_fld)].type() == QVariant.String:
            node_id = input_params_list[2]
        elif node_lyr.fields()[node_lyr.fields().lookupField(node_fld)].isNumeric():
            node_id = int(input_params_list[2])
        else:
            node_id = 'None'
            
        node_feats = [ft for ft in node_lyr.getFeatures() if ft[node_fld] == node_id]
        
        for feat in node_feats:
            continue
            # Do something with feat
        
        # Just for demonstration we can return the input parameters to check their values...
        return {'Input Node Layer': node_lyr,
                'Node Field': node_fld,
                'Node ID': node_id,
                'Node Features': node_feats}

# Widget Wrapper class
class CustomParametersWidget(WidgetWrapper):

    def createWidget(self):
        self.cpw = CustomWidget()
        return self.cpw
        
    def value(self):
        # This method gets the parameter values and returns them in a list...
        # which will be retrieved and parsed in the processAlgorithm() method
        self.lyr = self.cpw.getLayer()
        self.fld = self.cpw.getField()
        self.node_id = self.cpw.getFieldValue()
        return [self.lyr, self.fld, self.node_id]
        
# Custom Widget class
class CustomWidget(QWidget):
    
    def __init__(self):
        super(CustomWidget, self).__init__()
        self.setGeometry(500, 300, 500, 300)
        self.lyr_lbl = QLabel('Input Nodes Layer', self)
        self.lyr_cb = QgsMapLayerComboBox(self)
        self.fld_lbl = QLabel('Node ID Field')
        self.fld_cb = QgsFieldComboBox(self)
        self.id_lbl = QLabel('Node ID')
        self.id_cb = QComboBox(self)
        # Make combo box editable
        self.id_cb.setEditable(True)
        self.layout = QGridLayout(self)
        self.layout.addWidget(self.lyr_lbl, 0, 0, 1, 1, Qt.AlignRight)
        self.layout.addWidget(self.lyr_cb, 0, 1, 1, 2)
        self.layout.addWidget(self.fld_lbl, 1, 0, 1, 1, Qt.AlignRight)
        self.layout.addWidget(self.fld_cb, 1, 1, 1, 2)
        self.layout.addWidget(self.id_lbl, 2, 0, 1, 1, Qt.AlignRight)
        self.layout.addWidget(self.id_cb, 2, 1, 1, 2)
        # Set filter on the map layer combobox (here we show only point layers)
        self.lyr_cb.setFilters(QgsMapLayerProxyModel.PointLayer)
        self.fld_cb.setLayer(self.lyr_cb.currentLayer())
        # Set filters on field combobox (here we show only string and integer fields)
        self.fld_cb.setFilters(QgsFieldProxyModel.Int | QgsFieldProxyModel.LongLong | QgsFieldProxyModel.String)
        self.lyr_cb.layerChanged.connect(self.layerChanged)
        self.completer = QCompleter([])
        self.completer.setCaseSensitivity(Qt.CaseInsensitive)
        # Default is Qt.MatchStartsWith... (change if you want)
        self.completer.setFilterMode(Qt.MatchContains)
        self.id_cb.setCompleter(self.completer)
        self.populateComboBox()
        self.fld_cb.fieldChanged.connect(self.populateComboBox)
        
    def layerChanged(self):
        self.fld_cb.setLayer(self.lyr_cb.currentLayer())
        self.updateCompleter()
        
    def populateComboBox(self):
        '''Populate the combo box with all unique values in the selected field'''
        self.id_cb.clear()
        node_lyr = self.lyr_cb.currentLayer()
        id_fld = self.fld_cb.currentField()
        fld_idx = node_lyr.fields().lookupField(id_fld)
        id_vals = node_lyr.uniqueValues(fld_idx)
        self.id_cb.addItems([str(val) for val in id_vals])
        self.updateCompleter()
        
    def updateCompleter(self):
        node_lyr = self.lyr_cb.currentLayer()
        id_fld = self.fld_cb.currentField()
        fld_idx = node_lyr.fields().lookupField(id_fld)
        id_vals = node_lyr.uniqueValues(fld_idx)
        self.completer.model().setStringList([str(val) for val in id_vals])
    
    def getLayer(self):
        return self.lyr_cb.currentLayer()
        
    def getField(self):
        return self.fld_cb.currentField()
        
    def getFieldValue(self):
        return self.id_cb.currentText()

The resulting processing script UI looks like this:

enter image description here

You can find a blog post from Faunalia about using custom widgets in processing scripts here:

Adding Custom Widgets to a Processing script

3
  • I marked as correct because it works and it is what I asked. However, I was hoping for a more straightforward solution. Also for text input with autocomplete rather than a list (a normal layer can have 15 thousands nodes). Jul 5, 2023 at 12:34
  • 1
    @Josep Pueyo, you can use QCompleter with a QComboBox. I will update answer tomorrow.
    – Ben W
    Jul 5, 2023 at 13:31
  • 1
    @Josep Pueyo, answer updated to include completion/filtering of combo box items.
    – Ben W
    Jul 5, 2023 at 23:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.