顶级 window 关闭后在主 window 中滚动时出错

Error scrolling in main window after top level window was closed

我正在使用 Tkinter 构建应用程序,我需要可滚动 windows。我使用 Canvas 创建了一个可滚动容器: ScrollContainer 。在此之后,我将我的程序的主要逻辑合并到这个容器中,我在其中放置了一个按钮来打开另一个单独的 TopLevel window。这个单独的 window 也必须是可滚动的。因此我也将它包含在同一个容器中 class

现在,问题是:当我 运行 程序时,我的主程序 window 可以正常滚动。单击按钮后,我打开了 TopLevel window。辅助 window 滚动正常。在我 关闭 次要 window 并将鼠标再次悬停在主要 window 上之后,现在它不再滚动并且我在控制台中收到错误消息:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1883, in __call__
    return self.func(*args)
  File "C:/Users/mirel.voicu/Desktop/python_projects/TKINTER/standalone_program/test.py", line 45, in _on_mousewheel
    self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
  File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1929, in yview_scroll
    self.tk.call(self._w, 'yview', 'scroll', number, what)
_tkinter.TclError: invalid command name ".!toplevel.!frame.!canvas"

注: 我也尝试使用 self.my_canvas.bind("<MouseWheel>", self._on_mousewheel) 而不是 self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel),现在没有错误。但是,滚动发生了变化。如果将鼠标悬停在标签上,则无法再滚动。只有放大 window 并将鼠标悬停在右侧一点才能滚动。我想这是因为您必须将鼠标移到 canvas 上,因为它是唯一可滚动的实体

滚动容器 class:

from tkinter import *
from tkinter import ttk


class ScrollContainer (ttk.Frame):
    def __init__(self, container,w,h,*args, **kwargs):
        super().__init__(container, *args, **kwargs)

        # Create a main frame

        self.main_frame = Frame(container, width=w, height=h)
        self.main_frame.pack(side=TOP,fill=BOTH, expand=1)  # expand frame to the size of the container


        # create a canvas

        self.my_canvas = Canvas(self.main_frame)
        self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
        self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)

        # add h and v scrollbar to canvas

        self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
        self.my_vscrollbar.pack(side=RIGHT, fill=Y)

        self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
        self.my_hscrollbar.pack(side=BOTTOM, fill=X)

        # configure canvas
        self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
        self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))

        # create another frame inside the canvas
        self.second_frame = Frame(self.my_canvas)

        # add that new frame to a window in the canvas
        self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')

    def _on_mousewheel(self, event):

        self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")

主要程序逻辑:

def open():

    w=Toplevel()
    SecondContainer=ScrollContainer(w,1000,768)

    for thing in range(40):
        Label(SecondContainer.second_frame, text=f"It's Friday {thing} ").grid(row=thing, column=0)

root=Tk()

MainContainer=ScrollContainer(root,1000,768)

btn=Button(MainContainer.second_frame, text="New Window",bg='yellow',command=open)
btn.grid(row=0,column=0)


for thing in range(1,30):
    Label(MainContainer.second_frame,text=f"It's Friday {thing} ").grid(row=thing,column=0)

 # frame design

root.mainloop()

这是因为您使用了 bind_all(),这是应用程序智能绑定。因此 Toplevel() 中的绑定将覆盖 root 中的绑定。当顶层被销毁时,绑定函数仍然引用顶层中的 canvas,因此异常。

您应该使用 window 智能绑定:

class ScrollContainer(ttk.Frame):
    def __init__(self, container, w, h, *args, **kwargs):
        super().__init__(container, *args, **kwargs)
        container.bind("<MouseWheel>", self._on_mousewheel) # bind on the parent window

        # Create a main frame

        self.main_frame = Frame(container, width=w, height=h)
        self.main_frame.pack(side=TOP, fill=BOTH, expand=1)  # expand frame to the size of the container


        # create a canvas

        self.my_canvas = Canvas(self.main_frame)
        self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
        #self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)

        # add h and v scrollbar to canvas

        self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
        self.my_vscrollbar.pack(side=RIGHT, fill=Y)

        self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
        self.my_hscrollbar.pack(side=BOTTOM, fill=X)

        # configure canvas
        self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
        self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))

        # create another frame inside the canvas
        self.second_frame = Frame(self.my_canvas)

        # add that new frame to a window in the canvas
        self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')

    def _on_mousewheel(self, event):

        self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")

我注意到你已经解决了这个问题,但 @acw1668 给你的解决方案,就我而言,部分有效。所以,我决定 post 在这里我的代码,以防有人像我一样不幸。

我使用了@acw1668 class 但稍微修改了一下。

将 'bind_all' 替换为 'bind' 的唯一效果是在滚动条区域以外的任何地方都禁用鼠标滚动,因此我必须定义小部件的行为(self.canvas) 当事件 ('MouseWheel') 发生时。因此,我进行了一些搜索,最后找到了另一个问题 answered by @Saad,该问题解释了如何通过一种新方法 (set_mousewheel) 在光标悬停时更改小部件的行为。然后,我把所有东西放在一起,这就是我得到的:

class ScrollableFrame(Frame):
    """
    Defines a ScrollbarFrame class
    """
    def __init__(self, container, width=800, height=700, bg = "white", *args, **kwargs):
        super().__init__(container, *args, **kwargs)

        # to hide the border
        highlightthickness = 0

        # Create a main frame

        self.main_frame = Frame(container, width=width, height=height, bg=bg)
        self.main_frame.pack(side=TOP, fill=BOTH, expand=1)  # expand frame to the size of the container

        #create a canvas

        #highlightthickness = 0 to hide the border
        self.canvas = Canvas(self.main_frame, width=width, height=height, bg=bg, highlightthickness=highlightthickness)
        self.canvas.pack(side=LEFT, fill=BOTH, expand=1)

        self.scrollbar = Scrollbar(self.main_frame, orient=VERTICAL, command=self.canvas.yview)
        self.scrollbar.pack(side=RIGHT, fill=Y)

        self.scrollbar_x = Scrollbar(container, orient=HORIZONTAL, command=self.canvas.xview)
        self.scrollbar_x.pack(side=BOTTOM, fill=X)

        self.canvas.configure(yscrollcommand=self.scrollbar.set, xscrollcommand=self.scrollbar_x.set)

        self.canvas.bind(
            "<Configure>",
            lambda e: self.canvas.configure (
                scrollregion=self.canvas.bbox("all")
            )
        )

        self.scrollable_frame = Frame(self.canvas, bg=bg)
        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")

        #DO NOT PUT bind_all!!!
        self.canvas.bind("<MouseWheel>", self._on_mousewheel)
        self.canvas.bind("<MouseWheel>", self.set_mousewheel(self.canvas, self._on_mousewheel))

    def _on_mousewheel(self, event):
        """
        Allowes to scroll the mouse
        """
        self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

    def set_mousewheel(self, widget, command):
        """Activate / deactivate mousewheel scrolling when
        cursor is over / not over the widget respectively."""
        widget.bind("<Enter>", lambda _: widget.bind_all('<MouseWheel>', command))
        widget.bind("<Leave>", lambda _: widget.unbind_all('<MouseWheel>'))