import glob import os import time from PySide6.QtCore import Qt, QSize from PySide6.QtGui import QCursor, QPixmap, QImage, QPainter, QAction from PySide6.QtWidgets import QLabel, QMainWindow, QApplication, QStyleFactory from PySide6.QtPdf import QPdfDocument from PySide6.QtPdfWidgets import QPdfView from ui.MainWinddow_ui import Ui_MainWindow class MainWindow(QMainWindow): def __init__(self, parent=None): """ Konstruktor für die MainWindow-Klasse. Verwendet PySide6.QtPdf für optimale Performance. Args: parent: Übergeordnetes Widget, falls vorhanden """ super().__init__(parent) # UI einrichten self.ui = Ui_MainWindow() self.ui.setupUi(self) # Dict zum Speichern der Beziehung zwischen Thumbnails und Seitennummern self.thumbnail_to_page = {} # PDF-Dokumente für späteres On-Demand-Rendering speichern self.pdf_documents = {} # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}} # Aktueller Zoom-Faktor self.current_zoom = 100 # 100% # Aktuell angezeigte Seite self.current_page = 0 self.current_pdf = None # Cache für die aktuell gerenderten Pixmaps (Performance-Optimierung) self.current_rendered_pixmaps = None # Label für die Vollansicht (nur ein einziges Label) self.fullsize_label = None # Variablen für Drag-to-Scroll (Anti-Jitter für 4K/DPI-Skalierung) self.is_dragging = False self.last_drag_position = None self.drag_threshold = 3 # Mindestbewegung in Pixeln vor dem Scrollen self.scroll_sensitivity = 0.7 # Reduzierte Empfindlichkeit für sanfteres Scrollen # Theme-Menü initialisieren self._setup_theme_menu() # Bilder laden self._load_images() # Signale und Slots verbinden self._connect_signals() def _setup_theme_menu(self): """Initialisiert das Theme-Menü mit verfügbaren Themes.""" # Hole alle verfügbaren Themes available_themes = QStyleFactory.keys() current_theme = QApplication.style().objectName() print(f"Verfügbare Themes: {available_themes}") print(f"Aktuelles Theme: {current_theme}") # Füge Theme-Aktionen zum Menü hinzu for theme_name in available_themes: action = QAction(theme_name, self) action.setCheckable(True) # Markiere das aktuelle Theme if theme_name.lower() == current_theme.lower(): action.setChecked(True) # Verbinde die Aktion mit der Theme-Wechsel-Funktion action.triggered.connect(lambda checked, theme=theme_name: self.change_theme(theme)) # Füge die Aktion zum Theme-Menü hinzu self.ui.menuThema.addAction(action) def change_theme(self, theme_name): """ Wechselt das Theme der Anwendung. Args: theme_name: Name des zu verwendenden Themes """ print(f"Wechsle zu Theme: {theme_name}") try: # Erstelle den neuen Style style = QStyleFactory.create(theme_name) if style: # Wende den neuen Style auf die Anwendung an QApplication.setStyle(style) # Aktualisiere die Checkmarks im Menü for action in self.ui.menuThema.actions(): action.setChecked(action.text() == theme_name) print(f"Theme erfolgreich gewechselt zu: {theme_name}") else: print(f"Fehler: Theme '{theme_name}' konnte nicht erstellt werden") except Exception as e: print(f"Fehler beim Wechseln des Themes: {e}") def _load_images(self): """Lädt PDF-Thumbnails und bereitet On-Demand-Rendering vor.""" # Entferne bestehende Widgets aus den Layouts self._clear_layout(self.ui.verticalLayout_2) self._clear_layout(self.ui.verticalLayout_3) # Dicts zurücksetzen self.thumbnail_to_page = {} self.pdf_documents = {} self.current_rendered_pixmaps = None # Basis-Pfad zu den PDF-Ordnern base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pdf_base_dir = os.path.join(base_dir, "src", "ui", "res", "pdf") diff_dir = os.path.join(pdf_base_dir, "diff") ref_dir = os.path.join(pdf_base_dir, "ref") new_dir = os.path.join(pdf_base_dir, "new") # Prüfe ob diff-Ordner existiert und PDF-Dateien enthält if not os.path.exists(diff_dir): print(f"Diff-Ordner nicht gefunden: {diff_dir}") return # Finde alle PDF-Dateien im diff-Ordner diff_pdfs = glob.glob(os.path.join(diff_dir, "*.pdf")) if not diff_pdfs: print("Keine PDF-Dateien im diff-Ordner gefunden") return print(f"Gefundene PDF-Dateien im diff-Ordner: {diff_pdfs}") # Für jede PDF-Datei im diff-Ordner for diff_pdf_path in diff_pdfs: pdf_filename = os.path.basename(diff_pdf_path) ref_pdf_path = os.path.join(ref_dir, pdf_filename) new_pdf_path = os.path.join(new_dir, pdf_filename) # Prüfe ob gleichnamige PDFs in ref und new existieren if not os.path.exists(ref_pdf_path): print(f"Referenz-PDF nicht gefunden: {ref_pdf_path}") continue if not os.path.exists(new_pdf_path): print(f"New-PDF nicht gefunden: {new_pdf_path}") continue try: # Alle drei PDF-Dateien öffnen mit QtPdf diff_doc = QPdfDocument() ref_doc = QPdfDocument() new_doc = QPdfDocument() # PDF-Dateien laden diff_doc.load(diff_pdf_path) ref_doc.load(ref_pdf_path) new_doc.load(new_pdf_path) # Warten bis PDFs geladen sind if (diff_doc.status() != QPdfDocument.Status.Ready or ref_doc.status() != QPdfDocument.Status.Ready or new_doc.status() != QPdfDocument.Status.Ready): print(f"Fehler beim Laden der PDFs für {pdf_filename}") continue # PDF-Dokumente für später speichern self.pdf_documents[pdf_filename] = { 'diff': diff_doc, 'ref': ref_doc, 'new': new_doc } print(f"PDFs geladen: {pdf_filename}") print(f" diff: {diff_doc.pageCount()} Seiten") print(f" ref: {ref_doc.pageCount()} Seiten") print(f" new: {new_doc.pageCount()} Seiten") # Nehme die Seitenzahl der diff-PDF als Basis max_pages = diff_doc.pageCount() # Performance-Test: Messe Thumbnail-Erstellung start_time = time.time() # Erstelle nur Thumbnails (keine Vollbilder) for page_num in range(max_pages): # Nur diff-Seite für Thumbnail rendern page_size = diff_doc.pagePointSize(page_num) # Skalierung für Thumbnail (entspricht ca. Matrix(1.0, 1.0) in PyMuPDF) scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail # Seite rendern page_image = diff_doc.render(page_num, QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor))) diff_pixmap = QPixmap.fromImage(page_image) # Thumbnail erstellen und zur linken Spalte hinzufügen thumbnail = QLabel() thumbnail.setObjectName(f"thumbnail_{pdf_filename}_page_{page_num + 1}") thumbnail.setPixmap(diff_pixmap.scaledToWidth(200, Qt.TransformationMode.SmoothTransformation)) thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) thumbnail.setMouseTracking(True) self.ui.verticalLayout_2.addWidget(thumbnail) # Seitennummer für Thumbnail anzeigen thumbnail_info = QLabel(f"Seite {page_num + 1}") thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter) self.ui.verticalLayout_2.addWidget(thumbnail_info) # Beziehung zwischen Thumbnail und Seitennummer speichern self.thumbnail_to_page[thumbnail] = { 'pdf_filename': pdf_filename, 'page_num': page_num } # Click-Event für das Thumbnail einrichten thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t) print(f"Thumbnail für Seite {page_num + 1} erstellt") thumbnail_time = time.time() - start_time print(f"Performance: {max_pages} Thumbnails in {thumbnail_time:.3f}s") # Setze die erste PDF als aktuelle PDF if self.current_pdf is None: self.current_pdf = pdf_filename except Exception as e: print(f"Fehler beim Laden der PDFs: {e}") # Erstelle das eine Vollbild-Label für die rechte Spalte self.fullsize_label = QLabel() self.fullsize_label.setObjectName("fullsize_current_page") self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor)) self.ui.verticalLayout_3.addWidget(self.fullsize_label) # Drag-to-Scroll Events für das große Bild einrichten self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(event, self.fullsize_label) self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(event, self.fullsize_label) self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(event, self.fullsize_label) # Zeige die erste Seite initial an if self.current_pdf: self.render_and_display_page(self.current_pdf, 0) def render_and_display_page(self, pdf_filename, page_num): """ Rendert und zeigt eine spezifische Seite in der Vollansicht an. Cached die gerenderten Pixmaps für bessere Performance. Args: pdf_filename: Name der PDF-Datei page_num: Seitennummer (0-basiert) """ print(f"Rendere Seite {page_num + 1} von {pdf_filename}") if pdf_filename not in self.pdf_documents: print(f"PDF-Dokument {pdf_filename} nicht gefunden") return start_time = time.time() try: docs = self.pdf_documents[pdf_filename] # Diff-Seite laden (bestimmt die Abmessungen) diff_doc = docs['diff'] page_size = diff_doc.pagePointSize(page_num) # Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF) scale_factor = 2.0 render_size = QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor)) # Diff-Seite rendern (immer vorhanden) diff_image = diff_doc.render(page_num, render_size) diff_pixmap = QPixmap.fromImage(diff_image) # Ermittle die Abmessungen für weiße Seiten diff_width = diff_pixmap.width() diff_height = diff_pixmap.height() # Ref-Seite prüfen und rendern oder weiße Seite erstellen ref_doc = docs['ref'] if page_num < ref_doc.pageCount(): ref_image = ref_doc.render(page_num, render_size) ref_pixmap = QPixmap.fromImage(ref_image) print(f"Ref-Seite {page_num + 1} gerendert") else: # Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite ref_pixmap = QPixmap(diff_width, diff_height) ref_pixmap.fill(Qt.GlobalColor.white) print(f"Weiße Ref-Seite {page_num + 1} erstellt") # New-Seite prüfen und rendern oder weiße Seite erstellen new_doc = docs['new'] if page_num < new_doc.pageCount(): new_image = new_doc.render(page_num, render_size) new_pixmap = QPixmap.fromImage(new_image) print(f"New-Seite {page_num + 1} gerendert") else: # Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite new_pixmap = QPixmap(diff_width, diff_height) new_pixmap.fill(Qt.GlobalColor.white) print(f"Weiße New-Seite {page_num + 1} erstellt") # Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen self.current_rendered_pixmaps = { 'ref': ref_pixmap, 'diff': diff_pixmap, 'new': new_pixmap } # Aktualisiere aktuelle Seite self.current_page = page_num self.current_pdf = pdf_filename # Zeige das Bild mit aktuellem Alpha- und Zoom-Wert an self.update_current_display() render_time = time.time() - start_time print(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s") except Exception as e: print(f"Fehler beim Rendern der Seite {page_num + 1}: {e}") def update_current_display(self): """ Aktualisiert die Anzeige der aktuellen Seite basierend auf gecachten Pixmaps. Verwendet für Alpha- und Zoom-Änderungen ohne erneutes PDF-Rendering. """ if not self.current_rendered_pixmaps: print("Keine gerenderten Pixmaps verfügbar") return # Hole die gecachten Pixmaps ref_pixmap = self.current_rendered_pixmaps['ref'] diff_pixmap = self.current_rendered_pixmaps['diff'] new_pixmap = self.current_rendered_pixmaps['new'] # Erstelle das überlagerte Bild mit aktuellem Alpha-Wert alpha_value = self.ui.alpha.value() layered_pixmap = self.create_layered_pixmap(ref_pixmap, diff_pixmap, new_pixmap, alpha_value) # Wende aktuellen Zoom an zoom_factor = self.current_zoom / 100.0 if zoom_factor != 1.0: new_width = int(layered_pixmap.width() * zoom_factor) layered_pixmap = layered_pixmap.scaledToWidth(new_width, Qt.TransformationMode.SmoothTransformation) # Setze das überlagerte Bild self.fullsize_label.setPixmap(layered_pixmap) self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) def create_layered_pixmap(self, ref_pixmap, diff_pixmap, new_pixmap, alpha_value): """ Erstellt ein übergelagertes Pixmap basierend auf dem Alpha-Wert. Args: ref_pixmap: Unterste Ebene (ref) diff_pixmap: Mittlere Ebene (diff) new_pixmap: Oberste Ebene (new) alpha_value: Alpha-Wert (-100 bis 100) Returns: QPixmap: Das überlagerte Bild """ # Verwende die Größe des größten Bildes max_width = max(ref_pixmap.width(), diff_pixmap.width(), new_pixmap.width()) max_height = max(ref_pixmap.height(), diff_pixmap.height(), new_pixmap.height()) # Erstelle ein leeres Pixmap für das Ergebnis result = QPixmap(max_width, max_height) result.fill(Qt.GlobalColor.white) painter = QPainter(result) painter.setRenderHint(QPainter.RenderHint.Antialiasing) if alpha_value <= 0: # Alpha von -100 bis 0: Übergang von ref zu diff ref_opacity = 1.0 - (alpha_value + 100) / 100.0 diff_opacity = (alpha_value + 100) / 100.0 new_opacity = 0.0 else: # Alpha von 0 bis 100: Übergang von diff zu new ref_opacity = 0.0 diff_opacity = 1.0 - alpha_value / 100.0 new_opacity = alpha_value / 100.0 # Zeichne die Ebenen mit entsprechender Transparenz if ref_opacity > 0: painter.setOpacity(ref_opacity) painter.drawPixmap(0, 0, ref_pixmap) if diff_opacity > 0: painter.setOpacity(diff_opacity) painter.drawPixmap(0, 0, diff_pixmap) if new_opacity > 0: painter.setOpacity(new_opacity) painter.drawPixmap(0, 0, new_pixmap) painter.end() return result def _clear_layout(self, layout): """Entfernt alle Widgets aus einem Layout.""" if layout is not None: while layout.count(): item = layout.takeAt(0) widget = item.widget() if widget is not None: widget.deleteLater() def _connect_signals(self): """Verbindet Signale mit den entsprechenden Slots.""" # Button-Klicks verbinden self.ui.pushButton.clicked.connect(self.on_button_clicked) # Zoom-Slider verbinden self.ui.zoom.valueChanged.connect(self.apply_zoom) # Alpha-Slider verbinden self.ui.alpha.valueChanged.connect(self.on_alpha_changed) def on_alpha_changed(self, alpha_value): """ Wird ausgeführt, wenn der Alpha-Slider geändert wird. Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering. Args: alpha_value: Der neue Alpha-Wert (-100 bis 100) """ print(f"Alpha geändert auf {alpha_value}") start_time = time.time() # Verwende gecachte Pixmaps für schnelle Alpha-Änderungen self.update_current_display() alpha_time = time.time() - start_time print(f"Alpha-Update in {alpha_time:.6f}s") def on_button_clicked(self): """Wird ausgeführt, wenn der Button geklickt wird.""" print("Button wurde geklickt!") def on_thumbnail_clicked(self, event, thumbnail): """ Wird ausgeführt, wenn ein Thumbnail angeklickt wird. Args: event: Das Maus-Event thumbnail: Das geklickte Thumbnail-Label """ page_info = self.thumbnail_to_page.get(thumbnail) if page_info: pdf_filename = page_info['pdf_filename'] page_num = page_info['page_num'] print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt") # Rendere und zeige die gewählte Seite an self.render_and_display_page(pdf_filename, page_num) def apply_zoom(self, zoom_value): """ Wendet den Zoom-Faktor auf das aktuelle Bild an. Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering. Args: zoom_value: Der neue Zoom-Wert (in Prozent) """ self.current_zoom = zoom_value print(f"Zoom geändert auf {zoom_value}%") # Verwende gecachte Pixmaps für schnelle Zoom-Änderungen self.update_current_display() def on_fullsize_mouse_press(self, event, fullsize_label): """Wird ausgeführt, wenn die Maustaste auf einem großen Bild gedrückt wird.""" if event.button() == Qt.MouseButton.LeftButton: self.is_dragging = True self.last_drag_position = event.globalPosition().toPoint() fullsize_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor)) def on_fullsize_mouse_move(self, event, fullsize_label): """Wird ausgeführt, wenn die Maus über einem großen Bild bewegt wird.""" if self.is_dragging and self.last_drag_position is not None: current_pos = event.globalPosition().toPoint() delta = current_pos - self.last_drag_position if abs(delta.x()) >= self.drag_threshold or abs(delta.y()) >= self.drag_threshold: v_scrollbar = self.ui.scrollArea_2.verticalScrollBar() h_scrollbar = self.ui.scrollArea_2.horizontalScrollBar() scroll_delta_y = int(-delta.y() * self.scroll_sensitivity) scroll_delta_x = int(-delta.x() * self.scroll_sensitivity) new_v_value = v_scrollbar.value() + scroll_delta_y new_h_value = h_scrollbar.value() + scroll_delta_x v_scrollbar.setValue(new_v_value) h_scrollbar.setValue(new_h_value) self.last_drag_position = current_pos def on_fullsize_mouse_release(self, event, fullsize_label): """Wird ausgeführt, wenn die Maustaste auf einem großen Bild losgelassen wird.""" if event.button() == Qt.MouseButton.LeftButton: self.is_dragging = False self.last_drag_position = None fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor)) def closeEvent(self, event): """Wird beim Schließen der Anwendung aufgerufen.""" # PDF-Dokumente schließen ist bei QtPdf automatisch durch Garbage Collection super().closeEvent(event)