Chuyển đổi giữa hai khung hình trong tkinter


94

Tôi đã xây dựng một số tập lệnh đầu tiên của mình với một giao diện đồ họa nhỏ đẹp mắt trên đó, như các hướng dẫn đã chỉ cho tôi, nhưng không có tập lệnh nào trong số đó giải quyết những việc cần làm đối với một chương trình phức tạp hơn.

Nếu bạn có một cái gì đó với 'menu bắt đầu', cho màn hình mở của bạn và khi người dùng lựa chọn, bạn di chuyển đến một phần khác của chương trình và vẽ lại màn hình một cách thích hợp, cách thanh lịch để làm điều này là gì?

Có phải người ta chỉ .destroy()khung 'menu bắt đầu' và sau đó tạo một khung mới chứa đầy các tiện ích cho phần khác không? Và đảo ngược quá trình này khi họ nhấn nút quay lại?

Câu trả lời:


177

Một cách là xếp chồng các khung lên nhau, sau đó bạn chỉ cần nâng một khung lên trên khung kia theo thứ tự xếp chồng. Cái trên cùng sẽ là cái có thể nhìn thấy được. Điều này hoạt động tốt nhất nếu tất cả các khung có cùng kích thước, nhưng với một chút thao tác, bạn có thể làm cho nó hoạt động với bất kỳ khung có kích thước nào.

Lưu ý : để điều này hoạt động, tất cả các tiện ích con cho một trang phải có trang đó (tức là self:) hoặc con cháu là trang mẹ (hoặc trang chính, tùy thuộc vào thuật ngữ bạn thích).

Đây là một chút ví dụ có sẵn để cho bạn thấy khái niệm chung:

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

trang bắt đầu trang 1 trang 2

Nếu bạn thấy khái niệm tạo cá thể trong một lớp khó hiểu hoặc nếu các trang khác nhau cần các đối số khác nhau trong quá trình xây dựng, bạn có thể gọi từng lớp một cách rõ ràng. Vòng lặp phục vụ chủ yếu để minh họa điểm mà mỗi lớp là giống nhau.

Ví dụ: để tạo các lớp riêng lẻ, bạn có thể xóa vòng lặp ( for F in (StartPage, ...)với điều này:

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

Theo thời gian, mọi người đã hỏi các câu hỏi khác bằng cách sử dụng mã này (hoặc một hướng dẫn trực tuyến đã sao chép mã này) làm điểm bắt đầu. Bạn có thể muốn đọc câu trả lời cho những câu hỏi này:


3
Wow, cảm ơn bạn rất nhiều. Chỉ là một câu hỏi học thuật hơn là một câu hỏi thực tế, bạn sẽ cần bao nhiêu trong số các trang này được ẩn chồng lên nhau trước khi nó bắt đầu trở nên chậm và không phản hồi?
Max Tilley,

4
Tôi không biết. Có lẽ là hàng ngàn. Nó sẽ đủ dễ dàng để kiểm tra.
Bryan Oakley

1
Không có cách nào thực sự đơn giản để đạt được ảnh hưởng tương tự như cách này, với ít dòng mã hơn nhiều, mà không cần phải xếp chồng các khung lên nhau?
Parallax Sugar

2
@StevenVascellaro: có, pack_forgetcó thể được sử dụng nếu bạn đang sử dụng pack.
Bryan Oakley

2
Cảnh báo : Người dùng có thể chọn widget "ẩn" trên khung nền bằng cách nhấn Tab, sau đó kích hoạt chúng bằng Enter.
Stevoisiak

31

Đây là một câu trả lời đơn giản khác, nhưng không sử dụng các lớp.

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

Cảnh báo : Câu trả lời dưới đây có thể gây rò rỉ bộ nhớ do phá hủy và tạo lại các khung liên tục.

Một cách để chuyển đổi khung tkinterlà phá hủy khung cũ sau đó thay thế nó bằng khung mới của bạn.

Tôi đã sửa đổi câu trả lời của Bryan Oakley để phá hủy khung cũ trước khi thay thế nó. Như một phần thưởng bổ sung, điều này loại bỏ sự cần thiết của một containerđối tượng và cho phép bạn sử dụng bất kỳ Framelớp chung nào .

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

Trang bắt đầu Trang một Trang hai

Giải trình

switch_frame()hoạt động bằng cách chấp nhận bất kỳ đối tượng Lớp nào triển khai Frame. Sau đó, hàm sẽ tạo một khung mới để thay thế khung cũ.

  • Xóa cũ _framenếu nó tồn tại, sau đó thay thế nó bằng khung mới.
  • Các khung khác được thêm vào .pack(), chẳng hạn như thanh menu, sẽ không bị ảnh hưởng.
  • Có thể được sử dụng với bất kỳ lớp nào thực hiện tkinter.Frame.
  • Cửa sổ tự động thay đổi kích thước để phù hợp với nội dung mới

Lịch sử phiên bản

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.

1
Tôi nhận được chảy máu giao kỳ lạ này của khung khi tôi thử phương pháp này không chắc chắn nếu nó có thể được nhân rộng: imgur.com/a/njCsa
Quantik

2
@quantik Hiệu ứng tràn màn hình xảy ra do các nút cũ không được phá hủy đúng cách. Đảm bảo rằng bạn đang gắn các nút trực tiếp vào lớp khung (thông thường self).
Stevoisiak

1
Trong nhận thức muộn màng, tên switch_frame() có thể gây hiểu nhầm một chút. Tôi có nên đổi tên nó thành replace_frame()không?
Stevoisiak

11
tôi khuyên bạn nên xóa phần lịch sử phiên bản của câu trả lời này. Nó hoàn toàn vô dụng. Nếu bạn muốn xem lịch sử, stackoverflow cung cấp một cơ chế để làm điều đó. Hầu hết những người đọc câu trả lời này sẽ không quan tâm đến lịch sử.
Bryan Oakley

2
Cảm ơn bạn về lịch sử phiên bản. Thật vui khi biết rằng bạn đã cập nhật và sửa đổi câu trả lời theo thời gian.
Vasyl Vaskivskyi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.