Change the symbology of a vector output layer that is returned from a processing tool

propertiesqgis-pluginsqgis-processingsymbologyvector-layer

I want to change the symbology of a Vector Output Layer that is Returned after running a Processing Tool. I am building a processing plugin. I am trying to make the effects reflect in the displayed output. I want the changes effected on the layer property after running each Processing Tool.

I have copied the relevant section of my code below:

def processAlgorithm(self, parameters, context, model_feedback):
# Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the
# overall progress through the model

feedback = QgsProcessingMultiStepFeedback(2, model_feedback)
results = {}
outputs = {}                                
            
# Fix Line Geometries               
alg_params = {'INPUT': parameters['VectorLineLayer'], 'OUTPUT': parameters['FixedLines']}
    
outputs['FixGeometries1'] = processing.run('native:fixgeometries', alg_params, context=context, feedback=feedback, is_child_algorithm=True) #1

layer_A = QgsVectorLayer(outputs['CreateGrid']['OUTPUT'], 'layer_A')
renderer = layer_A.renderer()

symbol = QgsLineSymbol.createSimple({'line_color': '191,191,191,255', 'line_width': '0.20', 'line_style': 'dash'})
layer_A.renderer().setSymbol(symbol)
layer_A.triggerRepaint() # show the change
iface.layerTreeView().refreshLayerSymbology(layer_A.id()) # I want these changes to be effected in the output displayed.

feedback.setCurrentStep(1)
if feedback.isCanceled():
    return {}
    
# Using Field calculator to Add an ID
alg_params = {
    'FIELD_LENGTH': 10,
    'FIELD_NAME': 'ID',
    'FIELD_PRECISION': 0,
    'FIELD_TYPE': 1,
    'FORMULA': '$id',
    'INPUT': layer_A,
    'OUTPUT': parameters['LineID']
}
outputs['AddID'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) #2

layer_B = QgsVectorLayer(outputs['AddID']['OUTPUT'], 'layer_B')
renderer = layer_B.renderer()

symbol = QgsLineSymbol.createSimple({'line_color': '191,191,191,255', 'line_width': '0.20', 'line_style': 'dash'})
layer_B.renderer().setSymbol(symbol)
layer_B.triggerRepaint() # show the change
iface.layerTreeView().refreshLayerSymbology(layer_B.id()) # I want these changes to be effected in the output displayed.

results['LineID'] = layer_B

return results        

When I run the routine above, I get this error message: AttributeError: 'NoneType' object has no attribute 'setSymbol'. What can I do differently?

Best Answer

The recommended way to do this is via the QgsProcessingLayerPostProcessorInterface class. There are a few reasons for this. The first is that QGIS Processing Algorithms are run by default in a background thread. Therefore it is not thread-safe to interact with the canvas or interface from inside the processAlgorithm() method. While it is possible to re-implement the flags() method and return the FlagNoThreading flag, using a post-processor class makes sense because for an algorithm output, a proper destination parameter such as a QgsProcessingParameterFeatureSink should be used. This gives the user the option to save to a temporary layer or a file location and also adds the checkbox to 'Open output file after running algorithm', and this can be handled cleanly by checking if context.willLoadLayerOnCompletion(), then accessing the context.layerToLoadOnCompletionDetails() and setting the post processor to the returned QgsProcessingContext::LayerDetails object.

Below is an example script which should show you how to do this. You can test it by adding it to the Processing toolbox, and adapting for your needs.

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                        QgsProcessingAlgorithm,
                        QgsProcessingParameterFeatureSource,
                        QgsProcessingParameterExtent,
                        QgsProcessingParameterFeatureSink,
                        QgsProcessingMultiStepFeedback,
                        QgsCoordinateReferenceSystem,
                        QgsProcessingLayerPostProcessorInterface,
                        QgsVectorLayer,
                        QgsLineSymbol)
import processing
                       
class ExAlgo(QgsProcessingAlgorithm):
    
    INPUT_LINES = 'INPUT_LINES'
    GRID_EXTENT = 'GRID_EXTENT'
    OUTPUT_GRID = 'OUTPUT_GRID'
    OUTPUT_LINES = 'OUTPUT_LINES'
 
    post_processors = {}
 
    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 which styles an output layers")
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return ExAlgo()
        
    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSource(
            self.INPUT_LINES,
            self.tr("Input line layer"),
            [QgsProcessing.TypeVectorLine]))
            
        self.addParameter(QgsProcessingParameterExtent(
            self.GRID_EXTENT,
            self.tr("Line grid extent")
            ))
            
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT_GRID,
            self.tr("Output line grid"),
            QgsProcessing.TypeVectorAnyGeometry))
    
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT_LINES,
            self.tr("Output line layer"),
            QgsProcessing.TypeVectorAnyGeometry))

    def processAlgorithm(self, parameters, context, model_feedback):
        feedback = QgsProcessingMultiStepFeedback(3, model_feedback)
        results = {}
        outputs = {}
        
        # Create line grid
        grid_params = {'TYPE':1,
                        'EXTENT':parameters[self.GRID_EXTENT],
                        'HSPACING':10000,
                        'VSPACING':10000,
                        'HOVERLAY':0,
                        'VOVERLAY':0,
                        'CRS':QgsCoordinateReferenceSystem('EPSG:3857'),# Never use 3857 for accurate distance measurement IRL!
                        'OUTPUT':parameters[self.OUTPUT_GRID]}
                        
        feedback.setCurrentStep(1)
        if feedback.isCanceled():
            return {}
            
        outputs['line_grid'] = processing.run("native:creategrid", grid_params, context=context, feedback=feedback, is_child_algorithm=True)
        results['line_grid'] = outputs['line_grid']['OUTPUT']
                    
        # Fix Line Geometries
        fix_geom_params = {'INPUT':parameters[self.INPUT_LINES], 'OUTPUT':'TEMPORARY_OUTPUT'}
        
        feedback.setCurrentStep(2)
        if feedback.isCanceled():
            return {}
        
        outputs['fixed_geometries'] = processing.run('native:fixgeometries', fix_geom_params, context=context, feedback=feedback, is_child_algorithm=True) #1
        results['fixed_geometries'] = outputs['fixed_geometries']['OUTPUT']
            
        # Using Field calculator to Add an ID
        fld_calc_params = {
            'INPUT': results['fixed_geometries'],
            'FIELD_LENGTH': 10,
            'FIELD_NAME': 'ID',
            'FIELD_PRECISION': 0,
            'FIELD_TYPE': 1,
            'FORMULA': '$id',
            'OUTPUT': parameters[self.OUTPUT_LINES]
        }
        
        feedback.setCurrentStep(3)
        if feedback.isCanceled():
            return {}
                
        outputs['add_id'] = processing.run('native:fieldcalculator', fld_calc_params, context=context, feedback=feedback, is_child_algorithm=True) #2
        results['add_id'] = outputs['add_id']['OUTPUT']
        
        for lyr_id in results.values():
            if context.willLoadLayerOnCompletion(lyr_id):
                self.post_processors[lyr_id] = LayerPostProcessor.create()
                context.layerToLoadOnCompletionDetails(lyr_id).setPostProcessor(self.post_processors[lyr_id])

        return results
    

class LayerPostProcessor(QgsProcessingLayerPostProcessorInterface):

    instance = None

    def postProcessLayer(self, layer, context, feedback):
        print(layer.name())
        if not isinstance(layer, QgsVectorLayer):
            return
        renderer = layer.renderer().clone()
        symbol = QgsLineSymbol.createSimple({'line_color': '191,191,191,255', 'line_width': '0.20', 'line_style': 'dash'})
        renderer.setSymbol(symbol)
        layer.setRenderer(renderer)

    @staticmethod
    def create() -> 'LayerPostProcessor':
        LayerPostProcessor.instance = LayerPostProcessor()
        return LayerPostProcessor.instance

Result:

enter image description here

Acknowledgements:

I originally got the post processor example from a GitHub gist of QGIS core developer, Nyall Dawson. However this has now been deleted so I cannot provide a link.

The processing script template is adapted from here.