I have a QGIS project with multiple polygon layers organized inside a group. Using my Processing script, I select that group by name and then copy certain layers from it into a newly created group. The layers to copy are chosen based on an option I provide as input to the script. Additionally, the script applies styles to these copied layers from .qml files stored in a specified folder.
The script generally works, but I’m encountering a few issues:
- Intermittent crashes: When I run the script repeatedly, sometimes QGIS just crashes without any clear error message. It doesn’t happen every time.
- Layer visibility toggling issues: When I try to toggle visibility (hide/show) of individual layers in the new group, the visibility state doesn’t update immediately. Instead, I have to hide and then show the entire group for the changes to take effect on the individual layers.
- Visual gap under last layer: After the layers are copied into the new group, I notice a visual gap or space under the last layer in the layer panel (see attached screenshot - between pp and group F).
My code:
import os from qgis.PyQt.QtCore import QCoreApplication from qgis.core import ( QgsProcessing, QgsProcessingException, QgsProcessingAlgorithm, QgsProcessingParameterString, QgsProcessingParameterEnum, QgsProject, QgsLayerTreeLayer, QgsVectorLayer)
class UrAlgorithm(QgsProcessingAlgorithm):
INPUT_GROUP = 'INPUT_GROUP'
OPTION = 'OPTION'
STYLE_FOLDER = 'STYLE_FOLDER'
def tr(self, string):
return QCoreApplication.translate('Processing', string)
def createInstance(self):
return UrAlgorithm()
def name(self):
return 'Change_test'
def displayName(self):
return self.tr('Change_test')
def group(self):
return self.tr('Test')
def groupId(self):
return 'test'
def shortHelpString(self):
return self.tr("Copies certain layers from one group to a new group as memory layers, based on the selected option. "
"Also adds styles from a specified folder, if there are matching .qml files.")
def initAlgorithm(self, config=None):
self.addParameter(
QgsProcessingParameterString(
self.INPUT_GROUP,
self.tr('Group name'),
defaultValue='group'
)
)
self.addParameter(
QgsProcessingParameterEnum(
self.OPTION,
self.tr('Options'),
options=['A', 'B', 'C', 'D', 'E', 'F'],
defaultValue=0
)
)
self.addParameter(
QgsProcessingParameterString(
self.STYLE_FOLDER,
self.tr('File with .qml'),
defaultValue=r''
)
)
def copy_layer(self, orig_layer, feedback, style_folder, option):
try:
feats = list(orig_layer.getFeatures())
crs = orig_layer.crs().authid()
geometry_type = orig_layer.geometryType()
geometry_str = {0: "Point", 1: "LineString", 2: "Polygon"}.get(geometry_type, "Polygon")
uri = f"{geometry_str}?crs={crs}"
mem_layer = QgsVectorLayer(uri, orig_layer.name(), "memory")
mem_layer_dp = mem_layer.dataProvider()
mem_layer_dp.addAttributes(orig_layer.fields())
mem_layer.updateFields()
mem_layer_dp.addFeatures(feats)
mem_layer.setName(orig_layer.name())
# name of the style file
style_file_name = f"{orig_layer.name().lower()}.qml"
# exeptions
if option == 'B' and orig_layer.name().lower() == 'aa1':
style_file_name = "aa1_2.qml"
style_path = os.path.join(style_folder, style_file_name)
if os.path.exists(style_path):
success = mem_layer.loadNamedStyle(style_path)
mem_layer.triggerRepaint()
else:
feedback.pushInfo(self.tr(f"⚠Style for layer '{orig_layer.name()}' not found, skiped.⚠"))
return mem_layer
except Exception as e:
raise QgsProcessingException(self.tr(f"⚠Error while copying '{orig_layer.name()}': {str(e)}"))
def processAlgorithm(self, parameters, context, feedback):
input_group_name = self.parameterAsString(parameters, self.INPUT_GROUP, context)
option_index = self.parameterAsEnum(parameters, self.OPTION, context)
style_folder = self.parameterAsString(parameters, self.STYLE_FOLDER, context)
option_keys = ['A', 'B', 'C', 'D', 'E', 'F']
selected_option = option_keys[option_index]
option_map = {
'A': ('A', ['aa1', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg', 'hh', 'ii']),
'B': ('B', ['aa1', 'ee', 'ff', 'jj', 'kk', 'll']),
'C': ('C', ['ee', 'ff', 'pv', 'ps', 'pm', 'pp']),
'D': ('D', ['cc', 'ee', 'ff', 'mm', 'nn', 'oo']),
'E': ('E', ['cc', 'ee', 'ff', 'pp', 'rr', 'ss']),
'F': ('F', ['ee', 'ff', 'tt', 'uu', 'vv'])
}
output_group_name, desired_order = option_map[selected_option]
root = QgsProject.instance().layerTreeRoot()
input_group = root.findGroup(input_group_name)
if input_group is None:
raise QgsProcessingException(self.tr(f"⚠group '{input_group_name}' does not exsist."))
if root.findGroup(output_group_name) is not None:
feedback.reportError(self.tr(f"⚠group '{output_group_name}' already exsists."), fatalError=True)
return {}
output_group = root.addGroup(output_group_name)
feedback.pushInfo(self.tr(f"Group created: {output_group_name}"))
layer_dict = {}
for child in input_group.children():
if isinstance(child, QgsLayerTreeLayer):
layer = child.layer()
if layer is not None:
layer_dict[layer.name()] = layer
layers_copied = 0
project = QgsProject.instance()
for layer_name in desired_order:
if layer_name in layer_dict:
try:
new_layer = self.copy_layer(layer_dict[layer_name], feedback, style_folder, selected_option)
project.addMapLayer(new_layer, False)
output_group.addLayer(new_layer)
feedback.pushInfo(self.tr(f"Copy: {layer_name}"))
layers_copied += 1
except QgsProcessingException as e:
feedback.reportError(str(e), fatalError=True)
return {}
else:
feedback.pushInfo(self.tr(f"⚠Layer '{layer_name}' not found, skip.⚠"))
feedback.pushInfo(self.tr(f"No. of layers copied: {layers_copied}"))
return {}
I have translated the code into English to the best of my ability. However, due to language differences and possible changes in layer, group, or variable names, there may be inaccuracies or errors in the translation.
1 Answer 1
If you read the tip in this section of the docs: https://docs.qgis.org/3.40/en/docs/user_manual/processing/scripts.html#flags, it is explained clearly that processing algorithms are run in a background thread by default and any non-thread-safe API calls made in the processAlgorithm()
method will result in crashes & unexpected behavior which is what you are experiencing here. Interacting with the gui/interface, modifying the layer tree & changing layer styles are all non-thread safe operations and should not be done from a background thread.
The simplest solution in your case would probably be to disable threading by implementing the flags() method in your algorithm and returning the NoThreading
flag.
E.g.
def flags(self):
return Qgis.ProcessingAlgorithmFlag.NoThreading
Note that prior to QGIS 3.36, this would be:
def flags(self):
return QgsProcessingAlgorithm.FlagNoThreading
Another option, taking a cue from the native algorithm 'Set layer style' written in C++ (you can inspect the source code here: https://github.com/qgis/QGIS/blob/cd2944e57f98b434d7ab0643ecf7f909752cf970/src/analysis/processing/qgsalgorithmapplylayerstyle.cpp#L69) would be to implement the thread-sensitive logic in the prepareAlgorithm() method. Note that this method returns True
or False
, while processAlgorithm()
still needs to return a dictionary (can be empty). Disclaimer: Although it's implemented in the core code base, I'm not sure if there are any disadvantages to this approach.
I have answered similar questions in the past and advocated for using the QgsProcessingLayerPostProcessorInterface class. E.g.
QGIS: moving layers inside Layertree into a group crashes QGIS using python and Simple PyQGIS code working in QGIS python console but not in Python plugin. However in your case I would suggest that the effort outweighs the advantage and is probably not particularly useful given that you are not loading output layers via the processing framework. However, you can still see examples in the links above.
P.s. just a side note- in a processing algorithm you should access the project via the context object:
project = context.project()
instead of:
project = QgsProject.instance()
Explore related questions
See similar questions with these tags.