Seite 1 von 1

Programm Verbesserungen und Verständnis beim Klassen / Durchreichen von Klassen

Verfasst: Sonntag 9. Juni 2024, 11:48
von martinjo
Guten Tag

Ich habe ein Programm geschrieben mit GTK.Oberfläche und SQLite-Datenbank. Mich würden mal allgemeine Verbesserungsvorschläge interessieren, da ich, obwohl ich mich viel mit dem Thema beschäftige, ein paar Grundprinzipien noch nicht drin habe,

Das Programm ist ein Fenster mit einer (Haupt-)Box darin, oben ist ein Label und ein "Hinzufügen"-Knopf. In das Label schreibt man ein Datum, und es wird für diesen Tag, wenn er noch nicht existiert, ein neues Widget(Box) erstellt. Dieses wird dann zu Hauptbox hinzugefügt und ein Eintrag in der Datenbank wird angelegt.

Bei Änderungen am Text werden diese gespeichert,
Hier wusste ich noch nicht wie das besser umzusetzen ist, es ist ja nicht vorteilhaft, wenn ich 100 Buchstaben schreibe 100 Mal in der Datenbank gespeichert wird. Habe hier an einen Timer gedacht, der alle zwei Sekunden ausgeführt wird. Also das quasi die Speichern-Funktion eine Warteschleife bekommt. Schreibt man und klickt eine Sekunde später auf den Entferne-Knopf würde mein Widget und der Eintrag in der Datenbank dann entfernt, im Hintergrund wird der Datensatz dann jedoch wieder erstellt, was ja so nicht gewünscht ist.
Auch stellt sich mir hier die Frage, ob ich die Connection zu Datenbank nicht offen lassen sollte. Gerade wird diese 100 Mal geöffnet und geschlossen bei dem Beispiel oben.

Überprüfen von Werten und Formatierungen soll hier erstmal keine Rolle spielen (z.B. Datum-Format)

Bild

Hier der Code:

Code: Alles auswählen

#!/usr/bin/env python3

import sqlite3
import datetime
import sys
from gi.repository import Gtk
import gi
gi.require_version("Gtk", "3.0")


class MyWindow(Gtk.Window):
    database_name = "test.db"
    widgets = {}

    def __init__(self):
        super().__init__(title="MyWindow", border_width=5)
        self.set_default_size(500, 300)
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(
            Gtk.PolicyType.ALWAYS,
            Gtk.PolicyType.ALWAYS)

        self.add(scrolled_window)

        self.box = Gtk.VBox(spacing=10, border_width=15)
        scrolled_window.add(self.box)

        add_button = Gtk.Button(label="Add")
        self.box.pack_start(add_button, False, True, 0)
        add_button.connect("clicked", self.on_add_button_clicked)

        self.date_entry = Gtk.Entry(text="22.02.2024")
        self.box.pack_start(self.date_entry, False, True, 0)

        self.database = Database(database_name=self.database_name)
        self.create_database()
        self.load_from_database()

    def run(self):
        self.show_all()
        Gtk.main()

    def create_database(self):
        self.database.create()

    def load_from_database(self):
        data = self.database.get_data()
        for d in data:
            self.create_new_widget(date=d[0], text=d[1])

    def save_to_database(self, widget, update=False):
        self.database.save_or_update(
            [widget.get_date(), widget.get_text()],
            update=update)

    def remove_from_database(self, widget):
        self.database.remove_row(date=widget.get_date())

    def on_add_button_clicked(self, widget):
        date = self.date_entry.get_text()
        self.create_new_widget(date=date, save=True)

    def on_remove_button_clicked(self, button, new_widget):
        self.remove_from_database(new_widget)
        new_widget.destroy()

    def create_new_widget(self, date, text="", save=False):
        if date in self.widgets.keys():
            self.widgets[date].textview.grab_focus()
            return
        new_widget = DayEntry(date=date, text=text)
        # connect methodes from class DayEntry
        new_widget.textbuffer.connect(
            "changed", self.on_entry_changed, new_widget)
        new_widget.remove_button.connect(
            "clicked", self.on_remove_button_clicked, new_widget)
        self.box.pack_start(new_widget, True, True, 0)
        self.widgets[date] = new_widget
        new_widget.show_all()
        if save:
            self.save_to_database(new_widget)

    def on_entry_changed(self, textbuffer, new_widget):
        self.save_to_database(new_widget, update=True)


class Database():
    def __init__(self, database_name):
        self.database_name = database_name

    def create(self):
        con = sqlite3.connect(self.database_name)
        cur = con.cursor()
        cur.execute("CREATE TABLE IF NOT EXISTS days(date UNIQUE, text)")
        cur.execute(
            "INSERT OR IGNORE INTO days VALUES(?, ?)", [
                "01.02.2003", "Sample Text"])
        cur.execute(
            "INSERT OR IGNORE INTO days VALUES(?, ?)", [
                "15.04.2007", "Another Text"])
        con.commit()
        con.close()

    def remove_row(self, date):
        con = sqlite3.connect(self.database_name)
        cur = con.cursor()
        cur.execute('DELETE FROM days WHERE date=?', [date])
        con.commit()
        con.close()

    def save_or_update(self, data, update=False):
        date = data[0]
        text = data[1]
        con = sqlite3.connect(self.database_name)
        cur = con.cursor()
        if update:
            cur.execute('UPDATE days SET text=? WHERE date=?',
                        [text, date])
        else:
            cur.execute("INSERT OR IGNORE INTO days VALUES(?, ?)",
                        [date, text])
        con.commit()
        con.close()

    def get_data(self,):
        con = sqlite3.connect(self.database_name)
        cur = con.cursor()
        res = cur.execute("SELECT * FROM days")
        data = res.fetchall()
        con.close()
        return data


class DayEntry(Gtk.HBox):
    def __init__(self, date="", text=""):
        super().__init__(spacing=10, border_width=10)
        self.label = Gtk.Label(label=date)
        self.pack_start(self.label, False, True, 0)
        self.textview = Gtk.TextView(border_width=10)
        self.pack_start(self.textview, True, True, 0)
        self.textbuffer = self.textview.get_buffer()
        self.textbuffer.set_text(text, len(text))
        self.remove_button = Gtk.Button(label="Remove")
        self.pack_start(self.remove_button, False, True, 0)

    def get_text(self):
        start_iter = self.textbuffer.get_start_iter()
        end_iter = self.textbuffer.get_end_iter()
        return self.textbuffer.get_text(start_iter, end_iter, True)

    def set_text(self, text):
        self.textbuffer.set_text(text, len(text))

    def get_date(self):
        return self.label.get_label()


if __name__ == "__main__":
    app = MyWindow()
    app.run()

Re: Programm Verbesserungen und Verständnis beim Klassen / Durchreichen von Klassen

Verfasst: Sonntag 9. Juni 2024, 12:51
von martinjo
Beim Kommentar "# connect methodes from class DayEntry", da erstelle ich eine Klasse, und muss nachträglich im Hauptprogramm die Methoden verbinden damit es funktioniert wie erhofft. Das war der Hauptgrund für das erstellen dieses Threads. Das geht doch sicher schöner!?

Re: Programm Verbesserungen und Verständnis beim Klassen / Durchreichen von Klassen

Verfasst: Sonntag 9. Juni 2024, 15:20
von __blackjack__
Die Verbindung zur Datenbank würde ich offen lassen. Und jeden getippten Buchstaben speichern ist in der Tat reichlich übertrieben. Ein „autosave“ alle x Sekunden falls etwas geändert wurde ist sicher besser. Ergänzt durch eine Möglichkeit bewusst zu speichern. Und das ungespeicherte Änderungen bestehen, sollte angezeigt werden. Und beim verlassen des Eingabefeldes könnte/sollte man speichern, falls sich etwas geändert hat.

Das `gi.require_version()` kommt zu spät. Das muss *vor* dem entsprechenden Import kommen, weil das bei mehreren Versionen ja entscheidet welche man durch den Import bekommt.

`sys` und `datetime` werden importiert aber nicht verwendet. `datetime` sollte aber verwendet werden. SQLite3 ist es ja letztlich egal, aber das Datum würde man dort als DATE ”deklarieren” und nicht als Zeichenkette im Format Tag.Monat.Jahr speichern, sondern als `datetime.date` übergeben. Dann ist die Spalte beispielsweise auch zeitlich sortierbar von der Datenbank.

`con`, `cur`, `res` sollten `connection`, `cursor` und `result` sein.

Bei dem CREATE TABLE könnte man das Datum als Primärschlüssel deklarieren. Es fehlen dort die Datentypen und die Namen sind gültige Datentypen, müssen also entsprechend gequotet werden. Das SQLite3 so viel fehlerhaftes SQL einfach schluckt, sollte einen nicht dazu verleiten das nicht trotzdem korrekt zu machen.

`save_or_update()` sind ja eigentlich zwei Methoden wo über ein Flag entschieden wird
was die Methode letztendlich wirklich macht. Oder man macht da *ein* “UPSERT“ draus.

SELECT garantiert keine Reihenfolge in der die Daten geliefert werden wenn da kein ORDERED BY angegeben wird.

Bei `create_new_widget()` ist da auch so ein komisches Flag was entscheidet ob die Daten in dem neu erstellten Widget gespeichert werden sollen oder nicht. Das geht diese Methode doch überhaupt nichts an.

Die Daten sollten nicht als zweielementige Liste herumgereicht werden und es sollte nicht mit magischen Indexwerten darauf zugegriffen werden. Wenn man die Daten in einer Sequenz hat, zum Beispiel weil sie so aus der Datenbank kommen, ist es lesbarer wenn man statt `for d in …` und dann ``d[0]`` und ``d[1]`` besser ``for date, text in …`` schreibt.

In einigen Methoden gibt es `new_widget` auch wenn das gar nicht ”neu” ist, und wo es tatsächlich neu ist, da macht es nicht wirklich Sinn das durch den Präfix von etwas anderem abzugrenzen. Ausserdem ist das ja nicht irgendein generisches Widget, das heisst da wäre ein passendere Name besser.

`database_name` ist eine Konstante, und die würde ich nicht in einer Klasse verstecken.

`widgets` ist ein sehr generischer Name und auch das hat nichts auf der Klasse verloren. Da werden ausserdem immer nur Werte hinzugefügt, beim Zerstören aber gar nicht wieder entfernt!

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import sqlite3
from contextlib import closing

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

DATABASE_NAME = "test.db"


class Database:
    def __init__(self, database_name):
        self.connection = sqlite3.connect(database_name)

    def __enter__(self):
        return self

    def __exit__(self, _type, _value, _traceback):
        self.close()

    def close(self):
        self.connection.close()

    def create(self):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute(
                "CREATE TABLE IF NOT EXISTS day ("
                '  "date" DATE PRIMARY KEY,'
                '  "text" TEXT NOT NULL'
                ")"
            )
            cursor.executemany(
                "INSERT OR IGNORE INTO day VALUES (?, ?)",
                [
                    ["01.02.2003", "Sample Text"],
                    ["15.04.2007", "Another Text"],
                ],
            )
            self.connection.commit()

    def remove(self, date):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute('DELETE FROM day WHERE "date"=?', [date])
            self.connection.commit()

    def save(self, date, text):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute(
                (
                    'INSERT day ("date", "text") VALUES (?, ?)'
                    '  ON CONFLICT("date") DO UPDATE SET "text"=excluded."text"'
                ),
                [date, text],
            )
            self.connection.commit()

    def get_data(self):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute('SELECT "date", "text" FROM day ORDER BY "date"')
            return cursor.fetchall()


class DayEntry(Gtk.HBox):
    def __init__(self, date, text):
        super().__init__(spacing=10, border_width=10)
        self.label = Gtk.Label(label=date)
        self.pack_start(self.label, False, True, 0)
        textview = Gtk.TextView(border_width=10)
        self.pack_start(textview, True, True, 0)
        self.textbuffer = textview.get_buffer()
        self.set_text(text)
        self.remove_button = Gtk.Button(label="Remove")
        self.pack_start(self.remove_button, False, True, 0)

    def get_text(self):
        return self.textbuffer.get_text(
            self.textbuffer.get_start_iter(),
            self.textbuffer.get_end_iter(),
            True,
        )

    def set_text(self, text):
        self.textbuffer.set_text(text, len(text))

    def get_date(self):
        return self.label.get_label()


class Window(Gtk.Window):
    def __init__(self, database):
        super().__init__(title="MyWindow", border_width=5)
        self.set_default_size(500, 300)
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(
            Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS
        )
        self.add(scrolled_window)

        self.box = Gtk.VBox(spacing=10, border_width=15)
        scrolled_window.add(self.box)

        add_button = Gtk.Button(label="Add")
        self.box.pack_start(add_button, False, True, 0)
        add_button.connect("clicked", self.on_add_button_clicked)

        self.date_entry = Gtk.Entry(text="22.02.2024")
        self.box.pack_start(self.date_entry, False, True, 0)

        self.database = database
        self.date_to_day_entry = {}
        self.load_from_database()

    def run(self):
        self.show_all()
        Gtk.main()

    def load_from_database(self):
        for date, text in self.database.get_data():
            self.create_new_day_entry(date, text)

    def save(self, day_entry):
        self.database.save(day_entry.get_date(), day_entry.get_text())

    def remove_from_database(self, day_entry):
        self.database.remove(day_entry.get_date())

    def on_add_button_clicked(self, _widget):
        day_entry = self.create_new_day_entry(self.date_entry.get_text())
        if day_entry:
            self.save(day_entry)

    def on_remove_button_clicked(self, _button, day_entry):
        self.remove_from_database(day_entry)
        del self.date_to_day_entry[day_entry.get_date()]
        day_entry.destroy()

    def create_new_day_entry(self, date, text=""):
        if date in self.date_to_day_entry:
            self.date_to_day_entry[date].textview.grab_focus()
            return None

        day_entry = DayEntry(date, text)
        day_entry.textbuffer.connect(
            "changed", self.on_entry_changed, day_entry
        )
        day_entry.remove_button.connect(
            "clicked", self.on_remove_button_clicked, day_entry
        )
        self.box.pack_start(day_entry, True, True, 0)
        self.date_to_day_entry[date] = day_entry
        day_entry.show_all()
        return day_entry

    def on_entry_changed(self, _textbuffer, day_entry):
        self.save(day_entry)


def main():
    with Database(DATABASE_NAME) as database:
        database.create()
        app = Window(database)
        app.run()


if __name__ == "__main__":
    main()