Adding a scrollbar to a group of widgets in Tkinter
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame
aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create
method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion
option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_
methods. You cannot scroll items added to a canvas with pack
, place
, or grid
. By using a frame, you can use those methods inside the frame, and then call create_window
once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing)
. The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def populate(self):
'''Put in some fake data'''
for row in range(100):
tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(self.frame, text=t).grid(row=row, column=1)
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
import tkinter as tk
def populate(frame):
'''Put in some fake data'''
for row in range(100):
tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(frame, text=t).grid(row=row, column=1)
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")
frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
populate(frame)
root.mainloop()
How To Scroll Through A Group Of Label Widgets In Tkinter?
You need to make another frame within your canvas
and then use canvas.create_window
to create the frame. The label widgets should go inside that frame.
class Progress(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
frametwo = tk.Frame(self) #creates a secondary frame for the canvas?
frametwo.grid(row = 2, column = 0)
#attempts to create a functional scroll bar
canvas = tk.Canvas(frametwo)
canvas_frame = tk.Frame(canvas)
vscroll = tk.Scrollbar(frametwo, orient="vertical", command = canvas.yview)
canvas['yscrollcommand'] = vscroll.set
canvas.grid(row=2, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
vscroll.grid(row=2, column=6, sticky="nsew")
#creates the label widgets
self._widgets = []; columns = 6
for row in range(0,11):
current_row = []
for column in range(columns):
label = tk.Label(canvas_frame, text="%s/%s" % (row, column),
borderwidth=0, width=10, height =2 )
label.config(bg = "White", font=("Calibri bold", 20))
label.grid(row =(row+2), column=column, sticky="nsew", padx=1, pady=1)
current_row.append(label)
self._widgets.append(current_row)
for column in range(columns):
self.grid_columnconfigure(column, weight=1)
canvas.create_window((0, 0), window=canvas_frame, anchor='nw')
canvas_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion = canvas.bbox("all"),width=450,height=300))
Scroll a group of widgets in Tkinter
The solution using the canvas is the commonly accepted way to solve this problem. It's really not all that hacky, and the end result can be indistinguishable from having a native scrolling container widget.
If you're making a single column, another option is to use a text widget, and use the widget's ability to embed other widgets. Insert a widget, then insert a newline, insert another widget, etc. You then get the scrolling ability for free. The only thing you need to worry about is configuring the width of the embedded windows, which isn't too hard to do.
Python, tkinter and the scrollbar in a grid
is the scrollbar supposed to be in the same space (i.e. the same grid row and column) as the ListBox widget?
It's up to you. Typically you do not put the scrollbar inside the listbox, as it would obscure some of the data in the listbox. The listbox and scrollbar will have the same parent. If you're using grid
, usually the vertical scrollbar belongs in the column immediately to the right of the listbox.
What happens when I run this code? Well I don't see a ListBox, but I to see the scroll bar.
That is because the listbox shrinks to fit its children. The listbox is there, but it's only as wide and tall as the scrollbar. This is another reason why you shouldn't put the scrollbar inside the listbox.
If you want the scrollbar to appear as if it's inside the listbox, the best practice is to create a frame that holds the listbox and scrollbar.
You can make the frame sunken and the listbox flat, and it will give the illusion that the scrollbar is inside the listbox. You can then put that frame anywhere you like in your UI and the scrollbar will always be with the listbox.
Both the listbox and the scrollbar need to be children of the frame for this to work.
Here's a simple example:
import tkinter as tk
root = tk.Tk()
lb_frame = tk.Frame(root, bd=1, relief="sunken")
listbox = tk.Listbox(lb_frame, bd=0, yscrollcommand=lambda *args: vsb.set(*args))
vsb = tk.Scrollbar(lb_frame, orient="vertical", command=listbox.yview)
vsb.pack(side="right", fill="y")
listbox.pack(side="left", fill="both", expand=True)
lb_frame.pack(padx=20, pady=20)
for i in range(1, 100):
listbox.insert("end", f"Item {i}")
root.mainloop()
tkinter scrollable canvas after adding widgets with grid manager
From what I can tell you are looking for something like this..
from tkinter import *
class ProductItem(Frame):
def __init__(self, master, message, **kwds):
Frame.__init__(self, master, **kwds)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
self.text = Label(self, text=message, anchor='w')
self.text.grid(row=0, column=0, sticky='nsew')
self.check = Checkbutton(self, anchor='w')
self.check.grid(row=0, column=1)
class ScrollableContainer(Frame):
def __init__(self, master, **kwargs):
Frame.__init__(self, master, **kwargs) # holds canvas & scrollbars
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = Canvas(self, bd=0, highlightthickness=0)
self.hScroll = Scrollbar(self, orient='horizontal',
command=self.canvas.xview)
self.hScroll.grid(row=1, column=0, sticky='we')
self.vScroll = Scrollbar(self, orient='vertical',
command=self.canvas.yview)
self.vScroll.grid(row=0, column=1, sticky='ns')
self.canvas.grid(row=0, column=0, sticky='nsew')
self.canvas.configure(xscrollcommand=self.hScroll.set,
yscrollcommand=self.vScroll.set)
self.frame = Frame(self.canvas, bd=2)
self.frame.grid_columnconfigure(0, weight=1)
self.canvas.create_window(0, 0, window=self.frame, anchor='nw', tags='inner')
self.product_label = Label(self.frame, text='Products')
self.product_label.grid(row=0, column=0, sticky='nsew', padx=2, pady=2)
self.products = []
for i in range(1, 21):
item = ProductItem(self.frame, ('Product' + str(i)), bd=2)
item.grid(row=i, column=0, sticky='nsew', padx=2, pady=2)
self.products.append(item)
self.button_frame = Frame(self.frame)
self.button_frame.grid(row=21, column=0)
self.remove_server_button = Button(self.button_frame, text='Remove server')
self.remove_server_button.grid(row=0, column=0)
self.update_layout()
self.canvas.bind('<Configure>', self.on_configure)
def update_layout(self):
self.frame.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
self.canvas.yview('moveto', '1.0')
self.size = self.frame.grid_size()
def on_configure(self, event):
w, h = event.width, event.height
natural = self.frame.winfo_reqwidth()
self.canvas.itemconfigure('inner', width=w if w > natural else natural)
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
if __name__ == "__main__":
root = Tk()
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
sc = ScrollableContainer(root, bd=2)
sc.grid(row=0, column=0, sticky='nsew')
root.mainloop()
Related Topics
How to Set the 'Backend' in Matplotlib in Python
Pass Input/Variables to Command/Script Over Ssh Using Python Paramiko
Shebang Notation: Python Scripts on Windows and Linux
How to Find the Real User Home Directory Using Python
How to Get Pid by Process Name
Python: How to Kill Child Process(Es) When Parent Dies
Run Python Script At Startup in Ubuntu
How to Simulate Html5 Drag and Drop in Selenium Webdriver
How to Remove Script Tags With Beautifulsoup
Selenium.Common.Exceptions.Invalidselectorexception With "Span:Contains('String')"
How to Terminate a Python Subprocess Launched With Shell=True
Imagemagick Not Authorized to Convert Pdf to an Image
Unicodedecodeerror: 'Utf8' Codec Can't Decode Byte 0X9C
Why Does Multiprocessing Use Only a Single Core After I Import Numpy
Extracting Text from Ms Word Files in Python
How to Change the Environment of a Parent Process in Python