Tkinter canvas scrollbar with grid?
Relatively new to Tkinter and Python. So be so kind as to put up with me.
I am trying to display the following GUI and want to have a scrollbar in Frame2 to only display 5x5 buttons at a time. It looks like Tkinter Frames do not support scrollbar and therefore added a canvas (inside which the frame is embedded) and a scrollbar in the parent 'FMas' frame. But for some reason the scrollbar goes to the right end of the screen and does not scroll.
Shouldn't the canvas end at the edge of Frame2 and the scrollbar next to it? Also, I tried to increase the size of the scrollbar to fit the 5x5 buttons height. It doesn't work either.
CODE (using Python3.2):
from tkinter import *
import tkinter.ttk as ttk
mGui = Tk()
mGui.geometry("630x600")
mGui.configure(background="Gray")
mGui.columnconfigure(0, weight=1)
mGui.rowconfigure(0, weight=1)
FMas = Frame(mGui, bg="Gray")
FMas.grid(sticky=(N,E,S,W))
FMas.columnconfigure(0, weight=1)
L1 = Label(FMas, text="Frame 1 Contents")
L1.grid(row=0, column=0, pady=5, sticky=(N,W))
F1 = Frame(FMas, bg="Green", bd=2, relief=GROOVE)
F1.grid(row=1, column=0, sticky=(N,W))
ChkBox1=IntVar()
CB1 = Checkbutton(F1, text="StartCheckBox", variable=ChkBox1)
CB1.grid(row=0,column=0,padx=2)
L2 = Label(FMas, text="Frame 2 Contents")
L2.grid(row=2, column=0, pady=5, sticky=(N,W))
Can1 = Canvas(FMas, bg="Yellow")
Can1.grid(row=3, column=0, sticky=(N,W))
F2 = Frame(Can1, bg="Blue", bd=2, relief=GROOVE)
F2.grid(row=0, column=0, sticky=(N,W))
rows = 10
for i in range(1,rows):
for j in range(1,6):
button = Button(F2, padx=7, pady=7, text="[%d,%d]" % (i,j))
button.grid(row=i, column=j, sticky='news')
vsbar = Scrollbar(FMas, orient="vertical", command=Can1.yview)
vsbar.grid(row=3, column=1)
Can1.configure(yscrollcommand=vsbar.set, scrollregion=Can1.bbox("all"))
L3 = Label(FMas, text="Frame 3 Contents")
L3.grid(row=4, column=0, pady=5, sticky=(N,W))
F3 = Frame(FMas, bg="Red", bd=2, relief=GROOVE)
F3.grid(row=5, column=0, sticky=(N,W))
ChkBox2=IntVar()
CB2 = Checkbutton(F3, text="EndCheckBox", variable=ChkBox2)
CB2.grid(row=0,column=0,padx=2)
mGui.mainloop()
sys.exit()
source to share
The height of the scrollbar did not match the height of the button frame because you did not tell it to stick to north and south .grid(..., sticky='ns')
The following describes the scrolling behavior you want to achieve: adding a scrollbar to a group of widgets.
See also @ martineau's answer for a more general object oriented 2D scrolling solution (horizontal and vertical)
import tkinter as tk
root = tk.Tk()
root.grid_rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
frame_main = tk.Frame(root, bg="gray")
frame_main.grid(sticky='news')
label1 = tk.Label(frame_main, text="Label 1", fg="green")
label1.grid(row=0, column=0, pady=(5, 0), sticky='nw')
label2 = tk.Label(frame_main, text="Label 2", fg="blue")
label2.grid(row=1, column=0, pady=(5, 0), sticky='nw')
label3 = tk.Label(frame_main, text="Label 3", fg="red")
label3.grid(row=3, column=0, pady=5, sticky='nw')
# Create a frame for the canvas with non-zero row&column weights
frame_canvas = tk.Frame(frame_main)
frame_canvas.grid(row=2, column=0, pady=(5, 0), sticky='nw')
frame_canvas.grid_rowconfigure(0, weight=1)
frame_canvas.grid_columnconfigure(0, weight=1)
# Set grid_propagate to False to allow 5-by-5 buttons resizing later
frame_canvas.grid_propagate(False)
# Add a canvas in that frame
canvas = tk.Canvas(frame_canvas, bg="yellow")
canvas.grid(row=0, column=0, sticky="news")
# Link a scrollbar to the canvas
vsb = tk.Scrollbar(frame_canvas, orient="vertical", command=canvas.yview)
vsb.grid(row=0, column=1, sticky='ns')
canvas.configure(yscrollcommand=vsb.set)
# Create a frame to contain the buttons
frame_buttons = tk.Frame(canvas, bg="blue")
canvas.create_window((0, 0), window=frame_buttons, anchor='nw')
# Add 9-by-5 buttons to the frame
rows = 9
columns = 5
buttons = [[tk.Button() for j in xrange(columns)] for i in xrange(rows)]
for i in range(0, rows):
for j in range(0, columns):
buttons[i][j] = tk.Button(frame_buttons, text=("%d,%d" % (i+1, j+1)))
buttons[i][j].grid(row=i, column=j, sticky='news')
# Update buttons frames idle tasks to let tkinter calculate buttons sizes
frame_buttons.update_idletasks()
# Resize the canvas frame to show exactly 5-by-5 buttons and the scrollbar
first5columns_width = sum([buttons[0][j].winfo_width() for j in range(0, 5)])
first5rows_height = sum([buttons[i][0].winfo_height() for i in range(0, 5)])
frame_canvas.config(width=first5columns_width + vsb.winfo_width(),
height=first5rows_height)
# Set the canvas scrolling region
canvas.config(scrollregion=canvas.bbox("all"))
# Launch the GUI
root.mainloop()
source to share
While this is a somewhat outdated question, here's a different answer that doesn't use tkinter
events, tkinter
thus avoiding the unnecessary overhead that it requires.
Although the code came from the OP, I made a number of formatting changes to the code to better align with PEP 8 - Style Guide for Python Code, which resulted in many variable names being changed. I also changed the architecture so that the application is a subclass of the root tkinter.Tk
window widgets tkinter.Tk
. I did all this with the hope that the results would be clearer and provide a better pattern for writing similar tkinter
-based applications .
Like @ Josselin's answer , it nests Canvas
and each of the widgets " Scrollbar
inside the other, Frame
which makes it easy to place them side by side both vertically and horizontally using the layout manager tkinter
grid()
.
The code has been further extended so that the grid also has a horizontal scroll bar, allowing you to scroll its contents in both that and vertical direction.
import tkinter as tk
LABEL_BG = "#ccc" # Light gray.
ROWS, COLS = 10, 6 # Size of grid.
ROWS_DISP = 3 # Number of rows to display.
COLS_DISP = 4 # Number of columns to display.
class MyApp(tk.Tk):
def __init__(self, title="Sample App", *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title(title)
self.configure(background="Gray")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
master_frame = tk.Frame(self, bg="Light Blue", bd=3, relief=tk.RIDGE)
master_frame.grid(sticky=tk.NSEW)
master_frame.columnconfigure(0, weight=1)
label1 = tk.Label(master_frame, text="Frame1 Contents", bg=LABEL_BG)
label1.grid(row=0, column=0, pady=5, sticky=tk.NW)
frame1 = tk.Frame(master_frame, bg="Green", bd=2, relief=tk.GROOVE)
frame1.grid(row=1, column=0, sticky=tk.NW)
cb_var1 = tk.IntVar()
checkbutton1 = tk.Checkbutton(frame1, text="StartCheckBox", variable=cb_var1)
checkbutton1.grid(row=0, column=0, padx=2)
label2 = tk.Label(master_frame, text="Frame2 Contents", bg=LABEL_BG)
label2.grid(row=2, column=0, pady=5, sticky=tk.NW)
# Create a frame for the canvas and scrollbar(s).
frame2 = tk.Frame(master_frame)
frame2.grid(row=3, column=0, sticky=tk.NW)
# Add a canvas in that frame.
canvas = tk.Canvas(frame2, bg="Yellow")
canvas.grid(row=0, column=0)
# Create a vertical scrollbar linked to the canvas.
vsbar = tk.Scrollbar(frame2, orient=tk.VERTICAL, command=canvas.yview)
vsbar.grid(row=0, column=1, sticky=tk.NS)
canvas.configure(yscrollcommand=vsbar.set)
# Create a horizontal scrollbar linked to the canvas.
hsbar = tk.Scrollbar(frame2, orient=tk.HORIZONTAL, command=canvas.xview)
hsbar.grid(row=1, column=0, sticky=tk.EW)
canvas.configure(xscrollcommand=hsbar.set)
# Create a frame on the canvas to contain the buttons.
buttons_frame = tk.Frame(canvas, bg="Red", bd=2)
# Add the buttons to the frame.
for i in range(1, ROWS+1):
for j in range(1, COLS+1):
button = tk.Button(buttons_frame, padx=7, pady=7, relief=tk.RIDGE,
text="[%d, %d]" % (i, j))
button.grid(row=i, column=j, sticky='news')
# Create canvas window to hold the buttons_frame.
canvas.create_window((0,0), window=buttons_frame, anchor=tk.NW)
buttons_frame.update_idletasks() # Needed to make bbox info available.
bbox = canvas.bbox(tk.ALL) # Get bounding box of canvas with Buttons.
#print('canvas.bbox(tk.ALL): {}'.format(bbox))
# Define the scrollable region as entire canvas with only the desired
# number of rows and columns displayed.
w, h = bbox[2]-bbox[1], bbox[3]-bbox[1]
dw, dh = int((w/COLS) * COLS_DISP), int((h/ROWS) * ROWS_DISP)
canvas.configure(scrollregion=bbox, width=dw, height=dh)
label3 = tk.Label(master_frame, text="Frame3 Contents", bg=LABEL_BG)
label3.grid(row=4, column=0, pady=5, sticky=tk.NW)
frame3 = tk.Frame(master_frame, bg="Blue", bd=2, relief=tk.GROOVE)
frame3.grid(row=5, column=0, sticky=tk.NW)
cb_var2 = tk.IntVar()
checkbutton2 = tk.Checkbutton(frame3, text="EndCheckBox", variable=cb_var2)
checkbutton2.grid(row=0, column=0, padx=2)
if __name__ == "__main__":
app = MyApp("Scrollable Canvas")
app.mainloop()
This is how it looks:
source to share