pythontkintergridframescrollable

Aligning grid columns between parent and container frames using tkinter


Using Python and tkinter I have created a dummy app with a scrollable frame. There are two column headings in a container frame. The container frame also contains a canvas. Inside the canvas is an inner frame with two columns of scrollable content.

Problem: the column headings do not align with the columns, presumably because the columns in the container frame and the inner frame do not align. But, as far as I can see, I have configured the columns exactly the same way in both frames. Can anyone give me a clue as to what I am overlooking?

The code is below. I cannot claim credit for all of the code, as I was following a tutorial from Tutorialspoint (https://www.tutorialspoint.com/implementing-a-scrollbar-using-grid-manager-on-a-tkinter-window), but the tutorial only worked with a single column of scrollable data where the heading scrolled with the data. I changed this so the heading stays put when you scroll and I needed 2 columns of data.

N.B. If I uncomment the line starting innerframe.grid(..., the columns match up (almost), but the scrollbar disappears.

import tkinter as tk
from tkinter import ttk

def _on_mousewheel(event):
   scrollcanvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

root=tk.Tk()

root.title("Scrollable Grid Example")

# Create outer Frame for Grid Layout
outerframe = ttk.Frame(root)
outerframe.grid(row=0, column=0, columnspan=2, sticky="nsew")

# Create a Canvas and Scrollbar
scrollcanvas = tk.Canvas(outerframe)
scrollbar = ttk.Scrollbar(outerframe, orient="vertical",command=scrollcanvas.yview)
scrollcanvas.configure(yscrollcommand=scrollbar.set)

# Create label to outer frame/canvas so that it stays put when rows below scroll.
label = tk.Label(outerframe, text="Scrollable Buttons", width=20)
label.grid(row=0, column=0, pady=5, sticky="w")
label1 = tk.Label(outerframe, text="Scrollable Text", width=20)
label1.grid(row=0, column=1, pady=5, sticky="w")

# Create inner Frame for Scrollable Content
innerframe=ttk.Frame(scrollcanvas)

# Set binding to adjusts the canvas scroll region when size of the inner frame changes
innerframe.bind( "<Configure>", lambda e: scrollcanvas.configure( scrollregion=scrollcanvas.bbox("all") ) )

# Add labels and buttons to the Content Frame
for i in range(0, 20):
    button=ttk.Button(innerframe, text=f"Button {i}", width=20)
    button.grid(row=i, column=0, pady=5, sticky="w" )
    label2 = tk.Label(innerframe, text=f"Text Line {i}", width=20)
    label2.grid(row=i, column=1, pady=5, sticky="w")

# Ensure the window and components expand proportionally when resized.
root.rowconfigure(0, weight=1)

outerframe.rowconfigure(0, weight=1)
innerframe.rowconfigure(0, weight=1)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)

outerframe.columnconfigure(0, weight=1)
outerframe.columnconfigure(1, weight=1)

innerframe.columnconfigure(0, weight=1)
innerframe.columnconfigure(1, weight=1)

# Place the canvas and scrollbar onto the window, with the scrollbar adjacent to the canvas
scrollcanvas.create_window((0, 0), window=innerframe, anchor="nw")
scrollcanvas.grid(row=1, column=0, columnspan=2, sticky="nsew")
# innerframe.grid(row=1, column=0, columnspan=2, sticky="nw")
scrollbar.grid(row=1, column=2, sticky="ns")

# Bind the Canvas to Mousewheel Events
scrollcanvas.bind_all("<MouseWheel>", _on_mousewheel)

# Run the Tkinter Event Loop
root.mainloop()

def _on_mousewheel(event):
   scrollcanvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

Solution

  • It is because the innerframe does not have the same width of the canvas (which is the sum of the widths of the two labels at the top).

    You need to:

    Updated code:

    import tkinter as tk
    from tkinter import ttk
    
    def _on_mousewheel(event):
       scrollcanvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
    
    root=tk.Tk()
    
    root.title("Scrollable Grid Example")
    
    # Create outer Frame for Grid Layout
    outerframe = ttk.Frame(root)
    outerframe.grid(row=0, column=0, columnspan=2, sticky="nsew")
    
    # Create a Canvas and Scrollbar
    scrollcanvas = tk.Canvas(outerframe, highlightthickness=0)
    scrollbar = ttk.Scrollbar(outerframe, orient="vertical",command=scrollcanvas.yview)
    scrollcanvas.configure(yscrollcommand=scrollbar.set)
    
    # Create label to outer frame/canvas so that it stays put when rows below scroll.
    label = tk.Label(outerframe, text="Scrollable Buttons", width=20, bd=1, relief='raised')
    label.grid(row=0, column=0, pady=5, sticky="w")
    label1 = tk.Label(outerframe, text="Scrollable Text", width=20, bd=1, relief='raised')
    label1.grid(row=0, column=1, pady=5, sticky="w")
    
    # Create inner Frame for Scrollable Content
    innerframe=tk.Frame(scrollcanvas, bg='gray80')
    
    # Set binding to adjusts the canvas scroll region when size of the inner frame changes
    innerframe.bind( "<Configure>", lambda e: scrollcanvas.configure( scrollregion=scrollcanvas.bbox("all") ) )
    
    # Add labels and buttons to the Content Frame
    for i in range(0, 20):
        button=ttk.Button(innerframe, text=f"Button {i}", width=20)
        button.grid(row=i, column=0, pady=5, sticky="w" )
        label2 = tk.Label(innerframe, text=f"Text Line {i}", width=20, bd=1, relief='solid')
        label2.grid(row=i, column=1, pady=5, sticky="w")
    
    # Ensure the window and components expand proportionally when resized.
    root.rowconfigure(0, weight=1)
    
    outerframe.rowconfigure(0, weight=1)
    #innerframe.rowconfigure(0, weight=1) # this line is not necessary
    
    root.columnconfigure(0, weight=1)
    root.columnconfigure(1, weight=1)
    
    outerframe.columnconfigure(0, weight=1, uniform=1) # added setting uniform option
    outerframe.columnconfigure(1, weight=1, uniform=1)
    
    innerframe.columnconfigure(0, weight=1, uniform=1)
    innerframe.columnconfigure(1, weight=1, uniform=1)
    
    # Place the canvas and scrollbar onto the window, with the scrollbar adjacent to the canvas
    scrollcanvas.create_window((0, 0), window=innerframe, anchor="nw", tags=('inner')) # added tags
    scrollcanvas.grid(row=1, column=0, columnspan=2, sticky="nsew")
    scrollbar.grid(row=1, column=2, sticky="ns")
    
    # Bind the Canvas to Mousewheel Events
    scrollcanvas.bind_all("<MouseWheel>", _on_mousewheel)
    
    # set width of innerframe to same of scrollcanvas
    scrollcanvas.bind('<Configure>', lambda e: scrollcanvas.itemconfig('inner', width=e.width))
    
    # Run the Tkinter Event Loop
    root.mainloop()
    

    Note that I have add borders to those labels in order to see the effect easily.

    Result:

    enter image description here