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")
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:
highlightthickness=0
in tk.Canvas(...)
innerframe
to the same as scrollcanvas
(in callback of <Configure>
event on scrollcanvas
)uniform=...
to outframe.columnconfigure(...)
and innerframe.columnconfigure(...)
to make sure the two columns inside both outerframe
and innerframe
have same widthUpdated 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: