Search code examples
pythonuser-interfacetkinter

Tkinter scrollable frame cut off when adding horizontal scrollbar


I'm modifying this example to add a horizontal scrollbar. I've looked at similar answers on SO but they don't help, which I'll explain further at the bottom of the question.

Before adding the horizontal scrollbar, I added some code that instead of a single column of buttons, adds a grid of buttons:

for i in range(30):
for j in range(30):
    new_button = ttk.Button(
        master=scrollable_body,
        text="Button " + str(i) + ", " + str(j)
    )
    new_button.grid(
        column=j,
        row=i
    )

This works exactly as one would expect (of course with no horizontal scrollbar as I haven't added it yet):

enter image description here

However, if I modify the Scrollable class to add a horizontal scrollbar like so:

class Scrollable(tk.Frame):
def __init__(self, frame, width=8):

    scrollbar_v = tk.Scrollbar(frame, width=width)
    scrollbar_v.pack(side=tk.RIGHT, fill=tk.Y, expand=False)

    scrollbar_h = tk.Scrollbar(frame, width=width, orient="horizontal")
    scrollbar_h.pack(side=tk.BOTTOM, fill=tk.X, expand=False)

    self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar_v.set, xscrollcommand=scrollbar_h.set)
    self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

    scrollbar_v.config(command=self.canvas.yview)
    scrollbar_h.config(command=self.canvas.xview)

    self.canvas.bind('<Configure>', self.__fill_canvas)

    tk.Frame.__init__(self, frame)

    self.windows_item = self.canvas.create_window(0, 0, window=self, anchor=tk.NW)

def __fill_canvas(self, event):
    canvas_width = event.width
    canvas_height = event.height
    self.canvas.itemconfig(self.windows_item, width=canvas_width)
    self.canvas.itemconfig(self.windows_item, height=canvas_height)

def update(self):
    self.update_idletasks()
    self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))

The the buttons are cut off when scrolling:

enter image description here

There are similar questions on SO but they haven't helped. This and this suggest using .grid() method with the sticky= argument, but that didn't work. Changing the part of __init__() that adds the widgets to use .grid() instead of .pack() did not change the outcome whatsoever:

scrollbar_v = tk.Scrollbar(frame, width=width)
    scrollbar_v.grid(
        column=1,
        row=0,
        rowspan=2,
        sticky=(tk.N, tk.S)
    )

    scrollbar_h = tk.Scrollbar(frame, width=width, orient="horizontal")
    scrollbar_h.grid(
        column=0,
        row=1,
        columnspan=2,
        sticky=(tk.W, tk.E)
    )

    self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar_v.set, xscrollcommand=scrollbar_h.set)
    self.canvas.grid(
        column=0,
        row=0,
        sticky=(tk.N, tk.E, tk.S, tk.W)
    )

This and this suggests the answer may be to do with using self.canvas.config(scrollregion=self.canvas.bbox("all")) instead, but that didn't work either.

I feel the answer may be to do with the line self.windows_item = self.canvas.create_window(0, 0, window=self, anchor=tk.NW), but I don't understand how that's wrong - I thought the NW anchor would make sure it worked?


Solution

  • Here is a cut down version of your code that seems to solve the problem.

    The truncated buttons problem is solved by using rowconfigure and columnconfigure on app. This enables a grid object (canvas) to stick to the app window during resizing.

    The other problem is using a class to create a frame. This is due to frame (F) being the child of app when it needs to be the child of canvas. This fix prevents buttons from covering the scrollbars.

    import tkinter as tk
    from tkinter import ttk
    
    app = tk.Tk()
    app.geometry('548x266')
    
    app.rowconfigure(0, weight = 1)
    app.columnconfigure(0, weight = 1)
    
    V = tk.Scrollbar(app, width=12)
    V.grid(row = 0, column = 1, sticky = tk.NS)
    
    H = tk.Scrollbar(app, width=12, orient="horizontal")
    H.grid(row = 1, column = 0, sticky = tk.EW)
    
    canvas = tk.Canvas(
        app,
        # fix artifacts problem when scrolling
        xscrollincrement = 76, yscrollincrement = 25,
        xscrollcommand = H.set, yscrollcommand = V.set)
    canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
    
    V.config(command=canvas.yview)
    H.config(command=canvas.xview)
    
    F = tk.Frame(canvas)
    
    for i in range(30):
        for j in range(30):
            b = ttk.Button(master=F, text=f"Button {i:02d},{j:02d}")
            b.grid(column = j, row = i)
    
    F.bind(
        "<Configure>",
        lambda e: canvas.configure(scrollregion = canvas.bbox("all")))
    
    W = canvas.create_window(0, 0, window=F, anchor=tk.NW)
    app.mainloop()