"MVC" - Theorie vs Praxis ..

Fragen zu Tkinter.
Antworten
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

Hallo,

Ich schreibe mir gerade eine Anwendung die 2. Hauptfunktionen besitzt:

a) "Steuerung" einer Fremdanwendung via OLE.
- Die Fremdanwendung berechnet Werte und speichert diese in eine Datei ab.
- die Berechnung der Werte kann ich via einer Parameter.csv beeinflussen
(diese Parameter.csv wird bei jedem Aufruf der Fremdanwendung innerhalb der Fremdanwendung gelesen und ausgewertet.
- Mittels einer Steuerungs.csv lege ich pro Aufruf der Fremdanwendung fest wie die Parameter.csv aussieht, dh wie die Parameter der Berechnung sind.

b) editieren der Steuerungs.csv Datei
- löschen von einzelen Sätzen
- editieren der einzelnen Spalten/pro Satz

Die Anwendung schreibe ich mittels Tkinter, andere GUI-Frameworks kommen aus diversen Gründen noch nicht in Frage.Ich stehe vor dem grundlegendem Problem wie das nun als "MVC" umsetze werden kann. Als Ansatz habe ich mir mal das wie folgenden überlegt
Model:
- Datensatz
- DS-Container (Tabelle für die Datensätze)
View:
-Tkinter-Window
- Dialog Change_Parameter:
"Zeige die Datei als Tabelle an"
"der Benutzer kann die Daten ändern, löschen usw"
- Dialog Automation
"Anzeige diverse Zusammenfassung der Steuerungs.csv -> x Parameter gibt es, die Datensätze sind konsisten etc"
" Fortschritt des Ablaufs der Automation"
.....
Controller:
tja und hier fehlts mir ... soweit ich das das MVC verstanden habe wären hier dann wohl "objekte" wie:
-> Der View meldet "Anwender möchte neuen Datensatz erzeugen"
Der Controller sagt dem DS-Container: "Hey, fügt doch mal `nen neuen Datensatz ein"
Nachdem der DS-Container das getan hat, gibts in irgendeiner Form `ne Rückmeldung an den Controller und dieser meldet es dann dem View das er seine Anzeige updaten soll ?

-> Der View meldet "Der Anwender hat in Zeile 1 und Spalte 10 etwas eingegeben"
Der Controller sagt dem DS-Container: Ändere das 10.Feld des Datensatzes in der 1 Zeile
Der DS-Container meldet zurück an den Controller
Der Controller meldet dem View: "mach mal, zeig die Veränderung" an

tja, aber was bedeutet den das nun konkret ? :?

Fall1: neuer Datensatz:
- Der "callback" des Buttons auf dem Fenster ruft eine Methode im Controller-Objekt auf.
- Diese Methode ruft eine Methode des DS-Containers auf in dieser dann das neue Datensatzobjekt hinzugefügt wird
- Danach wird eine Methode im View aufgerufen um einen neuen Datensatz anzuzeigen

u]Fall2: Änderung:[/u]

- Der "callback" des Entry-Widgets ruft eine Methode im Controller-Objekt auf.
aehmm ... möglicjh als StrVar() traceback oder Event-Bindung
Der Controller will nun vom DS-Container einen Datensatz (??)
Der Controller sagt dem Datensatz-Objekt:"Prüf mal ob die Eingabe OK ist" (???)
Der Controller sagt dem Datensatz-Objekt:"Ändere den Wert, da dieser OK ist" (????)

oder

Der Controller übergibt den ganzen Schlammassel direkt an das Datensatzobjekt ... *g und das DS-Container Objekt ist ganz beleidigt weil es nicht gefragt wurde ????


*seufz ... kann mir da vielleicht mal jemmand auf die Sprünge helfen ???
BlackJack

@schnitt_tt: In der Praxis würde ich mal sagen ist ein komplett implementiertes MVC-Muster hier mit Kanonen auf Spatzen geschossen. Ich würde hier einfach nur Anwendungslogik von GUI-Code trennen. Aus MVC-Sicht kann dabei Controller-Code sowohl im Model als auch im View landen. Das wirklich in drei Objekte aufzuteilen macht IMHO nur Sinn wenn es von mindestens einem der drei Teile mehr als eine Implementierung gibt.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@BlackJack/all

als Model/View Version würde ich das in etwa so umsetzen. Was mich dabei etwas irritiert das ich die Daten aus der Datei 2x vorhalten muß.,da ich gerne die Logik aus dem View heraushalten möchten(gegenfalls steige ich mal auf ein anderes GUI-Framework um ...hmmm ob das dann soooo reibungslos funktioniert glaub`ich eigentlich nicht).

Kann es sein das der Controller-Part durch die StrVar() Variablen durch Tkinter schon übernommen wird ? hmm kann eigentlich nicht sein da ja lediglich der View "beobachtet wird, aber nicht das Model .... verflixt

Code: Alles auswählen

import tkinter     as tk
import tkinter.ttk as ttk
# Klassen MODEL
#-----------------------------------------
class myParam(object):
    """Datenobjekt wie in Datei.
    """
    def __init__(self,val,val2):
        self.feld1 = val
        self.feld2 = val2

    def calc_irgendwas(self):
        return self.feld1 * 5

class myTable(list):
    """Enhält die Datensätze aus der Datei
    """
    def __init__(self):
        super().__init__()

    def item_insert(self, data):
        #Datesätze eintragen
        self.append(data)
        pass
    def get_anzahl(self):
        return len(self)

    def summary(self):
        #loop über Datensätze, ermittle Summe in Spalte x usw.....
        print('Anzahl Datensätze:' , self.get_anzahl())
        print("Summe in Spalte x" , 'ist ok')


# Klassen View
#-----------------------------------------
class DialogEditParameter(tk.Toplevel):
    """Dialog zum Anzeigen/Ändern von Datensätzen.

    data: inital Tabelle
    """
    def __init__(self,parent,table_obj):
        """Dialog-Fenster zum ändern von Datensätzen,

        table_obj = Initial-Befüllung des Fensters aus Table Typ myTyble
        """
        tk.Toplevel.__init__(self, parent)
        self.transient(parent)
        self.parent =    parent
        self.frame       = tk.Frame(self)
        self.objekte     = myTable()
        self.gui_objekte = []
        self.frame.grid()
        self.grab_set()
        #initiales Einfügen von Daten
        for idx,datensatz in enumerate(table_obj):
            self.objekte.append(datensatz)
            self.create_widgets(self.frame,idx)

        self.btn = ttk.Button(self.frame,text='save',command=self.cb_save)
        self.btn.grid()    
        self.btn = ttk.Button(self.frame,text='insert',command=self.cb_insert)
        self.btn.grid()
        self.btn = ttk.Button(self.frame,text='show',command=self.cb_show)
        self.btn.grid()

        self.wait_window(self)
    def cb_save(self,*args):
        for gui_objekt in self.gui_objekte:
            #speichern in Datei oder Datenbank
            pass

    def cb_insert(self,*args):
        new_item = myParam(0,0)
        self.objekte.item_insert(new_item)
        aktuell= self.objekte.get_anzahl()
        print("aktuell in objekt-liste:", aktuell)
        zeile = GUIParam(self.frame, new_item.feld1 , new_item.feld2, aktuell)

    def cb_show(self):
        self.objekte.summary()

    def create_widgets(self,frame,idx):
        #erzeugen der Datensaetze auf der GUI
        # frame: Parent-Widget
        # idx:   Zeile in der die Objekte auf der Gui angezeigt werden sollen
        for object in self.objekte:
            widget = GUIParam( frame,
                               object.feld1,
                               object.feld2,
                               1
                             )
            self.gui_objekte.append(widget)                 



class GUIParam(object):

    def __init__(self,parent,one,two,zeile):

        self.parent   = parent
        self.checkbox = tk.IntVar()
        self.feld1    = tk.StringVar()
        self.feld2    = tk.StringVar()

        self.gui_checkbox = ttk.Checkbutton(master=parent,variable=self.checkbox)
        self.gui_checkbox.grid(row=zeile,column=1)

        self.gui_feld1 = ttk.Entry(master=parent,textvariable=self.feld1)
        self.gui_feld1.grid(row=zeile,column=2)

        #befüllung
        # ----------------------
        self.feld1.set(one)
        self.feld2.set(two)

        #Trace / callback
        # ----------------------
        self.feld1.trace_variable('w', self.cb_change)
    def cb_change(self,a,b,c):
        eingabe = self.feld1.get()
        pass

if __name__ == "__main__":
    root = tk.Tk()
    root.geometry('800x500+300+10')
    frame = ttk.Frame(root)
    frame.grid()
    it_table = myTable()
    data = myParam(1.22,133)
    it_table.item_insert(data)

    dialog = DialogEditParameter(frame, it_table)

    root.mainloop()
BlackJack

@schnitt_tt: In der `cb_save()` steckt Programmlogik. Das Speicher der Daten ist keine GUI-Aufgabe. Oder anders gesagt wenn Du Die GUI wechselst, müsstest Du den Code zum Speichern neu schreiben obwohl der ja eigentlich gar nicht von der GUI abhängt.

Beim `GUIParam` ist es in der Tat ungünstig, dass Du die Werte dort noch einmal unabhängig vom Model-Objekt speicherst. Da würde man eher einen Verweis auf das Model-Objekt behalten. Dann kann man den Wert dort auch gleich neu binden, wenn er vom Benutzer geändert wurde.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@BlackJack

cb_save()

hätte ich dann wie folgt implementiert: Die myTable Klasse bekäme eine speichern Methode

Code: Alles auswählen

    def cb_save(self,*args):
        # alle Einträge löschen,
        # dann die GUI-Objekte mappen und neu eintragen
        # dann die Tabelle als File etc speichern
        del self.objekte[:]
        for gui_objekt in self.gui_objekte:
            #speichern in Datei oder Datenbank
            save = myParam(gui_objekt.feld1,
                           gui_objekt.feld2
                          )
            self.objekte.item_insert(save)

        self.objekte.save_to_file()
GUIParam
das habe ich jetzt nicht ganz verstanden .. Mein Bauch sagt mir auch die ganze Zeit das wenn ich Daten 2x vorhalte, einmal als myParam und das andere mal als GUIParam, das ich immer zwischen diesen beiden Objekte mappen muß bzw diese syncron halten muß... ist gefühlsmäßig kompliziert bzw fehleranfällig.

Die andere Seite bzw mein aktuelles Verständniss von MV ist jedoch:

Ich habe Daten (Model) die irgendwie dargestellt (View) werden sollen, dh das Model übergibt diese Daten als myParam und der View stellt sie dann als Widgets dar (GUIParam) ... *grübel ...
Falls ich zb die Daten in einem Tree darstellen will dann muß ich halt einen Mapper für dieses TreeWidget schreiben, was ja in den Aufgabenbereich des Views fällt, oder ?

hmmm ich vermute du siehst das anderst ?
Da würde man eher einen Verweis auf das Model-Objekt behalten
<- das kapier ich dann wohl nicht ! :oops:

thx
BlackJack

@schnitt_tt: Beim `cb_save()` würdest Du das komplette Model leeren, mit Daten aus der GUI befüllen und dann erst speichern!? Da ist dann ja doch letztendlich die komplette Datenhaltung in der GUI. Genau das möchte man nicht. `cb_save()` sollte eher so aussehen:

Code: Alles auswählen

def cb_save(self, *args):
    self.objekte.save()
Das Modell enthält zu dem Zeitpunkt alle aktuellen Daten — sonst stimmt an dem Programm etwas nicht. Wenn Daten verändert werden, dann werden diese Änderungen im Modell vorgenommen. Die Daten die *dort* stehen ist das womit gearbeitet wird.

Bei `GUIParam` hat Dein Bauch recht — da sollten die Daten aus dem Modell nicht noch einmal *extra* gespeichert werden. Im Moment hast Du die Daten dreimal: Im Modell `MyParam`, im GUI-Objekt `GUIParam`, und in den Textfeldern (o.ä.) des GUI-Toolkits. Als einzelne Attribute auf `GUIParam` sind sie zu viel. Da sollte es nur einen Verweis auf das Modellobjekt geben. Ein `GUIParam`-Exemplar kennt das `MyParam`-Exemplar dass es darstellt, kann das also nach den Werten befragen und auch Werte dort verändern. Wenn solche Veränderungen jetzt an anderer Stelle ”bemerkt” werden sollen, müsste man noch das Observer-Entwurfsmuster einbauen. Da ist die Frage wer von welcher Änderung in Kenntnis gesetzt werden soll. Das kann man beliebig generisch/kompliziert machen.

Bei Deinem MV-Verständnis formulierst Du das Model ein wenig zu aktiv. Das übergibt keine Daten an dem View – der View holt sich die Daten vom Model.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@BlackJack

*seufz, keine Ahnung wo mein Denkfehler ist bzw vielleicht Stelle ich das auch falsch dar.

also, GUIParam ist eigentlich dazu gedacht die Daten aus dem Model als Widgets darzustellen, bzw es ist die Möglichkeit des Views Daten aus dem Model darzustellen. Hmm, oder denke ich bei Model wohl zusehr an Datenbankmodel bzw Daten aus der Datenbank ermitteln, diese dann anzeigen, wenn diese geändert werden, dann die Änderungen wieder zurückschreiben .... *grübel

well, ich versuchs nochmal:

Mein Model: soll Datensätze beinhalten die in einem Container "verwaltet" werden (myParam,MyTable)
Mein View: Stellt diese Tabelle mit Datensätze dar, hierbei werden zbl. Zusatzfelder benötigt um zb. Datensätze zu markieren damit man diese löschen kann.(hat mit meinem Model nichts zu tun. oder liegt hier der falsche Denkansatz)

Im View werden Änderungen am Datensatz über Tkinter.StrVar() bzw über einen Trace auf diese Variable erkannt und "gehändelt". Falls ich meinem Model diese Tkinter.StrVar() zuweise(Ich denke damit meinst du das referenzieren auf das Model), binde ich mein Model an meinen View... *grummel das ist auch nicht richtig ....
Deswegen komme ich auch auf den Trichter immer zwischen Model-Objekten auf View-Objekte zu mappen
BlackJack

@schnitt_tt: Natürlich darfst Du Deinem Modell keine `Tkinter.StrVar()`\s zuweisen, aber deren *Inhalt* soll doch in das Modell übertragen werden.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@BlackJack

hmm das mache ich doch mit cb_save(). Ich muß halt immer zwischen GUI-Objekt(n) und Model-Objekte(n) mappen, aber da führt doch kein weg vorbei oder ? Ich wüßte nicht wie ich sonst die Änderung vom Widget ins Model bekomme außer ich bindet das Model über tkinter.StrVar() an die View -> aber das soll ja vermieden werden.

-> Der Anwender hat alle Änderungen erledigt.
Anwender drückt button: "Save"
- lese alle GUIParam-Objekte (sprich alle Datensätze und alle Änderungen die im View gemacht wurden)
- und speichere genau diese in das Model (zuvor im Model alle löschen)
- teile dem Model mit das die Datensätze auf Festplatte gespeichert werden sollen

während der Anwender den Dialog offen hat und eine Änderung vornimmt
Anwender ändert ein Feld des Datensatzes
- anderer callback => cb_change_row

Code: Alles auswählen

    def cb_change_row(self,*args):
        #Hier vielleicht noch die Eingabe prüfen - Aufruf an das Model
        self.cb_save('ohne_speichern_auf_Platte')
BlackJack

@schnitt_tt: Das Modell ist zur Datenhaltung da. Wenn Du die Daten dort komplett ausliest und in die GUI kopierst, und dann beim Speichern erst das Modell komplett leerst und mit den Daten aus der GUI neu befüllst, dann hast Du die Datenhaltung effektiv in der GUI und nicht im Modell.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@BlackJack
könnte natürlich auch lediglich den Datensatz aus dem Model löschen der eine Änderung bekommen hat und in dann neu einfügen .... um noch tiefer zu gehen, dann nur die Spalte in der die Änderung vorkam ...

das Prinzip bleibt aber doch gleich:

View holt sich aus Daten dem Modelobjekt (Model hat hierfür eine Methode)
View erstellt ein Widget und stellt die Daten in diesem Widget bereit
View erkennt eine Änderung
View ruft eine Model-Methode auf: "Ändere Feld1 in ParamObj

was anderes mache ich doch auch nicht .... :shock:


bzw
*verzweifel .... wenn das so nicht in Ordnung ist wie soll ichs denn dann vorsehen :cry:
deets

Das entscheidende ist, dass man eine Beziehung zwischen einem Model und potentiell mehreren Views herstellen kann. Und dazu braucht man im Grunde dann immer ein Event-Modell, damit das geht. Und dann ist es halt noch die Frage, wie granular das ist. Reicht ein generelles "model changed" event, oder geht man auf feinere Aenderungen von zB einem Knoten eines Baumes ein.

Auch die Aenderungen selbst sollten Events sein. Das ermoeglicht dann gleich noch die Bereitstellung von Undo ueber ein Command-Pattern.

Und last but not least eine moeglichst deklarative Bindung eines Modells an einen View.

Andere GUI-Frameworks wie zB Cocoa unter OSX oder auch Qt unterstuetzen das. Fue Qt zB hier:

http://doc.qt.nokia.com/4.3/qabstractitemmodel.html

AFAIK kennt TK sowas nicht als solches. Da muesstest du dir dann schon was basteln, das dem nahe kommt.
BlackJack

@schnitt_tt: Alles wegwerfen und komplett neu aufbauen und "Ändere Feld1 in ParamObj" ist für mich schon etwas anderes. Das ist als würde man eine Datenbank komplett auslesen und für Veränderungen dann immer alle Datensätze löschen und neu schreiben.

Wie Du das jetzt genau machen solltest kann man schlecht sagen. Dazu fehlen einfach die konkreten Anforderungen, die das erfüllen muss. Müssen zum Beispiel die `MyParam`-Objekte sein? Denn da stellt sich ja auch die Frage ob, wann, und wie die Veränderungen weiter melden. Und ob und wie die informiert werden, falls sie ungültig werden, weil zum Beispiel das Container-Objekt geleert wurde.
schnitt_tt
User
Beiträge: 10
Registriert: Sonntag 15. Mai 2011, 09:12

@Deets,
thx, werde ich mir mal bei Gelegenheit anschauen .. spätestens wenn QT für mich akut wird *G

@Blackjack
leider kann ich keine Bilder hochladen um die konkreten Anforderung besser zu erläutern, bzw wo man diese hosten kann -> kein Ahnung
@schnitt_tt: Alles wegwerfen und komplett neu aufbauen und "Ändere Feld1 in ParamObj" ist für mich schon etwas anderes. Das ist als würde man eine Datenbank komplett auslesen und für Veränderungen dann immer alle Datensätze löschen und neu schreiben.
dagebe ich dir vollkommen recht. Für meine Fall jedoch imho ok (max 20 Zeilen in der Tabelle) -> erleichtert die umsetzung, alles raus, das neue rein ist einfacher wie ... "suche mir die richtige Zeile, die richtige spalte und ändere diese".
Aber das Prinzip war mir nicht ganz klar:
Model-Objekte halten die Daten vor , Der View holt sich vom Model die Daten und stellt sie in einem eigenem Objekt dar (widget) usw ...

thx schnitt
Antworten