Переключение между двумя кадрами в tkinter

94

Я создал свои первые несколько скриптов с красивым маленьким графическим интерфейсом, как показали мне учебники, но ни один из них не описывает, что делать для более сложной программы.

Если у вас есть что-то с «стартовым меню» для вашего начального экрана, и после выбора пользователя вы переходите в другой раздел программы и соответствующим образом перерисовываете экран, какой элегантный способ сделать это?

Один только .destroy()фрейм «Пусковое меню», а затем создать новый, заполненный виджетами для другой части? И отменить этот процесс, когда они нажимают кнопку возврата?

Макс Тилли
источник

Ответы:

177

Один из способов - наложить рамки друг на друга, а затем можно просто поднять одну над другой в порядке наложения. Тот, что находится сверху, будет видимым. Лучше всего это работает, если все рамки имеют одинаковый размер, но немного поработав, вы можете заставить его работать с рамками любого размера.

Примечание : для того, чтобы это работало, все виджеты для страницы должны иметь эту страницу (например:) selfили потомка в качестве родительского (или главного, в зависимости от выбранной вами терминологии).

Вот небольшой надуманный пример, чтобы показать вам общую концепцию:

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

Начальная страница Страница 1 страница 2

Если вы считаете, что концепция создания экземпляра в классе сбивает вас с толку или если разные страницы нуждаются в разных аргументах во время построения, вы можете явно вызвать каждый класс отдельно. Цикл служит главным образом для иллюстрации того, что каждый класс идентичен.

Например, чтобы создать классы по отдельности, вы можете удалить цикл ( for F in (StartPage, ...)с помощью этого:

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

Со временем люди задавали другие вопросы, используя этот код (или онлайн-руководство, которое скопировало этот код) в качестве отправной точки. Возможно, вы захотите прочитать ответы на эти вопросы:

Брайан Окли
источник
3
Вау, спасибо большое. Как академический вопрос, а не практический: сколько из этих страниц, спрятанных друг над другом, вам нужно, прежде чем они начнут тормозить и не отвечать?
Макс Тилли,
4
Я не знаю. Наверное, тысячи. Это было бы достаточно легко проверить.
Брайан Окли
1
Нет ли действительно простого способа добиться эффекта, подобного этому, с гораздо меньшим количеством строк кода, без необходимости складывать кадры друг на друга?
Parallax Sugar
2
@StevenVascellaro: да, pack_forgetможно использовать, если вы используете pack.
Bryan Oakley
2
Предупреждение : пользователи могут выбрать «скрытые» виджеты на фреймах фона, нажав Tab, а затем активировать их, нажав Enter.
Stevoisiak
31

Вот еще один простой ответ, но без использования классов.

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

Предупреждение : ответ ниже может вызвать утечку памяти из- за многократного уничтожения и воссоздания кадров.

Один из способов переключения фреймов tkinter- уничтожить старый фрейм, а затем заменить его новым.

Я изменил ответ Брайана Окли, чтобы уничтожить старую раму перед ее заменой. В качестве дополнительного бонуса это устраняет необходимость в containerобъекте и позволяет использовать любой универсальный Frameкласс.

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

Начальная страница Первая страница Страница вторая

Объяснение

switch_frame()работает, принимая любой объект класса, который реализует Frame. Затем функция создает новый фрейм для замены старого.

  • Удаляет старый, _frameесли он существует, затем заменяет его новым фреймом.
  • Другие кадры, добавленные с помощью .pack(), например строки меню, не будут затронуты.
  • Может использоваться с любым классом, который реализует tkinter.Frame.
  • Размер окна автоматически изменяется в соответствии с новым содержимым

История версий

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
источник
1
Я получаю это странное кровотечение кадров, когда пробую этот метод, не уверен, можно ли его воспроизвести: imgur.com/a/njCsa
quantik
2
@quantik Эффект просачивания возникает из-за того, что старые кнопки не уничтожаются должным образом. Убедитесь, что вы прикрепляете кнопки непосредственно к классу фрейма (обычно self).
Stevoisiak
1
Оглядываясь назад, switch_frame()можно сказать, что это название вводит в заблуждение. Переименовать в replace_frame()?
Stevoisiak
11
Я рекомендую удалить часть истории версий этого ответа. Это совершенно бесполезно. Если вы хотите увидеть историю, stackoverflow предоставляет механизм для этого. Большинство людей, читающих этот ответ, не заботятся об истории.
Bryan Oakley
2
Спасибо за историю версий. Приятно знать, что вы со временем обновляли и изменяли ответ.
Василий Васьковский