How to add a Scrollbar on the main window when only Label and Button are used

Question:

I know how to add a scrollbar on a tkinter window, frame, canvas.
I also know how to do it on a listbox.

Problem is, I have a window that doesn’t have any of those, and only use Label and Button:

from tkinter import *

test1 = 100
test2 = 100
test3 = 100
test4 = 100
root = Tk()
root.title("Program")
root.geometry('350x250')

# first group of labels & buttons
label = Label(root, text="test1")
label.grid(row=0, column=0, columnspan=2)
label = Label(root, text=test1)
label.grid(row=1, column=0, columnspan=2)
button = Button(root, text="Up")
button.grid(row=2, column=0)
button = Button(root, text="Down")
button.grid(row=2, column=1)
#
label = Label(root, text="test2")
label.grid(row=3, column=0, columnspan=2)
label = Label(root, text=test2)
label.grid(row=4, column=0, columnspan=2)
button = Button(root, text="Up")
button.grid(row=5, column=0)
button = Button(root, text="Down")
button.grid(row=5, column=1)
#
label = Label(root, text="test3")
label.grid(row=6, column=0, columnspan=2)
label = Label(root, text=test3)
label.grid(row=7, column=0, columnspan=2)
button = Button(root, text="Up")
button.grid(row=8, column=0)
button = Button(root, text="Down")
button.grid(row=8, column=1)
#
label = Label(root, text="test4")
label.grid(row=9, column=0, columnspan=2)
label = Label(root, text=test4)
label.grid(row=10, column=0, columnspan=2)
button = Button(root, text="Up")
button.grid(row=11, column=0)
button = Button(root, text="Down")
button.grid(row=11, column=1)
root.mainloop()

The above has a small window resolution on purpose, because, while it may work in maximizing the window, once there are too many Label’s text or Button, then a Scrollbar will be needed. This is intended to test that.

How can I add a scrollbar to the above code?

Asked By: Nordine Lotfi

||

Answers:

You need a scrollable frame. See example here: https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01

And for buttons and labels, instead of using root as parent, use the scrollable frame as parent. For example:

from tkinter import *

c1 = "#999999"
c2 = "#000000"


class ScrollFrame(Frame):
    """
    A simple scrollable frame class for tkinter

    Source: https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01
    """

    def __init__(self, parent):
        # create a frame (self)
        super().__init__(parent, background=c1)
        # place canvas on self
        self.canvas = Canvas(
            self, bd=0, bg=c1, relief="flat", highlightthickness=0
        )
        # place a frame on the canvas, this frame will hold the child widgets
        self.viewPort = Frame(self.canvas, background=c1)
        self.viewPort.grid_columnconfigure(0, weight=1)
        # place a scrollbar on self
        self.vsb = Scrollbar(self, orient="vertical", command=self.canvas.yview)
        # attach scrollbar action to scroll of canvas
        self.canvas.configure(yscrollcommand=self.vsb.set)

        # pack scrollbar to right of self
        self.vsb.pack(side="right", fill="y")
        # pack canvas to left of self and expand to fil
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas_frame = self.canvas.create_window(
            (0, 0),
            window=self.viewPort,
            anchor="nw",  # add view port frame to canvas
            tags="self.viewPort",
        )

        # bind an event whenever the size of the viewPort frame changes.
        self.viewPort.bind("<Configure>", self.onFrameConfigure)
        self.canvas.bind("<Configure>", self.FrameWidth)
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

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

    def FrameWidth(self, event):
        canvas_width = event.width
        self.canvas.itemconfig(self.canvas_frame, width=canvas_width)

    def onFrameConfigure(self, event):
        """Reset the scroll region to encompass the inner frame"""
        # whenever the size of the frame changes, alter the scroll region respectively.
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))


tests = [100, 99, 98, 101]
root = Tk()
root.title("Program")
root.geometry('350x250')
scroll_frame = ScrollFrame(root)

for i, testi in enumerate(tests):
    # grouping labels and buttons together in a subframe
    # so that the row numbers of the labels and buttons 
    # are always 0 to 2 within the sub-frame
    f1 = Frame(scroll_frame.viewPort)

    # first group of labels & buttons
    label = Label(f1, text=f"test{i}")
    label.grid(row=0, column=0, columnspan=2)
    label = Label(f1, text=testi)
    label.grid(row=1, column=0, columnspan=2)
    button = Button(f1, text="Up")
    button.grid(row=2, column=0)
    button = Button(f1, text="Down")
    button.grid(row=2, column=1)

    # defining this group to have 2 columns with equal weight
    f1.grid_columnconfigure(0, weight=1)
    f1.grid_columnconfigure(1, weight=1)

    # expand this sub-frame horizontally to it's parent, sticking to West and East of parent frame
    f1.grid(sticky="we", ipadx=2)

    # adding a separator
    Frame(f1, height=1, background=c2).grid(
        sticky="we", pady=5, padx=5, columnspan=2
    )

# expand the scroll_frame in all 4 directions to fill the parent frame, sticking to West, East, North and South of parent frame
scroll_frame.grid(sticky="wens", row=0, column=0)

# set root frame only has 1 column (filled by scroll_frame)
root.grid_columnconfigure(0, weight=1)
root.mainloop()

Answered By: Tim

I recently stumbled on an easier way to do that (also use less code to do so):

import tkinter as tk

root = tk.Tk()
text = tk.Text(wrap="none")
vsb = tk.Scrollbar(orient="vertical", command=text.yview)
text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
text.pack(fill="both", expand=True)

for i in range(30):
    test1 = "test" + str(i)
    test2 = "Button" + str(i)
    c = tk.Label(text=test1)
    k = tk.Label(text=i)
    b = tk.Button(text=test2)
    d = tk.Button(text=test2)
    text.window_create("end", window=c)
    text.insert("end", "n")
    text.window_create("end", window=k)
    text.insert("end", "n")
    text.window_create("end", window=b)
    text.window_create("end", window=d)
    text.insert("end", "n")
text.configure(state="disabled")
root.mainloop()

This is based on this answer. It doesn’t use a canvas.

There is also another similar answer to the one I accepted on this post, here.

Answered By: Nordine Lotfi