@lokiak: Der Breakpoint bezieht sich ja auch auf die Zuweisung an `self.resizeEvent` in der `__init__()`, der wird *nicht* ausgelöst wenn die Fenstergrösse verändert wird. Dazu müsstest Du einen in `on_main_window_resize()` setzen.
Der Lambda-Ausdruck in der Zeile macht übrigens keinen Sinn. Wenn die anonyme Funktion einfach nur eine andere Funktion oder Methode mit den genau gleichen Argumenten aufruft, die sie selbst schon bekommen hat, dann kann man einfach gleich die Funktion/Methode verwendenden ohne die noch mal unnötigerweise in eine anonyme Funktion zu verpacken.
Sternchen-Importe sind Böse™. Es besteht die Gefahr von Namenskollisionen, und man sieht auch nicht mehr so leicht wo eigentlich welcher Name her kommt.
Der Code geht auch komisch mit Wahrheitswerten um. Man vergleicht Werte nicht explizit mit literalen Wahrheitswerten. Da kommt ja nur wieder ein Wahrheitswert heraus. Entweder der, den man sowieso schon hatte, dann kann man den auch gleich verwenden, oder dessen Gegenteil — dafür gibt es ``not``. Und die Testfunktion ob man eine Grafikdatei hat, gibt in einem ``if`` den Wert `True` zurück und im ``else`` den Wert `False` je nach dem ob die Bedingung beim ``if`` den Wert `True` oder `False` ergibt — also kann man auch einfach das Ergebnis der Bedingung zurück geben, und sich das ``if``/``else`` sparen.
Die Backslashes am Zeilenende sind nicht überall notwendig, denn wenn es noch ausstehende schliessende Klammern gibt, ist der Compiler schon so schlau zu wissen, dass die Anweisung/der Ausdruck noch nicht zu ende sein kann. Dementsprechend kann man durch gegebenenfalls zusätzliche Klammern in der Regel auch diese Backslashes komplett vermeiden.
Kommentare werden mit *einem* "#" eingeleitet. Warum der Autor da immer zwei nimmt, wird wohl sein Geheimnis bleiben. Wobei die Kommentare vor Klassen, Funktionen, und Methoden auch eher Docstrings sein sollten.
In `filename_has_image_extension()` würde man eher die Dateinamenserweiterung abtrennen statt die letzten 3 und die letzten 4 Zeichen zu verwenden. `os.path()` hat da eine Funktion für. Allerdings würde man in neuem Code dieses Modul nicht mehr verwenden, wenn es auch mit dem `pathlib`-Modul geht. Pfadteile mit ``+`` zusammensetzen geht in gar keinem Fall.
Der Code im ``if __name__ …`` gehört in eine Funktion, sonst definiert der zwei globale Variablen.
Das ein `App`-Objekt an den Namen `ex` gebunden wird ist komisch. Was soll `ex` denn bedeuten? Das ist das Hauptfenster also sollte das `window` oder `main_window` heissen. Und der `show()`-Aufruf gehört dort hin und nicht in die Widget-Klasse wo sie beim erstellen des Objekts schon aufgerufen wird. Das macht kein anderes Widget.
Beim `exec_()`-Aufruf gehört seit Python 3 kein Unterstrich mehr hin, der wird in zukünftigen PyQt-Anbindungen auch verschwinden.
In einer `__init__()` am Ende noch zwei drei Zeilen in eine zusätzliche Methode auszulagern die alleine nie aufgerufen wird, macht keinen Sinn.
An das `App`-Objekt werden Werte gebunden die gar nicht wirklich zum Zustand gehören, beziehungsweise dadurch redundant vorhanden sind. Der Titel wird ja beispielsweise schon als `windowTitle`-Qt-Property auf dem Objekt gesetzt und da auch wieder abgefragt werden. Es gibt keinen Grund den noch mal als Attribut unter einem anderen Namen verfügbar zu haben.
Bei `DisplayImage` ist `parent` redundant weil `QObject` bereits `parent` als Qt-Property besitzt. Das heisst es ist sogar ein echtes Problem die Methode `parent()` einfach so mit einem `parent`-Attribut zu verdecken. Das zu benutzen ist ein bisschen unschön, weil man sich in diese Richtung der Objekthierarchie eigentlich nur sehr ungern Abhängigkeiten einhandelt. Das ist vielleicht kein konkretes Problem weil `DisplayImage` zwar ein `QWidget` ist, aber nirgends wirklich verwendet wird. Was komisch bis falsch aussieht. Die Klasse sollte eher von `QLabel` abgeleitet werden.
Die Fenstergrösse hart selbst vorzugeben ist schon grenzwertig, aber die Fensterposition — damit zieht man den Hass von Anwendern auf sich. Wenn man die letzte Position speichert und beim Start wiederherstellt okay, aber generell bei 0, 0 zu starten geht gar nicht.
Statt `resizeEvent` etwas zuzuweisen würde man die Methode eher überschreiben/implementieren. Oder man weist dort die Methode auf dem `DisplayImage`-Objekt zu und spart sich eine extra Methode auf dem `App`-Objekt die einfach nur weiterdelegiert.
``nav = scroll`` ist sinnlos. Da hat man dann in der Methode das selbe Objekt unter zwei Namen verfügbar, ohne dass das irgendwie Sinn machen würde.
Wenn man das filtern nach Bilddateien *vor* der Schleife macht, dann kann man die Layoutzeile ganz einfach mit `enumerate()` aufzählen. Dann wird auch das ermitteln der ersten Bilddatei viel einfacher — das ist einfach das erste Element.
Komplett ungetestet:
Code: Alles auswählen
#!/usr/bin/env python3
import sys
from functools import partial
from pathlib import Path
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
QApplication,
QBoxLayout,
QGridLayout,
QLabel,
QScrollArea,
QWidget,
)
DEFAULT_IMAGE_ALBUM_DIRECTORY = Path("my-album")
def filename_has_image_extension(file_path):
"""
Check that a file name has a valid image extension for QPixmap.
"""
return file_path.suffix.lower() in [
".bmp",
".gif",
".jpeg",
".jpg",
".pbm",
".pgm",
".png",
".ppm",
".xbm",
".xpm",
]
class DisplayImage(QLabel):
"""
Widget for the single image that is currently on display.
"""
def __init__(self, parent=None):
QLabel.__init__(self, parent)
self.path_to_image = ""
def update_display_image(self, path_to_image):
self.path_to_image = path_to_image
self.on_main_window_resize()
def on_main_window_resize(self, _event=None):
size = self.parent().size()
self.setPixmap(
QPixmap(str(self.path_to_image)).scaled(
QSize(size.height() - 50, size.width() - 200),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
class ImageFileSelector(QWidget):
"""
Widget for selecting an image in the directory to display.
Makes a vertical scrollable widget with selectable image thumbnails.
"""
def __init__(self, album_path, display_image, parent=None):
QWidget.__init__(self, parent=parent)
self.display_image = display_image
self.grid_layout = QGridLayout(self, verticalSpacing=30)
image_file_paths = [
path
for path in album_path.iterdir()
if path.is_file() and filename_has_image_extension(path)
]
for row_index, image_file_path in enumerate(image_file_paths):
image_label = QLabel(
pixmap=QPixmap(str(image_file_path)).scaled(
QSize(100, 100),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
),
alignment=Qt.AlignCenter,
)
text_label = QLabel(
text=image_file_path.name, alignment=Qt.AlignCenter
)
mouse_press_handler = partial(
self.on_thumbnail_click, row_index, image_file_path
)
image_label.mousePressEvent = mouse_press_handler
text_label.mousePressEvent = mouse_press_handler
thumbnail = QBoxLayout(QBoxLayout.TopToBottom)
thumbnail.addWidget(image_label)
thumbnail.addWidget(text_label)
self.grid_layout.addLayout(thumbnail, row_index, 0, Qt.AlignCenter)
# Automatically select the first file in the list during init.
self.on_thumbnail_click(0, image_file_paths[0])
def _get_label_at(self, index):
return self.grid_layout.itemAtPosition(index, 0).itemAt(1).widget()
def on_thumbnail_click(self, index, image_file_path, _event=None):
for label_index in range(len(self.grid_layout)):
self._get_label_at(label_index).setStyleSheet(
"background-color:none;"
)
self._get_label_at(index).setStyleSheet("background-color:blue;")
self.display_image.update_display_image(image_file_path)
class App(QWidget):
def __init__(self):
super().__init__(windowTitle="Photo Album Viewer")
self.resize(800, 600)
self.display_image = DisplayImage(self)
self.resizeEvent = self.display_image.on_main_window_resize
self.image_file_selector = ImageFileSelector(
DEFAULT_IMAGE_ALBUM_DIRECTORY, self.display_image
)
scroll_area = QScrollArea(
widgetResizable=True, widget=self.image_file_selector
)
scroll_area.setFixedWidth(140)
layout = QGridLayout(self)
layout.addWidget(scroll_area, 0, 0, Qt.AlignLeft)
layout.addWidget(self.display_image, 0, 1, Qt.AlignRight)
def main():
app = QApplication(sys.argv)
window = App()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Sypder finde ich um Programme zu entwickeln nicht so gut, weil man da immer dran denken muss, dass sich das anders verhält als normale IDEs, also das Werte aus vorherigen Programmläufen existent bleiben und nicht sauber neu angefangen wird.
Debugger benutze ich auch gaaanz selten mal. Ein paar strategische `print()`-Anweisungen um Werte auszugeben und zu vergleichen ob das die sind die man erwartet, reichen in der Regel aus.