Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

tkinter scrollable canvas after adding widgets with grid manager

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?

like image 945
Carl Kevinson Avatar asked Oct 17 '22 10:10

Carl Kevinson


1 Answers

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()
like image 158
Daniel Siegel Avatar answered Oct 18 '22 23:10

Daniel Siegel