Spyder, GUIs, Matlab, debugging

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

Hallo,

ich habe in der Vergangenheit mit Matlab gearbeitet und war mit der Art zu debuggen sehr zufrieden. Nun versuche ich mich an Python, als IDE verwende ich Spyder. Als Fernziel möche ich eine (Rasperry-) Kamera ansteuern, die Bilddaten ausgeben bzw. weiter auswerten. Um zu verstehen wie ich GUIs erstelle, arbeite ich mich durch folgenden Bespielcode: https://medium.com/@lelandzach/building ... dc71485d60
Ich würde gerne, so wie ich es von Matlab gewöhnt bin, an bestimmten Punkten breakpoints setzen und beim debuggen die Inhalte der im jeweiligen workspace befindlichen Variablen (Objekte) sehen. Zum Beispiel das ausgelöste event beim Drücken eines Buttons. Doch leider passiert nichts, wenn ich beispielsweise in Zeile 130

Code: Alles auswählen

self.resizeEvent = lambda e : self.on_main_window_resize(e)
einen breakpoint setze und den debugger bis dorthin laufen lasse. Vielmehr sehe Unmengen an Variablen, mit denen ich nichts anfangen kann:
Bild
In vielen Fällen führt Rechte-Maus-Taste/"view with the object explorer" auch nur zu einer Fehlermeldung "the variable is not picklable".
Ist ein debugging à la Matlab in Python überhaupt möglich? Ich habe nun schon mehrfach von PyCharm gelesen, haltet Ihr es ratsam dorthin zu wechseln?

Vielen Dank & Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 10669
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@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.
„With the neutron bomb, which destroys life but not property, capitalism has found the weapon of its dreams.” — Edward Abbey
lokiak
User
Beiträge: 7
Registriert: Samstag 18. Juni 2022, 20:58

@blackjack: vielen Dank für die Infos und den code.

Korrigiere mich bitte wenn ich falsch liege:
Bei `DisplayImage` ist `parent` redundant weil `QObject` bereits `parent` als Qt-Property besitzt.
'DisplayImage' erbt von 'QWidget' welches von 'QObject' erbt. Die Zuweisung "self.parent=parent" ist kein "schöner" code, besser man klärt die parent-child-Beziehung im Konstruktor des vererbenden QLabels?
...die Methode `parent()` ...

Warum sprichst Du von einer Methode?

VG
Benutzeravatar
__blackjack__
User
Beiträge: 10669
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@lokiak: Die Zuweisung an `self.parent` ist nicht nur unschön die ist ein echter, harter Fehler, weil `QObject` bereits eine Methode `parent()` hat, die man damit verdeckt. Die liefert das Elternobjekt das entweder beim erstellen übergeben oder später mit `setParent()` gesetzt wurde.

Und Qt bekommt nichts von der Eltern/Kind-Beziehung mit wenn man das einfach nur auf Python-Seite mit einem Attribut regelt. Das kann Auswirkungen auf die Speicherverwaltung haben, denn Qt regelt da viel über diese Beziehungen und den Objektbaum der dadurch entsteht.
„With the neutron bomb, which destroys life but not property, capitalism has found the weapon of its dreams.” — Edward Abbey
Antworten