diff --git a/ui/layout.py b/ui/layout.py
index 4aa52c8..a2ec82f 100644
--- a/ui/layout.py
+++ b/ui/layout.py
@@ -4,12 +4,14 @@ sn_plan41/ui/layout.py – Aufbau von Drucklayouts für Plan41.
from __future__ import annotations
import math
+import importlib
from typing import Any
from sn_basis.functions.qt_wrapper import QFont
from sn_basis.functions.qgiscore_wrapper import (
QgsLayoutItem,
QgsLayoutItemLabel,
+ QgsLayoutItemLegend,
QgsLayoutItemMap,
QgsLayoutPoint,
QgsLayoutSize,
@@ -26,8 +28,816 @@ MM = QgsUnitTypes.LayoutMillimeters
class Layout:
"""Erzeugt ein QGIS-Layout für den Druck."""
+ _TEXT_FORMAT_TEMPLATES = {
+ "Format_1": {
+ "font_family": "Arial",
+ "font_size": 11,
+ "font_style": "normal",
+ },
+ "Format_2": {
+ "font_family": "Arial",
+ "font_size": 13,
+ "font_style": "bold",
+ },
+ "Format_3": {
+ "font_family": "Arial",
+ "font_size": 16,
+ "font_style": "bold",
+ },
+ }
+
+ _OBJECT_TEMPLATES = {
+ "hauptkarte": {
+ "id": "Hauptkarte",
+ "object_name": "Hauptkarte",
+ },
+ "quellenangabe": {
+ "id": "Quellenangabe",
+ "object_name": "Quellenangabe",
+ "size_mm": (180.0, 100.0),
+ "format": "Format_1",
+ },
+ "seitenzahl": {
+ "id": "Seitenzahl",
+ "object_name": "Seitenzahl",
+ "size_mm": (60.0, 8.0),
+ "format": "Format_1",
+ },
+ "textfeld": {
+ "id": "Textfeld",
+ "object_name": "Textfeld",
+ "size_mm": (60.0, 8.0),
+ "format": "Format_1",
+ "html_mode": True,
+ "text": "",
+ },
+ "legende": {
+ "id": "Legende",
+ "object_name": "Legende",
+ "size_mm": (180.0, 60.0),
+ },
+ }
+
+ _QUELLENANGABE_TEXT = (
+ "Quelle Geobasisdaten: GeoSN, "
+ "dl-de/by-2-0
"
+ "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
+ "Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie
"
+ "Basemap:"
+ "© GeoBasis-DE / BKG ([%year($now)%]) "
+ "CC BY 4.0 "
+ "mit teilweise angepasster Signatur
"
+ )
+
+ _SEITENZAHL_TEXT = "Blatt [% attribute(@atlas_feature, 'Seitenzahl') %] von [% @atlas_totalfeatures %]"
+
def __init__(self, project: Any | None = None) -> None:
self.project = project or QgsProject.instance()
+ self._current_layout: Any | None = None
+
+ def _create_map_item(
+ self,
+ layout: Any,
+ template_name: str,
+ x_mm: float,
+ y_mm: float,
+ width_mm: float,
+ height_mm: float,
+ object_name: str | None = None,
+ ) -> Any:
+ template = self._OBJECT_TEMPLATES[template_name]
+ map_item = QgsLayoutItemMap(layout)
+ map_item.setId(template["id"])
+ set_object_name = getattr(map_item, "setObjectName", None)
+ if callable(set_object_name):
+ set_object_name(object_name or template.get("object_name", template["id"]))
+ layout.addLayoutItem(map_item)
+ map_item.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
+ map_item.attemptResize(QgsLayoutSize(width_mm, height_mm, MM))
+ return map_item
+
+ def _set_item_frame_stroke_width(self, item: Any, stroke_width_mm: float) -> None:
+ set_frame_stroke_width = getattr(item, "setFrameStrokeWidth", None)
+ if not callable(set_frame_stroke_width):
+ return
+
+ try:
+ set_frame_stroke_width(stroke_width_mm)
+ return
+ except Exception:
+ pass
+
+ try:
+ qgis_core = importlib.import_module("qgis.core")
+ QgsLayoutMeasurement = getattr(qgis_core, "QgsLayoutMeasurement", None)
+ except Exception:
+ QgsLayoutMeasurement = None
+
+ if QgsLayoutMeasurement is not None:
+ try:
+ set_frame_stroke_width(QgsLayoutMeasurement(stroke_width_mm, MM))
+ return
+ except Exception:
+ pass
+
+ frame_stroke_width = getattr(item, "frameStrokeWidth", None)
+ if callable(frame_stroke_width):
+ try:
+ measurement = frame_stroke_width()
+ set_length = getattr(measurement, "setLength", None)
+ if callable(set_length):
+ set_length(stroke_width_mm)
+ set_frame_stroke_width(measurement)
+ except Exception:
+ pass
+
+ def add_text_label(
+ self,
+ text: str,
+ x_mm: float,
+ y_mm: float,
+ template_name: str = "textfeld",
+ text_format: str | None = None,
+ width_mm: float | None = None,
+ height_mm: float | None = None,
+ html_mode: bool = False,
+ expression_enabled: bool = False,
+ object_name: str | None = None,
+ ) -> Any:
+ """Einfache öffentliche Methode zum Hinzufügen eines Text-Labels.
+
+ Beispiel: add_text_label("Ausgabedatum: 2026-03-23", 100, 200, text_format="Format_1")
+ """
+ if self._current_layout is None:
+ raise RuntimeError("Kein Layout aktiv. Bitte create_single_page_layout() oder create_atlas_layout() aufrufen.")
+ return self._create_label_item(
+ self._current_layout,
+ template_name,
+ text,
+ x_mm,
+ y_mm,
+ text_format=text_format,
+ width_mm=width_mm,
+ height_mm=height_mm,
+ html_mode=html_mode,
+ expression_enabled=expression_enabled,
+ object_name=object_name,
+ )
+
+ def _setup_layout_objects(
+ self,
+ kartenfenster_untere_rechte_ecke_mm: tuple,
+ hauptkarte: Any,
+ map_top_mm: float,
+ map_height_mm: float,
+ is_atlas: bool = False,
+ ) -> None:
+ """Zentrale Methode zum Hinzufügen aller Standard-Objekte ins Layout.
+
+ Diese Methode wird sowohl von create_single_page_layout() als auch von create_atlas_layout() aufgerufen.
+ Relatipunkt des Objekts ist die untere linke Ecke, berechnet von der unteren, rechten Ecke des Kartefensters
+ Args:
+ kartenfenster_untere_rechte_ecke_mm: Tupel (x_mm, y_mm) der unteren rechten Kartenecke
+ is_atlas: True für Atlas-Layout, False für Einzelblatt-Layout
+ """
+ if self._current_layout is None:
+ raise RuntimeError("Kein Layout aktiv.")
+
+ def _coords_rel_untere_rechte_ecke(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]:
+ return (
+ kartenfenster_untere_rechte_ecke_mm[0] + rel_x_mm,
+ kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm,
+ )
+
+ mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
+ print(f"[Layout] QgsLayoutItemLabel.ModeHtml={mode_html!r}")
+ lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
+ print(f"[Layout] QgsLayoutItem.ReferencePoint.LowerLeft={lower_left!r}")
+
+ legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0
+ legende_height_mm = max(map_height_mm - 122.0, 20.0)
+ legende_y = map_top_mm
+ legende = self._create_legend_item(
+ self._current_layout,
+ "legende",
+ hauptkarte,
+ legende_x,
+ legende_y,
+ width_mm=180.0,
+ height_mm=legende_height_mm,
+ object_name="Legende",
+ )
+
+ legende_hoehe_mm = self._configure_legend_size(
+ legende,
+ width_limit_mm=180.0,
+ column_count=1,
+ equal_column_width=False,
+ )
+
+ quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0)
+ quellenangabe_hoehe_mm = 20.0
+ quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm
+ legende_untere_y = legende_y + legende_hoehe_mm
+
+ if legende_untere_y > quellenangabe_obere_y:
+ legende_hoehe_mm = self._configure_legend_size(
+ legende,
+ width_limit_mm=180.0,
+ column_count=2,
+ equal_column_width=True,
+ )
+ print("[Layout] Legende überlappt Quellenangabe – auf 2 Spalten umgestellt")
+
+ print("[Layout] Legende zum Layout hinzugefügt")
+
+ # Quellenangabe:
+
+ self.add_text_label(
+ self._QUELLENANGABE_TEXT,
+ quellenangabe_x,
+ quellenangabe_y,
+ width_mm=180.0,
+ height_mm=20.0,
+ text_format="Format_1",
+ html_mode=True,
+ expression_enabled=True,
+ object_name="Quellenangabe",
+ )
+ print("[Layout] Quellenangabe zum Layout hinzugefügt")
+ # Kartenschild-Box, -Linien und -Text werden hier ergänzt (TODO)
+ #Kartenschild-Box
+ kartenschild_x, kartenschild_y = _coords_rel_untere_rechte_ecke(10.0, 0.0)
+ self._create_rectangle_box_item(
+ self._current_layout,
+ "Kartenschild",
+ kartenschild_x,
+ kartenschild_y,
+ 180.0,
+ 92.0,
+ stroke_width_mm=1.0,
+ )
+ print("[Layout] Kartenschild-Box zum Layout hinzugefügt")
+
+ #Kartenschild-Linien
+ kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0)
+ kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.0, -62.0, -82.0)
+ for index, offset_y in enumerate(kartenschild_line_offsets_y, start=1):
+ _, line_y = _coords_rel_untere_rechte_ecke(0.0, offset_y)
+ self._create_rectangle_box_item(
+ self._current_layout,
+ f"Kartenschild_Linie_{index}",
+ kartenschild_line_x,
+ line_y,
+ 180.0,
+ 0.0,
+ stroke_width_mm=0.1,
+ )
+ print("[Layout] 6 Kartenschild-Linien zum Layout hinzugefügt")
+
+ #Kartenschild-Text
+ #Herausgeber
+ herausgeber_x, herausgeber_y = _coords_rel_untere_rechte_ecke(16.0, -84.0)
+ self.add_text_label(
+ "[% @sn_amt %]",
+ herausgeber_x,
+ herausgeber_y,
+ width_mm=171.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Herausgeber",
+ )
+ #Bezeichnung_Flurbereinigung
+ bezeichnung_flurbereinigung_x, bezeichnung_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -75.0)
+ self.add_text_label(
+ "[% @sn_bezeichnung %]",
+ bezeichnung_flurbereinigung_x,
+ bezeichnung_flurbereinigung_y,
+ width_mm=171.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Bezeichnung_Flurbereinigung",
+ )
+
+ #Name Flurbereinigung
+ name_flurbereinigung_x, name_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -64.0)
+ self.add_text_label(
+ "[% @sn_name %]",
+ name_flurbereinigung_x,
+ name_flurbereinigung_y,
+ width_mm=171.0,
+ height_mm=5.0,
+ text_format="Format_2",
+ expression_enabled=True,
+ object_name="Name_Flurbereinigung",
+ )
+
+ #Stadt_Gemeinde
+ stadt_gemeinde_x, stadt_gemeinde_y = _coords_rel_untere_rechte_ecke(16.0, -56.0)
+ self.add_text_label(
+ "[% @sn_gemeinden %]",
+ stadt_gemeinde_x,
+ stadt_gemeinde_y,
+ width_mm=171.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Stadt_Gemeinde",
+ )
+
+ #Landkreis
+ landkreis_x, landkreis_y = _coords_rel_untere_rechte_ecke(16.0, -48.0)
+ self.add_text_label(
+ "[% @sn_landkreise_proj %]",
+ landkreis_x,
+ landkreis_y,
+ width_mm=171.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Landkreis",
+ )
+
+ #Kartenname
+ kartenname_x, kartenname_y = _coords_rel_untere_rechte_ecke(16.0, -34.0)
+ self.add_text_label(
+ "[% @sn_kartenname %]",
+ kartenname_x,
+ kartenname_y,
+ width_mm=137.0,
+ height_mm=5.0,
+ text_format="Format_3",
+ expression_enabled=True,
+ object_name="Kartenname",
+ )
+
+ #Ergänzung(Seitenzahl)
+
+ #gefertigt
+ gefertigt_x, gefertigt_y = _coords_rel_untere_rechte_ecke(16.0, -23.0)
+ self.add_text_label(
+ "Gefertigt: ",
+ gefertigt_x,
+ gefertigt_y,
+ width_mm=81.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Gefertigt",
+ )
+ #geprüft
+ geprüft_x, geprüft_y = _coords_rel_untere_rechte_ecke(101.0, -23.0)
+ self.add_text_label(
+ "Geprüft: ",
+ geprüft_x,
+ geprüft_y,
+ width_mm=81.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Geprüft",
+ )
+ #Maßstab
+ maßstab_x, maßstab_y = _coords_rel_untere_rechte_ecke(16.0, -13.0)
+ self.add_text_label(
+ "Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
+ maßstab_x,
+ maßstab_y,
+ width_mm=81.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Maßstab",
+ )
+ #Ausgabedatum
+ ausgabedatum_x, ausgabedatum_y = _coords_rel_untere_rechte_ecke(101.0, -13.0)
+ self.add_text_label(
+ "Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]",
+ ausgabedatum_x,
+ ausgabedatum_y,
+ width_mm=81.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Ausgabedatum",
+ )
+ #Aktenzeichen
+ aktenzeichen_x, aktenzeichen_y = _coords_rel_untere_rechte_ecke(16.0, -3.0)
+ self.add_text_label(
+ "Aktenzeichen: ",
+ aktenzeichen_x,
+ aktenzeichen_y,
+ width_mm=171.0,
+ height_mm=4.0,
+ text_format="Format_1",
+ expression_enabled=True,
+ object_name="Aktenzeichen",
+ )
+ # Seitenzahl nur bei Atlas-Layout
+ if is_atlas:
+ seitenzahl_x, seitenzahl_y = _coords_rel_untere_rechte_ecke(166.0, -33.0)
+ self._create_label_item(
+ self._current_layout,
+ "seitenzahl",
+ self._SEITENZAHL_TEXT,
+ seitenzahl_x,
+ seitenzahl_y,
+ expression_enabled=True,
+ )
+ print("[Layout] Seitenzahl zum Layout hinzugefügt")
+
+ def _create_label_item(
+ self,
+ layout: Any,
+ template_name: str,
+ text: str,
+ x_mm: float,
+ y_mm: float,
+ *,
+ text_format: str | None = None,
+ font_name: str | None = None,
+ font_size: int | None = None,
+ font_style: str | None = None,
+ width_mm: float | None = None,
+ height_mm: float | None = None,
+ html_mode: bool = False,
+ expression_enabled: bool = False,
+ object_name: str | None = None,
+ ) -> Any:
+ template = self._OBJECT_TEMPLATES[template_name]
+ label = QgsLayoutItemLabel(layout)
+ item_id = object_name or template["id"]
+ label.setId(item_id)
+ set_object_name = getattr(label, "setObjectName", None)
+ if callable(set_object_name):
+ set_object_name(item_id)
+ label.setText(text)
+
+ if html_mode:
+ set_mode = getattr(label, "setMode", None)
+ mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
+ if callable(set_mode) and mode_html is not None:
+ set_mode(mode_html)
+
+ if expression_enabled:
+ set_expr_enabled = getattr(label, "setExpressionEnabled", None)
+ if callable(set_expr_enabled):
+ try:
+ set_expr_enabled(True)
+ except Exception:
+ pass
+
+ format_name = text_format or template.get("format", "Format_1")
+ format_template = self._TEXT_FORMAT_TEMPLATES.get(format_name, self._TEXT_FORMAT_TEMPLATES["Format_1"])
+ resolved_font_name = font_name or format_template["font_family"]
+ resolved_font_size = font_size or format_template["font_size"]
+ resolved_font_style = font_style or format_template["font_style"]
+
+ font = QFont(resolved_font_name, resolved_font_size)
+ if hasattr(font, "setBold"):
+ font.setBold(resolved_font_style == "bold")
+ if hasattr(font, "setItalic"):
+ font.setItalic(resolved_font_style == "italic")
+ label.setFont(font)
+
+ set_reference_point = getattr(label, "setReferencePoint", None)
+ lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
+ if callable(set_reference_point) and lower_left is not None:
+ set_reference_point(lower_left)
+
+ template_width, template_height = template.get("size_mm", (0.0, 0.0))
+ label.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
+ label.attemptResize(
+ QgsLayoutSize(
+ width_mm if width_mm is not None else template_width,
+ height_mm if height_mm is not None else template_height,
+ MM,
+ )
+ )
+ set_locked = getattr(label, "setLocked", None)
+ if callable(set_locked):
+ try:
+ set_locked(True)
+ except Exception:
+ pass
+ layout.addLayoutItem(label)
+ return label
+
+ def _resolve_visible_map_layers(self, map_item: Any) -> list[Any]:
+ layers_method = getattr(map_item, "layers", None)
+ if callable(layers_method):
+ try:
+ resolved_layers = layers_method()
+ if isinstance(resolved_layers, (list, tuple)):
+ layers = [layer for layer in resolved_layers if layer is not None]
+ if layers:
+ return layers
+ except Exception:
+ pass
+
+ layer_tree_root_method = getattr(self.project, "layerTreeRoot", None)
+ if not callable(layer_tree_root_method):
+ return []
+
+ try:
+ root = layer_tree_root_method()
+ except Exception:
+ return []
+
+ find_layers = getattr(root, "findLayers", None)
+ if not callable(find_layers):
+ return []
+
+ visible_layers: list[Any] = []
+ try:
+ tree_layers = find_layers()
+ if not isinstance(tree_layers, (list, tuple)):
+ return []
+ for tree_layer in tree_layers:
+ is_visible = getattr(tree_layer, "itemVisibilityChecked", None)
+ if callable(is_visible) and not is_visible():
+ continue
+ layer = getattr(tree_layer, "layer", None)
+ resolved_layer = layer() if callable(layer) else None
+ if resolved_layer is not None:
+ visible_layers.append(resolved_layer)
+ except Exception:
+ return []
+
+ return visible_layers
+
+ def _create_legend_item(
+ self,
+ layout: Any,
+ template_name: str,
+ linked_map: Any,
+ x_mm: float,
+ y_mm: float,
+ *,
+ width_mm: float | None = None,
+ height_mm: float | None = None,
+ object_name: str | None = None,
+ ) -> Any:
+ template = self._OBJECT_TEMPLATES[template_name]
+ legend = QgsLayoutItemLegend(layout)
+ item_id = object_name or template["id"]
+ legend.setId(item_id)
+
+ set_object_name = getattr(legend, "setObjectName", None)
+ if callable(set_object_name):
+ set_object_name(item_id)
+
+ set_linked_map = getattr(legend, "setLinkedMap", None)
+ if callable(set_linked_map):
+ try:
+ set_linked_map(linked_map)
+ except Exception:
+ pass
+
+ set_title = getattr(legend, "setTitle", None)
+ if callable(set_title):
+ try:
+ set_title("Legende")
+ except Exception:
+ pass
+
+ set_auto_update_model = getattr(legend, "setAutoUpdateModel", None)
+ if callable(set_auto_update_model):
+ try:
+ set_auto_update_model(False)
+ except Exception:
+ pass
+
+ set_filter_by_map = getattr(legend, "setLegendFilterByMapEnabled", None)
+ if callable(set_filter_by_map):
+ try:
+ set_filter_by_map(False)
+ except Exception:
+ pass
+
+ model_method = getattr(legend, "model", None)
+ if callable(model_method):
+ try:
+ model = model_method()
+ root_group_method = getattr(model, "rootGroup", None)
+ root_group = root_group_method() if callable(root_group_method) else None
+ clear = getattr(root_group, "clear", None)
+ if callable(clear):
+ clear()
+ add_layer = getattr(root_group, "addLayer", None)
+ if callable(add_layer):
+ for layer in self._resolve_visible_map_layers(linked_map):
+ add_layer(layer)
+ except Exception:
+ pass
+
+ set_reference_point = getattr(legend, "setReferencePoint", None)
+ upper_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "UpperLeft", None)
+ if callable(set_reference_point) and upper_left is not None:
+ set_reference_point(upper_left)
+
+ template_width, template_height = template.get("size_mm", (0.0, 0.0))
+ legend.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
+ legend.attemptResize(
+ QgsLayoutSize(
+ width_mm if width_mm is not None else template_width,
+ height_mm if height_mm is not None else template_height,
+ MM,
+ )
+ )
+
+ set_frame_enabled = getattr(legend, "setFrameEnabled", None)
+ if callable(set_frame_enabled):
+ try:
+ set_frame_enabled(True)
+ except Exception:
+ pass
+
+ self._set_item_frame_stroke_width(legend, 0.3)
+
+ set_background_enabled = getattr(legend, "setBackgroundEnabled", None)
+ if callable(set_background_enabled):
+ try:
+ set_background_enabled(False)
+ except Exception:
+ pass
+
+ set_locked = getattr(legend, "setLocked", None)
+ if callable(set_locked):
+ try:
+ set_locked(True)
+ except Exception:
+ pass
+
+ layout.addLayoutItem(legend)
+
+ refresh = getattr(legend, "refresh", None)
+ if callable(refresh):
+ try:
+ refresh()
+ except Exception:
+ pass
+
+ return legend
+
+ def _get_item_size_mm(self, item: Any) -> tuple[float | None, float | None]:
+ def _to_float(value: Any) -> float | None:
+ if isinstance(value, (int, float)):
+ return float(value)
+ if isinstance(value, str):
+ try:
+ return float(value)
+ except Exception:
+ return None
+ return None
+
+ size_with_units = getattr(item, "sizeWithUnits", None)
+ if callable(size_with_units):
+ try:
+ size = size_with_units()
+ get_width = getattr(size, "width", None)
+ get_height = getattr(size, "height", None)
+ width = _to_float(get_width()) if callable(get_width) else None
+ height = _to_float(get_height()) if callable(get_height) else None
+ if width is not None and height is not None:
+ return width, height
+ except Exception:
+ pass
+
+ rect = getattr(item, "rect", None)
+ if callable(rect):
+ try:
+ item_rect = rect()
+ get_width = getattr(item_rect, "width", None)
+ get_height = getattr(item_rect, "height", None)
+ width = _to_float(get_width()) if callable(get_width) else None
+ height = _to_float(get_height()) if callable(get_height) else None
+ return width, height
+ except Exception:
+ pass
+
+ return None, None
+
+ def _configure_legend_size(
+ self,
+ legend: Any,
+ *,
+ width_limit_mm: float,
+ column_count: int,
+ equal_column_width: bool,
+ ) -> float:
+ set_locked = getattr(legend, "setLocked", None)
+ if callable(set_locked):
+ try:
+ set_locked(False)
+ except Exception:
+ pass
+
+ set_column_count = getattr(legend, "setColumnCount", None)
+ if callable(set_column_count):
+ try:
+ set_column_count(column_count)
+ except Exception:
+ pass
+
+ set_equal_column_width = getattr(legend, "setEqualColumnWidth", None)
+ if callable(set_equal_column_width):
+ try:
+ set_equal_column_width(equal_column_width)
+ except Exception:
+ pass
+
+ refresh = getattr(legend, "refresh", None)
+ if callable(refresh):
+ try:
+ refresh()
+ except Exception:
+ pass
+
+ adjust_box_size = getattr(legend, "adjustBoxSize", None)
+ if callable(adjust_box_size):
+ try:
+ adjust_box_size()
+ except Exception:
+ pass
+
+ width_mm, height_mm = self._get_item_size_mm(legend)
+ resolved_width = width_mm if width_mm is not None else width_limit_mm
+ resolved_height = height_mm if height_mm is not None else 20.0
+ final_width = min(resolved_width, width_limit_mm)
+
+ legend.attemptResize(QgsLayoutSize(final_width, resolved_height, MM))
+
+ if callable(refresh):
+ try:
+ refresh()
+ except Exception:
+ pass
+
+ _, updated_height_mm = self._get_item_size_mm(legend)
+
+ if callable(set_locked):
+ try:
+ set_locked(True)
+ except Exception:
+ pass
+
+ return updated_height_mm if updated_height_mm is not None else resolved_height
+
+ def _create_rectangle_box_item(
+ self,
+ layout: Any,
+ object_name: str,
+ x_mm: float,
+ y_mm: float,
+ width_mm: float,
+ height_mm: float,
+ stroke_width_mm: float = 0.3,
+ ) -> Any:
+ box = QgsLayoutItemLabel(layout)
+ box.setId(object_name)
+
+ set_object_name = getattr(box, "setObjectName", None)
+ if callable(set_object_name):
+ set_object_name(object_name)
+
+ box.setText("")
+
+ set_reference_point = getattr(box, "setReferencePoint", None)
+ lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
+ if callable(set_reference_point) and lower_left is not None:
+ set_reference_point(lower_left)
+
+ box.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
+ box.attemptResize(QgsLayoutSize(width_mm, height_mm, MM))
+
+ set_frame_enabled = getattr(box, "setFrameEnabled", None)
+ if callable(set_frame_enabled):
+ try:
+ set_frame_enabled(True)
+ except Exception:
+ pass
+
+ self._set_item_frame_stroke_width(box, stroke_width_mm)
+
+ set_background_enabled = getattr(box, "setBackgroundEnabled", None)
+ if callable(set_background_enabled):
+ try:
+ set_background_enabled(False)
+ except Exception:
+ pass
+
+ set_locked = getattr(box, "setLocked", None)
+ if callable(set_locked):
+ try:
+ set_locked(True)
+ except Exception:
+ pass
+
+ layout.addLayoutItem(box)
+ return box
def create_single_page_layout(
self,
@@ -53,6 +863,7 @@ class Layout:
layout = QgsPrintLayout(self.project)
layout.initializeDefaults()
layout.setName(name)
+ self._current_layout = layout
print(f"[Layout] QgsPrintLayout erstellt: {layout!r}")
page = layout.pageCollection().page(0)
@@ -63,21 +874,21 @@ class Layout:
map_top_mm = 10.0
map_right_mm = map_left_mm + map_width_mm
map_bottom_mm = map_top_mm + map_height_mm
+ kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm)
print(
f"[Layout] Kartenbild-Kanten: rechts={map_right_mm:.1f} mm, unten={map_bottom_mm:.1f} mm"
)
- hauptkarte = QgsLayoutItemMap(layout)
- hauptkarte.setId("Hauptkarte")
+ hauptkarte = self._create_map_item(
+ layout,
+ "hauptkarte",
+ map_left_mm,
+ map_top_mm,
+ map_width_mm,
+ map_height_mm,
+ )
print(f"[Layout] QgsLayoutItemMap erstellt")
-
- # Zum Layout hinzufügen, BEVOR Eigenschaften gesetzt werden
- layout.addLayoutItem(hauptkarte)
print("[Layout] Hauptkarte zum Layout hinzugefügt")
-
- # Position und Größe setzen
- hauptkarte.attemptMove(QgsLayoutPoint(map_left_mm, map_top_mm, MM))
- hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM))
print(f"[Layout] Position und Größe gesetzt: pos=({map_left_mm}, {map_top_mm}), size=({map_width_mm:.1f}x{map_height_mm:.1f})")
# Extent und Maßstab setzen
@@ -118,7 +929,7 @@ class Layout:
set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
if callable(set_frame_stroke_width):
try:
- set_frame_stroke_width(0.5)
+ self._set_item_frame_stroke_width(hauptkarte, 0.5)
print(f"[Layout] Rahmenstrichbreite auf 0.5 mm gesetzt")
except Exception as exc:
print(f"[Layout] WARN: setFrameStrokeWidth(0.5) fehlgeschlagen: {exc}")
@@ -163,39 +974,18 @@ class Layout:
print(f"[Layout] WARN: setRefreshStrategy fehlgeschlagen: {exc}")
print(f"[Layout] Hauptkarte vollständig konfiguriert")
-
- quellenangabe = QgsLayoutItemLabel(layout)
- quellenangabe.setId("Quellenangabe")
- quellenangabe.setText(
- "Quelle Geobasisdaten: GeoSN, "
- "dl-de/by-2-0
"
- "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
- "Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie
"
- "Basemap:
"
- "© GeoBasis-DE / BKG ([%year($now)%]) "
- "CC BY 4.0 "
- "mit teilweise angepasster Signatur
"
+
+ self._setup_layout_objects(
+ kartenfenster_untere_rechte_ecke_mm,
+ hauptkarte,
+ map_top_mm,
+ map_height_mm,
+ is_atlas=False,
)
- set_mode = getattr(quellenangabe, "setMode", None)
- mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
- print(f"[Layout] QgsLayoutItemLabel.ModeHtml={mode_html!r}")
- if callable(set_mode) and mode_html is not None:
- set_mode(mode_html)
- quellenangabe.setFont(QFont("Arial", 12))
- set_reference_point = getattr(quellenangabe, "setReferencePoint", None)
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- print(f"[Layout] QgsLayoutItem.ReferencePoint.LowerLeft={lower_left!r}")
- if callable(set_reference_point) and lower_left is not None:
- set_reference_point(lower_left)
- quellenangabe_x_mm = map_right_mm + 20.0
- quellenangabe_y_mm = map_bottom_mm - 120.0
- quellenangabe.attemptMove(QgsLayoutPoint(quellenangabe_x_mm, quellenangabe_y_mm, MM))
- quellenangabe.attemptResize(QgsLayoutSize(180.0, 100.0, MM))
- layout.addLayoutItem(quellenangabe)
- print("[Layout] Quellenangabe zum Layout hinzugefügt")
-
+
layout_manager.addLayout(layout)
print("[Layout] Layout zum LayoutManager hinzugefügt")
+ self._current_layout = None
open_layout_designer(layout)
print("[Layout] Layout Designer geöffnet")
return layout
@@ -221,6 +1011,7 @@ class Layout:
layout = QgsPrintLayout(self.project)
layout.initializeDefaults()
layout.setName(name)
+ self._current_layout = layout
page = layout.pageCollection().page(0)
page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM))
@@ -237,12 +1028,16 @@ class Layout:
map_top_mm = 10.0
map_right_mm = map_left_mm + map_width_mm
map_bottom_mm = map_top_mm + map_height_mm
+ kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm)
- hauptkarte = QgsLayoutItemMap(layout)
- hauptkarte.setId("Hauptkarte")
- layout.addLayoutItem(hauptkarte)
- hauptkarte.attemptMove(QgsLayoutPoint(map_left_mm, map_top_mm, MM))
- hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM))
+ hauptkarte = self._create_map_item(
+ layout,
+ "hauptkarte",
+ map_left_mm,
+ map_top_mm,
+ map_width_mm,
+ map_height_mm,
+ )
# Verifiziere mit Units-bewussten Methoden (rect() kann andere Units verwenden).
actual_w = None
@@ -308,7 +1103,7 @@ class Layout:
set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
if callable(set_frame_stroke_width):
try:
- set_frame_stroke_width(0.5)
+ self._set_item_frame_stroke_width(hauptkarte, 0.5)
except Exception:
pass
@@ -360,52 +1155,14 @@ class Layout:
# Sicherheit: Größe nochmal nach Atlas-Konfiguration setzen, um sicherzustellen, dass sie nicht von der Atlas-Einstellung überschrieben wurde.
hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM))
print(f"[Layout] Atlas Hauptkarte Größe nach Atlas-Konfiguration erneut gesetzt: {map_width_mm:.1f}mm × {map_height_mm:.1f}mm")
-
- quellenangabe = QgsLayoutItemLabel(layout)
- quellenangabe.setId("Quellenangabe")
- quellenangabe.setText(
- "Quelle Geobasisdaten: GeoSN, "
- "dl-de/by-2-0
"
- "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
- "Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie
"
- "Basemap:
"
- "© GeoBasis-DE / BKG ([%year($now)%]) "
- "CC BY 4.0 "
- "mit teilweise angepasster Signatur
"
+
+ self._setup_layout_objects(
+ kartenfenster_untere_rechte_ecke_mm,
+ hauptkarte,
+ map_top_mm,
+ map_height_mm,
+ is_atlas=True,
)
- set_mode = getattr(quellenangabe, "setMode", None)
- mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
- if callable(set_mode) and mode_html is not None:
- set_mode(mode_html)
- quellenangabe.setFont(QFont("Arial", 12))
- set_reference_point = getattr(quellenangabe, "setReferencePoint", None)
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- if callable(set_reference_point) and lower_left is not None:
- set_reference_point(lower_left)
- quellenangabe.attemptMove(QgsLayoutPoint(map_right_mm + 5.0, map_bottom_mm, MM))
- quellenangabe.attemptResize(QgsLayoutSize(180.0, 100.0, MM))
- layout.addLayoutItem(quellenangabe)
-
- seitenzahl_label = QgsLayoutItemLabel(layout)
- seitenzahl_label.setId("Seitenzahl")
- seitenzahl_label.setFont(QFont("Arial", 12))
- set_expr_enabled = getattr(seitenzahl_label, "setExpressionEnabled", None)
- if callable(set_expr_enabled):
- try:
- set_expr_enabled(True)
- except Exception:
- pass
- # Ausdrucksauswertung robust über [% ... %]-Platzhalter.
- seitenzahl_label.setText(
- "Seite [% attribute(@atlas_feature, 'Seitenzahl') %] von [% @atlas_totalfeatures %]"
- )
- set_reference_point = getattr(seitenzahl_label, "setReferencePoint", None)
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- if callable(set_reference_point) and lower_left is not None:
- set_reference_point(lower_left)
- seitenzahl_label.attemptMove(QgsLayoutPoint(map_right_mm + 5.0, map_bottom_mm - 2.0, MM))
- seitenzahl_label.attemptResize(QgsLayoutSize(60.0, 8.0, MM))
- layout.addLayoutItem(seitenzahl_label)
atlas = layout.atlas()
if atlas is not None:
@@ -427,5 +1184,8 @@ class Layout:
set_page_name("attribute(@atlas_feature, 'Seitenzahl')")
layout_manager.addLayout(layout)
+ print("[Layout] Layout zum LayoutManager hinzugefügt")
+ self._current_layout = None
open_layout_designer(layout)
+ print("[Layout] Layout Designer geöffnet")
return layout
diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py
index a1d1b35..986bae6 100644
--- a/ui/tab_b_logic.py
+++ b/ui/tab_b_logic.py
@@ -22,11 +22,11 @@ from sn_basis.modules.layerpruefer import Layerpruefer
from sn_plan41.ui.layout import Layout
-KARTENNAME_VAR = "sn_kartenname"
-PLOTMASSSTAB_VAR = "sn_plotmassstab"
-VIEW_VAR = "sn_view"
-ZIELGROESSE_VAR = "sn_zielgroesse"
-FORMFAKTOR_VAR = "sn_formfaktor"
+KARTENNAME_VAR = "kartenname"
+PLOTMASSSTAB_VAR = "plotmassstab"
+VIEW_VAR = "view"
+ZIELGROESSE_VAR = "zielgroesse"
+FORMFAKTOR_VAR = "formfaktor"
KARTENNAME_38 = "§38"
KARTENNAME_41 = "§41"
MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster"
@@ -287,12 +287,12 @@ class TabBLogic:
return loaded_layer
def set_kartenname_for_auswahl(self, auswahl: str) -> None:
- """Setzt die Projektvariable ``sn_kartenname`` anhand der Kartennamen-Auswahl."""
+ """Setzt die Projektvariable ``kartenname`` anhand der Kartennamen-Auswahl."""
kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "")
set_variable(KARTENNAME_VAR, kartenname, scope="project")
def set_plotmassstab_for_auswahl(self, auswahl: str, aktueller_massstab: float | None = None) -> None:
- """Setzt die Projektvariable ``sn_plotmassstab`` anhand der Maßstabsauswahl."""
+ """Setzt die Projektvariable ``plotmassstab`` anhand der Maßstabsauswahl."""
if auswahl == MASSSTAB_WIE_KARTENFENSTER:
if aktueller_massstab and aktueller_massstab > 0:
set_variable(PLOTMASSSTAB_VAR, str(int(round(aktueller_massstab))), scope="project")
@@ -304,7 +304,7 @@ class TabBLogic:
set_variable(PLOTMASSSTAB_VAR, value, scope="project")
def set_view_for_auswahl(self, auswahl: str) -> None:
- """Setzt ``sn_view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas."""
+ """Setzt die Projektvariable ``view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas."""
if auswahl == THEMA_WIE_KARTENFENSTER:
set_variable(VIEW_VAR, "aktuell", scope="project")
return
@@ -312,11 +312,11 @@ class TabBLogic:
set_variable(VIEW_VAR, auswahl or "", scope="project")
def set_zielgroesse_for_auswahl(self, auswahl: str) -> None:
- """Setzt ``sn_zielgroesse`` auf den gewählten DIN-Namen."""
+ """Setzt ``zielgroesse`` auf den gewählten DIN-Namen."""
set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project")
def set_formfaktor(self, endlosrolle: bool) -> None:
- """Setzt ``sn_formfaktor`` auf ``Endlosrolle`` oder ``Blatt``."""
+ """Setzt die Projektvariable ``formfaktor`` auf ``Endlosrolle`` oder ``Blatt``."""
set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project")
# ─────────────────────────────────────────────────────────────────────────
@@ -358,7 +358,7 @@ class TabBLogic:
return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0}
layer_id = getattr(layer, "id", lambda: "")() or ""
- set_variable("sn_verfahrensgebietslayer", layer_id, scope="project")
+ set_variable("verfahrensgebietslayer", layer_id, scope="project")
# ─── 2. Kartenname prüfen ─────────────────────────────────────────
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41):
diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py
index 3f1f98b..be1206f 100644
--- a/ui/tab_b_ui.py
+++ b/ui/tab_b_ui.py
@@ -206,7 +206,7 @@ class TabB(QWidget):
self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle")
def _on_kartenname_changed(self, value: str) -> None:
- """Persistiert die Kartennamen-Auswahl und setzt ``sn_kartenname``."""
+ """Persistiert die Kartennamen-Auswahl und setzt ``kartenname``."""
if value in (KARTENNAME_38, KARTENNAME_41):
set_variable(KARTENNAME_VAR, value, scope="project")
else:
@@ -216,26 +216,26 @@ class TabB(QWidget):
self.logic.set_kartenname_for_auswahl(value)
def _on_massstab_changed(self, value: str) -> None:
- """Persistiert Maßstabsauswahl und setzt ``sn_plotmassstab``."""
+ """Persistiert Maßstabsauswahl und setzt ``plotmassstab``."""
set_variable(MASSSTAB_VAR, value, scope="project")
if self.logic:
self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale())
def _on_thema_changed(self, value: str) -> None:
- """Persistiert die Thema-Auswahl und setzt ``sn_view``."""
+ """Persistiert die Thema-Auswahl und setzt ``view``."""
set_variable(THEMA_VAR, value, scope="project")
if self.logic:
self.logic.set_view_for_auswahl(value)
def _on_zielgroesse_changed(self, value: str) -> None:
- """Persistiert Blattgröße und setzt ``sn_zielgroesse``."""
+ """Persistiert Blattgröße und setzt ``zielgroesse``."""
set_variable(ZIELGROESSE_UI_VAR, value, scope="project")
if self.logic:
self.logic.set_zielgroesse_for_auswahl(value)
def _on_formfaktor_changed(self, state: int) -> None:
- """Persistiert Endlosrolle-Zustand und setzt ``sn_formfaktor``."""
+ """Persistiert Endlosrolle-Zustand und setzt ``formfaktor``."""
checked = bool(state)
set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project")
if self.logic:
@@ -335,17 +335,22 @@ class TabB(QWidget):
def _disconnect_theme_collection_signals(self) -> None:
"""Trennt Signale der alten Theme-Collection sauber."""
collection = self._connected_theme_collection
- if collection is None:
- return
- for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
- signal = getattr(collection, signal_name, None)
- if signal is None:
- continue
- try:
- signal.disconnect(self._refresh_thema_combo_live)
- except Exception:
- pass
+ if collection is not None:
+ for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
+ try:
+ signal = getattr(collection, signal_name, None)
+ except RuntimeError:
+ break
+ except Exception:
+ continue
+ if signal is None:
+ continue
+ try:
+ signal.disconnect(self._refresh_thema_combo_live)
+ except Exception:
+ pass
self._connected_theme_collection = None
+ self._theme_signal_connected = False
def _connect_theme_collection_signals(self) -> None:
"""Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste."""