3

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.

Vince
20.5k16 gold badges49 silver badges65 bronze badges
asked Jun 30, 2023 at 8:49

1 Answer 1

9

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

answered Jul 1, 2023 at 6:27
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). Commented Jul 5, 2023 at 12:34
  • 1
    @Josep Pueyo, you can use QCompleter with a QComboBox. I will update answer tomorrow. Commented Jul 5, 2023 at 13:31
  • 1
    @Josep Pueyo, answer updated to include completion/filtering of combo box items. Commented Jul 5, 2023 at 23:22

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.