0

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?

Nordine Lotfi
  • 463
  • 2
  • 5
  • 20
  • 1
    Unfortunately there's no easy way to implement this in tkinter *without* using something like a `canvas`. You're better off creating a canvas widget in your root window and making all of your other widgets children of the canvas. There's plenty of info out there about scrolling canvases, as that's part of why they exist. – JRiggles Aug 23 '22 at 19:44

2 Answers2

1

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

Tim
  • 3,178
  • 1
  • 13
  • 26
  • tried your class, it looks nice. Do you mind showing me how to integrate it with the code in my post? (Also, I think there is a slight error on the name of the class, should be named "ScrollFrame") – Nordine Lotfi Aug 23 '22 at 20:08
  • Tried your class a bit more, [here my attempt](https://gist.github.com/secemp9/f3f294f90b96345df006272d7c93bfe0). It throw the following error: `_tkinter.TclError: cannot use geometry manager grid inside .!scrollframe which already has slaves managed by pack` – Nordine Lotfi Aug 23 '22 at 20:55
  • 1
    Updated with full example, turns out needed to use `scroll_frame.viewPort` instead of `scroll_frame` as parent. – Tim Aug 23 '22 at 20:59
0

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.

Nordine Lotfi
  • 463
  • 2
  • 5
  • 20