From dfb9b847b797828c8d6836c5e1239d72d0415228 Mon Sep 17 00:00:00 2001 From: Tobias Rosskopf <42942019+TobiasRosskopf@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:30:22 +0200 Subject: [PATCH 1/2] Added support for QGIS 4 by adjusting code to Qt6-API. Preserve compatibility to Qt5 with helper functions (`qt_compat.py`). --- classes/ActionTabBase.py | 8 ++++++-- classes/ActionTabRADOLANAdder.py | 15 +++++++++----- classes/ActionTabRADOLANLoader.py | 12 +++++------ classes/ActionTabRegnie.py | 17 +++++++++++----- classes/LayerLoader.py | 2 +- classes/SettingsTab.py | 15 +++++++++----- classes/qt_compat.py | 33 +++++++++++++++++++++++++++++++ metadata.txt | 1 + pb_tool.cfg | 3 +++ radolan2map.py | 7 +++---- 10 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 classes/qt_compat.py diff --git a/classes/ActionTabBase.py b/classes/ActionTabBase.py index f5e5f88..556f96b 100644 --- a/classes/ActionTabBase.py +++ b/classes/ActionTabBase.py @@ -2,7 +2,7 @@ from pathlib import Path import time -from qgis.core import QgsProject, QgsPrintLayout, QgsReadWriteContext +from qgis.core import Qgis, QgsProject, QgsPrintLayout, QgsReadWriteContext from qgis.PyQt.QtWidgets import QMessageBox # For the QGIS printing template: from qgis.PyQt.QtXml import QDomDocument @@ -195,7 +195,11 @@ def _load_print_layout(self, layer_name, prod_id, dt=None): ''' # uses existing legend object (see above), so we preserve it's layout position: - legend.setAutoUpdateModel(False) + # QGIS 4: setAutoUpdateModel() is deprecated, use explicit sync mode instead. + if hasattr(legend, 'setSyncMode'): + legend.setSyncMode(Qgis.LegendSyncMode.Manual) + else: + legend.setAutoUpdateModel(False) group = legend.model().rootGroup() group.clear() group.addLayer(active_raster_layer) diff --git a/classes/ActionTabRADOLANAdder.py b/classes/ActionTabRADOLANAdder.py index b4f9187..bfc6899 100644 --- a/classes/ActionTabRADOLANAdder.py +++ b/classes/ActionTabRADOLANAdder.py @@ -14,6 +14,8 @@ from .GDALProcessing import GDALProcessing from .LayerLoader import LayerLoader +from .qt_compat import dont_use_native_dialog_option + """ The format of date and datetime is different from the format of QDate and QDateTime, you should not use % in the Qt format! @@ -63,12 +65,15 @@ def __init__(self, iface, model, dock): def _select_input_dir(self): text = self.tf_path self._last_dir = text if text else str(Path.home()) - + # parent, caption, directory, file_filter - selected_dir = QFileDialog.getExistingDirectory(self.dock, 'Select RADOLAN directory', self._last_dir, - # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path: - QFileDialog.DontUseNativeDialog) - + selected_dir = QFileDialog.getExistingDirectory( + self.dock, + 'Select RADOLAN directory', + self._last_dir, + dont_use_native_dialog_option() + ) + if not selected_dir: return # preserve possibly filled line diff --git a/classes/ActionTabRADOLANLoader.py b/classes/ActionTabRADOLANLoader.py index 611c890..872eb04 100644 --- a/classes/ActionTabRADOLANLoader.py +++ b/classes/ActionTabRADOLANLoader.py @@ -14,6 +14,8 @@ #from .Model import test_product_get_id # import a function from .LayerLoader import LayerLoader +from .qt_compat import ask_user_yes_no, dont_use_native_dialog_option + class ActionTabRADOLANLoader(ActionTabBase): """ @@ -65,12 +67,9 @@ def __init__(self, iface, model, dock): self.mask_file = model.default_border_shape # default: "DEU_adm0" self._qml_file = None self._files_to_process = [] - + def _ask_user_load_template_project(self): - reply = QMessageBox.question(self._iface.mainWindow(), 'Continue?', - 'Do you want to load template project?', - QMessageBox.Yes, QMessageBox.No) - if reply == QMessageBox.No: + if not ask_user_yes_no(self._iface.mainWindow(), 'Continue?', 'Do you want to load template project?', default_yes=False): return """ Related to Windows / QGIS 3.10 @@ -339,8 +338,7 @@ def _select_symbology(self): def _show_open_file_dialog(self, title, path, file_filter, allow_select_multiple_files=False): funcname = QFileDialog.getOpenFileName if not allow_select_multiple_files else QFileDialog.getOpenFileNames - selection, _ = funcname(self.dock, title, path, file_filter, - None, QFileDialog.DontUseNativeDialog) + selection, _ = funcname(self.dock, title, path, file_filter, None, dont_use_native_dialog_option()) # last two params: # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path: return selection # can be None diff --git a/classes/ActionTabRegnie.py b/classes/ActionTabRegnie.py index 3aa8272..58aaf31 100644 --- a/classes/ActionTabRegnie.py +++ b/classes/ActionTabRegnie.py @@ -16,6 +16,8 @@ from .Regnie import Regnie from . import regnie2raster as r2r +from .qt_compat import dont_use_native_dialog_option + class ActionTabRegnie(ActionTabBase): ''' classdocs @@ -43,12 +45,17 @@ def __init__(self, iface, model, dock): def _select_regnie_file(self): text = self.regnie_file start_dir = Path(text).parent if text else Path.home() - + # after title string: start path, file_filter - regnie_file, _ = QFileDialog.getOpenFileName(self.dock, "Please select a REGNIE file", - str(start_dir), None, - # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path: - None, QFileDialog.DontUseNativeDialog) + regnie_file, _ = QFileDialog.getOpenFileName( + self.dock, + "Please select a REGNIE file", + str(start_dir), + None, + None, + dont_use_native_dialog_option() + ) + if not regnie_file: return # keep path anyway diff --git a/classes/LayerLoader.py b/classes/LayerLoader.py index 008cbd1..3e5a162 100644 --- a/classes/LayerLoader.py +++ b/classes/LayerLoader.py @@ -3,7 +3,7 @@ import sys from pathlib import Path from datetime import datetime, timedelta -from PyQt5.QtCore import Qt +from qgis.PyQt.QtCore import Qt from qgis.core import ( Qgis, diff --git a/classes/SettingsTab.py b/classes/SettingsTab.py index 64e1b99..accd2bc 100644 --- a/classes/SettingsTab.py +++ b/classes/SettingsTab.py @@ -7,6 +7,8 @@ # own classes: from .ActionTabBase import ActionTabBase # base class +from .qt_compat import dont_use_native_dialog_option + class SettingsTab(ActionTabBase): @@ -54,12 +56,15 @@ def _select_storage_dir(self): # We assume a set up file here: if not start_dir: start_dir = str(Path.home()) - + # parent, caption, directory, file_filter - selected_dir = QFileDialog.getExistingDirectory(dock, 'Select RADOLAN/RADKLIM directory', start_dir, - # these additional parameters are used, because QFileDialog otherwise doesn't start with the given path: - QFileDialog.DontUseNativeDialog) - + selected_dir = QFileDialog.getExistingDirectory( + dock, + 'Select RADOLAN/RADKLIM directory', + start_dir, + dont_use_native_dialog_option() + ) + if not selected_dir: return # preserve evtl. filled line diff --git a/classes/qt_compat.py b/classes/qt_compat.py new file mode 100644 index 0000000..7757281 --- /dev/null +++ b/classes/qt_compat.py @@ -0,0 +1,33 @@ +"""Qt compatibility helpers for running the plugin on both Qt5 and Qt6.""" + +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtWidgets import QFileDialog, QMessageBox + + +def dont_use_native_dialog_option(): + """Return the QFileDialog option value compatible with Qt5 and Qt6.""" + option_enum = getattr(QFileDialog, "Option", None) + if option_enum is not None: + return option_enum.DontUseNativeDialog + return QFileDialog.DontUseNativeDialog + + +def right_dock_widget_area(): + """Return RightDockWidgetArea value compatible with Qt5 and Qt6.""" + dock_area_enum = getattr(Qt, "DockWidgetArea", None) + if dock_area_enum is not None: + return dock_area_enum.RightDockWidgetArea + return Qt.RightDockWidgetArea + + +def ask_user_yes_no(parent, title, text, default_yes=False): + """Show a yes/no question dialog and return True when user chooses Yes.""" + standard_button_enum = getattr(QMessageBox, "StandardButton", None) + if standard_button_enum is not None: + buttons = standard_button_enum.Yes | standard_button_enum.No + default_button = standard_button_enum.Yes if default_yes else standard_button_enum.No + reply = QMessageBox.question(parent, title, text, buttons, default_button) + return reply == standard_button_enum.Yes + + reply = QMessageBox.question(parent, title, text, QMessageBox.Yes, QMessageBox.No) + return reply == QMessageBox.Yes diff --git a/metadata.txt b/metadata.txt index 4fb2bb5..1eba9e8 100644 --- a/metadata.txt +++ b/metadata.txt @@ -5,6 +5,7 @@ [general] name=radolan2map qgisMinimumVersion=3.0 +qgisMaximumVersion=4.99 # short text which describes the plugin, no HTML allowed: description=Brings DWD precipitation products like RADOLAN, RADKLIM and REGNIE onto a map version=1.8 diff --git a/pb_tool.cfg b/pb_tool.cfg index bd2508a..8993380 100644 --- a/pb_tool.cfg +++ b/pb_tool.cfg @@ -47,3 +47,6 @@ locales: # dir: help/build/html # # the name of the directory to target in the deployed plugin # target: help + +[help] +dir: diff --git a/radolan2map.py b/radolan2map.py index 275d5f1..b1f8f9e 100644 --- a/radolan2map.py +++ b/radolan2map.py @@ -45,6 +45,7 @@ from .classes.SettingsTab import SettingsTab # ......................................................... +from .classes.qt_compat import ask_user_yes_no, right_dock_widget_area class Radolan2Map: @@ -240,9 +241,7 @@ def open_dock(self): self.out(f"check file '{self._model.check_file}' removed, message doesn't appear again.") msg = "It's recommended to exit QGIS now.\n\nExit QGIS?" - reply = QMessageBox.question(self.iface.mainWindow(), 'Continue?', - msg, QMessageBox.Yes, QMessageBox.No) - if reply == QMessageBox.Yes: + if ask_user_yes_no(self.iface.mainWindow(), 'Continue?', msg, default_yes=False): self.iface.actionExit().trigger() return # maybe unnecessary # if @@ -278,7 +277,7 @@ def open_dock(self): # -> prevents a too small Widget # show the dockwidget - self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock) + self.iface.addDockWidget(right_dock_widget_area(), self.dock) self.dock.show() # Ohne diese Anweisung wurde das Fenster ab QGIS3 im Hintergrund geƶffnet. #self.dock.setWindowFlags(Qt.WindowStaysOnTopHint) From 66b59d3dc1d5325c3fb960425235687cc8d195cd Mon Sep 17 00:00:00 2001 From: Tobias Rosskopf <42942019+TobiasRosskopf@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:40:49 +0200 Subject: [PATCH 2/2] Deleted obsolete `resources.qrc`. --- .flake8 | 1 - .gitignore | 1 - pb_tool.cfg | 2 +- radolan2map.py | 12 ++---------- resources.qrc | 5 ----- 5 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 resources.qrc diff --git a/.flake8 b/.flake8 index eb17a81..f1d967c 100644 --- a/.flake8 +++ b/.flake8 @@ -8,7 +8,6 @@ exclude = example, img, symbology, - resources.py, ignore = # C901 # function is too complex (mccabe) # E114, # indentation is not a multiple of four (comment) diff --git a/.gitignore b/.gitignore index 8c9fca2..202f633 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ __pycache__ classes/__pycache__ .idea settings.json -resources.py # pb_tools zip build zip_build/ # VS Code diff --git a/pb_tool.cfg b/pb_tool.cfg index 8993380..e5ff2ce 100644 --- a/pb_tool.cfg +++ b/pb_tool.cfg @@ -28,7 +28,7 @@ main_dialog: dock_widget.ui compiled_ui_files: # Resource file(s) that will be compiled -resource_files: resources.qrc +resource_files: # Other files required for the plugin extras: metadata.txt config.ini diff --git a/radolan2map.py b/radolan2map.py index b1f8f9e..be991e0 100644 --- a/radolan2map.py +++ b/radolan2map.py @@ -28,11 +28,10 @@ # Import the PyQt and QGIS libraries # from qgis.core import QgsProject -from qgis.PyQt.QtCore import Qt, QCoreApplication, QSettings, QTranslator, QSize # , QRect +from qgis.PyQt.QtCore import QCoreApplication, QSettings, QTranslator, QSize # , QRect from qgis.PyQt.QtWidgets import QMessageBox, QDockWidget, QAction from qgis.PyQt.QtGui import QIcon -# Initialize Qt resources from file resources.py -# from .resources import * + from console import console # show python console automatically # Eigene Klassen @@ -170,13 +169,6 @@ def add_action( def initGui(self): """ Create the menu entries and toolbar icons inside the QGIS GUI. """ - """ - note the icon path: - ':/plugins/my_plugin/icon.png'. - The colon instructs QGIS to use the compiled resources.py file to locate the icon. - Open the original resources.qrc file and you'll see the connection. - """ - #icon_path = ':/plugins/radolan2map/icon.png' icon_path = Path(__file__).parent / "img/icon.png" if not icon_path.exists(): diff --git a/resources.qrc b/resources.qrc deleted file mode 100644 index 42b1d73..0000000 --- a/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - img/icon.png - -