Alternar entre dois frames no tkinter

97

Eu construí meus primeiros scripts com uma pequena GUI neles, como os tutoriais me mostraram, mas nenhum deles aborda o que fazer para um programa mais complexo.

Se você tiver algo com um 'menu iniciar', para a sua tela de abertura, e após a seleção do usuário você mover para uma seção diferente do programa e redesenhar a tela apropriadamente, qual é a maneira elegante de fazer isso?

Alguém abre .destroy()o quadro do 'menu iniciar' e cria um novo preenchido com os widgets para outra parte? E reverter esse processo quando pressionam o botão Voltar?

Max Tilley
fonte

Respostas:

181

Uma maneira é empilhar as molduras umas sobre as outras, então você pode simplesmente elevar uma acima da outra na ordem de empilhamento. O que está em cima será o que está visível. Isso funciona melhor se todas as molduras forem do mesmo tamanho, mas com um pouco de trabalho você pode fazer com que funcione com molduras de qualquer tamanho.

Nota : para que isso funcione, todos os widgets de uma página devem ter essa página (ou seja:) selfou um descendente como pai (ou mestre, dependendo da terminologia que você preferir).

Aqui está um exemplo artificial para mostrar o conceito geral:

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

página inicial Página 1 página 2

Se você achar o conceito de criação de instância em uma classe confuso, ou se páginas diferentes precisarem de argumentos diferentes durante a construção, você pode chamar explicitamente cada classe separadamente. O loop serve principalmente para ilustrar o ponto de que cada classe é idêntica.

Por exemplo, para criar as classes individualmente, você pode remover o loop ( for F in (StartPage, ...)com isto:

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

Com o tempo, as pessoas fizeram outras perguntas usando esse código (ou um tutorial online que copiava esse código) como ponto de partida. Você pode querer ler as respostas a estas perguntas:

Bryan Oakley
fonte
3
Uau, muito obrigado. Apenas como uma questão acadêmica, em vez de prática, de quantas dessas páginas ocultas uma sobre a outra você precisaria antes de começar a ficar vagarosa e sem resposta?
Max Tilley
4
Eu não sei. Provavelmente milhares. Seria muito fácil testar.
Bryan Oakley
1
Não existe uma maneira verdadeiramente simples de atingir um efeito semelhante a este, com muito menos linhas de código, sem ter que empilhar quadros uns sobre os outros?
Parallax Sugar
2
@StevenVascellaro: sim, pack_forgetpode ser usado se você estiver usando pack.
Bryan Oakley
2
Aviso : é possível para os usuários selecionar widgets "ocultos" em quadros de fundo pressionando Tab e ativá-los com Enter.
Stevoisiak
31

Aqui está outra resposta simples, mas sem usar classes.

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
fonte
28

Aviso : a resposta abaixo pode causar vazamento de memória ao destruir e recriar quadros repetidamente.

Uma maneira de alternar entre os quadros tkinteré destruir o quadro antigo e substituí-lo pelo novo.

Eu modifiquei de Bryan Oakley resposta para destruir o velho quadro antes de substituí-lo. Como um bônus adicional, isso elimina a necessidade de um containerobjeto e permite que você use qualquer Frameclasse genérica .

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

Página inicial Página um Página dois

Explicação

switch_frame()funciona aceitando qualquer objeto Class que implemente Frame. A função então cria um novo quadro para substituir o antigo.

  • Exclui o antigo, _framese existir, e o substitui pelo novo quadro.
  • Outros quadros adicionados .pack(), como as barras de menu, não serão afetados.
  • Pode ser usado com qualquer classe que implemente tkinter.Frame.
  • A janela é redimensionada automaticamente para caber no novo conteúdo

Histórico da versão

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
fonte
1
Eu recebo esse sangramento estranho de frames quando tento esse método, sem saber se ele pode ser replicado: imgur.com/a/njCsa
quantik
2
@quantik O efeito de sangramento acontece porque os botões antigos não estão sendo destruídos corretamente. Certifique-se de anexar botões diretamente à classe do frame (geralmente self).
Stevoisiak
1
Em retrospectiva, o nome switch_frame()pode ser um pouco enganador. Devo renomear para replace_frame()?
Stevoisiak de
12
Eu recomendo remover a parte do histórico de versões desta resposta. É completamente inútil. Se você quiser ver o histórico, stackoverflow fornece um mecanismo para fazer isso. A maioria das pessoas que lêem esta resposta não se preocupa com a história.
Bryan Oakley
2
Obrigado pelo histórico de versões. É bom saber que você atualizou e revisou a resposta ao longo do tempo.
Vasyl Vaskivskyi