OOP Ansatz zur Trennung von GUI Layout und Funktion

Fragen zu Tkinter.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Hallo,

habe gestern ein kleines Konvertierungstool für meine Arbeitsgruppe geschrieben. Als bisheriger PHP-Freund habe ich auf Python umgesattelt, weil ich hier GUI-Unterstützung habe. GUI ist aber für eine sinnvolle Nutzung des Tools Pflicht.

Ich bin also absoluter Python-Neuling und auch sonst eher nur Gelegenheitsskripter. Nun muss ich aber ein GUI anfertigen - besser gestern als heute - und möchte dabei das Layout von der Funktion trennen.

Als OO-Ansatz hatte ich mir erst überlegt eine Klasse anzulegen mit dem ganzen Layout und leeren Methodenrümpfen für die Aktionen (z.B. beiMausklick()) und diese Methoden dann in einer Unterklasse zu überschreiben bzw. mit Funktion zu füllen.

Dann kann ich aber von der Subklasse z.B. nicht mehr auf die Widgetkonfiguration zugreifen (z.B. button.config(text="Buttontext")).

Wie müsste ein OOP-Ansatz für die Trennung von Layout und Funktion mit Tkinter aussehen.

Ich könnte sicherlich noch ganz viel dazu lesen, aber ich brauche ja wahrscheinlich nur einen kleinen "Anstoß" und die Zeit drängt etwas.

Kann mir bitte jemand einen kleinen Codeblock geben, der mir das Prinzip klarmacht.

Vielen Dank.
pyStyler
User
Beiträge: 311
Registriert: Montag 12. Juni 2006, 14:24

boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Vielen Dank schonmal. Da habe ich aber doch noch Fragen:

Dieses Listing war es wohl auf welches Du mich aufmerksam machen wolltest:
http://ubuntuusers.de/paste/11311/

Demnach wird das GUI in der Klasse MainGui() definiert. Der Aufruf und die Bedienung scheint vor allem durch TkApp() bestimmt zu sein. DataBook() scheint nur die Daten zu handeln und Main() habe ich jetzt noch nicht ganz verstanden. Unter anderem weil ich manche Funktionen nicht nachvollziehen kann. Ich kann z.B. nirgends etwas wie pack() od. grid() finden, was doch für die Anzeige der Widgets wesentlich ist - oder?

Zeile 443: nameGet = self.maingui._entry_1.get()

Wo ist denn die Methode get() definiert? Ich kenne da bisher ähnlich eigentlich nur cget().

Überhaupt ist doch _xxx_ eher als privat anzusehen. Warum wird es dann genutzt?
BlackJack

@boid: Wenn Du GUI und Funktionalität trennen willst, dann schreib am besten erst einmal die Funktionalität komplett ohne GUI.

Wenn das fertig und getestet ist, kannst Du eine GUI drauf setzen.

`get()` ist dem Fall eine Methode von Tkinter-Entries.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Joo, das ist ja mein Problem.

Ich habe eine Klasse die die Datei handelt.

Eine weitere fügt die Daten zusammen (damit die Konvertierungen mehrerer Dateien in einer Excel-Tabelle zusammengefasst werden können).

Nun braucht die Arbeitsgruppe noch ein GUI. Nur wie mache ich das? Ich würde es schon irgendwie hinkriegen, aber ich möchte es eben gleich richtig und als OOP machen.

Das GUI selbst ist nicht schwer. 1 Listbox, 1 Dateiauswahl, 3 Radiobuttons würdens schon tun.

Ich habe bisher mehreres versucht und das beste scheint mir zu sein das GUI einfach in eine Klasse zu packen und damit ein GUI-Object zu instanzieren. Den Ansatz mit der Vererbung habe ich schon wieder verworfen. Trotzdem, weder so noch so erhalte ich die gleiche Funktionalität die ich habe wenn ich die einzelnen Handlungsroutinen direkt mit dem GUI verküpfe. Mit Handlung meine ich nicht die eigentliche Konvertierung, sondern das z.B. das Trennzeichen nach Auswahl der Listbox eben als Komma gesetzt wird. Oder das Öffnen des Dateiauswahldialoges nach klicken des entsprechenden Buttons.
BlackJack

Das klingt nach einer einfachen GUI-Klasse wo man die verschiedenen Einstellungen vornehmen kann und ein "Go"-Button, dessen `command` die Einstellungen von der GUI abfragt und die anderen Objekte entsprechend erzeugt und "startet". So etwas sollte man nicht komplizierter machen, als es ist. :-)
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Ja, nun ist es einfach. Aber wenn es mal komplizierter werden sollte wüßte ich doch schon gern was die grundlegendsten Richtlinien sind.

Es ist z.B. gut möglich das diese kleine Tool weiter wächst - auch wenn das dann weniger eilt. Ich möchte dann aber nicht nochmal den ganzen Code erstmal aufräumen müssen. Das habe ich eine Weile gemacht nachdem ich mich besser mit PHP auskannte und das möchte ich nicht nochmal machen.

Python kenne ich praktisch erst seit einem Tag. Was aber nicht heißt das ich null Ahnung habe. Ich habe halt vor allem bisher noch keine GUIs gemacht.

Würdest Du die GUI eher vererben oder eher instanzieren oder sonstwas und warum?
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

In den meisten Fällen ist es so:
Zuerst schreibst du die Datenklasse(n). Wenn alles läuft, kommt die Gui drauf: Diese sollte, da sie Events verarbeiten muss, die "oberste" Klasse sein. Du kannst also in deiner GUI-Klasse entweder eine Instanz deiner (Haupt)Datenklasse erzeugen und die Buttons ect. an deren Methoden binden, oder aber von der Datenklasse erben, dann hast du alles getrennt und doch zusammen (direkter Zugriff auf alles).
Wie du das machst kannst du dir selbst überlegen, es kommt halt darauf an, wie sehr GUI und Datenklasse zusammengehören.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Ok, das heißt aber das ich die GUI-spezifischen Handlungen nicht vom GUI trennen kann - oder?

Ich meine das so (vergleich mit einem CMS):
- es gibt einmal das was das Programm tun soll (Plugin eines CMS)
- es gibt das Layout des GUIs (HTML-Ausgabe)
- und es gibt die Funktionalität des GUIs (CMS selbst)

Ich kann also mit Tkinter die Programmlogik vom GUI kapseln, aber nicht die GUI-Logik vom GUI-Layout?
pyStyler
User
Beiträge: 311
Registriert: Montag 12. Juni 2006, 14:24

Hallo boid,

um was für einen Code handelt es sich den bei dir?
Ist es möglich es zu veröffntlichen? Dann könnte man überlgen, wie man es am besten lösen kann.

Gruss
pyStyler
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

Du kanns die Gui natürlich auch nochmal teilen. Das ist aber etwas übertrieben, wenn du keine sehr, sehr große Klasse hast. Zum einen kannst du die Gui in verschiedene Klassen unterteilen, zum anderen kannst du Metaklassen schreiben, die auf die Attribute und Methoden der EndGuiKlasse zugreifen, selbs t also nicht lauffähig sind. Das ist aber wie gesagt schon recht kompliziert und bei normalen GUIs eher verwirrend als empfehlenswert.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

Metaklassen schreiben, die auf die Attribute und Methoden der EndGuiKlasse zugreifen, selbs t also nicht lauffähig sind. Das ist aber wie gesagt schon recht kompliziert und bei normalen GUIs eher verwirrend als empfehlenswert.
Ok, es geht also doch. Habe demnach bloß noch nicht rausbekommen wie.

Ich lese aber auch das es sinnvoller ist bei in absehbarer Zeit überschaubaren GUI ruhig GUI-Logik und Layout in einer Klasse zu kapseln. Weil es sonst nicht leichter, sondern den gegebenen Umständen entsprechend schwerer wird.

Nun denn OOP generell und speziell bei Python werde ich wohl noch üben müssen. Ich habe es bei Vererbung z.B. noch nicht hinbekommen eine definierte Methode der Oberklasse in der Unterklasse nutzen zu können. Es geht halt immer nur zu überschreiben oder über einen Methodenaufruf des instanzierten Objektes der Unterklasse.

Was gilt es eigentlich bei py2exe mit Tkinter zu beachten? Das wäre dann der letzte Knackpunkt.

Hier mein bisheriger Code (werde eventuell weitere bei irgendeinem Paste-Dienst einstellen):

Code: Alles auswählen

import tempfile
import ConfigParser
import struct
import os
import glob


class Dat(object):
    """
    Die *.dat-Datei als Objekt
    @param datPath: Der Dateipfad der *.dat-Datei
    """
    
    Count = 0 # Anzahl der zu konvertierenden *.dat-Dateien

    def __init__(self, datPath):
        self.datPath = datPath # Dateipfad
        self.f = open(self.datPath, 'rb') # *.dat-Dateihandle
        Dat.Count += 1
        
        self.header = self.readHeader()
        self.data = self.readData()
        self.f.close()
        
    def __del__(self):
        Dat.Count -= 1

    def readHeader(self):
        datBytePosition = 0 
        tmp = tempfile.TemporaryFile()
    
        while True:
            line = self.f.readline()
            datBytePosition = datBytePosition + len(line)

            tmp.write(line)

            if line.strip() == "[DATA]":
                datDataStart = datBytePosition + 8
                break
    
        datHeader = ConfigParser.SafeConfigParser()
        tmp.seek(0)
        datHeader.readfp(tmp, "r")
        tmp.close()
        datHeader.set("DATA", "XSTART", str(datDataStart))

        return datHeader

    def readData(self):
        datXData = ()
        datYData = ()
        datZData = ()
        
        self.f.seek(int(self.header.get("DATA", "XSTART")))
        for i in range(int(self.header.get("GENERAL", "NPOINTS"))):
            datXData += (self.byteToFloat(self.f.read(4)),)
            #print str(self.f.tell()) + ":" + str(i) + " " + str(self.byteToFloat(self.f.read(4)))
        #print datXData
            
        self.header.set("DATA", "YSTART", str(self.f.tell()+10))

        self.f.seek(int(self.header.get("DATA", "YSTART")))
        for i in range(int(self.header.get("GENERAL", "NPOINTS"))):
            datYData += (self.byteToFloat(self.f.read(4)),)

        datData = (tuple(datXData), tuple(datYData), tuple(datZData))

        return datData

    def byteToFloat(self, bytearray):
        """
        Rechnet jeweils 4 bytes in Little-Endian Order (Windows) in float um
        @param bytearray: String aus 4 Bytes
        """
        return struct.unpack("f", bytearray)[0]
    
    def getData(self, axis):
        """
        @param axis: "X" od. "Y"
        """
        
        if axis == "Y":
            return self.data[1]
        else:
            return self.data[0]
    
    def getHeader(self):
        return self.header



class Dat2CSV(object):
    
    def __init__(self):
        self.allData = ()
        self.maxDataLines = 0
        self.maxDataFiles = ""
        self.delim = ";"
        
    def __del__(self):
        return None

    def addDatFile(self, f):
        """
        @param f: Dateipfad absolut od. relativ "./datei.dat"
        """
        csv = Dat(f)

        currentDatFile = (f[len(os.path.dirname(f))+1:]) # Filename
        currentDatDir = (os.path.dirname(f)) # Verzeichnispfad
        currentAllData = (currentDatFile, currentDatDir, csv.getData("X"), csv.getData("Y"))
        
        self.allData += (currentAllData,)
        
        self.maxDataFiles = int(Dat.Count)+1
        if len(self.allData[0][2])+1 > self.maxDataLines:
            self.maxDataLines = len(self.allData[0][2])

    def convAddFiles(self):
        """
        Konvertiert alle Dateien in eine CSV mit nur einer X-Spalte aus der ersten Datei
        """
        csvOutName = str(self.allData[0][1]) + "/convsumDat2CSV.csv"
        csvOut = open(csvOutName, "w")
    
        csvHeaderLine = "X" + self.delim
        for l in range (self.maxDataFiles):
            csvHeaderLine = csvHeaderLine + self.allData[l][0] + self.delim
        csvOut.write(csvHeaderLine + "\n")
        
        # Schreiben der Spalte
        for l in range (self.maxDataLines):
            # X-Werte
            csvLine = str(self.allData[0][2][l]) + self.delim

            # Schreiben der Zeile
            for f in range (self.maxDataFiles):
                if l < len(self.allData[f][3]):
                    csvLine += str(self.allData[f][3][l]) + self.delim
                else:
				    csvLine += self.delim

            csvOut.write(csvLine + "\n")
        
        csvOut.close()
        print "Die Ausgabe ist nach \"" + csvOutName + "\" erfolgt"

    def convFile(self):
        for f in range(self.maxDataFiles):
            csvOutName = str(self.allData[f][1]) + "/" + str(self.allData[f][0]) + ".csv"
            csvOut = open(csvOutName, "w")
            
            csvHeaderLine = "X" + self.delim + self.allData[f][0]
            csvOut.write(csvHeaderLine + "\n")
            
            for i in range(len(self.allData[f][2])):
                csvLine = str(self.allData[f][2][i]) + self.delim
                csvLine += str(self.allData[f][3][i])
                csvOut.write(csvLine + "\n")
            
            csvOut.close()
            print "Die Ausgabe ist nach \"" + csvOutName + "\" erfolgt"
        


class Menu(object):
    """
    Bedienungsmenu

    @author XXX
    @date 2008-01-26
    """
    
    def __init__(self):
		print "------------------------------------------------";
		print "Konvertiert *.dat von ZEISS Spektrometern in CSV";
		print "XXXX";
		print "------------------------------------------------";
		print "";

		# Konvertierungmodus
		print "Alles in eine CSV (1) oder getrennt (2)?";
		convMode = raw_input()
		if convMode != "1" and convMode != "2":
			convMode = "1"

		# Konvertierungsort mit den Dateien
		print "Pfad zu den *.dat:";
		convPath = raw_input()
		
		# Und los gehts
		self.dirListing(convMode, convPath)

    def dirListing(self, convMode, convPath):

        dat = Dat2CSV()
        datFiles = glob.glob(convPath + '*.dat')

        for datFile in datFiles:
            print datFile
            dat.addDatFile(datFile)

		# Konvertiert
        if convMode == "1":
			dat.convAddFiles()
        if convMode == "2":
            dat.convFile()

        del dat



main = Menu()
schlangenbeschwörer
User
Beiträge: 419
Registriert: Sonntag 3. September 2006, 15:11
Wohnort: in den weiten von NRW
Kontaktdaten:

boid hat geschrieben:Ich habe es bei Vererbung z.B. noch nicht hinbekommen eine definierte Methode der Oberklasse in der Unterklasse nutzen zu können. Es geht halt immer nur zu überschreiben oder über einen Methodenaufruf des instanzierten Objektes der Unterklasse.
oder was? Einfach die Methode der Überklasse aufrufen und self übergeben.
boid hat geschrieben:Was gilt es eigentlich bei py2exe mit Tkinter zu beachten? Das wäre dann der letzte Knackpunkt.
Du musst die ganzen tk-Sachen mit reinpacken, wenn das nicht automatisch passiert, was aber eigentlich geschehen sollte. Siehe auch
http://www.py2exe.org
http://www.py2exe.org/index.cgi/TixSetup (als Bsp. wie man Libs hinzufügt)
boid hat geschrieben:Hier mein bisheriger Code (werde eventuell weitere bei irgendeinem Paste-Dienst einstellen):
Diesen könntest du auch schon auslagern. Oder wenigstens als Pythoncode kennzeichnen.

Hier noch ein Beispiel zur Trennung von Daten, Logic und GUI:
http://paste.pocoo.org/show/24920/
Ich denke, man sieht gut, dass es geht, aber auch, das es nicht wirklich toll ist. Man kann auch die Logic von der Data Klasse erben lassen. Bei der GUI kommts drauf an, was du als erste Superclass angibst, damit die richtigen Methoden verfügbar sind.
BlackJack

@boid: Vorsicht, es kommt ein wenig Kritik.

`__del__()`-Methoden sind unnütz bis schädlich. Es wird nicht garantiert, dass die überhaupt jemals aufgerufen werden, und wenn sie vorhanden sind, kann es Situationen geben, in denen die Objekte nicht mehr vom Garbage-Collector freigegeben werden.

Mit dem `StringIO`-Modul käme man in `readHeader()` ohne temporäre Datei auf der Festplatte aus.

Statt der Tupel sollte man besser Listen verwenden. Das sieht nicht nur besser aus, als dass "anhängen" von einelementigen Tupeln, sondern ist auch performanter, weil nicht jedes mal die ganzen Daten kopiert werden müssen.

Anstelle von `struct`, könnte man mit dem `array`-Modul etwas komfortabler mehrere gleichartige Binärdaten lesen.

Nach einem kurzen Blick über den Quelltext scheint das `ConfigParser`-Objekt auserhalb von `Dat` nicht gebraucht zu werden. Der Quelltext wäre wohl kürzer und verständlicher wenn man die benötigten Daten dort einfach heraus holt und an Namen bindet, anstatt noch mehr hinein zu stecken.

`Dat.byteToFloat()` benutzt `self` nicht, wäre also eher eine Funktion.

`Dat.getHeader()` wird nicht verwendet. Wäre auch etwas überflüssig, weil man in Python normalerweise keine Getter und Setter schreibt, sondern direkt auf die Attribute zugreift. Falls man dann doch mal etwas komplexeres bei so einem Zugriff machen muss, gibt es Properties (`property()`).

In `Dat2CSV` werden wieder Tupel als Listen missbraucht.

`addDatFile()` ist reichlich kompliziert. Es gibt `os.path.split()` um Pfadnamen und Dateinamen voneinander zu trennen. Und alles was in `currentAllData` in einem Tupel zusammen gefasst wird, ist im Grunde redundant, weil die Daten alle im `Dat`-Objekt stecken. Dort könnte man sie auch einfach als Attribute zur Verfügung stellen. Womit die `addDatFile()` eigentlich zu einem ``self.dats.append(Dat(f))`` zusammen schrumpfen könnte. Die maximale Länge würde ich hier noch nicht ermitteln.

`convAddFiles()` und `convFile()` sind mit der Inederei zu komplex. Bitte wo möglich umschreiben, so dass direkt über die Elemente iteriert wird. Ausserdem habe ich das Gefühl, dass man die zu einer Methode zusammen fassen kann. Das `csv`-Modul bringt sicher auch etwas Übersichtlichkeit in die Sache.

In `Menu.__init__()` sind ein paar überflüssige Semikolons.

Insgesamt ist das `Menu` keine Klasse (wert). Wahrscheinlich käme man auch ganz gut ohne `Dat2CSV`-Klasse aus, was ja eher ein Funktionsname ist.

Und wie immer der Hinweis auf den Style Guide.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

ok, vielen Dank. Die letzten zwei Beiträge haben mir sehr geholfen.

Das GUI habe ich zwar gestern abend fertig gemacht auf eine Weise die mir persönlich nicht so gefällt, aber für die Größe des Projektes vermutlich angemessen ist. Nun muss ich daraus nur noch eine exe zimmern.

Ja, die Klassen enthalten tatsächlich ein paar ordentliche Designfehler. Tupel hatte ich extra gewählt, weil diese wohl performanter sein sollten als Listen. Wenn ich aber die Daten in zwei Klassen vorhalte und ständig ganze Tupel hin und her kopiere, brauche ich wirklich nicht über Performanz nachzudenken.
Die get/set Methoden habe ich gemacht, weil ich mittlerweile Funktionen wie readHeader in __readHeader umbenannt habe.

Da werde ich wohl nochmal nachbessern müssen. Manche Funktionen/Klassen wie array kannte ich bisher noch gar nicht.

Es wäre ja auch ein Wunder gewesen wenn ich an einem Tag eine neue Sprache perfekt beherrschen würde, zumal ich den OOP-Ansatz erst kürzlich intensiver nutze. Ich habe halt wirklich nur von PHP abstrahiert was ich nun ungefähr brauche und mal bei Python Openbook reingeschaut wie das ungefähr geht.
BlackJack

@boid: Tupel sind im Grunde wie Listen, nur dass man sie nicht verändern kann. Es gibt einen kleinen Speicherplatzvorteil, weil Tupel, da nicht veränderbar, keinen Platz für spätere `append()`-Operationen "überbelegen", aber das ist hier ziemlich teuer erkauft, weil Du das `append()` ja recht häufig brauchst und es bei Tupeln halt von der Laufzeit echt ungünstig ist.

Auch bei einer `__readHeader()` braucht man keine extra `get`-Methode wenn die das Ergebnis an das Attribut `header` bindet. Wobei die zwei Unterstriche etwas übertrieben sind, wenn nicht gar schlechter Entwurf. Zwei führende Unterstriche sind dazu da, um Namenskollisionen in Unterklassen oder "Mixin"-Klassen zu vermeiden und nicht um so eine Art `private` wie in C++/C#/Java zu erzwingen. `private` ist in Python per Konvention *ein* führender Unterstrich. Das sagt anderen Programmierern, dass ist ein Implementierungsdetail, benutzen auf eigene Gefahr.

Meine Kritiken sind meistens kurz, knapp und technisch. Bitte nicht in den falschen Hals bekommen, ist wirklich nett gemeint und nicht als Angriff. Natürlich kann man nicht alles in der Standardbibliothek kennen, gerade als Einsteiger in die Sprache.
boid
User
Beiträge: 9
Registriert: Mittwoch 30. Januar 2008, 17:10

keine Sorge, habe es nur als konstruktive Kritik aufgefasst und auch als solche empfunden.
BlackJack

Mal ein völlig ungetesteter Anfang, wie man das mit weniger Klasse(n) ;-) und mehr und abstrakteren Funktionen machen kann: http://paste.pocoo.org/show/25093/

Ist vielleicht genauso unverständlich wie dreifach indirekter Indexzugriff in tief verschachtelten Schleifen, aber dafür kompakter. :-D
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

BlackJack hat geschrieben:Meine Kritiken sind meistens kurz, knapp und technisch. Bitte nicht in den falschen Hals bekommen, ist wirklich nett gemeint und nicht als Angriff.
Daraus solltest du dir eine Signatur machen. :wink:
Offizielles Python-Tutorial (Deutsche Version)

Urheberrecht, Datenschutz, Informationsfreiheit: Piratenpartei
Antworten