Beralih di antara dua bingkai di tkinter

94

Saya telah membangun beberapa skrip pertama saya dengan GUI kecil yang bagus di atasnya, seperti yang ditunjukkan tutorial kepada saya, tetapi tidak ada yang membahas apa yang harus dilakukan untuk program yang lebih kompleks.

Jika Anda memiliki sesuatu dengan 'menu mulai', untuk layar pembuka Anda, dan setelah pemilihan pengguna, Anda pindah ke bagian lain dari program dan menggambar ulang layar dengan tepat, cara elegan apa untuk melakukan ini?

Apakah seseorang hanya .destroy()bingkai 'start menu' dan kemudian membuat yang baru diisi dengan widget untuk bagian lain? Dan membalikkan proses ini saat mereka menekan tombol kembali?

Max Tilley
sumber

Jawaban:

177

Salah satu caranya adalah dengan menumpuk bingkai di atas satu sama lain, lalu Anda cukup menaikkan satu bingkai di atas yang lain dalam urutan susun. Yang di atas akan menjadi salah satu yang terlihat. Ini bekerja paling baik jika semua bingkai berukuran sama, tetapi dengan sedikit usaha, Anda dapat membuatnya berfungsi dengan bingkai berukuran berapa pun.

Catatan : agar ini berfungsi, semua widget untuk halaman harus memiliki halaman itu (yaitu:) selfatau turunan sebagai induk (atau master, tergantung pada terminologi yang Anda inginkan).

Berikut ini sedikit contoh yang dibuat untuk menunjukkan kepada Anda konsep umum:

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

halaman awal Halaman 1 halaman 2

Jika Anda merasa konsep pembuatan instance di kelas membingungkan, atau jika halaman yang berbeda memerlukan argumen yang berbeda selama konstruksi, Anda dapat secara eksplisit memanggil setiap kelas secara terpisah. Perulangan berfungsi terutama untuk menggambarkan poin bahwa setiap kelas identik.

Misalnya, untuk membuat kelas satu per satu, Anda dapat menghapus loop ( for F in (StartPage, ...)dengan ini:

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

Seiring waktu orang mengajukan pertanyaan lain menggunakan kode ini (atau tutorial online yang menyalin kode ini) sebagai titik awal. Anda mungkin ingin membaca jawaban atas pertanyaan-pertanyaan ini:

Bryan Oakley
sumber
3
Wow, terima kasih banyak. Sama seperti pertanyaan akademis dan bukan pertanyaan praktis, berapa banyak dari halaman-halaman ini yang tersembunyi di atas satu sama lain yang Anda perlukan sebelum mulai menjadi lamban dan tidak responsif?
Max Tilley
4
Saya tidak tahu. Mungkin ribuan. Cukup mudah untuk menguji.
Bryan Oakley
1
Apakah tidak ada cara yang benar-benar sederhana untuk mencapai pengaruh yang serupa dengan ini, dengan baris kode yang jauh lebih sedikit, tanpa harus menumpuk bingkai di atas satu sama lain?
Gula Paralaks
2
@StevenVascellaro: ya, pack_forgetdapat digunakan jika Anda menggunakan pack.
Bryan Oakley
2
Peringatan : Anda dapat memilih widget "tersembunyi" pada bingkai latar belakang dengan menekan Tab, lalu mengaktifkannya dengan Enter.
Stevoisiak
31

Berikut adalah jawaban sederhana lainnya, tetapi tanpa menggunakan kelas.

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

Peringatan : Jawaban di bawah ini dapat menyebabkan kebocoran memori dengan berulang kali menghancurkan dan membuat ulang frame.

Salah satu cara untuk mengganti frame tkinteradalah dengan menghancurkan frame lama kemudian menggantinya dengan frame baru Anda.

Saya telah memodifikasi jawaban Bryan Oakley untuk menghancurkan bingkai lama sebelum menggantinya. Sebagai bonus tambahan, ini menghilangkan kebutuhan akan containerobjek dan memungkinkan Anda menggunakan Framekelas generik apa pun .

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

Halaman awal Halaman satu Halaman dua

Penjelasan

switch_frame()bekerja dengan menerima objek Kelas apa pun yang diimplementasikan Frame. Fungsi tersebut kemudian membuat bingkai baru untuk menggantikan yang lama.

  • Menghapus yang lama _framejika ada, lalu menggantinya dengan bingkai baru.
  • Bingkai lain yang ditambahkan .pack(), seperti batang menu, tidak akan terpengaruh.
  • Dapat digunakan dengan kelas apa pun yang mengimplementasikan tkinter.Frame.
  • Jendela secara otomatis mengubah ukuran agar sesuai dengan konten baru

Riwayat Versi

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
sumber
1
Saya mendapatkan bleed-over frame yang aneh ini ketika saya mencoba metode ini tidak yakin apakah dapat direplikasi: imgur.com/a/njCsa
quantik
2
@quantik Efek bleed-over terjadi karena tombol lama tidak dihancurkan dengan benar. Pastikan Anda memasang tombol langsung ke kelas bingkai (biasanya self).
Stevoisiak
1
Kalau dipikir-pikir, namanya switch_frame()bisa jadi agak menyesatkan. Haruskah saya mengganti namanya menjadi replace_frame()?
Stevoisiak
11
Saya sarankan menghapus bagian riwayat versi dari jawaban ini. Itu sama sekali tidak berguna. Jika Anda ingin melihat history, stackoverflow menyediakan mekanisme untuk melakukannya. Kebanyakan orang yang membaca jawaban ini tidak peduli dengan sejarahnya.
Bryan Oakley
2
Terima kasih atas riwayat versinya. Tenang rasanya mengetahui bahwa Anda memperbarui dan merevisi jawaban dari waktu ke waktu.
Vasyl Vaskivskyi