1

I'm coding an archaeological site prediction model. Researchers are able to load multipolygons (i.e. soil type, landform), choose the field to 'weight' or 'rank'. My data has both soil and landform as different fields in the same multipolygon. This part of the coding has been stuck on passing the values in a field to a combobox (or whatever). I am thinking to use QgsProcessingParameterEnum but populate the list using values in the Field that is chosen, rather than pre-defined values.

Is that possible?

This answer helped lots 'QGIS processing parameter using values from layer feature in processing plugin', so did the 30 or so attempts I made. Code below is my best attempt.

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (QgsProcessingAlgorithm, QgsProcessingParameterEnum,QgsProcessingParameterVectorLayer,QgsProcessingParameterField)
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsFeatureSink,
                       QgsProcessingParameterMatrix,
                       QgsProcessingParameterField,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink)
from qgis import processing
class ExAlgo(QgsProcessingAlgorithm):
    OPTIONS = 'OPTIONS'
    INPUT = 'INPUT'
    FIELDS = 'FIELDS'
    
    def __init__(self):
        super().__init__()
 
    def name(self):
        return "exalgo"
     
    def tr(self, text):
        return QCoreApplication.translate("exalgo", text)
         
    def displayName(self):
        return self.tr("Example script")
 
    def group(self):
        return self.tr("Examples")
 
    def groupId(self):
        return "examples"
 
    def shortHelpString(self):
        return self.tr("Example script with an Enum Parameter")
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return type(self)()
   
    def initAlgorithm(self, config=None):
        
        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )
       
        self.addParameter(
            QgsProcessingParameterField(
                self.FIELDS,
                'Choose X Field',
                '',
                self.INPUT))
        
        self.option_list = ['Option 1','Option 2','Option 3','Option 4','Option 5']
        self.addParameter(QgsProcessingParameterEnum(
            self.OPTIONS,
            self.tr("Select options"),
            options=self.option_list,
            
            allowMultiple=True))

 
    def processAlgorithm(self, parameters, context, feedback):
        
        source = self.parameterAsVectorLayer(
            parameters,
            self.INPUT,
            context
        )
        
        fields = source.fields()
        all_fields = fields.lookupField(self.parameterAsString(parameters, self.FIELDS, context))
        
      
        # retrieve your input file parameter
        user_options = self.parameterAsEnums(parameters, self.OPTIONS, context)
        selected_items = [self.option_list[i] for i in user_options]
        
        return {self.OPTIONS: selected_items}
0

1 Answer 1

1

In the docs the QgsProcessingParameterEnum class is described as

An enum based parameter for processing algorithms, allowing for selection from predefined values.

As far as I know, the options are passed and evaluated when the parameter is initialized and I don't believe it's possible to update them dynamically based on selections in other parameters.

I think that using a custom widget wrapper is your best option here.

You can use this example as a basis. The custom widget uses a QgsMapLayerComboBox, a QgsFieldComboBox and a QListWidget with some additional functionalities. The list widget is based on a previous answer I posted here.

"""
/***************************************************************************
    A QGIS processing algorithm with a custom checkable list widget wrapper
    
        begin                : August 2023
        copyright            : (C) 2023 by Ben Wirf
        email                : [email protected]
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from processing.gui.wrappers import WidgetWrapper
from qgis.PyQt.QtWidgets import (QWidget, QListWidget, QListWidgetItem,
                                QLabel, QLineEdit, QGridLayout, QMenu,
                                QAction)
from qgis.PyQt.QtCore import (QCoreApplication, QVariant, Qt, QEvent)
from qgis.PyQt.QtGui import QCursor
from qgis.core import (QgsProcessingAlgorithm, QgsProcessingParameterMatrix,
                        QgsMapLayerProxyModel)
from qgis.gui import (QgsMapLayerComboBox, QgsFieldComboBox)

import re

class CustomListWidget(QgsProcessingAlgorithm):
    INPUT_PARAMS = 'INPUT_PARAMS'
 
    def __init__(self):
        super().__init__()
 
    def name(self):
        return "widgetwrapperexample"
     
    def tr(self, text):
        return QCoreApplication.translate("widgetwrapperexample", text)
         
    def displayName(self):
        return self.tr("Widget wrapper example")
 
    def group(self):
        return self.tr("Examples")
 
    def groupId(self):
        return "examples"
 
    def shortHelpString(self):
        return self.tr("Example script with custom widgets")
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return type(self)()
   
    def initAlgorithm(self, config=None):
        
        param = QgsProcessingParameterMatrix(self.INPUT_PARAMS, 'Input parameters')
        param.setMetadata({'widget_wrapper': {'class': CheckableListWidget}})
        self.addParameter(param)
 
    def processAlgorithm(self, parameters, context, feedback):
        user_options = self.parameterAsMatrix(parameters, self.INPUT_PARAMS, context)
        input_vector_layer = user_options[0] # QgsVectorLayer object
        # Do something with input_vector_layer...
        
        weight_field = user_options[1] # Name (string) of selected field
        # Do something with weight_field...
        
        field_values = user_options[2]# list of selected values
        # if field_values:
            # Do something with field_values...
        
        # Just return the input parameter values for demo purposes
        return {'INPUT_LAYER': input_vector_layer,
                'INPUT_FIELD': weight_field,
                'VALUE_LIST': field_values}
        
class CheckableListWidget(WidgetWrapper):

    def createWidget(self):
        self.custom_widget = customParametersWidget()

        return self.custom_widget

    def value(self):
        source_layer = self.custom_widget.get_layer()
        source_field = self.custom_widget.get_field()
        checked_values = self.custom_widget.get_values()
        # cast int/float items back to numeric values if possible
        options = [int(v) if v.isdigit() else float(v) if re.match(r'^-?\d+(?:\.\d+)$', v) else str(v) for v in checked_values]
        params = [source_layer, source_field, options]
        
        return params
        
class customParametersWidget(QWidget):
    def __init__(self):
        super(customParametersWidget, self).__init__()
        
        self.lyr_lbl = QLabel('Input vector layer:')
        self.lyr_cb = QgsMapLayerComboBox(self)
        self.lyr_cb.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.lyr_cb.layerChanged.connect(self.layer_changed)
        self.fld_lbl = QLabel('Select field:')
        self.fld_cb = QgsFieldComboBox(self)
        self.fld_cb.setLayer(self.lyr_cb.currentLayer())
        self.fld_cb.fieldChanged.connect(self.populate_list_widget)
        self.val_lbl = QLabel('Select values:')
        self.filter_le = QLineEdit(self)
        self.filter_le.setPlaceholderText('Type to filter...')
        self.filter_le.textChanged.connect(self.filter_items)
        self.value_list = QListWidget(self)
        
        self.layout = QGridLayout()
        self.layout.addWidget(self.lyr_lbl, 0, 0, 1, 1)
        self.layout.addWidget(self.lyr_cb, 0, 1, 1, 3)
        self.layout.addWidget(self.fld_lbl, 1, 0, 1, 1)
        self.layout.addWidget(self.fld_cb, 1, 1, 1, 3)
        self.layout.addWidget(self.val_lbl, 2, 0, 1, 1)
        self.layout.addWidget(self.filter_le, 3, 0, 1, 4)
        self.layout.addWidget(self.value_list, 4, 0, 1, 4)
        
        self.setLayout(self.layout)
        
        self.populate_list_widget()
        
        self.context_menu = QMenu(self)
        self.action_check_all = QAction('Select All', self)
        self.action_check_all.triggered.connect(self.select_all)
        self.action_uncheck_all = QAction('De-select All', self)
        self.action_uncheck_all.triggered.connect(self.deselect_all)
        self.context_menu.addAction(self.action_check_all)
        self.context_menu.addAction(self.action_uncheck_all)
        
        self.value_list.viewport().installEventFilter(self)
        
    def layer_changed(self, lyr):
        self.fld_cb.setLayer(lyr)
        self.populate_list_widget()
        
    def eventFilter(self, obj, event):
        if event.type() == QEvent.MouseButtonPress and obj == self.value_list.viewport():
            if event.button() == Qt.LeftButton:
                clicked_item = self.value_list.itemAt(event.pos())
                if clicked_item.checkState() == Qt.Checked:
                    clicked_item.setCheckState(Qt.Unchecked)
                else:
                    clicked_item.setCheckState(Qt.Checked)
            elif event.button() == Qt.RightButton:
                self.context_menu.exec(QCursor.pos())
            return True
        return False
        
    def select_all(self):
        for i in range(self.value_list.count()):
            item = self.value_list.item(i)
            if not item.isHidden():
                item.setCheckState(Qt.Checked)

    def deselect_all(self):
        for i in range(self.value_list.count()):
            item = self.value_list.item(i)
            if not item.isHidden():
                item.setCheckState(Qt.Unchecked)
        
    def filter_items(self, filter_txt):
        for i in range(self.value_list.count()):
            item = self.value_list.item(i)
            filter = filter_txt.lower() not in item.text().lower()
            self.value_list.setRowHidden(i, filter)
        
    def populate_list_widget(self):
        self.value_list.clear()
        lyr = self.lyr_cb.currentLayer()
        if not lyr:
            return
        fld_name = self.fld_cb.currentField()
        fld_idx = lyr.fields().lookupField(fld_name)
        list_items = lyr.uniqueValues(fld_idx)
        if list_items:
            for i in list_items:
                li = QListWidgetItem(str(i))
                li.setFlags(li.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                li.setCheckState(Qt.Unchecked)
                self.value_list.addItem(li)
            self.filter_le.clear()
                
    def get_layer(self):
        return self.lyr_cb.currentLayer()
        
    def get_field(self):
        return self.fld_cb.currentField()
    
    def get_values(self):
        return [self.value_list.item(i).data(0) for i in range(self.value_list.count()) if self.value_list.item(i).checkState() == Qt.Checked]

In the following screenshots I have selected a land systems layer which contains soil and landform fields. I suggest you add this example algorithm to your toolbox and have a play around with it, inspecting the return values to see how it works and how you can adapt it for your case.

enter image description here

enter image description here

enter image description here

2
  • Hi Ben, although I've adapted your answer to a plugin, I will make sure to acknowledge you by name and the answer by URL. Thank you for being generous with your time, there are quite a lot on this forum that are.
    – Andrew
    Commented Sep 3, 2023 at 3:20
  • @Andrew, no worries. Happy to help :-) cheers
    – Ben W
    Commented Sep 6, 2023 at 11:02

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.