Wechseln Sie in tkinter zwischen zwei Frames


89

Ich habe meine ersten Skripte mit einer netten kleinen Benutzeroberfläche erstellt, wie die Tutorials gezeigt haben, aber keines von ihnen befasst sich mit der Vorgehensweise für ein komplexeres Programm.

Wenn Sie etwas mit einem 'Startmenü' für Ihren Startbildschirm haben und nach Benutzerauswahl zu einem anderen Abschnitt des Programms wechseln und den Bildschirm entsprechend neu zeichnen, wie können Sie dies auf elegante Weise tun?

Wird nur .destroy()der Rahmen "Startmenü" erstellt und dann ein neuer erstellt, der mit den Widgets für einen anderen Teil gefüllt ist? Und diesen Vorgang umkehren, wenn sie die Zurück-Taste drücken?

Antworten:


166

Eine Möglichkeit besteht darin, die Frames übereinander zu stapeln. Anschließend können Sie sie einfach in der Stapelreihenfolge übereinander heben. Der obere wird derjenige sein, der sichtbar ist. Dies funktioniert am besten, wenn alle Frames die gleiche Größe haben, aber mit ein wenig Arbeit können Sie es mit Frames beliebiger Größe zum Laufen bringen.

Hinweis : Damit dies funktioniert, müssen alle Widgets für eine Seite diese Seite (dh :) selfoder einen Nachkommen als übergeordnetes Element (oder Master, je nach der von Ihnen bevorzugten Terminologie) haben.

Hier ist ein Beispiel, das Ihnen das allgemeine Konzept zeigt:

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

Startseite Seite 1 Seite 2

Wenn Sie das Konzept des Erstellens einer Instanz in einer Klasse verwirrend finden oder wenn verschiedene Seiten während der Erstellung unterschiedliche Argumente benötigen, können Sie jede Klasse explizit separat aufrufen. Die Schleife dient hauptsächlich dazu, den Punkt zu veranschaulichen, dass jede Klasse identisch ist.

Um beispielsweise die Klassen einzeln zu erstellen, können Sie die Schleife entfernen ( for F in (StartPage, ...)damit:

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

Im Laufe der Zeit haben Leute andere Fragen gestellt, wobei sie diesen Code (oder ein Online-Tutorial, das diesen Code kopiert hat) als Ausgangspunkt verwendet haben. Vielleicht möchten Sie die Antworten auf diese Fragen lesen:


3
Wow vielen Dank. Wie viele dieser übereinander versteckten Seiten würden Sie als akademische und nicht als praktische Frage benötigen, bevor sie langsam verzögert und nicht mehr reagieren?
Max Tilley

4
Ich weiß es nicht. Wahrscheinlich Tausende. Es wäre leicht genug zu testen.
Bryan Oakley

1
Gibt es keinen wirklich einfachen Weg, um einen ähnlichen Effekt mit viel weniger Codezeilen zu erzielen, ohne Frames übereinander stapeln zu müssen?
Parallax Sugar

2
@StevenVascellaro: Ja, pack_forgetkann verwendet werden, wenn Sie verwenden pack.
Bryan Oakley

2
Warnung : Benutzer können "versteckte" Widgets in Hintergrundrahmen auswählen, indem sie die Tabulatortaste drücken und sie dann mit der Eingabetaste aktivieren.
Stevoisiak

30

Hier ist eine weitere einfache Antwort, jedoch ohne Verwendung von Klassen.

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

26

Warnung : Die folgende Antwort kann zu einem Speicherverlust führen, indem Frames wiederholt zerstört und neu erstellt werden.

Eine Möglichkeit, Frames einzuschalten, tkinterbesteht darin, den alten Frame zu zerstören und ihn dann durch Ihren neuen Frame zu ersetzen.

Ich habe Bryan Oakleys Antwort geändert , um den alten Rahmen zu zerstören, bevor ich ihn ersetzte. Als zusätzlichen Bonus wird ein containerObjekt überflüssig und Sie können jede generische FrameKlasse verwenden.

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

Startseite Seite eins Seite zwei

Erläuterung

switch_frame()Akzeptiert jedes implementierte Klassenobjekt Frame. Die Funktion erstellt dann einen neuen Rahmen, um den alten zu ersetzen.

  • Löscht alte, _framefalls vorhanden, und ersetzt sie durch den neuen Frame.
  • Andere mit hinzugefügte Frames .pack(), wie z. B. Menüleisten, bleiben davon unberührt.
  • Kann mit jeder Klasse verwendet werden, die implementiert tkinter.Frame.
  • Die Größe des Fensters wird automatisch an neue Inhalte angepasst

Versionsgeschichte

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.

1
Ich bekomme dieses seltsame Ausbluten von Frames, wenn ich diese Methode versuche, nicht sicher, ob sie repliziert werden kann: imgur.com/a/njCsa
quantik

2
@quantik Der Überlaufeffekt tritt auf, weil die alten Schaltflächen nicht richtig zerstört werden. Stellen Sie sicher, dass Sie Schaltflächen (normalerweise self) direkt an die Rahmenklasse anhängen .
Stevoisiak

1
Im Nachhinein switch_frame()könnte der Name etwas irreführend sein. Soll ich es umbenennen replace_frame()?
Stevoisiak

10
Ich empfehle, den Versionsverlauf dieser Antwort zu entfernen. Es ist völlig nutzlos. Wenn Sie den Verlauf anzeigen möchten, bietet Stackoverflow einen Mechanismus, um dies zu tun. Die meisten Leute, die diese Antwort lesen, interessieren sich nicht für die Geschichte.
Bryan Oakley

2
Vielen Dank für den Versionsverlauf. Es ist gut zu wissen, dass Sie die Antwort im Laufe der Zeit aktualisiert und überarbeitet haben.
Vasyl Vaskivskyi
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.