pythonuser-interfacetkinterscroll

Custom Tk Scrollable Frame scrolls when the window is not full


I've been trying to create a scrollable frame in with Tkinter / ttk using the many answers on here and guides scattered around. The frame is to hold a table which can have rows added and deleted. One issue I'm having that I can't find elsewhere is that if there is space for more content within the frame, I can scroll the contents of the table to the bottom of the frame. When the frame is full, it behaves as expected.

I can't convert to a gif at the moment, so I've added some screenshots to try and illustrate.

Here is my scrollable frame class, it's pretty standard from examples on the web;

class ScrollableFrame(ttk.Frame):

    def __init__(self, parent):

        super().__init__(parent)
        
        self.canvas = tk.Canvas(self)
        self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.scrollableFrame = ttk.Frame(self.canvas)
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        
        self.scrollableFrame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.bind("<Enter>", self.bind_to_mousewheel)
        self.bind("<Leave>", self.unbind_from_mousewheel)
        
        self.scrollbar.pack(side='right', fill='y')
        self.canvas.pack(side='top', expand=0, fill='x')
        self.canvas.create_window((0, 0), window=self.scrollableFrame)
        
    def bind_to_mousewheel(self, event):
        self.canvas.bind_all("<MouseWheel>", self.on_mousewheel)
        
    def unbind_from_mousewheel(self, event):
        self.canvas.unbind_all("<MouseWheel>")

    def on_mousewheel(self, event):
        self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

The table rows are added to the ScrollableFrame.scrollableFrame widget by the parent GUI, which I figured was necessary to trigger the <Configure> bound to the ScrollableFrame.canvas.

Does anyone have any suggestions?

No scroll required as frame not full No scroll required as frame not full

Table still scrolls Table still scrolls

Scroll stops with row widgets at bottom of frame Scroll stops with row widgets at bottom of frame


Solution

  • I found an answer thanks to the hint from @acw1668;

    I adjusted the <Configure> command to check the size of the scrollregion and compare it to the parent ScrollableFrame class window. If it is smaller, I change the scroll region to be the size of the ScrollableFrame.

    Here are the adjustments I made to my class, including changing the window anchor to sit at the top left of the frame;

    class ScrollableFrame(ttk.Frame):
        """
        """
        def __init__(self, parent):
            """
            """
            ...
            self.scrollableFrame.bind("<Configure>", self.update_scroll_region)
            ...
            self.canvas.create_window((0, 0), window=self.scrollableFrame, anchor='nw')
    
        def update_scroll_region(self, event):
            bbox = self.canvas.bbox('all')  
            sfHeight = self.winfo_height()
            if (bbox[3] - bbox[1]) < sfHeight:
                newBbox = (bbox[0], 0, bbox[2], sfHeight)
                self.canvas.configure(scrollregion=newBbox)
            else:
                self.canvas.configure(scrollregion=self.canvas.bbox("all"))
    

    As pointed out, it would be possible and more Pythonic to use bbox[-1] = max(bbox[-1], sfHeight), then call self.canvas.configure(scrollregion=bbox) however I found due to my canvas' placement I had to set the first Y coordinate to 0.