Cambiar entre dos fotogramas en tkinter

94

He creado mis primeros scripts con una pequeña interfaz gráfica de usuario, como me han mostrado los tutoriales, pero ninguno de ellos aborda qué hacer para un programa más complejo.

Si tiene algo con un 'menú de inicio', para su pantalla inicial, y luego de la selección del usuario, se mueve a una sección diferente del programa y vuelve a dibujar la pantalla de manera apropiada, ¿cuál es la forma elegante de hacerlo?

¿Uno simplemente .destroy()el marco del 'menú de inicio' y luego crea uno nuevo lleno de widgets para otra parte? ¿Y revertir este proceso cuando presionan el botón Atrás?

Max Tilley
fuente

Respuestas:

177

Una forma es apilar los marcos uno encima del otro, luego simplemente puede levantar uno encima del otro en el orden de apilamiento. El de arriba será el que esté visible. Esto funciona mejor si todos los marcos son del mismo tamaño, pero con un poco de trabajo puede hacer que funcione con marcos de cualquier tamaño.

Nota : para que esto funcione, todos los widgets de una página deben tener esa página (es decir:) selfo un descendiente como padre (o maestro, según la terminología que prefiera).

Aquí hay un pequeño ejemplo artificial para mostrarle el concepto general:

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 de inicio Página 1 página 2

Si encuentra confuso el concepto de crear una instancia en una clase, o si diferentes páginas necesitan diferentes argumentos durante la construcción, puede llamar explícitamente a cada clase por separado. El ciclo sirve principalmente para ilustrar el punto de que cada clase es idéntica.

Por ejemplo, para crear las clases individualmente puede eliminar el bucle ( for F in (StartPage, ...)con esto:

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

Con el tiempo, la gente ha hecho otras preguntas utilizando este código (o un tutorial en línea que copia este código) como punto de partida. Es posible que desee leer las respuestas a estas preguntas:

Bryan Oakley
fuente
3
Vaya, muchas gracias. Solo como una pregunta académica en lugar de práctica, ¿cuántas de estas páginas ocultas una encima de la otra necesitaría antes de que comenzara a volverse lenta y sin respuesta?
Max Tilley
4
No lo sé. Probablemente miles. Sería bastante fácil de probar.
Bryan Oakley
1
¿No existe una forma realmente sencilla de lograr un efecto similar a este, con muchas menos líneas de código, sin tener que apilar marcos uno encima del otro?
Parallax Sugar
2
@StevenVascellaro: sí, pack_forgetse puede usar si está usando pack.
Bryan Oakley
2
Advertencia : Es posible que los usuarios seleccionen widgets "ocultos" en los marcos de fondo presionando la tecla Tab y luego los activen con Enter.
Stevoisiak
31

Aquí hay otra respuesta simple, pero sin usar clases.

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

Advertencia : La respuesta a continuación puede causar una pérdida de memoria al destruir y recrear marcos repetidamente.

Una forma de cambiar los marcos tkinteres destruir el marco antiguo y luego reemplazarlo por el nuevo.

He modificado la respuesta de Bryan Oakley para destruir el marco anterior antes de reemplazarlo. Como ventaja adicional, esto elimina la necesidad de un containerobjeto y le permite usar cualquier Frameclase 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 de inicio Página uno Página dos

Explicación

switch_frame()funciona aceptando cualquier objeto Class que implemente Frame. Luego, la función crea un nuevo marco para reemplazar el anterior.

  • Elimina el antiguo _framesi existe, luego lo reemplaza con el nuevo marco.
  • Otros marcos agregados .pack(), como las barras de menú, no se verán afectados.
  • Se puede utilizar con cualquier clase que implemente tkinter.Frame.
  • La ventana cambia de tamaño automáticamente para adaptarse al nuevo contenido

Historial de versiones

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
fuente
1
Recibo este extraño sangrado de marcos cuando pruebo este método, no estoy seguro de si se puede replicar: imgur.com/a/njCsa
quantik
2
@quantik El efecto de sangrado ocurre porque los botones antiguos no se destruyen correctamente. Asegúrese de adjuntar botones directamente a la clase de marco (generalmente self).
Stevoisiak
1
En retrospectiva, el nombre switch_frame()podría ser un poco engañoso. ¿Debería cambiarle el nombre a replace_frame()?
Stevoisiak
11
Recomiendo eliminar la parte del historial de versiones de esta respuesta. Es completamente inútil. Si desea ver el historial, stackoverflow proporciona un mecanismo para hacerlo. A la mayoría de las personas que leen esta respuesta no les importará la historia.
Bryan Oakley
2
Gracias por el historial de versiones. Es bueno saber que actualizó y revisó la respuesta con el tiempo.
Vasyl Vaskivskyi