Search code examples
pythonpython-3.xtkintergrid-layouttkinter-canvas

How to create a Dynamic Scrollable Grid on tkinter


When I say a dynamic grid I'm saying a grid that adjusts the number of columns(like bootstrap) according to the window width, so it must adjust the "cards" accordingly, and when I say scrollable... well... easier to understand.

I've tried 2 aproachs:

  1. create a dynamic grid and then make it scrollable.
  2. create a scrollable grid and then make it dynamic.

I've failed in both ways! I've found out that for being scrollable the grid can't be in a simple frame, it must be in a Canvas. And for the canvas I'm having a hard time making it dynamic.

Down here is my dynamic grid code

class DynaGrid(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, **kwargs)
        self.columns = None
        self.bind('<Configure>', self.re_grid)

    def re_grid(self, event=None):
        grid_width = self.winfo_width()
        slaves = self.grid_slaves()
        slaves_width = slaves[1].winfo_width()
        cols = grid_width // slaves_width
        if (cols == self.columns) | (cols == 0):  
            return
        for i, slave in enumerate(reversed(slaves)):
            slave.grid_forget()
            slave.grid(row=i // cols, column=i % cols)
        self.columns = cols

class CardFrame(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, bd=1, relief=tk.RAISED, **kwargs)

        tk.Label(self, text="Hello").pack()
   
def main():
    root = tk.Tk()
    frame = DynaGrid(root)
    frame.pack(fill=tk.BOTH, expand=True)

CardFrame(frame).grid() 
CardFrame(frame).grid()
CardFrame(frame).grid() 
CardFrame(frame).grid()
CardFrame(frame).grid() 
CardFrame(frame).grid()
CardFrame(frame).grid() 
CardFrame(frame).grid()
root.mainloop()

if __name__ == '__main__':
    main()

I wont waste other people's time posting my messy canvas code here instead I've got one from https://blog.tecladocode.com/tkinter-scrollable-frames/ which I've made the change to use my "cards" instead of labels.

import tkinter as tk
from tkinter import ttk


class ScrollableFrame(ttk.Frame):
    def __init__(self, container, *args, **kwargs):
        super().__init__(container, *args, **kwargs)
        canvas = tk.Canvas(self)
        scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
        self.scrollable_frame = ttk.Frame(canvas)

        self.scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(
                scrollregion=canvas.bbox("all")
            )
        )

        canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")

        canvas.configure(yscrollcommand=scrollbar.set)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

class TestFrame(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, bd=5, relief=tk.RAISED, **kwargs)

        tk.Label(self, text="name").pack(pady=10)
  
root = tk.Tk()
frame = ScrollableFrame(root)

for i in range(50):

    TestFrame(frame.scrollable_frame).grid()

frame.pack()
root.mainloop()

To make the dynamic scrollable canvas the tricky part here is use the re_grid function inside the canvas. I'm lost in how I'll get the window width correctly like I did in the dynaGrid code.

In the end I want a mash up of these two codes; a Class that is some sort of frame with dynamic grid with lateral scroll.


Solution

  • Your canvas is a frame inside your class, so, what you need to do is bind your re-grid function to your master frame and keep the scrollable frame config callback as it is. Then alter the grid, inside your canvas, instead of your 'dynamic grid' from your re-grid function . Simple as that! And I think it was easier to say: I want to create a window that behaves like a File Explorer

    import tkinter as tk
    from tkinter import ttk
    
    
    class ScrollableFrame(ttk.Frame):
        def _init_(self, container, *args, **kwargs):
            super()._init_(container, *args, **kwargs)
            canvas = tk.Canvas(self)
            scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
            self.scrollable_frame = ttk.Frame(canvas)
            self.columns=0
    
            self.scrollable_frame.bind(
                "<Configure>",
                lambda e: canvas.configure(
                    scrollregion=canvas.bbox("all")
                )
            )
    
            self.bind('<Configure>', self.regrid)
    
            canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
    
            canvas.configure(yscrollcommand=scrollbar.set)
    
            canvas.pack(side="left", fill="both", expand=True)
            scrollbar.pack(side="right", fill="y")
    
        def regrid(self, event=None):
            print(type(self))
            grid_width = self.winfo_width()
            slaves = self.scrollable_frame.grid_slaves()
            print(len(slaves))
            slaves_width = slaves[1].winfo_width()
            cols = grid_width // slaves_width
            if (cols == self.columns) | (cols == 0):  # if the column number has not changed, abort
                return
            for i, slave in enumerate(reversed(slaves)):
                slave.grid_forget()
                slave.grid(row=i // cols, column=i % cols)
            self.columns = cols
    
    class TestFrame(tk.Frame):
        def _init_(self, master=None, **kwargs):
            tk.Frame._init_(self, master, bd=5, relief=tk.RAISED, **kwargs)
    
            tk.Label(self, text="name").pack(pady=10)
            tk.Label(self, text=" info ........ info ").pack(pady=10)
    
    
    root = tk.Tk()
    frame = ScrollableFrame(root)
    
    for i in range(10):
        TestFrame(frame.scrollable_frame).grid()
        TestFrame(frame.scrollable_frame).grid()
    
    frame.pack(side="left", fill=tk.BOTH, expand=True)
    root.mainloop()