Przełączaj się między dwiema ramkami w tkinter

98

Zbudowałem kilka moich pierwszych skryptów z ładnym małym GUI, jak pokazały mi tutoriale, ale żaden z nich nie dotyczy tego, co zrobić dla bardziej złożonego programu.

Jeśli masz coś z „menu startowym”, na ekranie początkowym i po wybraniu przez użytkownika przechodzisz do innej sekcji programu i odpowiednio przerysowujesz ekran, jaki jest elegancki sposób na zrobienie tego?

Czy po prostu .destroy()tworzy się ramkę „menu startowego”, a następnie tworzy nową, wypełnioną widżetami dla innej części? I odwrócić ten proces po naciśnięciu przycisku Wstecz?

Max Tilley
źródło

Odpowiedzi:

182

Jednym ze sposobów jest układanie ramek jedna na drugiej, a następnie można po prostu unieść jedną nad drugą w kolejności układania. Ten na górze będzie tym, który jest widoczny. Działa to najlepiej, jeśli wszystkie ramki mają ten sam rozmiar, ale przy odrobinie pracy można sprawić, że będzie działać z ramkami o dowolnym rozmiarze.

Uwaga : aby to zadziałało, wszystkie widżety dla strony muszą mieć tę stronę (tj .:) selflub element podrzędny jako element nadrzędny (lub wzorzec, w zależności od preferowanej terminologii).

Oto trochę wymyślony przykład, aby pokazać ogólną koncepcję:

try:
    import tkinter as tk                # python 3
    from tkinter import font as tkfont  # python 3
except ImportError:
    import Tkinter as tk     # python 2
    import tkFont as tkfont  # python 2

class SampleApp(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")

        # the container is where we'll stack a bunch of frames
        # on top of each other, then the one we want visible
        # will be raised above the others
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}
        for F in (StartPage, PageOne, PageTwo):
            page_name = F.__name__
            frame = F(parent=container, controller=self)
            self.frames[page_name] = frame

            # put all of the pages in the same location;
            # the one on the top of the stacking order
            # will be the one that is visible.
            frame.grid(row=0, column=0, sticky="nsew")

        self.show_frame("StartPage")

    def show_frame(self, page_name):
        '''Show a frame for the given page name'''
        frame = self.frames[page_name]
        frame.tkraise()


class StartPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is the start page", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        button1 = tk.Button(self, text="Go to Page One",
                            command=lambda: controller.show_frame("PageOne"))
        button2 = tk.Button(self, text="Go to Page Two",
                            command=lambda: controller.show_frame("PageTwo"))
        button1.pack()
        button2.pack()


class PageOne(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 1", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
        button.pack()


class PageTwo(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is page 2", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
        button = tk.Button(self, text="Go to the start page",
                           command=lambda: controller.show_frame("StartPage"))
        button.pack()


if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

Strona startowa Strona 1 Strona 2

Jeśli koncepcja tworzenia instancji w klasie jest myląca lub jeśli różne strony wymagają różnych argumentów podczas konstruowania, możesz jawnie wywołać każdą klasę osobno. Pętla służy głównie do zilustrowania faktu, że każda klasa jest identyczna.

Na przykład, aby utworzyć klasy indywidualnie, możesz usunąć pętlę (za for F in (StartPage, ...)pomocą tego:

self.frames["StartPage"] = StartPage(parent=container, controller=self)
self.frames["PageOne"] = PageOne(parent=container, controller=self)
self.frames["PageTwo"] = PageTwo(parent=container, controller=self)

self.frames["StartPage"].grid(row=0, column=0, sticky="nsew")
self.frames["PageOne"].grid(row=0, column=0, sticky="nsew")
self.frames["PageTwo"].grid(row=0, column=0, sticky="nsew")

Z biegiem czasu ludzie zadawali inne pytania, używając tego kodu (lub samouczka online, który skopiował ten kod) jako punktu wyjścia. Możesz przeczytać odpowiedzi na następujące pytania:

Bryan Oakley
źródło
3
Wow, bardzo dziękuję. Tak jak pytanie akademickie, a nie praktyczne, ile z tych stron ukrytych jedna na drugiej potrzebujesz, zanim zacznie się opóźniać i nie odpowiadać?
Max Tilley,
4
Nie wiem Prawdopodobnie tysiące. To byłoby dość łatwe do przetestowania.
Bryan Oakley,
1
Czy nie ma naprawdę prostego sposobu na osiągnięcie efektu podobnego do tego, przy znacznie mniejszej liczbie wierszy kodu, bez konieczności układania ramek jeden na drugim?
Cukier Parallax
2
@StevenVascellaro: tak, pack_forgetmoże być używany, jeśli używasz pack.
Bryan Oakley,
2
Ostrzeżenie : Istnieje możliwość wybrania przez użytkowników „ukrytych” widżetów na ramkach tła poprzez naciśnięcie klawisza Tab, a następnie aktywowania ich klawiszem Enter.
Stevoisiak
31

Oto kolejna prosta odpowiedź, ale bez użycia klas.

from tkinter import *


def raise_frame(frame):
    frame.tkraise()

root = Tk()

f1 = Frame(root)
f2 = Frame(root)
f3 = Frame(root)
f4 = Frame(root)

for frame in (f1, f2, f3, f4):
    frame.grid(row=0, column=0, sticky='news')

Button(f1, text='Go to frame 2', command=lambda:raise_frame(f2)).pack()
Label(f1, text='FRAME 1').pack()

Label(f2, text='FRAME 2').pack()
Button(f2, text='Go to frame 3', command=lambda:raise_frame(f3)).pack()

Label(f3, text='FRAME 3').pack(side='left')
Button(f3, text='Go to frame 4', command=lambda:raise_frame(f4)).pack(side='left')

Label(f4, text='FRAME 4').pack()
Button(f4, text='Goto to frame 1', command=lambda:raise_frame(f1)).pack()

raise_frame(f1)
root.mainloop()
recobayu
źródło
28

Ostrzeżenie : poniższa odpowiedź może spowodować wyciek pamięci przez wielokrotne niszczenie i odtwarzanie ramek.

Jednym ze sposobów przełączania ramek tkinterjest zniszczenie starej ramki, a następnie zastąpienie jej nową ramką.

Zmodyfikowałem odpowiedź Bryana Oakleya, aby zniszczyć starą ramę przed jej wymianą. Jako dodatkowy bonus eliminuje to potrzebę posiadania containerobiektu i pozwala na użycie dowolnej Frameklasy ogólnej .

# Multi-frame tkinter application v2.3
import tkinter as tk

class SampleApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self._frame = None
        self.switch_frame(StartPage)

    def switch_frame(self, frame_class):
        """Destroys current frame and replaces it with a new one."""
        new_frame = frame_class(self)
        if self._frame is not None:
            self._frame.destroy()
        self._frame = new_frame
        self._frame.pack()

class StartPage(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is the start page").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Open page one",
                  command=lambda: master.switch_frame(PageOne)).pack()
        tk.Button(self, text="Open page two",
                  command=lambda: master.switch_frame(PageTwo)).pack()

class PageOne(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is page one").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Return to start page",
                  command=lambda: master.switch_frame(StartPage)).pack()

class PageTwo(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Label(self, text="This is page two").pack(side="top", fill="x", pady=10)
        tk.Button(self, text="Return to start page",
                  command=lambda: master.switch_frame(StartPage)).pack()

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

Strona startowa Strona Pierwsza Strona druga

Wyjaśnienie

switch_frame()działa poprzez akceptowanie dowolnego obiektu klasy, który implementuje Frame. Następnie funkcja tworzy nową ramkę, aby zastąpić starą.

  • Usuwa starą _frameramkę, jeśli istnieje, a następnie zastępuje ją nową ramką.
  • Inne ramki dodane za pomocą .pack(), takie jak paski menu, pozostaną niezmienione.
  • Może być używany z każdą klasą, która implementuje tkinter.Frame.
  • Okno automatycznie zmienia rozmiar w celu dopasowania do nowej zawartości

Historia wersji

v2.3

- Pack buttons and labels as they are initialized

v2.2

- Initialize `_frame` as `None`.
- Check if `_frame` is `None` before calling `.destroy()`.

v2.1.1

- Remove type-hinting for backwards compatibility with Python 3.4.

v2.1

- Add type-hinting for `frame_class`.

v2.0

- Remove extraneous `container` frame.
    - Application now works with any generic `tkinter.frame` instance.
- Remove `controller` argument from frame classes.
    - Frame switching is now done with `master.switch_frame()`.

v1.6

- Check if frame attribute exists before destroying it.
- Use `switch_frame()` to set first frame.

v1.5

  - Revert 'Initialize new `_frame` after old `_frame` is destroyed'.
      - Initializing the frame before calling `.destroy()` results
        in a smoother visual transition.

v1.4

- Pack frames in `switch_frame()`.
- Initialize new `_frame` after old `_frame` is destroyed.
    - Remove `new_frame` variable.

v1.3

- Rename `parent` to `master` for consistency with base `Frame` class.

v1.2

- Remove `main()` function.

v1.1

- Rename `frame` to `_frame`.
    - Naming implies variable should be private.
- Create new frame before destroying old frame.

v1.0

- Initial version.
Stevoisiak
źródło
1
Widzę to dziwne krwawienie klatek, gdy próbuję tej metody, nie jestem pewien, czy można ją replikować: imgur.com/a/njCsa
quantik
2
@quantik Efekt krwawienia występuje, ponieważ stare przyciski nie są prawidłowo niszczone. Upewnij się, że dołączasz przyciski bezpośrednio do klasy ramki (zwykle self).
Stevoisiak
1
Z perspektywy czasu nazwa ta switch_frame()może być nieco myląca. Czy powinienem zmienić jego nazwę na replace_frame()?
Stevoisiak
12
Zalecam usunięcie części tej odpowiedzi dotyczącej historii wersji. To zupełnie bezużyteczne. Jeśli chcesz zobaczyć historię, stackoverflow zapewnia mechanizm do tego. Większość ludzi, którzy przeczytają tę odpowiedź, nie będzie przejmować się historią.
Bryan Oakley
2
Dziękuję za historię wersji. Dobrze jest wiedzieć, że z czasem aktualizowałeś i poprawiałeś odpowiedź.
Vasyl Vaskivskyi