5

I'm trying to create a canvas widget with a number of widgets embedded within it. Since there will frequently be too many widgets to fit in the vertical space I have for the canvas, it'll need to be scrollable.

import tkinter as tk                # for general gui
import tkinter.ttk as ttk           # for notebook (tabs)

class instructionGeneratorApp():

    def __init__(self, master):

        # create a frame for the canvas and scrollbar
        domainFrame = tk.LabelFrame(master)
        domainFrame.pack(fill=tk.BOTH, expand=1)

        # make the canvas expand before the scrollbar
        domainFrame.rowconfigure(0,weight=1)
        domainFrame.columnconfigure(0,weight=1)

        vertBar = ttk.Scrollbar(domainFrame)
        vertBar.grid(row=0, column=1, sticky=tk.N + tk.S)

        configGridCanvas = tk.Canvas(domainFrame,
                                    bd=0,
                                    yscrollcommand=vertBar.set)
        configGridCanvas.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)

        vertBar.config(command=configGridCanvas.yview)

        # add widgets to canvas

        l = tk.Label(configGridCanvas, text='Products')
        l.grid(row=1, column=0)

        r = 2
        for product in ['Product1','Product2','Product3','Product4','Product5','Product6','Product7','Product8','Product9','Product10','Product11','Product12','Product13','Product14','Product15','Product16','Product17','Product18','Product19','Product20']:
            l = tk.Label(configGridCanvas, text=product)
            l.grid(row=r, column=0)
            c = tk.Checkbutton(configGridCanvas)
            c.grid(row=r, column=1)
            r += 1

        ButtonFrame = tk.Frame(domainFrame)
        ButtonFrame.grid(row=r, column=0)

        removeServerButton = tk.Button(ButtonFrame, text='Remove server')
        removeServerButton.grid(row=0, column=0)

        # set scroll region to bounding box?
        configGridCanvas.config(scrollregion=configGridCanvas.bbox(tk.ALL))


root = tk.Tk()
mainApp = instructionGeneratorApp(root)

root.mainloop()

As best as I can tell, I'm following the effbot pattern for canvas scrollbars, but I end up with either a scrollbar that isn't bound to the canvas, or a canvas that is extending beyond the edges of its master frame:

screenshot of application smaller screenshot of application

I've attempted the solutions on these questions, but there's still something I'm missing:

resizeable scrollable canvas with tkinter

Tkinter, canvas unable to scroll

Any idea what I'm doing wrong?

1
  • I would recommend to follow the approach from this answer. Commented Nov 20, 2018 at 8:40

2 Answers 2

3

I have added some comments to @The Pinapple 's solution for future reference.

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):
        #our scrollable container is a frame, this frame holds the canvas we draw our widgets on
        Frame.__init__(self, master, **kwargs)
        #grid and rowconfigure with weight 1 are used for the scrollablecontainer to utilize the full size it can get from its parent  
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        #canvas and scrollbars are positioned inside the scrollablecontainer frame
        #the scrollbars take a command parameter which is used to position our view on the canvas
        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')
        #We do not only need a command to position but also one to scroll
        self.canvas.configure(xscrollcommand=self.hScroll.set,
                              yscrollcommand=self.vScroll.set)

        #This is the frame where the magic happens, all of our widgets that are needed to be scrollable will be positioned here
        self.frame = Frame(self.canvas, bd=2)
        self.frame.grid_columnconfigure(0, weight=1)

        #A canvas itself is blank, we must tell the canvas to create a window with self.frame as content, anchor=nw means it will be positioned on the upper left corner
        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()
        #If the widgets inside the canvas / the canvas itself change size,
        #the <Configure> event is fired which passes its new width and height to the corresponding callback
        self.canvas.bind('<Configure>', self.on_configure)

    def update_layout(self):
        #All pending events, callbacks, etc. are processed in a non-blocking manner
        self.frame.update_idletasks()
        #We reconfigure the canvas' scrollregion to fit all of its widgets
        self.canvas.configure(scrollregion=self.canvas.bbox('all'))
        #reset the scroll
        self.canvas.yview('moveto', '1.0')
        #fit the frame to the size of its inner widgets (grid_size)
        self.size = self.frame.grid_size()

    def on_configure(self, event):
        w, h = event.width, event.height
        natural = self.frame.winfo_reqwidth() #natural width of the inner frame
        #If the canvas changes size, we fit the inner frame to its size
        self.canvas.itemconfigure('inner', width=w if w > natural else natural)
        #dont forget to fit the scrollregion, otherwise the scrollbar might behave strange
        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()
Sign up to request clarification or add additional context in comments.

Comments

0

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

3 Comments

Thanks, that does seem to work! I'm going to try to pick it apart and understand it. This will likely end up the accepted answer.
This answer would be much better if you explained what you did differently. As written, we're forced to read the code line-by-line and character-by-character to see what you might have changed from the original code.
Hey could you please explain this? So much changed and with out comments it doesn't really make sense were Op's issue was or what needs to be done.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.