Meine erste Python Applikation

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
MADI'S Tech
User
Beiträge: 10
Registriert: Freitag 1. Mai 2020, 18:43

Ich habe Python März/April begonnen zu lernen und habe heute meine erste ausführbare App fertiggestellt und dazu ein Video gemacht:
https://youtu.be/Z3xYgVhyL8s
Es handelt sich um ein Reisetagebuch, mit dem man Ereignisse speichern kann. Genaueres dazu findest du unter https://cutt.ly/viBpywh auf Youtube.
Die App kann man hier kostenlos downloaden:
https://easyupload.io/k0sqdc

Konstruktive Kritik oder Lob ist gern gesehen 😉
Vom Pogrammierneuling zum Profi!

Ich dokumentiere meinen (Irr-)Weg in der Pogrammiersprache Python:
Youtube - MADI'S Tech
Twitch - MADIS_Tech
Twitter - @MadisTech
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MADI'S Tech: Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Einbuchstabige Namen wie `a`, `b`, `c`, … sind schlecht weil sie nicht beschreiben was denn der Wert bedeutet. Genau dafür sind Namen aber da. Die sollten deshalb auch keine kryptischen Abkürzungen enthalten.

Man muss auch nicht alles an einen Namen binden wenn man das danach nie wieder für irgend etwas braucht.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist.

`thumbnail()` ist die falsche Methode wenn man kein kleines Vorschaubild erzeugen will. Zum Grösse ändern ist die `resize()`-Methode da.

An den Namen `Ausgabefeld` wird ein Frame gebunden und ein Label danach an den Namen `Ausgabeframe`. Verwirrend ohne Ende.

Das "Eingabefeld 970 x 360 px.png"-Bild ist einfach nur ein einfarbiges Rechteck. Wozu soll das gut sein? Für eine einfarbige Fläche braucht man keine Bilddatei. Und am Ende werden in diese Zellen auch noch andere Widgets gesetzt‽

Der Dateiname sollte nur *einmal* im Quelltext stehen, als Konstante, damit man den leicht ändern kann ohne den im ganzen Programm suchen und ersetzen zu müssen.

Funktionen (und Methoden) bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben.

"end" gibt es als Konstante im `tkinter`-Modul.

Der `isfile()`-Test ist überflüssig, denn der Dateimodus "a" legt eine nichtvorhandene Datei an.

`Ereignis` ist kein guter Name für eine Datei.

Dateien sollte man wo möglich mit der ``with``-Anweisung öffnen.

Bei Textdateien sollte man immer explizit die Kodierung angeben. Wenn man die selbst in der Hand hat, bietet sich UTF-8 an.

CSV-Dateien schreibt und liest man nicht selbst, die sind komplizierter als sie auf den ersten Blick aussehen. Das Programm fällt beispielsweise auf die Nase wenn man irgendwo in der Eingabe ein Semikolon verwendet, oder in den Notizen einen Zeilenumbruch. Beides erlaubt CSV, man muss das nur der Spezifikation entsprechend umsetzen. Das gibt es schon fertig als `csv`-Modul in der Standardbibliothek. Dann muss man beim öffnen der Datei noch ``newline=""`` als Argument angeben.

``global`` hat in einem sauberen Programm nichts zu suchen. Das ist dann auch der Punkt wo man um objektorientierte Programmierung nicht herum kommt. Das braucht man für jede nicht-triviale GUI.

`Arbeitsliste` ist kein guter Name — er sagt nicht was der Inhalt bedeutet und Grunddatentypen haben nichts in Namen verloren. Zudem sollte diese Liste von Anfang an existieren, halt leer am Anfang, sonst gibt es einen `NameError` wenn man versucht etwas zu löschen bevor man die Datei geladen hat.

Komisch bis Falsch ist ausserdem das neu erzeuge Einträge zwar gespeichert werden, aber nur in der Datei und nicht in dieser Liste. Auf diese weise können Einträge verloren gehen (wenn das Löschen funktionieren würde und dabei nicht sowieso Einträge verloren gehen würden.)

``for i in range(len(Arbeitsliste)):`` ist in Python ein „anti pattern“. Man kann in Python direkt über die Elemente von Sequenztypen wie Listen iterieren. Ohne den Umweg über einen Index. Der wird in `abrufen()` auch noch dazu verwendet um die Listenelemente (Zeichenketten) durch Listen mit Zeichenketten zu ersetzen. Da würde man eine neue Liste erstellen statt eine alte so zu verändern. Letztlich wird einem aber auch das vom `csv`-Modul an der Stelle abgenommen.

`abrufen()` berücksichtig nicht, dass die Datei eventuell leer sein könnte, und das Anzeigen des ersten Eintrags dann nicht funktioniert. Auch der Fall, dass die Datei nicht gelesen werden kann, beispielsweise weil sie noch gar nicht existiert, wird nicht behandelt.

`eintrag_loeschen()` ist dann ziemlich wirr und undurchsichtig. Das die Schleife abbricht nach dem *ein* Datensatz gelöscht wurde ist beispielsweise ziemlich versteckt.

Das ``else`` mit dem ``continue`` macht keinen Sinn, denn das ändert nichts am Programmfluss.

`vergleich1` und `vergleich2` ist ein „code smell“. Man nummeriert keine Namen. Da will man entweder bessere Namen oder eine Datenstruktur. Oft eine Liste. `c` (schlechter Name) wird definiert, aber nirgends verwendet.

Hier ist wieder das ``for i in range(len(…)):`` „anti pattern“.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
"""
Dieses Pogramm erzeugt eine GUI, in dem Daten zu Ereignissen eingegeben, auf
eine .csv Datei gespeichert, aus der selben .csv Datei gelesen und gelöscht
werden.

Copyright (C) 2020  MADI'S Tech

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see https://www.gnu.org/licenses/.
"""
import csv
import tkinter as tk
from tkinter import messagebox

from PIL import Image, ImageTk

EREIGNISSE_DATEINAME = "Ereignisse.csv"
ENCODING = "UTF-8"
DELIMITER = ";"


class Reisetagebuch:
    def __init__(self, master):
        self.eintraege = list()

        self.datum_var = tk.StringVar()
        self.ort_var = tk.StringVar()
        self.bild_pfad_var = tk.StringVar()

        self.ausgabedatum_var = tk.StringVar()
        self.ausgabe_ort_var = tk.StringVar()
        self.ausgabe_bild_var = tk.StringVar()

        frame = tk.Frame(master)
        self.logo_image = ImageTk.PhotoImage(
            Image.open("GUI Header 970x250 px.png").resize((698.4, 180))
        )
        tk.Label(
            frame, borderwidth=0, highlightthickness=0, image=self.logo_image
        ).pack()
        frame.grid(row=0, column=0, rowspan=2, columnspan=6)

        tk.Label(
            master,
            text="Ereigniseingabe:",
            bg="#242254",
            fg="white",
            font=("Poppins", "18", "bold"),
        ).grid(row=2, column=0, columnspan=2)
        tk.Button(
            master,
            text="Eintrag speichern",
            bd=0,
            bg="white",
            command=self.eintrag_speichern,
        ).grid(row=2, column=2, columnspan=4)

        tk.Label(
            master,
            text="Datum: ",
            bg="#242254",
            fg="white",
            font=("Poppins", "12", "bold"),
        ).grid(row=3, column=0)
        tk.Entry(master, textvariable=self.datum_var, width=40).grid(
            row=3, column=1, columnspan=2, sticky=tk.W
        )
        tk.Label(
            master,
            text="Datenpfad zum Foto/Fotos: ",
            bg="#242254",
            fg="white",
            font=("Poppins", "12", "bold"),
        ).grid(row=3, column=3, columnspan=3, sticky=tk.S)

        tk.Label(
            master,
            text="Ort: ",
            bg="#242254",
            fg="white",
            font=("Poppins", "12", "bold"),
        ).grid(row=4, column=0)
        tk.Entry(master, textvariable=self.ort_var, width=40).grid(
            row=4, column=1, columnspan=2, sticky=tk.W
        )
        tk.Entry(master, textvariable=self.bild_pfad_var, width=45).grid(
            row=4, column=3, columnspan=3
        )

        tk.Label(
            master,
            text="Notizen: ",
            bg="#242254",
            fg="white",
            font=("Poppins", "12", "bold"),
        ).grid(row=5, column=0, rowspan=2)
        self.notizen_text_editor = tk.Text(master, height=4, width=61)
        self.notizen_text_editor.grid(
            row=5, column=1, rowspan=2, columnspan=5, sticky=tk.W
        )

        tk.Label(
            master,
            text="Ereignisausgabe:",
            bg="#242254",
            fg="white",
            font=("Poppins", "18", "bold"),
        ).grid(row=8, column=0, columnspan=2)
        tk.Button(
            master,
            text="Ausgabe starten",
            bd=0,
            bg="white",
            command=self.abrufen,
        ).grid(row=8, column=2, columnspan=4)

        tk.Entry(master, textvariable=self.ausgabedatum_var, width=33).grid(
            row=9, column=0, columnspan=2
        )
        tk.Entry(master, width=67, textvariable=self.ausgabe_bild_var).grid(
            row=9, column=2, columnspan=4, sticky=tk.W
        )

        tk.Entry(master, textvariable=self.ausgabe_ort_var, width=33).grid(
            row=10, column=0, columnspan=2
        )
        self.ausgabe_notiz_text_editor = tk.Text(master, height=4, width=50)
        self.ausgabe_notiz_text_editor.grid(
            row=10, column=2, columnspan=4, sticky=tk.W
        )

        tk.Label(
            master,
            text="Diesen Eintrag fertig bearbeitet?: ",
            bg="#242254",
            fg="white",
            font=("Poppins", "13", "bold"),
        ).grid(row=11, column=2, columnspan=3)
        tk.Button(
            master, text="Eintrag löschen", bd=0, command=self.eintrag_loeschen
        ).grid(row=11, column=5)

    def show_first_entry(self):
        if self.eintraege:
            datum, ort, bild_pfad, notizen = self.eintraege[0]
        else:
            datum, ort, bild_pfad, notizen = [""] * 4

        self.ausgabedatum_var.set(datum)
        self.ausgabe_ort_var.set(ort)
        self.ausgabe_bild_var.set(bild_pfad)
        self.ausgabe_notiz_text_editor.delete(1.0, tk.END)
        self.ausgabe_notiz_text_editor.insert(1.0, notizen)

    def eintrag_speichern(self):
        datum = self.datum_var.get().strip()
        if not datum:
            messagebox.showinfo("Fehler", "Gebe bitte ein Datum ein")
        else:
            eintrag = [
                datum,
                self.ort_var.get().strip(),
                self.bild_pfad_var.get().strip(),
                self.notizen_text_editor.get(1.0, tk.END).rstrip(),
            ]
            with open(
                EREIGNISSE_DATEINAME, "a", encoding=ENCODING, newline=""
            ) as csv_file:
                csv.writer(csv_file, delimiter=DELIMITER).writerow(eintrag)
            self.eintraege.append(eintrag)
            messagebox.showinfo("Eintrag", "Eintrag wurde gespeichert")

    def abrufen(self):
        try:
            with open(
                EREIGNISSE_DATEINAME, "r", encoding=ENCODING, newline=""
            ) as csv_file:
                self.eintraege = list(
                    csv.reader(csv_file, delimiter=DELIMITER)
                )
        except FileNotFoundError:
            self.eintraege = list()

        self.show_first_entry()

    def eintrag_loeschen(self):
        key = [
            self.ausgabedatum_var.get().strip(),
            self.ausgabe_ort_var.get().strip(),
            self.ausgabe_bild_var.get().strip(),
        ]
        index = None
        for i, eintrag in enumerate(self.eintraege):
            if key == eintrag[:3]:
                index = i
                break

        if index is not None:
            self.eintraege.pop(index)
            with open(
                EREIGNISSE_DATEINAME, "w", encoding=ENCODING, newline=""
            ) as csv_file:
                csv.writer(csv_file, delimiter=DELIMITER).writerows(
                    self.eintraege
                )
            messagebox.showinfo("Löschen", "Eintrag wurde gelöscht")
            self.show_first_entry()
        else:
            messagebox.showinfo("Löschen", "Eintrag nicht gefunden")


def main():
    root = tk.Tk()
    root.configure(bg="#242254")
    root.title("Reisetagebuch")
    root.iconbitmap("Icon 40x40 px.ico")
    _ = Reisetagebuch(root)
    root.mainloop()


if __name__ == "__main__":
    main()
Die Bedienung ist aber komisch. Man sieht immer nur den ersten Eintrag in der Datei im Ausgabebereich. Warum gibt es die gleichen Felder zweimal? Benutzer sind es ja eigentlich gewohnt das man eine Datei laden, bearbeiten, und dann speichern kann. Und das man in den Datensätzen vor- und zurück blättern kann. Und das man nur eine Maske für Ein- und Ausgabe hat. Dafür noch eine Übersicht über alle Datensätze in Form einer Liste/Tabelle.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Ich habe mir nur kurz das Video angeschaut, aber ich finde es umständlich den Pfad zu den Bildern manuell einzutragen. Über ein File Dialog (ggf. mit Multiselect) wäre das ganze angenehmer.
Auch würde ich es schön finden wenn sich bei der Ereignisausgabe ein kleines Fenster öffnet, wo das Bild direkt angezeigt wird und der Text halt daneben. Das ist aber nur Geschmackssache.
Wenn man für einen Tag zwei Einträge hat, wird bei der Ausgabe nur der erste Eintrag ausgegeben.
Man kann Strings in das "Datum" Feld eintragen, das sollte man unterbinden. Da man die ja auch nicht abfragen kann.
Das Datum bei der Abfrage kann man nicht ändern.
Du schreibst "Diesen Eintrag fertig bearbeitet?" Dabei finde ich keine Möglichkeit zum Bearbeiten, nur zum Löschen.
Mir fehlt auch eine Übersicht der bereits eingetragenen Tage.
MADI'S Tech
User
Beiträge: 10
Registriert: Freitag 1. Mai 2020, 18:43

Jankie hat geschrieben: Montag 13. Juli 2020, 06:35 Ich habe mir nur kurz das Video angeschaut, aber ich finde es umständlich den Pfad zu den Bildern manuell einzutragen. Über ein File Dialog (ggf. mit Multiselect) wäre das ganze angenehmer.
Auch würde ich es schön finden wenn sich bei der Ereignisausgabe ein kleines Fenster öffnet, wo das Bild direkt angezeigt wird und der Text halt daneben. Das ist aber nur Geschmackssache.
Wenn man für einen Tag zwei Einträge hat, wird bei der Ausgabe nur der erste Eintrag ausgegeben.
Man kann Strings in das "Datum" Feld eintragen, das sollte man unterbinden. Da man die ja auch nicht abfragen kann.
Das Datum bei der Abfrage kann man nicht ändern.
Du schreibst "Diesen Eintrag fertig bearbeitet?" Dabei finde ich keine Möglichkeit zum Bearbeiten, nur zum Löschen.
Mir fehlt auch eine Übersicht der bereits eingetragenen Tage.
Danke für dein Feedback, Jankie.
Ich wollte mit dem, was ich mir über Python beigebracht habe, etwas umsetzen. Im Laufe der Zeit, solange ich mich mit Python regelmäßig beschäftige, werde ich die Anwendung verbessern - eine 2.0 Version wird in noch kommen.
Deine Anmerkungen/Kritik habe ich schon von anderen gehört und für die 2.0 Version notiert.
Mit "Diesen Eintrag fertig bearbeitet" hab ich gemeint, dass man die ausgegeben Informationen nimmt und in ein digitales Fotobuch einpflegt, z.B. mithilfe von canva.com (Dort hab ich ein Fotobuchcover entworfen, dass im Video eingeblendet wird).
Vom Pogrammierneuling zum Profi!

Ich dokumentiere meinen (Irr-)Weg in der Pogrammiersprache Python:
Youtube - MADI'S Tech
Twitch - MADIS_Tech
Twitter - @MadisTech
Antworten