Adding a Scrollbar to a Group of Widgets in Tkinter

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()

screenshot of window with listbox

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



Leave a reply



Submit