3

I have a QGIS python processing scripts that return multiple rasters. I want to return them in a group named 'Aligned'. There is nothing in the context.addLayerToLoadOnCompletion() function to specify group.

I tried doing something like this to achieve what I want but it didn't work. It did create a group at the end of all layers in the layers panel, but the outputs were still added outside the group.

root = context.project().instance().layerTreeRoot()
group = root.addGroup('Aligned')
context.addLayerToLoadOnCompletion(
    outputs["Runoff Local"]["OUTPUT"],
    QgsProcessingContext.LayerDetails(
        f"Runoff Local (L) ", context.project(), "Runoff Local"
    ),
)

Is there a way to cleanly group all layers from the script in one group?

2
  • 1
    Just for reference, there is a Results group name option in QGIS --> Settings --> Options --> Processing, which you can use to get all results from processing algorithms to a group in the Layers panel. See docs.qgis.org/testing/en/docs/user_manual/processing/… Nov 22, 2021 at 1:48
  • It is also possible to use a QgsProcessingLayerPostProcessorInterface and move it to the group in its postProcessLayer() method. Apr 6, 2023 at 22:40

2 Answers 2

2

I was able to achieve this and this is my understanding:

If you do it through postProcessAlgorithm, QGIS will natively output layer as well, so you will have duplicate layers in your Canvas. So postProcessAlgorithm is not the best approach.

This is how I did it using the native QGIS output, rather than adding a layer to the canvas through my algorithm.

I first created a group through postProcessAlgorithm using Ben W's answer. This only creates and selects a group. Does not add anything.

    def postProcessAlgorithm(self, context, feedback):

        # following code make(if doesn't exist) and select a group so that the QGIS  spits layer at that location

        project = context.project()
        root = project.instance().layerTreeRoot()  # get base level node

        group = root.findGroup(self.run_name)  # find group in whole hierarchy
        if not group:  # if group does not already exists
            selected_nodes = (
                iface.layerTreeView().selectedNodes()
            )  # get all selected nodes
            if selected_nodes:  # if a node is selected
                # check the first node is group
                if isinstance(selected_nodes[0], QgsLayerTreeGroup):
                    # if it is add a group inside
                    group = selected_nodes[0].insertGroup(0, self.run_name)
                else:
                    parent = selected_nodes[0].parent()
                    # get current index so that new group can be inserted at that location
                    index = parent.children()(selected_nodes[0])
                    group = parent.insertGroup(index, self.run_name)
            else:
                group = root.insertGroup(0, self.run_name)

        # select the group
        select_group(self.run_name)

        return {}

Selection function made using the help of this Selecting subgroup in Layers panel using PyQGIS

def select_group(name: str) -> bool:
    """
    Select group item of a node tree
    """

    view = iface.layerTreeView()
    m = view.model()

    listIndexes = m.match(
        m.index(0, 0),
        Qt.DisplayRole,
        name,
        1,
        Qt.MatchFixedString | Qt.MatchRecursive | Qt.MatchCaseSensitive | Qt.MatchWrap,
    )

    if listIndexes:
        i = listIndexes[0]
        view.selectionModel().setCurrentIndex(i, QItemSelectionModel.ClearAndSelect)
        return True

    else:
        return False

0

Adding this here as a working example of doing this via the QgsProcessingLayerPostProcessorInterface class. As described here, to set post processors to multiple output layers it is necessary to store references to the created QgsProcessingLayerPostProcessorInterface instances with class-level scope e.g. in a dictionary declared as an instance attribute of the QgsProcessingAlgorithm sub-class.

from qgis.core import QgsProcessing
from qgis.core import QgsProcessingAlgorithm
from qgis.core import QgsProcessingParameterField
from qgis.core import QgsProcessingParameterVectorLayer
from qgis.core import QgsProcessingUtils
from qgis.core import QgsProcessingContext
from qgis.core import QgsProcessingLayerPostProcessorInterface
from qgis.core import QgsRasterLayer

import processing


class Group_output_layers(QgsProcessingAlgorithm):
    
    post_processors = {}
    
    def name(self):
        return 'group_output_layers'

    def displayName(self):
        return 'group_output_layers'

    def group(self):
        return 'Models'

    def groupId(self):
        return 'Models'

    def createInstance(self):
        return Group_output_layers()

    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterVectorLayer('Inputpolygons', 'Input_polygons', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None))
        self.addParameter(QgsProcessingParameterField('Burnfields', 'Burn_fields', type=0, parentLayerParameterName='Inputpolygons', allowMultiple=True, defaultToAllFields=True,))

    def processAlgorithm(self, parameters, context, feedback):
        results = {}
        outputs = {}
        in_layer = self.parameterAsVectorLayer(parameters, 'Inputpolygons', context)
        fields = self.parameterAsFields(parameters, 'Burnfields', context)
        for index, field in enumerate(fields):
            if feedback.isCanceled():
                break
            # Rasterize
            alg_params = {
                    'BURN': 0,
                    'DATA_TYPE': 5,
                    'EXTENT': in_layer.extent(),
                    'EXTRA': '',
                    'FIELD': field,
                    'INIT': None,
                    'INPUT': parameters['Inputpolygons'],
                    'INVERT': False,
                    'NODATA': -9999,
                    'OPTIONS': '',
                    'UNITS': 1,
                    'HEIGHT': 90,
                    'WIDTH': 90,
                    'OUTPUT': 'TEMPORARY_OUTPUT',
                    }
        
            outputs[f'Outlayer{field}'] = processing.run('gdal:rasterize',
                                                        alg_params,
                                                        is_child_algorithm=True,
                                                        context=context,
                                                        feedback=feedback)
                                                        
            results[f'Rasterized_{field}'] = outputs[f'Outlayer{field}']['OUTPUT']
            
            for result_name, lyr_id in results.items():
                context.addLayerToLoadOnCompletion(lyr_id, QgsProcessingContext.LayerDetails(result_name, context.project(), result_name))
                self.post_processors[lyr_id] = GroupRasterPostProcessor.create()
                context.layerToLoadOnCompletionDetails(lyr_id).setPostProcessor(self.post_processors[lyr_id])
                
        return results

        
class GroupRasterPostProcessor(QgsProcessingLayerPostProcessorInterface):
    
    instance = None
    group_name = 'group1'

    def postProcessLayer(self, layer, context, feedback):
        if not isinstance(layer, QgsRasterLayer):
            return
        project = context.project()
        root_group = project.layerTreeRoot()
        if not root_group.findGroup(self.group_name):
            root_group.insertGroup(0, self.group_name)
        group1 = root_group.findGroup(self.group_name)
        lyr_node = root_group.findLayer(layer.id())
        if lyr_node:
            node_clone = lyr_node.clone()
            group1.addChildNode(node_clone)
            lyr_node.parent().removeChildNode(lyr_node)
    
    # Hack to work around sip bug!
    @staticmethod
    def create() -> 'GroupRasterPostProcessor':
        """
        Returns a new instance of the post processor, keeping a reference to the sip
        wrapper so that sip doesn't get confused with the Python subclass and call
        the base wrapper implementation instead...
        """
        GroupRasterPostProcessor.instance = GroupRasterPostProcessor()
        return GroupRasterPostProcessor.instance

References & Acknowledgements:

Nyall Dawson for the post processor example adapted here.

https://github.com/qgis/QGIS/issues/47533

Set display name of vector layer in processing script

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.