Frage zu objektorientiereter GUI

Fragen zu Tkinter.
Antworten
sommer87
User
Beiträge: 6
Registriert: Freitag 10. Juni 2011, 16:24

Hi,

ich bin noch recht neu im python programmieren und vorallem mit der GUI.
Allso bitte nicht gleich steinigen, wenn die Frage doof ist ;) Konnte nur bisher leider nichts dazu finden...

Mein Problem ist folgendes:
Ich habe ein Programm, dass aus vielen Spalten mit Buttons besteht. Durch klicken auf einen Button kommt
dann immer eine neue Spalte dazu. Dazu habe ich eine eine Klasse geschrieben, in der die Spalte definiert ist,
bei der ich mittels Konstruktor alle Buttons erzeugen kann. Dabei habe ich das Problem, dass ich irgendwie
das Frame, in dem die Spalte platziert werden muss in der Klasse verfügbar machen muss um es anzugeben.
Meine bisherige Lösung ist hier, dass ich dem Konstruktor das Frame von der main Klasse übergebe und es
dann immer weiterreiche. Funktioniert, ist aber finde ich ziemlich unschön.

Habt ihr eine Idee, wie man das irgendwie besser hinbekommen kann? Gibt es dazu irgendeinen schönen Weg,
oder ist mein Vorgehen an sich schon zu hässlich?

Das Prinzip mit der Klasse, in der alles Buttons gesetzt werden würde ich nur ungerne aufgeben, da die Spalten
als Adjazenzliste verwaltet werden, was es für mich leichter macht.
Das gleiche Problem besteht mit dem canvas, in dem alles Platziert wird. Da sollen Scrollbalken erscheinen, sobald
die Breite der Buttons zu groß wird. Da ich aber nur innerhalb der Klasse Spalten erzeuge bekommt main davon nix mit
und das canvas kann nicht aktualisiert werden. (Hab es erstmal über seqCanvas.after(1000, update_label) alle Sekunde
aktualisiert, was ich ziemlich überflüssig finde...

Schon mal danke für eure Hilfe!

Viele Grüße,
sommer87

Hier ein Minimalbeispiel:
Hauptprogramm in dem die Oberfläche definiert wird (gesamt Anordnung / Scrollbalken etc...)

Code: Alles auswählen

import Tkinter as tk	# Abkuerzung fuer Tkinter
from Tkinter import *
import tkMessageBox

# eigene Klassen
from sequenzOptions import SequenzOptions

root = tk.Tk()

seqCanvas = Canvas(root,width=__seqCanvasWidth,height=__seqCanvasHeight,xscrollcommand=seqHscrollbar.set)
seqCanvas.grid(row=0,column=1,stick=N+S+E+W)
seqHscrollbar.config(command=seqCanvas.xview)

root.grid_rowconfigure(0,weight=1)
root.grid_columnconfigure(0,weight=1)

seqFrame = Frame(seqCanvas)
seqFrame.rowconfigure(1,weight=2)
seqFrame.columnconfigure(1,weight=2)

# Zeichne seqCanvas mit Scrolloption
seqCanvas.create_window(0,0,anchor=NW,window=seqFrame)
seqFrame.update_idletasks()
seqCanvas.config(scrollregion=seqCanvas.bbox("all"))

SequenzOptions(1,seqFrame,seqName="Hallo")

root.mainloop()
Und die Klasse für die Buttons:
seqFrame wird übergeben um darin das seqConfFrame (und noch einiges mehr) zu platzieren.

Code: Alles auswählen

import Tkinter as tk    # Abkuerzung fuer Tkinter
from Tkinter import *
import tkMessageBox

class SequenzOptions(object):

	# Konstruktor
	def __init__(self,seqId,seqFrame,prevSeq=None,nextSeq=None,seqName="",seqTime=0):
		self.seqId	= seqId
		self.seqFrame	= seqFrame		# muss immer uebergeben werden zur Platzierung

		self.seqConfFrame		= Frame(seqFrame)
		self.seqConfFrame.rowconfigure(1,weight=1)
		self.seqConfFrame.columnconfigure(1,weight=1)
		self.seqConfFrame.grid(row=0,column=self.seqId)
		self.seqConfFrame.update_idletasks()
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hallo sommer87

Verstehe noch nicht genau was du willst. Habe hier etwas zusammengebraut was noch verbessert, verfeinert und erweitert werden kann:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial 
import Tkinter as tk    # Abkuerzung fuer Tkinter
import tkMessageBox

class AutoScrollbar(tk.Scrollbar):
    
    def set(self, lo, hi):
        if float(lo) <= 0.0 and float(hi) >= 1.0:
            # grid_remove is currently missing from Tkinter!
            self.tk.call("grid", "remove", self)
        else:
            self.grid()
        tk.Scrollbar.set(self, lo, hi)
        
    def pack(self, **kw):
        raise TclError, "cannot use pack with this widget"
        
    def place(self, **kw):
        raise TclError, "cannot use place with this widget"

class ScrolledCanvas(tk.Canvas):
    
    def __init__(self, parent, **keys):
        
        self.frame = tk.Frame(parent)

        xscrollbar = AutoScrollbar(self.frame, troughcolor='gray',
            orient='horizontal')
        xscrollbar.grid(row=1, column=0, sticky='we')

        yscrollbar = AutoScrollbar(self.frame, troughcolor='gray')
        yscrollbar.grid(row=0, column=1, sticky='ns')
        
        tk.Canvas.__init__(self, self.frame, xscrollcommand=xscrollbar.set,
            yscrollcommand=yscrollbar.set, bd=0,
            highlightthickness=0, **keys)
        self.grid(row=0, column=0, sticky='nswe')
  
        xscrollbar.config(command=self.xview)
        yscrollbar.config(command=self.yview)

        self.frame.grid_rowconfigure(0, weight=1)
        self.frame.grid_columnconfigure(0, weight=1)
        
    def pack(self, **keys):
        self.frame.pack_configure(**keys)

    def configure(self, **keys):
        self.frame.configure( **keys)

    def update(self):
        self.update_idletasks()
        
        #~~ Skaliere die Ziehleisten auf den neuen Canvas-Inhalt
        x0, y0, x1, y1 = self.bbox('all')   
        region = (x0, y0, x1, y1)
        self.config(scrollregion=region)
        
        #~~ Setze die Zieleisten in Ausgangslage
        self.xview('moveto', 0.0,None)
        self.yview('moveto', 0.0,None)

class SequenzConfFrame(tk.Frame):
    
    def __init__(self, parent, callback, **keys):
        
        self.callback = callback
        
        tk.Frame.__init__(self, parent, **keys)
        
        self.buttons = list()
        
    def add_button(self, button_name, **keys):
        
        cmd_button = partial(self.callback, button_name)
        
        button = tk.Button(self, text=button_name, bd=1, highlightthickness=0,
            command=cmd_button)
        button.pack(padx=5, pady=2)
        
        self.buttons.append(button)

def button_callback(button):
    print button
    
def app():
    app_win = tk.Tk()

    PLANE_BG = 'steelblue3'

    #~~ Erzeuge eine ziehbare Canvas
    plane = ScrolledCanvas(app_win, bg=PLANE_BG)

    #~~ Erzeuge den Hauptrahmen für alle Sequenzrahmen
    main_frame = tk.Frame(plane, bg=PLANE_BG)
    main_frame.pack(fill='both', expand='yes')

    #~~ Platziere den Hauptrahmen auf die Canvas
    plane.create_window(0,0,anchor='nw',window=main_frame)
    plane.pack(fill='both', expand='yes')

    #~~ Erster Sequenzrahmen
    seq_frame_1 = SequenzConfFrame(main_frame, button_callback,
        bg=main_frame['bg'])
    seq_frame_1.pack(side='left')
    #~~ Platziere Schalfächen im ersten Sequenzrahmen
    for index in xrange(10):
        button = seq_frame_1.add_button('Seq-1: Button-%d' % index)

    #~~ Zweiter Sequenzrahmen
    seq_frame_2 = SequenzConfFrame(main_frame, button_callback,
        bg=main_frame['bg'])
    seq_frame_2.pack(side='left')
    #~~ Platziere Schalfächen im zweiten Sequenzrahmen
    for index in xrange(10):
        button = seq_frame_2.add_button('Seq-2: Button-%d' % index)
        
    plane.update()

    app_win.mainloop()

if __name__ == '__main__':
    
    app()
Gruß wuf :wink:
Take it easy Mates!
sommer87
User
Beiträge: 6
Registriert: Freitag 10. Juni 2011, 16:24

Hi,

erstmal vielen Dank für dein viele Mühe.

Die Scrolloption hatte ich auch in etwa so gelöst, hatte es nur nicht mit angegeben.

Hab ein paar Fragen zu deinem Code (wie schon geschrieben, bin erst am anfangen mit python):
Du hast in den einzelnen Klassen von tk.canvas etc geerbt. Ich hab irgendwo gelesen, dass man besser "new style"-Klassen verwenden soll, die nur von object erben. Hatte es auch mal so wie du versucht, hat aber viele (mir damals unerklärliche) Fehler geworfen, weshalb ich es dann gelassen habe. In wie weit hat das Vor-/Nachteile?

Die Idee mit der eigenen Klasse für die verschiedenen Ebenen finde ich recht praktisch, muss ich noch mal versuchen, ob sich das noch anpassen lässt.

Ich sehe hier aber weiterhin mein Problem mit den Zugriffen. Innerhalb der Klasse sequenzOptions (bei dir dann sequenzConfFrame) gibt es bei mir dann bspw. eine Methode

Code: Alles auswählen

        # fuege neuen Kanal links ein
        def addSeqLeft(self):
                newSeq = SequenzOptions(self.getSeqId(),self.seqFrame,self.getPrevSeq(),self,self.seqName,self.getSeqTime())
                self.setPrevSeq(newSeq)

                # aktualisiere alle rechtsstehenden Sequenzen
                helpSeq = self
                while helpSeq != None:
                        helpSeq.setSeqId(helpSeq.getSeqId()+1)
                        helpSeq = helpSeq.getNextSeq()
Dort brauche ich ja wieder das seqFrame, um zu sagen, wo es platziert werden soll (landet alles im selben Frame und wird über die id in einer grid angeordnet. Bisher habe ich dann im Konstruktor das seqFrame (bei dir dann parent in seqConfFrame) über self für das Objekt verfügbar gemacht. Aber das wollte ich irgendwie umgehen, für jedes Objekt auf mehrere Grafikelemente immer wieder neu zu referenzieren. Dein parent wäre hier ja auch wieder nur in __init__ verfügbar.

Du führst dann ja zB. plane.update() in app() aus. Kann ich das (möglichst ohne plane als Argument zu übergeben) auch in seqConfFrame ausführen?
Steh ich da nur auf dem Schlauch und sehe keine "schönere" Lösung? Oder muss ich wirklich den Weg über die Argumente machen? Oder ist mein Ansatz zu kompliziert und ich sollte die einzelnen Abschnitte irgendwie sinnvoller Trennen?

Weiß irgendwie nicht, wie ich das besser ausdrücken soll, sorry. Wenns immer noch nicht verständlich war einfach sagen, dann versuch ichs nochmal ;)

Vielen Dank und lieber Grüße,
Benjamin
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hallo sommer87
sommer87 hat geschrieben:]Du hast in den einzelnen Klassen von tk.canvas etc geerbt. Ich hab irgendwo gelesen, dass man besser "new style"-Klassen verwenden soll, die nur von object erben.
Erlich gesagt ich habe mich bis jetzt noch nicht näher mit 'new style' Klassen beschäftigt. In meinen relativ dicken Leitbüchern wird auf diese nur oberflächlich, wenn überhaupt eingegangen. Obwohl ich bei Klassen die nichts erben instinktiv den Namen 'object' einsetze. Aber hier in diesem Forum gibt es sicher einige Mitglieder die sich intensiv mit diesem Typ Klassen näher auseinander gesetzt haben und mehr darüber sagen können.

Erste Frage: Das Code-Snippet, welches ich hier zur Verfügung stelle läuft bei dir auch ohne Fehlermeldung zu werfen?
Zweite Frage: Enstpricht die GUI-Ausgabe meines Skriptes etwa deinen Vorstellungen?

Um deine Anwendung besser zu verstehen brauche ich noch eine detailliertere Beschreibung ohne zuviel Python-Code. Deine Methode:

Code: Alles auswählen

def addSeqLeft(self):
verwirrt mich noch ein wenig. Mit 'neuer Kanal' bzw. 'neue Sequenz' meinst du auch wie ich ein neues Frame, welches mit Schaltflächen gefüllt werden soll?
Bei meinem Skript wird mit der Klasse:

Code: Alles auswählen

class SequenzConfFrame(tk.Frame):
automatisch rechts neben einem bestehenden Frame ein nächstes platziert. So wie ich dich verstehe möchtest du diese Frames mit einer ID versehen und mit dieser Framebezogene Atribute und Methoden aufrufen? Müssen bestehende Frames mit Inhalt miteinander getauscht werden können?

Gruß wuf
Take it easy Mates!
sommer87
User
Beiträge: 6
Registriert: Freitag 10. Juni 2011, 16:24

Hi,
wuf hat geschrieben:Erste Frage: Das Code-Snippet, welches ich hier zur Verfügung stelle läuft bei dir auch ohne Fehlermeldung zu werfen?
Geht ohne Probleme, ja.
Zweite Frage: Enstpricht die GUI-Ausgabe meines Skriptes etwa deinen Vorstellungen?
Vom aussehen prinzipiell ja. Aber: bisher habe ich in ein Frame (seqFrame) alles reingepackt. Die Buttons werden dann über grid angeordnet.
Ist aber nicht zwingend notwendig, wenns einfacher wird kann ich auch für jede Buttonspalte ein neues Frame anlegen. Solange alles zusammen in einem Canvas bleibt, dass ich scrollen kann...
Mit 'neuer Kanal' bzw. 'neue Sequenz' meinst du auch wie ich ein neues Frame, welches mit Schaltflächen gefüllt werden soll?
Den Kommentar hab ich vergessen zu löschen. Das Programm steuert später Kanäle von nem Microprozessor an. Wegen dem Frame s. o..
So wie ich dich verstehe möchtest du diese Frames mit einer ID versehen und mit dieser Framebezogene Atribute und Methoden aufrufen? Müssen bestehende Frames mit Inhalt miteinander getauscht werden können?
Die "ID" ist bei mir bislang eindeutig aber nicht fest und dient der Anordnung in der Grid. Wenn sich was verschiebt müssen daher alle IDs neu berechnet werden. Da ich keine Möglichkeit zur relativen Anordnung von Objekten in einem grid gefunden habe müssen die Positionierungen also immer aktualisiert werden können.
Die einzelnen Spalten sollen jeweils nach beiden Seiten durch gleiche Spalten erweiterbar sein und beliebig gelöscht werden können. Ein Verschieben von Spalten nach rechts oder links wird in späteren Versionen aber auch interessant sein. Die Spalten haben untereinander aber keine direkte Interaktion, außer dass beim Erstellen einer neuen Spalte die "Elternspalte" kopiert werden soll.

Viele Grüße und vielen Dank
sommer87
Antworten