Passa da un frame all'altro in tkinter


94

Ho costruito i miei primi script con una piccola e graziosa GUI, come mi hanno mostrato i tutorial, ma nessuno di questi si occupa di cosa fare per un programma più complesso.

Se hai qualcosa con un "menu di avvio", per la schermata di apertura, e dopo la selezione dell'utente ti sposti in una sezione diversa del programma e ridisegna lo schermo in modo appropriato, qual è il modo elegante per farlo?

Si fa solo .destroy()il frame del 'menu di avvio' e quindi ne crea uno nuovo riempito con i widget per un'altra parte? E invertire questo processo quando premono il pulsante Indietro?

Risposte:


177

Un modo è impilare i frame uno sopra l'altro, quindi puoi semplicemente sollevarli uno sopra l'altro nell'ordine di sovrapposizione. Quello in alto sarà quello visibile. Funziona meglio se tutti i frame hanno le stesse dimensioni, ma con un po 'di lavoro puoi farlo funzionare con frame di qualsiasi dimensione.

Nota : affinché funzioni, tutti i widget di una pagina devono avere quella pagina (es .:) selfo un discendente come genitore (o master, a seconda della terminologia che preferisci).

Ecco un esempio un po 'artificioso per mostrarti il ​​concetto generale:

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

pagina iniziale Pagina 1 pagina 2

Se trovi confuso il concetto di creazione di istanze in una classe, o se pagine diverse richiedono argomenti diversi durante la costruzione, puoi chiamare esplicitamente ciascuna classe separatamente. Il ciclo serve principalmente per illustrare il punto che ogni classe è identica.

Ad esempio, per creare le classi individualmente puoi rimuovere il ciclo ( for F in (StartPage, ...)con questo:

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

Nel tempo le persone hanno posto altre domande utilizzando questo codice (o un tutorial online che ha copiato questo codice) come punto di partenza. Potresti leggere le risposte a queste domande:


3
Wow, grazie mille. Proprio come una domanda accademica piuttosto che pratica, quante di queste pagine nascoste l'una sull'altra sarebbero necessarie prima che iniziasse a diventare lente e non rispondente?
Max Tilley

4
Non lo so. Probabilmente migliaia. Sarebbe abbastanza facile testare.
Bryan Oakley,

1
Non esiste un modo veramente semplice per ottenere un effetto simile a questo, con molte meno righe di codice, senza dover impilare i frame uno sopra l'altro?
Parallax Sugar

2
@StevenVascellaro: sì, pack_forgetpuò essere utilizzato se lo stai utilizzando pack.
Bryan Oakley

2
Attenzione : è possibile per gli utenti selezionare widget "nascosti" sui frame di sfondo premendo Tab, quindi attivarli con Invio.
Stevoisiak

31

Ecco un'altra semplice risposta, ma senza usare le classi.

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

28

Avviso : la risposta seguente potrebbe causare una perdita di memoria distruggendo e ricreando ripetutamente i frame.

Un modo per cambiare frame tkinterè distruggere il vecchio frame e poi sostituirlo con il nuovo frame.

Ho modificato la risposta di Bryan Oakley per distruggere il vecchio telaio prima di sostituirlo. Come bonus aggiuntivo, questo elimina la necessità di un containeroggetto e ti consente di utilizzare qualsiasi Frameclasse generica .

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

Pagina iniziale Pagina uno Pagina due

Spiegazione

switch_frame()funziona accettando qualsiasi oggetto Class che implementa Frame. La funzione crea quindi un nuovo frame per sostituire quello vecchio.

  • Elimina il vecchio _framese esistente, quindi lo sostituisce con il nuovo frame.
  • Altri frame aggiunti con .pack(), come le barre dei menu, non saranno influenzati.
  • Può essere utilizzato con qualsiasi classe che implementa tkinter.Frame.
  • La finestra si ridimensiona automaticamente per adattarsi al nuovo contenuto

Cronologia delle versioni

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
Ottengo questo strano sanguinamento dei fotogrammi quando provo questo metodo non sono sicuro che possa essere replicato: imgur.com/a/njCsa
quantik

2
@quantik L'effetto bleed-over si verifica perché i vecchi pulsanti non vengono distrutti correttamente. Assicurati di collegare i pulsanti direttamente alla classe frame (di solito self).
Stevoisiak

1
Col senno di poi, il nome switch_frame()potrebbe essere un po 'fuorviante. Devo rinominarlo in replace_frame()?
Stevoisiak

11
consiglio di rimuovere la parte della cronologia delle versioni di questa risposta. È completamente inutile. Se vuoi vedere la cronologia, stackoverflow fornisce un meccanismo per farlo. La maggior parte delle persone che leggono questa risposta non si preoccupano della storia.
Bryan Oakley

2
Grazie per la cronologia delle versioni. È bello sapere che hai aggiornato e rivisto la risposta nel tempo.
Vasyl Vaskivskyi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.