Basculer entre deux cadres dans tkinter

94

J'ai construit mes premiers scripts avec une jolie petite interface graphique, comme les tutoriels me l'ont montré, mais aucun d'entre eux ne traite de ce qu'il faut faire pour un programme plus complexe.

Si vous avez quelque chose avec un `` menu de démarrage '', pour votre écran d'ouverture, et lors de la sélection de l'utilisateur, vous vous déplacez vers une section différente du programme et redessinez l'écran de manière appropriée, quelle est la manière élégante de le faire?

Est-ce que l'on se contente du cadre .destroy()du «menu de démarrage», puis en crée un nouveau rempli des widgets pour une autre partie? Et inverser ce processus lorsqu'ils appuient sur le bouton de retour?

Max Tilley
la source

Réponses:

177

Une façon consiste à empiler les cadres les uns sur les autres, puis vous pouvez simplement les élever l'un au-dessus de l'autre dans l'ordre d'empilage. Celui du dessus sera celui qui est visible. Cela fonctionne mieux si tous les cadres sont de la même taille, mais avec un peu de travail, vous pouvez le faire fonctionner avec des cadres de toutes tailles.

Remarque : pour que cela fonctionne, tous les widgets d'une page doivent avoir cette page (c'est-à-dire:) selfou un descendant comme parent (ou maître, selon la terminologie que vous préférez).

Voici un exemple artificiel pour vous montrer le concept général:

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

page de démarrage Page 1 page 2

Si vous trouvez le concept de création d'instance dans une classe déroutant, ou si différentes pages ont besoin d'arguments différents pendant la construction, vous pouvez appeler explicitement chaque classe séparément. La boucle sert principalement à illustrer le fait que chaque classe est identique.

Par exemple, pour créer les classes individuellement, vous pouvez supprimer la boucle ( for F in (StartPage, ...)avec ceci:

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

Au fil du temps, les gens ont posé d'autres questions en utilisant ce code (ou un didacticiel en ligne copiant ce code) comme point de départ. Vous voudrez peut-être lire les réponses à ces questions:

Bryan Oakley
la source
3
Woah, merci beaucoup. Juste comme une question académique plutôt que pratique, combien de ces pages cachées les unes sur les autres auriez-vous besoin avant que cela commence à devenir lent et insensible?
Max Tilley
4
Je ne sais pas. Probablement des milliers. Ce serait assez facile à tester.
Bryan Oakley
1
N'existe-t-il pas de moyen vraiment simple d'obtenir un effet similaire à celui-ci, avec beaucoup moins de lignes de code, sans avoir à empiler des cadres les uns sur les autres?
Parallax Sugar
2
@StevenVascellaro: oui, pack_forgetpeut être utilisé si vous utilisez pack.
Bryan Oakley
2
Attention : Il est possible pour les utilisateurs de sélectionner des widgets "cachés" sur les cadres d'arrière-plan en appuyant sur Tabulation, puis de les activer avec Entrée.
Stevoisiak
31

Voici une autre réponse simple, mais sans utiliser de 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
la source
28

Avertissement : la réponse ci-dessous peut provoquer une fuite de mémoire en détruisant et recréant à plusieurs reprises des images.

Une façon de changer de cadre tkinterest de détruire l'ancien cadre, puis de le remplacer par votre nouveau cadre.

J'ai modifié la réponse de Bryan Oakley pour détruire l'ancien cadre avant de le remplacer. En prime, cela élimine le besoin d'un containerobjet et vous permet d'utiliser n'importe quelle Frameclasse générique .

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

Page de démarrage Page un Page deux

Explication

switch_frame() fonctionne en acceptant tout objet Class qui implémente Frame . La fonction crée alors un nouveau cadre pour remplacer l'ancien.

  • Supprime l'ancien _frame s'il existe, puis le remplace par le nouveau cadre.
  • Autres cadres ajoutés avec .pack() , tels que les barres de menus, ne seront pas affectés.
  • Peut être utilisé avec n'importe quelle classe qui implémente tkinter.Frame .
  • La fenêtre se redimensionne automatiquement pour s'adapter au nouveau contenu

Historique des versions

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
la source
1
J'obtiens ce saignement étrange d'images lorsque j'essaye cette méthode, je ne sais pas si elle peut être reproduite: imgur.com/a/njCsa
quantik
2
@quantik L'effet de fond perdu se produit parce que les anciens boutons ne sont pas détruits correctement. Assurez-vous que vous attachez des boutons directement à la classe de cadres (généralement self).
Stevoisiak
1
Avec le recul, le nom switch_frame()pourrait être un peu trompeur. Dois-je le renommer replace_frame()?
Stevoisiak
11
Je recommande de supprimer la partie historique des versions de cette réponse. C'est complètement inutile. Si vous voulez voir l'historique, stackoverflow fournit un mécanisme pour le faire. La plupart des gens qui lisent cette réponse ne se soucient pas de l'histoire.
Bryan Oakley
2
Merci pour l'historique des versions. Il est bon de savoir que vous avez mis à jour et révisé la réponse au fil du temps.
Vasyl Vaskivskyi