Der beste Weg, um eine tkinter-Anwendung zu strukturieren?

136

Das Folgende ist die Gesamtstruktur meines typischen Python-Tkinter-Programms.

def funA():
    def funA1():
        def funA12():
            # stuff

    def funA2():
        # stuff

def funB():
    def funB1():
        # stuff

    def funB2():
        # stuff

def funC():
    def funC1():
        # stuff

    def funC2():
        # stuff


root = tk.Tk()

button1 = tk.Button(root, command=funA)
button1.pack()
button2 = tk.Button(root, command=funB)
button2.pack()
button3 = tk.Button(root, command=funC)
button3.pack()

funA funBund funCöffnet ein weiteres ToplevelFenster mit Widgets, wenn der Benutzer auf die Schaltflächen 1, 2, 3 klickt.

Ich frage mich, ob dies der richtige Weg ist, um ein Python-Tkinter-Programm zu schreiben. Sicher, es wird funktionieren, auch wenn ich so schreibe, aber ist es der beste Weg? Es klingt dumm, aber wenn ich die Codes sehe, die andere Leute geschrieben haben, ist ihr Code nicht mit einer Reihe von Funktionen durcheinander und meistens haben sie Klassen.

Gibt es eine bestimmte Struktur, der wir als bewährte Praxis folgen sollten? Wie soll ich planen, bevor ich mit dem Schreiben eines Python-Programms beginne?

Ich weiß, dass es keine bewährten Methoden für die Programmierung gibt, und ich frage auch nicht danach. Ich möchte nur einige Ratschläge und Erklärungen, um mich auf dem richtigen Weg zu halten, während ich Python selbst lerne.

Chris Aung
quelle
2
Hier ist ein exzellentes Tutorial zum Design von tkinter GUI mit einigen Beispielen - python-textbok.readthedocs.org/en/latest/… Hier ist ein weiteres Beispiel mit einem MVC-Designmuster - sukhbinder.wordpress.com/2014/12/ 25 /…
Bondolin
12
Diese Frage mag weit gefasst sein, ist aber nützlich und eine relativ beliebte Antwort (im Vergleich zu fast allen anderen [tkinter] Antworten). Ich nominiere für die Wiedereröffnung, da ich sehe, dass es nützlicher ist, es zu öffnen, als es zu schließen.
Bryan Oakley

Antworten:

271

Ich befürworte einen objektorientierten Ansatz. Dies ist die Vorlage, mit der ich beginne:

# Use Tkinter for python 2, tkinter for python 3
import tkinter as tk

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        <create the rest of your GUI here>

if __name__ == "__main__":
    root = tk.Tk()
    MainApplication(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

Die wichtigsten Dinge zu beachten sind:

  • Ich verwende keinen Platzhalterimport. Ich importiere das Paket als "tk", was erfordert, dass ich allen Befehlen ein Präfix vorstelle tk.. Dies verhindert eine globale Verschmutzung des Namespace und macht den Code vollständig offensichtlich, wenn Sie Tkinter-Klassen, ttk-Klassen oder einige Ihrer eigenen verwenden.

  • Die Hauptanwendung ist eine Klasse . Dies gibt Ihnen einen privaten Namespace für alle Ihre Rückrufe und privaten Funktionen und erleichtert im Allgemeinen die Organisation Ihres Codes. In einem prozeduralen Stil müssen Sie von oben nach unten codieren, Funktionen definieren, bevor Sie sie verwenden usw. Mit dieser Methode tun Sie dies nicht, da Sie das Hauptfenster erst im letzten Schritt erstellen. Ich bevorzuge es zu erben, tk.Framenur weil ich normalerweise mit dem Erstellen eines Rahmens beginne, aber es ist keineswegs notwendig.

Wenn Ihre App über zusätzliche Fenster auf oberster Ebene verfügt, empfehle ich, jede dieser Fenster zu einer separaten Klasse zu machen, von der geerbt wird tk.Toplevel. Dies bietet Ihnen alle oben genannten Vorteile: Die Fenster sind atomar, sie haben einen eigenen Namespace und der Code ist gut organisiert. Außerdem ist es einfach, jedes Modul in ein eigenes Modul zu integrieren, sobald der Code groß wird.

Schließlich möchten Sie möglicherweise Klassen für jeden Hauptteil Ihrer Benutzeroberfläche verwenden. Wenn Sie beispielsweise eine App mit einer Symbolleiste, einem Navigationsbereich, einer Statusleiste und einem Hauptbereich erstellen, können Sie jede dieser Klassen erstellen. Dies macht Ihren Hauptcode ziemlich klein und leicht verständlich:

class Navbar(tk.Frame): ...
class Toolbar(tk.Frame): ...
class Statusbar(tk.Frame): ...
class Main(tk.Frame): ...

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.statusbar = Statusbar(self, ...)
        self.toolbar = Toolbar(self, ...)
        self.navbar = Navbar(self, ...)
        self.main = Main(self, ...)

        self.statusbar.pack(side="bottom", fill="x")
        self.toolbar.pack(side="top", fill="x")
        self.navbar.pack(side="left", fill="y")
        self.main.pack(side="right", fill="both", expand=True)

Da alle diese Instanzen ein gemeinsames übergeordnetes Element haben, wird das übergeordnete Element effektiv zum "Controller" -Teil einer Model-View-Controller-Architektur. So könnte beispielsweise das Hauptfenster durch Aufrufen etwas in der Statusleiste platzieren self.parent.statusbar.set("Hello, world"). Auf diese Weise können Sie eine einfache Schnittstelle zwischen den Komponenten definieren und so die Kopplung auf ein Minimum beschränken.

Bryan Oakley
quelle
22
@Bryan Oakley Kennen Sie gute Beispielcodes im Internet, mit denen ich ihre Struktur studieren kann?
Chris Aung
2
Ich stimme dem objektorientierten Ansatz zu. Nach meiner Erfahrung ist es jedoch eine gute Idee, keine Vererbung für Ihre Klasse zu verwenden, die die GUI aufruft. Es bietet Ihnen mehr Flexibilität, wenn sowohl das Tk- als auch das Frame-Objekt Attribute einer Klasse sind, die nichts erbt. Auf diese Weise können Sie einfacher (und weniger mehrdeutig) auf die Objekte Tk und Frame zugreifen, und wenn Sie eines zerstören, wird nicht alles in Ihrer Klasse zerstört, wenn Sie dies nicht möchten. Ich habe den genauen Grund vergessen, warum dies in einigen Programmen wichtig ist, aber es ermöglicht Ihnen, mehr Dinge zu tun.
Brōtsyorfuzthrāx
1
Wird Ihnen eine Klasse nicht einfach einen privaten Namespace geben? Warum sollte die Unterklasse des Frames das verbessern?
GCB
3
@gcb: Ja, jede Klasse gibt dir einen privaten Namespace. Warum einen Frame unterordnen? Normalerweise werde ich sowieso einen Frame erstellen, daher ist eine Klasse weniger zu verwalten (Unterklasse von Frame im Vergleich zu einer Klasse, die vom Objekt erbt, mit einem Frame als Attribut). Ich habe die Antwort leicht umformuliert, um das klarer zu machen. Danke für die Rückmeldung.
Bryan Oakley
2
@madtyn: Es ist nicht erforderlich, einen Verweis auf zu speichern parent, es sei denn, Sie werden ihn später verwenden. Ich habe es nicht gespeichert, da für keinen der Codes in meinem Beispiel das Speichern erforderlich war.
Bryan Oakley
39

Wenn Sie jedes Fenster der obersten Ebene in eine eigene Klasse einordnen, können Sie den Code wiederverwenden und den Code besser organisieren. Alle im Fenster vorhandenen Schaltflächen und relevanten Methoden sollten in dieser Klasse definiert werden. Hier ist ein Beispiel (von hier genommen ):

import tkinter as tk

class Demo1:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.button1 = tk.Button(self.frame, text = 'New Window', width = 25, command = self.new_window)
        self.button1.pack()
        self.frame.pack()
    def new_window(self):
        self.newWindow = tk.Toplevel(self.master)
        self.app = Demo2(self.newWindow)

class Demo2:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.quitButton = tk.Button(self.frame, text = 'Quit', width = 25, command = self.close_windows)
        self.quitButton.pack()
        self.frame.pack()
    def close_windows(self):
        self.master.destroy()

def main(): 
    root = tk.Tk()
    app = Demo1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

Siehe auch:

Hoffentlich hilft das.

Alecxe
quelle
6

Dies ist keine schlechte Struktur; es wird gut funktionieren. Sie müssen jedoch Funktionen in einer Funktion haben, um Befehle ausführen zu können, wenn jemand auf eine Schaltfläche oder etwas klickt

Sie können also Klassen für diese schreiben und dann Methoden in der Klasse haben, die Befehle für die Schaltflächenklicks und dergleichen verarbeiten.

Hier ist ein Beispiel:

import tkinter as tk

class Window1:
    def __init__(self, master):
        pass
        # Create labels, entries,buttons
    def button_click(self):
        pass
        # If button is clicked, run this method and open window 2


class Window2:
    def __init__(self, master):
        #create buttons,entries,etc

    def button_method(self):
        #run this when button click to close window
        self.master.destroy()

def main(): #run mianloop 
    root = tk.Tk()
    app = Window1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

Normalerweise sind tk-Programme mit mehreren Fenstern mehrere große Klassen und in der __init__ allen Einträgen werden Beschriftungen usw. erstellt. Anschließend werden bei jeder Methode Schaltflächenklickereignisse verarbeitet

Es gibt nicht wirklich einen richtigen Weg, was auch immer für Sie funktioniert und die Arbeit erledigt, solange es lesbar ist und Sie es leicht erklären können, denn wenn Sie Ihr Programm nicht einfach erklären können, gibt es wahrscheinlich einen besseren Weg, es zu tun .

Schauen Sie sich Thinking in Tkinter an .

Seriennummer
quelle
3
"Thinking in Tkinter" befürwortet globale Importe, was ich für einen sehr schlechten Rat halte.
Bryan Oakley
1
Das stimmt, ich schlage nicht vor, dass Sie Globals verwenden, nur einige der Methos-Strukturen der Hauptklasse, die Sie richtig haben :)
Serial
2

OOP sollte der Ansatz sein und frameeine Klassenvariable anstelle einer Instanzvariablen sein .

from Tkinter import *
class App:
  def __init__(self, master):
    frame = Frame(master)
    frame.pack()
    self.button = Button(frame, 
                         text="QUIT", fg="red",
                         command=frame.quit)
    self.button.pack(side=LEFT)
    self.slogan = Button(frame,
                         text="Hello",
                         command=self.write_slogan)
    self.slogan.pack(side=LEFT)
  def write_slogan(self):
    print "Tkinter is easy to use!"

root = Tk()
app = App(root)
root.mainloop()

Geben Sie hier die Bildbeschreibung ein

Referenz: http://www.python-course.eu/tkinter_buttons.php

Trevor
quelle
2
Sie können nur TKinterPython 2 verwenden. Ich würde die Verwendung tkinterfür Python 3 empfehlen . Ich würde auch die letzten drei Codezeilen unter einer main()Funktion platzieren und diese am Ende des Programms aufrufen. Ich würde es definitiv vermeiden, es zu verwenden, from module_name import *da es den globalen Namespace verschmutzt und die Lesbarkeit beeinträchtigen kann.
Zac
1
Wie können Sie den Unterschied zwischen button1 = tk.Button(root, command=funA)und erkennen, button1 = ttk.Button(root, command=funA)ob das tkinterErweiterungsmodul auch importiert wurde? Mit der *Syntax scheinen beide Codezeilen zu sein button1 = Button(root, command=funA). Ich würde die Verwendung dieser Syntax nicht empfehlen.
Zac
0

Das Organisieren Ihrer Anwendung mithilfe von Klassen erleichtert Ihnen und anderen, die mit Ihnen zusammenarbeiten, das Debuggen von Problemen und das einfache Verbessern der App.

Sie können Ihre Anwendung einfach so organisieren:

class hello(Tk):
    def __init__(self):
        super(hello, self).__init__()
        self.btn = Button(text = "Click me", command=close)
        self.btn.pack()
    def close():
        self.destroy()

app = hello()
app.mainloop()
Muyustan
quelle
-2

Der wahrscheinlich beste Weg, um zu lernen, wie Sie Ihr Programm strukturieren, ist das Lesen des Codes anderer Personen, insbesondere wenn es sich um ein großes Programm handelt, zu dem viele Personen beigetragen haben. Nachdem Sie sich den Code vieler Projekte angesehen haben, sollten Sie eine Vorstellung davon bekommen, wie der Konsensstil aussehen sollte.

Python als Sprache ist insofern besonders, als es einige strenge Richtlinien gibt, wie Sie Ihren Code formatieren sollten. Das erste ist das sogenannte "Zen of Python":

  • Schön ist besser als hässlich.
  • Explizit ist besser als implizit.
  • Einfach ist besser als komplex.
  • Komplex ist besser als kompliziert.
  • Wohnung ist besser als verschachtelt.
  • Spärlich ist besser als dicht.
  • Lesbarkeit zählt.
  • Sonderfälle sind nicht speziell genug, um gegen die Regeln zu verstoßen.
  • Obwohl Praktikabilität die Reinheit übertrifft.
  • Fehler sollten niemals stillschweigend vergehen.
  • Sofern nicht ausdrücklich zum Schweigen gebracht.
  • Verweigern Sie angesichts von Zweideutigkeiten die Versuchung zu raten.
  • Es sollte einen - und vorzugsweise nur einen - offensichtlichen Weg geben, dies zu tun.
  • Obwohl dieser Weg zunächst vielleicht nicht offensichtlich ist, es sei denn, Sie sind Niederländer.
  • Jetzt ist besser als nie.
  • Obwohl nie ist oft besser als Recht jetzt.
  • Wenn die Implementierung schwer zu erklären ist, ist es eine schlechte Idee.
  • Wenn die Implementierung leicht zu erklären ist, ist dies möglicherweise eine gute Idee.
  • Namespaces sind eine großartige Idee - lasst uns mehr davon machen!

Auf einer praktischeren Ebene gibt es PEP8 , den Styleguide für Python.

Vor diesem Hintergrund würde ich sagen, dass Ihr Codestil nicht wirklich passt, insbesondere die verschachtelten Funktionen. Finden Sie eine Möglichkeit, diese zu reduzieren, indem Sie entweder Klassen verwenden oder sie in separate Module verschieben. Dadurch wird die Struktur Ihres Programms viel einfacher zu verstehen.

Inbar Rose
quelle
12
-1 für die Verwendung des Zen of Python. Es ist zwar alles ein guter Rat, aber es geht nicht direkt auf die gestellte Frage ein. Nehmen Sie den letzten Absatz heraus und diese Antwort könnte für fast jede Python-Frage auf dieser Site gelten. Es ist ein guter, positiver Rat, aber er beantwortet die Frage nicht.
Bryan Oakley
1
@BryanOakley Da stimme ich dir nicht zu. Ja, das Zen von Python ist breit und kann verwendet werden, um viele Fragen zu beantworten. Er erwähnte im letzten Absatz, sich für Klassen zu entscheiden oder die Funktionen in separate Module zu platzieren. Er erwähnte auch PEP8, einen Styleguide für Python, mit Verweisen darauf. Obwohl dies keine direkte Antwort ist, halte ich diese Antwort für glaubwürdig, da sie viele verschiedene Wege erwähnt, die eingeschlagen werden können. Das ist nur meine Meinung
Zac
1
Ich bin hierher gekommen, um Antworten auf diese spezielle Frage zu suchen. Selbst bei einer offenen Frage kann ich mit dieser Antwort nichts anfangen. Ich würde auch von mir.
Jonathan
Auf keinen Fall geht es bei der Frage um die Strukturierung einer tkinter- App, nicht um Styling- / Codierungs- / Zen-Richtlinien. Einfach wie das Zitieren von @Arbiter "Obwohl keine direkte Antwort", ist es also KEINE Antwort. Dies ist wie "vielleicht ja und vielleicht nein", wobei Zen vorangestellt ist.
m3nda
-7

Ich persönlich verwende den objektorientierten Ansatz nicht, hauptsächlich, weil er a) nur im Weg steht; b) Sie werden das niemals als Modul wiederverwenden.

sondern etwas , das nicht hier diskutiert wird, ist , dass Sie müssen verwenden Threading oder Multiprocessing. Immer. Andernfalls wird Ihre Bewerbung schrecklich sein.

Führen Sie einfach einen einfachen Test durch: Starten Sie ein Fenster und rufen Sie dann eine URL oder etwas anderes ab. Änderungen sind, dass Ihre Benutzeroberfläche nicht aktualisiert wird, während die Netzwerkanforderung ausgeführt wird. Das heißt, Ihr Anwendungsfenster wird beschädigt. Dies hängt vom Betriebssystem ab, auf dem Sie sich befinden. In den meisten Fällen wird es jedoch nicht neu gezeichnet. Alles, was Sie über das Fenster ziehen, wird darauf verputzt, bis der Vorgang wieder im TK-Mainloop ausgeführt wird.

gcb
quelle
4
Was Sie sagen, ist einfach nicht wahr. Ich habe Hudreds von tk-basierten Anwendungen geschrieben, sowohl persönliche als auch kommerzielle, und musste fast nie Threads verwenden. Themen haben ihren Platz, aber es ist einfach nicht wahr , dass Sie müssen sie verwenden , wenn tkinter Programme zu schreiben. Wenn Sie lange laufende Funktionen haben, benötigen Sie möglicherweise Threads oder Multiprocessing, aber es gibt viele, viele Arten von Programmen, die Sie schreiben können und die keine Threads benötigen.
Bryan Oakley
Ich denke, wenn Sie Ihre Antwort umformulieren würden, um etwas klarer zu sein, wäre es eine bessere Antwort. Es wäre auch sehr hilfreich, ein kanonisches Beispiel für die Verwendung von Threads mit tkinter zu haben.
Bryan Oakley
Es war mir egal, ob ich hier die beste Antwort bin, weil es ein bisschen vom Thema abweicht. Beachten Sie jedoch, dass das Starten mit Threading / Multip sehr einfach ist. Wenn Sie später hinzufügen müssen, ist es eine verlorene Schlacht. und heutzutage gibt es absolut keine Anwendung, die niemals mit dem Netzwerk kommunizieren würde. und selbst wenn Sie ignorieren und denken, dass ich nur wenig Festplatten-E / A habe, entscheidet Ihr Client morgen, dass die Datei auf NFS gespeichert wird, und Sie warten auf Netzwerk-E / A, und Ihre App scheint tot zu sein.
GCB
2
@ erm3nda: "Jede App, die mit dem Netzwerk verbunden ist oder E / A-Schreibvorgänge ausführt, ist mithilfe von Threads oder Unterprozessen viel schneller" - das ist einfach nicht wahr. Durch das Threading wird Ihr Programm nicht unbedingt schneller und in einigen Fällen langsamer. Bei der GUI-Programmierung besteht der Hauptgrund für die Verwendung von Threads darin, dass Code ausgeführt werden kann, der sonst die GUI blockieren würde.
Bryan Oakley
2
@ erm3nda: Nein, ich bin nicht sagen Fäden werden nicht benötigt überhaupt . Sie werden definitiv für viele Dinge benötigt (naja, Threads oder Multiprocessing). Es gibt nur eine sehr große Klasse von GUI-Anwendungen, bei denen tkinter geeignet ist, Threads jedoch einfach nicht benötigt werden. Und ja, "Installer, Notizblöcke und andere einfache Tools" fallen in diese Kategorie. Die Welt besteht aus mehr dieser "einfachen Werkzeuge" als aus Dingen wie Word, Excel, Photoshop usw. Und denken Sie daran, dass der Kontext hier tkinter ist . Tkinter wird normalerweise nicht für sehr große, komplexe Anwendungen verwendet.
Bryan Oakley