Najlepszy sposób na ustrukturyzowanie aplikacji tkinter?

136

Poniżej przedstawiono ogólną strukturę mojego typowego programu tkinter w języku Python.

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 funBi funCprzywoła innyToplevel okno z widżetami, gdy użytkownik kliknie przycisk 1, 2, 3.

Zastanawiam się, czy to jest właściwy sposób na napisanie programu tkinter w Pythonie? Jasne, zadziała, nawet jeśli napiszę w ten sposób, ale czy to najlepszy sposób? Brzmi głupio, ale kiedy widzę kody napisane przez innych ludzi, ich kod nie jest pomieszany z wieloma funkcjami i przeważnie mają klasy.

Czy jest jakaś konkretna struktura, której powinniśmy przestrzegać jako dobrej praktyki? Jak powinienem zaplanować przed rozpoczęciem pisania programu w Pythonie?

Wiem, że nie ma czegoś takiego jak najlepsze praktyki w programowaniu i też o to nie proszę. Chcę tylko kilku rad i wyjaśnień, które utrzymają mnie we właściwym kierunku, gdy sam uczę się Pythona.

Chris Aung
źródło
2
Oto doskonały samouczek dotyczący projektowania GUI tkinter, z kilkoma przykładami - python-textbok.readthedocs.org/en/latest/ ... Oto kolejny przykład ze wzorcem projektowym MVC - sukhbinder.wordpress.com/2014/12/ 25 /…
Bondolin
12
To pytanie może być szerokie, ale jest przydatne i jest stosunkowo popularną odpowiedzią (w stosunku do prawie wszystkich innych odpowiedzi [tkinter]). Wyznaczam ponowne otwarcie, ponieważ widzę, że otwarcie go jest bardziej przydatne niż zamknięcie.
Bryan Oakley,

Odpowiedzi:

271

Jestem zwolennikiem podejścia obiektowego. Oto szablon, od którego zaczynam:

# 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()

Ważne rzeczy, na które należy zwrócić uwagę, to:

  • Nie używam importu symboli wieloznacznych. Importuję pakiet jako „tk”, co wymaga poprzedzenia wszystkich poleceń przedrostkiem tk.. Zapobiega to zanieczyszczaniu globalnej przestrzeni nazw, a ponadto sprawia, że ​​kod jest całkowicie oczywisty, gdy używasz klas Tkinter, klas ttk lub niektórych własnych.

  • Głównym zastosowaniem jest klasa . Daje to prywatną przestrzeń nazw dla wszystkich wywołań zwrotnych i funkcji prywatnych i po prostu ogólnie ułatwia organizowanie kodu. W stylu proceduralnym musisz kodować od góry do dołu, definiując funkcje przed ich użyciem itp. W przypadku tej metody nie jest to możliwe, ponieważ w rzeczywistości nie tworzy się głównego okna aż do ostatniego kroku. Wolę dziedziczyć tk.Frametylko dlatego, że zazwyczaj zaczynam od stworzenia ramki, ale nie jest to wcale konieczne.

Jeśli twoja aplikacja ma dodatkowe okna najwyższego poziomu, polecam uczynić każde z nich osobną klasą, dziedzicząc po tk.Toplevel . Zapewnia to wszystkie wymienione powyżej zalety - okna są atomowe, mają własną przestrzeń nazw, a kod jest dobrze zorganizowany. Ponadto ułatwia umieszczenie każdego z nich we własnym module, gdy kod zacznie się rozrastać.

Na koniec możesz rozważyć użycie klas dla każdej większej części interfejsu. Na przykład, jeśli tworzysz aplikację z paskiem narzędzi, okienkiem nawigacji, paskiem stanu i obszarem głównym, możesz utworzyć każdą z tych klas. To sprawia, że ​​twój główny kod jest dość mały i łatwy do zrozumienia:

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)

Ponieważ wszystkie te instancje mają wspólnego rodzica, rodzic faktycznie staje się częścią „kontrolera” architektury model-widok-kontroler. Na przykład główne okno może umieścić coś na pasku stanu, wywołując self.parent.statusbar.set("Hello, world"). Pozwala to na zdefiniowanie prostego interfejsu między komponentami, pomagając utrzymać połączenie z minimun.

Bryan Oakley
źródło
22
@Bryan Oakley Czy znasz jakieś dobre przykładowe kody w Internecie, które mogę zbadać ich strukturę?
Chris Aung,
2
Popieram podejście obiektowe. Jednak z mojego doświadczenia wynika, że ​​powstrzymanie się od używania dziedziczenia w klasie, która wywołuje GUI, jest dobrym pomysłem. Zapewnia większą elastyczność, jeśli zarówno obiekty Tk, jak i Frame są atrybutami klasy, która nie dziedziczy z niczego. W ten sposób możesz łatwiej uzyskać dostęp do obiektów Tk i Frame (i mniej niejednoznacznie), a zniszczenie jednego nie zniszczy wszystkiego w twojej klasie, jeśli tego nie chcesz. Zapomniałem dokładnego powodu, dla którego jest to istotne w niektórych programach, ale pozwala zrobić więcej rzeczy.
Brōtsyorfuzthrāx
1
czy po prostu posiadanie klasy nie da ci prywatnej przestrzeni nazw? dlaczego tworzenie podklas w Frame jest lepsze?
gcb
3
@gcb: tak, każda klasa zapewni ci prywatną przestrzeń nazw. Po co tworzyć podklasy Frame? Zazwyczaj i tak zamierzam utworzyć ramkę, więc jest to o jedną klasę mniej do zarządzania (podklasa Frame, a klasa dziedzicząca po obiekcie, z ramką jako atrybutem). Lekko przeformułowałem odpowiedź, aby było jaśniej. Dziękujemy za opinię.
Bryan Oakley,
2
@madtyn: nie ma potrzeby zapisywania odwołania do parent, chyba że zamierzasz go później użyć. Nie zapisałem go, ponieważ żaden kod w moim przykładzie nie wymagał zapisania.
Bryan Oakley,
39

Umieszczenie każdego z okien najwyższego poziomu w osobnej klasie zapewnia ponowne użycie kodu i lepszą organizację kodu. Wszelkie przyciski i odpowiednie metody, które są obecne w oknie, powinny być zdefiniowane w tej klasie. Oto przykład (zaczerpnięty stąd ):

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()

Zobacz także:

Mam nadzieję, że to pomoże.

alecxe
źródło
6

To nie jest zła konstrukcja; będzie działać dobrze. Jednak musisz mieć funkcje w funkcji, aby wykonywać polecenia, gdy ktoś kliknie przycisk lub coś

Więc to, co możesz zrobić, to napisać dla nich klasy, a następnie mieć w klasie metody, które obsługują polecenia dla kliknięć przycisku i tym podobne.

Oto przykład:

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()

Zazwyczaj programy tk z wieloma oknami są wieloma dużymi klasami i we __init__wszystkich wpisach tworzone są etykiety itp., A następnie każda metoda obsługuje zdarzenia kliknięcia przycisku

Naprawdę nie ma właściwego sposobu, aby to zrobić, cokolwiek działa dla ciebie i wykonuje zadanie, o ile jest czytelne i możesz to łatwo wyjaśnić, ponieważ jeśli nie możesz łatwo wyjaśnić swojego programu, prawdopodobnie jest lepszy sposób, aby to zrobić .

Spójrz na Thinking in Tkinter .

Seryjny
źródło
3
„Thinking in Tkinter” opowiada się za globalnym importem, co moim zdaniem jest bardzo złą radą.
Bryan Oakley,
1
To prawda, nie sugeruję, abyś używał globali, tylko niektóre z głównych struktur metosowych klasy masz rację :)
Serial,
2

OOP powinno być podejściem i framepowinno być zmienną klasy, a nie zmienną instancji .

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()

wprowadź opis obrazu tutaj

Źródła: http://www.python-course.eu/tkinter_buttons.php

Trevor
źródło
2
Możesz używać tylko TKinterw Pythonie 2. Polecam używanie tkinterw Pythonie 3. Umieściłbym również ostatnie trzy linie kodu pod main()funkcją i wywołałbym ją na końcu programu. Chciałbym zdecydowanie unikać from module_name import *, ponieważ zanieczyszcza globalnej przestrzeni nazw i może zmniejszyć czytelność.
Zac
1
Po czym można było określić różnicę między importem modułu rozszerzenia button1 = tk.Button(root, command=funA)i button1 = ttk.Button(root, command=funA)czy tkinterbył on również importowany? Przy *składni wyglądałyby tak oba wiersze kodu button1 = Button(root, command=funA). Nie polecałbym używania tej składni.
Zac
0

Organizowanie aplikacji przy użyciu klas ułatwia Tobie i innym współpracownikom łatwe debugowanie problemów i ulepszanie aplikacji.

Możesz łatwo zorganizować swoją aplikację w ten sposób:

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
źródło
-2

Prawdopodobnie najlepszym sposobem nauczenia się struktury programu jest czytanie kodu innych osób, zwłaszcza jeśli jest to duży program, w którego tworzeniu uczestniczyło wiele osób. Po przyjrzeniu się kodowi wielu projektów powinieneś zorientować się, jaki powinien być styl konsensusu.

Python jako język wyróżnia się tym, że istnieją pewne silne wytyczne dotyczące formatowania kodu. Pierwszy to tak zwany „Zen Pythona”:

  • Piękne jest lepsze niż brzydkie.
  • Jawne jest lepsze niż niejawne.
  • Proste jest lepsze niż złożone.
  • Złożoność jest lepsza niż skomplikowana.
  • Płaskie jest lepsze niż zagnieżdżone.
  • Rzadkie jest lepsze niż gęste.
  • Liczy się czytelność.
  • Specjalne przypadki nie są na tyle wyjątkowe, aby łamać zasady.
  • Chociaż praktyczność przewyższa czystość.
  • Błędy nigdy nie powinny przejść bezgłośnie.
  • Chyba że wyraźnie uciszono.
  • W obliczu niejasności odrzuć pokusę zgadywania.
  • Powinien być jeden - a najlepiej tylko jeden - oczywisty sposób na zrobienie tego.
  • Chociaż na początku może to nie być oczywiste, chyba że jesteś Holendrem.
  • Teraz jest lepiej niż nigdy.
  • Chociaż nigdy nie jest często lepsze niż prawo teraz.
  • Jeśli implementacja jest trudna do wyjaśnienia, to zły pomysł.
  • Jeśli implementacja jest łatwa do wyjaśnienia, może to być dobry pomysł.
  • Przestrzenie nazw to świetny pomysł - zróbmy ich więcej!

Na bardziej praktycznym poziomie jest PEP8 , przewodnik po stylu dla Pythona.

Mając to na uwadze, powiedziałbym, że twój styl kodu nie pasuje, szczególnie funkcje zagnieżdżone. Znajdź sposób na ich spłaszczenie, używając klas lub przenosząc je do oddzielnych modułów. Dzięki temu struktura programu będzie znacznie łatwiejsza do zrozumienia.

Inbar Rose
źródło
12
-1 za korzystanie z Zen of Python. Chociaż jest to dobra rada, nie odnosi się bezpośrednio do zadanego pytania. Usuń ostatni akapit, a ta odpowiedź może dotyczyć prawie każdego pytania Pythona na tej stronie. To dobra, pozytywna rada, ale nie odpowiada na pytanie.
Bryan Oakley
1
@BryanOakley Nie zgadzam się z tobą w tej sprawie. Tak, Zen Pythona jest szeroki i można go wykorzystać do odpowiedzi na wiele pytań. W ostatnim akapicie wspomniał o wyborze zajęć lub umieszczeniu funkcji w oddzielnych modułach. Wspomniał również o PEP8, przewodniku po stylu dla Pythona, z odniesieniami do niego. Chociaż nie jest to bezpośrednia odpowiedź, myślę, że jest ona wiarygodna, ponieważ wspomina o wielu różnych drogach, które można obrać. To tylko moja opinia
Zac
1
Przyjechałem tutaj, szukając odpowiedzi na to konkretne pytanie. Nawet w przypadku pytania otwartego nie mogę nic zrobić z tą odpowiedzią. - ode mnie również.
Jonathan
Nie ma mowy, pytanie dotyczy struktury aplikacji tkinter , nic o stylizacji / kodowaniu / wytycznych zen. Proste jak zacytowanie @Arbiter "Chociaż nie jest to bezpośrednia odpowiedź", więc NIE jest to odpowiedź. To jest jak „może tak, a może nie” z dołączonym zen.
m3nda
-7

Osobiście nie stosuję podejścia zorientowanego obiektowo, głównie dlatego, że a) tylko przeszkadza; b) nigdy nie użyjesz tego ponownie jako modułu.

ale coś, co nie jest tutaj omawiane, to fakt, że musisz używać wątków lub przetwarzania wieloprocesowego. Zawsze. inaczej twoja aplikacja będzie okropna.

po prostu zrób prosty test: uruchom okno, a następnie pobierz jakiś adres URL lub cokolwiek innego. zmiany to twój interfejs użytkownika nie zostanie zaktualizowany, gdy trwa żądanie sieciowe. Oznacza to, że okno aplikacji zostanie zepsute. zależy od używanego systemu operacyjnego, ale w większości przypadków nie będzie przerysowywany, wszystko, co przeciągniesz przez okno, zostanie na nim otynkowane, dopóki proces nie powróci do głównej pętli TK.

gcb
źródło
4
To, co mówisz, jest po prostu nieprawdą. Napisałem wiele aplikacji opartych na tk, zarówno osobistych, jak i komercyjnych, i prawie nigdy nie musiałem używać wątków. Wątki mają swoje miejsce, ale po prostu nie jest prawdą, że musisz ich używać podczas pisania programów tkinter. Jeśli masz długo działające funkcje, możesz potrzebować wątków lub przetwarzania wieloprocesowego, ale istnieje wiele, wiele typów programów, które możesz napisać, które nie wymagają wątków.
Bryan Oakley,
Myślę, że gdybyś przeformułował swoją odpowiedź, aby była bardziej jasna, byłaby to lepsza odpowiedź. Przydałby się również kanoniczny przykład używania wątków z tkinter.
Bryan Oakley
nie dbałem o to, aby być najlepszą odpowiedzią, ponieważ jest to trochę poza tematem. ale pamiętaj, że rozpoczęcie od obsługi wątków / wielu jest bardzo łatwe. jeśli będziesz musiał dodać później, jest to przegrana bitwa. a obecnie nie ma absolutnie żadnej aplikacji, która nigdy nie rozmawiałaby z siecią. a nawet jeśli zignorujesz i pomyślisz „mam tylko małe we / wy dysku”, jutro twój klient zdecyduje, że plik będzie żył w NFS i czekasz na sieciowe IO, a twoja aplikacja wygląda na martwą.
gcb
2
@ erm3nda: „każda aplikacja połączona z siecią lub wykonująca zapis we / wy będzie dużo szybsza przy użyciu wątków lub podprocesów” - to po prostu nieprawda. Wątkowanie niekoniecznie przyspieszy program, aw niektórych przypadkach spowoduje jego spowolnienie. W programowaniu GUI głównym powodem używania wątków jest możliwość uruchomienia kodu, który w przeciwnym razie blokowałby GUI.
Bryan Oakley,
2
@ erm3nda: nie, nie mówię, że wątki w ogóle nie są potrzebne . Zdecydowanie są potrzebne (no cóż, wątki lub przetwarzanie wieloprocesowe) do wielu rzeczy. Chodzi o to, że istnieje bardzo duża klasa aplikacji GUI, w których tkinter jest odpowiedni, ale wątki po prostu nie są potrzebne. I tak, „instalatory, notatniki i inne proste narzędzia” należą do tej kategorii. Świat składa się z większej liczby „łatwych narzędzi” niż z rzeczy takich jak Word, Excel, Photoshop itp. Poza tym pamiętaj, że kontekst jest tutaj tkinter . Tkinter zazwyczaj nie jest używany do bardzo dużych, złożonych aplikacji.
Bryan Oakley,