GUI Designer und eigene Klassen

Fragen zu Tkinter.
Antworten
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

GUI Designer und eigene Klassen

Es gibt bisher zwei Arten der GUI Erzeugung:

- Laden der GUI mit tk.Dynload und Zugriff durch tk.DynAccess
- Exportieren als tkinter code

Im zweiten Fall sind Container Widgets bereits eigene Klassen, die jeder erweitern kann, wie er will. Ich sollte beim Update noch den Austausch der Klassendefinition herausnehmen.
Im ersten Fall aber wird die GUI erzeugt ohne Änderungsmöglichkeit. Man kann dann keine eigenen Klassen verwenden. Und das sollte man ändern.

Meine Vorstellung:

Code: Alles auswählen

# statt tk.DynLoad('mygui.gui'
tk.DynLoad_GuiData('mygui.gui')

# und man kann dann eigene Widget Klassen definieren.
class MyClass(tk):
    def __init__(self,parent_or_name,**kwargs):
        ....
    
# Der Zugriff auf die nur Datengui geschieht genau wie zuvor durch
access = tk.DynAccess('mygui.access')

# und dann tauscht man die Klasse aus
access.mywidget.class = MyClass

# und danach kann man die GUI erzeugen
root = tk.create_gui_from_data()

# und erhält wiederum Zugriff auf die widgets durch
access = tk.DynAccess('mygui.access')
Der Vorteil gegenüber pygubu: man tauscht nur aus, was man austauschen will und den Rest lässt man.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: was steht in .gui-Dateien und was in .access-Dateien? Von einer load-Funktion erwarte ich, dass sie mir das Geladene zurückgibt. Deine Funktion scheint aber wieder irgendwo global herumzumurksen. Wenn ich eigene Klassen haben will, dann würde ich die registrieren, bevor ich irgendetwas lade, dann kennt die Laderoutine schon alle Klassen und ich brauche nicht alles doppelt zu tun.

Also in etwa so:

Code: Alles auswählen

import DynTkInter as tk

class SuperButton(tk.Frame):
    ...

tk.register_class(SuperButton)

class MainTk(tk.Tk):
    def __init__(self, *arg, **kw):
        tk.Tk.__init__(self, *arg, **kw)
        self.dyn_load('mygui.gui')
        ...

root = MainTk()
root.mainloop()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:@Alfons Mittelmeyer: was steht in .gui-Dateien und was in .access-Dateien? Von einer load-Funktion erwarte ich, dass sie mir das Geladene zurückgibt. Deine Funktion scheint aber wieder irgendwo global herumzumurksen. Wenn ich eigene Klassen haben will, dann würde ich die registrieren, bevor ich irgendetwas lade, dann kennt die Laderoutine schon alle Klassen und ich brauche nicht alles doppelt zu tun.
Von global rummurksen kann keine Rede sein. In den GUI Dateien steht die Definition der GUI in Baumform und zwar mit Namen und direkt in DynTkInter ausführbar.

Da steht etwa so etwas wie:

Code: Alles auswählen

Button('mybutton',**('text':'hello')).grid(**{'row':'1','column':'2})
In der Accessdatei, steht für die, welche die Programmierung nicht gleich in den GUI Scripts vornehmen wollen und auch den Parent Children Baum nicht selber abgrasen wollen, eine Definition des Zugriffs als Klassendefinition. Man kann den Access bis auf Widgettiefe haben. Dann steht in der Accessdatei:

Code: Alles auswählen

class Access:
    ...
    self.mybutton = widget('mybutton')
Empfehlenswert ist der Access nur bis Containertiefe, damit man nicht ständig wegen eindeutigen Namen aufpassen muss. Dann würde man in der Accessdefinition finden:

Code: Alles auswählen

class Access:
    ...
    self.myframe = widget('myframe')
und würde dann auf 'mybutton' als child von 'myframe' zugreifen mit:

Code: Alles auswählen

mybutton = tk.widget(access.myframe,'mybutton')
Und das ist auch nichts Globales, denn mit:

Code: Alles auswählen

access = tk.DynAccess('mygui.access')
erhält man eine Instanz der Accessklasse. Und die ist auch nicht global, denn sie existiert nur mehr dadurch, dass man eine Instanz von ihr hat.

Deine Vorstellung wäre also, dass man die Klasse gleich im GUI Designer eingibt. Ja das wäre auch eine Idee. Dann müßte beim Reinladen automatisch erst die Daten GUI erzeugt werden, die registrierte Klassenreferenz eingetragen werden und dann erst die GUI erzeugt werden. Und wenn die eingetragene Klasse nicht existiert, dann eine Info darüber ausgeben, die Gui aber vielleicht trotzdem mit der Defaultklasse erzeugen, oder abbrechen?

Wie sollte man den Eintrag im GUI Designer nennen? class darf man ihn nicht nennen, weil man dann mit class von ttk durcheinander kommen würde.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Also, ich würde dann an einen Eintrag 'myclass' im GUI Designer denken. Oder wie sollte er sonst heißen?

@Sirius3 apropos global. Das sollte man eigentlich nicht tun:

Code: Alles auswählen

tk.register_class(SuperButton)
Das wäre ja global. Oder sollte ich die Registrierung nach Aktualisierung selbständig löschen? Oder sollte man eine Liste als Parameter übergeben?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Da braucht man gar keine DatenGui. Das ist einfacher, als zuerst gedacht:

Code: Alles auswählen

import tkinter as tk

class_register = {}

def create_widget(default_class,name,**kwargs):
    myclass = default_class
    if 'myclass' in kwargs:
        myclass = class_register[kwargs.pop('myclass')]
    myclass(name,**kwargs)

def Button(name,**kwargs): create_widget(tk.Button,name,**kwargs)

class MyClass:
    def __init__(self,name,**kwargs):
        print(name,kwargs)
    
class_register['MyClass'] = MyClass

# test =================================================
Button("mybutton",**{'text':'Hello','myclass':'MyClass'})
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Also es klappt. Hab in diesem Script den einen Button als Klasse 'SuperButton' gekennzeichnet.
test.py:

Code: Alles auswählen

Button('Button',**{'text': 'Button','myclass':'SuperButton'}).place(**{'y': '105', 'x': '87'})
Label('Label',**{'text': 'Label'}).place(**{'y': '52', 'x': '40'})
Hab dann ein Sript geschrieben, welches die Klasse einsetzt.
dload.py:

Code: Alles auswählen

def Access(param):

    import DynTkInter as tk

    filename = param[0]
    classlist = param[1]

    class_register = {}

    def class_type(myclass):
        classString = str(myclass)
        begin = classString.find(".")+1
        end = classString.find("'",begin)
        return classString[begin:end]
    
    for entry in classlist: class_register[class_type(entry)] = entry

    def create_widget(default_class,name,**kwargs):
        myclass = default_class
        if 'myclass' in kwargs:
            myclass = class_register[kwargs.pop('myclass')]
        return myclass(name,**kwargs)

    def Button(name,**kwargs): return create_widget(tk.Button,name,**kwargs)
    def Toplevel(name,**kwargs): return create_widget(tk.Topleve,name,**kwargs)
    def Message(name,**kwargs): return create_widget(tk.Message,name,**kwargs)
    def Label(name,**kwargs): return create_widget(tk.Label,name,**kwargs)
    def Checkbutton(name,**kwargs): return create_widget(tk.Checkbutton,name,**kwargs)
    def Radiobutton(name,**kwargs): return create_widget(tk.Radiobutton,name,**kwargs)
    def Entry(name,**kwargs): return create_widget(tk.Entry,name,**kwargs)
    def Text(name,**kwargs): return create_widget(tk.Text,name,**kwargs)
    def Spinbox(name,**kwargs): return create_widget(tk.Spinbox,name,**kwargs)
    def Scale(name,**kwargs): return create_widget(tk.Scale,name,**kwargs)
    def Listbox(name,**kwargs): return create_widget(tk.Listbox,name,**kwargs)
    def Scrollbar(name,**kwargs): return create_widget(tk.Scrollbar,name,**kwargs)
    def Frame(name,**kwargs): return create_widget(tk.Frame,name,**kwargs)
    def LabelFrame(name,**kwargs): return create_widget(tk.LabelFrame,name,**kwargs)
    def PanedWindow(name,**kwargs): return create_widget(tk.PanedWindow,name,**kwargs)
    def Canvas(name,**kwargs): return create_widget(tk.Canvas,name,**kwargs)
    def Menu(name,**kwargs): return create_widget(tk.Menu,name,**kwargs)
    def MenuDelimiter(name,**kwargs): return create_widget(tk.MenuDelimiter,name,**kwargs)
    def Menubutton(name,**kwargs): return create_widget(tk.Menubutton,name,**kwargs)

    def MenuItem(name,item_type,**kwargs):
        myclass = tk.MenuItem
        if 'myclass' in kwargs:
            myclass = class_register[kwargs.pop('myclass')]
        return myclass(name,item_type,**kwargs)

    exec(compile(open(filename, "r").read(), filename, 'exec'))
Hab die Funktion DynAccess erweitert, sodass sie auch einen zusätzlichen Parameter außer nur dem Filenamen annimmt und dann funktionierte es. Hier das Main Script:

Code: Alles auswählen

import DynTkInter as tk

root = tk.Tk()

class SuperButton(tk.Button):
    def __init__(self,name,**options):
        tk.Button.__init__(self,name,**options)
        self['bg'] = 'red'

tk.DynAccess('dload.py',('test.py',[SuperButton]))

root.mainloop()
Und der Button war rot.

Na, eigentlich sollte man den import an den Anfang schreiben:

Code: Alles auswählen

import DynTkInter as tk

def Access(param,tk=tk):
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hab es jetzt load_script genannt und die Angabe einer Klassenliste ist optional. Natürlich, wenn man Klassen eingegeben hat, muss man sie auch übergeben:

Code: Alles auswählen

import DynTkInter as tk

class SuperButton(tk.Button):
    def __init__(self,name,**options):
        tk.Button.__init__(self,name,**options)
        self['bg'] = 'red'

root = tk.Tk()
tk.load_script('test.gui',[SuperButton])

root.mainloop()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Man kann natürlich auch Scripts laden, welche bereits die Klassen enthalten. Hier ein Versuch.
try_cat.py:

Code: Alles auswählen

Button('no_cat',**{'text': 'Button','myclass':'NotACat'}).place(**{'y': '105', 'x': '87'})
Label('Label',**{'text': 'Label'}).place(**{'y': '52', 'x': '40'})

widget('no_cat').miau()
Das geht natürlich nicht, wenn man es mit DynLoad lädt. Man kann es aber indirekt doch durch DynLoad laden, indem man zwei Scripts hat.
Hier prepare_cat.py:

Code: Alles auswählen

class NotACat(Button):
    def __init__(self,name,**options):
        Button.__init__(self,name,**options)
        
    def miau(self): print("I'm not a cat")

load_script('try_cat.py',[NotACat])
Und das kann man dann mit DynLoad laden:

Code: Alles auswählen

import DynTkInter as tk

root = tk.Tk()
tk.DynLoad('prepare_cat.py')

root.mainloop()
Dass man das kann, ist wichtig. Wenn man etwa einen Next-Button hat, der die GUI löscht und eine neue nachlädt, sollte die Klassendefinition der neuen GUI ja nicht in der vorigen GUI erfolgen. Sinngemäss gehört sie doch zur neuen GUI oder?

Man könnte ja auch mit LinkLabels von verschiedenen Gui Seiten aus auf eine bestimmte GUI kommen. Da wäre es doch nicht richtig, da überall die Klassendefinition für die Seite, auf die man dann springt, hineinzuschreiben.
Zuletzt geändert von Alfons Mittelmeyer am Dienstag 20. Oktober 2015, 12:14, insgesamt 2-mal geändert.
Benutzeravatar
sparrow
User
Beiträge: 4184
Registriert: Freitag 17. April 2009, 10:28

Findest du diese vielen Monologe betreffend des GUI-Designers im tkinter Unterforum nicht auch ein bisschen seltsam?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

sparrow hat geschrieben:Findest du diese vielen Monologe betreffend des GUI-Designers im tkinter Unterforum nicht auch ein bisschen seltsam?
Ja schon, die Idee dazu hatte ja Sirius3 mit:

Code: Alles auswählen

tk.register_class(SuperButton)
Weiss auch nicht, weshalb er sich dann nicht mehr dazu geäußert hat.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Es ist ganz gut gelungen und ich spiele es dann bis morgen ein.
DynLoad braucht man nicht mehr - sie bricht nicht ab, wenn das File nicht da ist und ändert evtl. die Selection.

Stattdessen verwendet man dann die Funktion load_script. Sie hat folgende Parameter:

- filename (Script das geladen werden soll)
- optionale Klassenliste (list oder tuple)
- optionales Containerwidget (Referenz auf das Widget oder die Root, in das die GUI geladen werden soll)

Hier ein Beispiel:

Code: Alles auswählen

import DynTkInter as tk

root = tk.Tk()
root.config(width=300,height=150)

myframe = tk.LabelFrame(root,text='Lesson 1')
myframe.place(x=30,y=10)

tk.load_script('lesson1_page1.py',myframe)

root.mainloop()
Hier wird die GUI in den LabelFrame myframe geladen.

Und dann habe ich noch zwei spezielle DynTkInter Widgets dazu aufgenommen, nämlich LinkButton und LinkLabel. Hier das Script lesson1_page1.py:

Code: Alles auswählen

config(**{'grid_cols': '(3, 75, 0, 0)', 'grid_rows': '(4, 25, 0, 0)'})

LinkButton('LinkButton',**{'text': 'Next', 'link': 'lesson1_page2.py'}).grid(**{'column': '2', 'sticky': 'nesw', 'row': '3'})
Label('Title',**{'text': 'Page 1', 'bg': 'yellow'}).grid(**{'sticky': 'nesw', 'row': '0'})
Der config Eintrag 'link': 'lesson1_page2.py' bewirkt, dass beim Drücken des Buttons alles aus dem betreffenden Container Widget gelöscht wird und dann die GUI aus 'lesson1_page2.py' dafür hereingeladen wird.

In lesson1_page2.py haben wir dann statt dem Next einen Back-Button:

Code: Alles auswählen

config(**{'grid_cols': '(3, 75, 0, 0)', 'grid_rows': '(4, 25, 0, 0)'})

LinkButton('LinkButton',**{'text': 'Back', 'link': 'lesson1_page1.py'}).grid(**{'sticky': 'nesw', 'row': '3'})
Label('Title',**{'text': 'Page 2', 'bg': 'yellow'}).grid(**{'sticky': 'nesw', 'row': '0'})
Da sieht man dann, dass diese statische globale Programmierung im Main Script überhaupt keinen Sinn macht, denn woher soll man wissen, welche GUI der User ausgewählt hat?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Trotzdem braucht man nicht im GUI Script selber programmieren - was auch Vorteile hätte - etwa wenn man die GUI weiter zerteilt - macht man mit dem GUI Designer über Menü File -> Split & Join und die link option bei Container Widgets setzen. Man kann stattdessen noch ein Code Script dazwischen schieben, welches die GUI nachlädt und auch die Definition von Klassen für GUI Widgets vornehmen kann.

Statt nur einem einzigen Main Script kann man in DynTkInter beliebig viele Scripts haben. Und idealerweise startet das Main Script nur das Start Script.
Antworten