Search code examples
pythontkintercanvasscrollframe

Tkinter - Creating scrollable frame


I'm trying to create a scrollable frame with customtkinter following this guide. The result I'm trying to obtain is a series of frames packed one over the other just like in the linked guide. What I get is just the red canvas which can be scrolled.

The page initially shows only a button and nothing else. Clicking this button calls the function search_keywords(). This function obtains the list of strings filtered_keywords and then uses it as Input of a ListFrame object.

#The class is a frame because it represents only one of the pages of my app. It is raised by a Controller with tkraise()
class KeywordsFound(ctk.CTkFrame):
    def __init__(self, parent):
        super().__init__(parent)

        self.search_button = ctk.CTkButton(self, 
                                        text = 'Search', 
                                        font = ('Inter', 25),
                                        border_color = '#8D9298', 
                                        fg_color = '#FFBFBF',
                                        hover_color='#C78B8B',
                                        border_width=2,
                                        text_color='black',
                                        command = lambda: self.search_keywords(parent))
        
        self.search_button.place(relx = 0.5, 
                                   rely = 0.2, 
                                   relwidth = 0.2, 
                                   relheight = 0.075, 
                                   anchor = 'center')  
    
    def search_keywords(self, parent):

        #Machine learning is used to find the keywords in a pdf document.
        #The code is not included because it is not relevant, although I've checked
        #that the variable (list) 'filtered keywords' is assigned correctly.

        self.keywords_frame = ListFrame(self, str(self.filtered_keywords), 50)

The ListFrame class is a frame that contains a frame (the one I want to scroll) and a canvas, it uses two functions: create_item fills the scrollable frame with the elements I want it to contain. update_size is called every time the window size is changed and also when the ListFrame is created.

class ListFrame(ctk.CTkFrame):
    def __init__(self, parent, text_data, item_height):
        super().__init__(parent)
        self.pack(expand = True, fill = 'both')
        self._fg_color = 'white'

        # widget data
        self.text_data = text_data
        self.item_number = len(text_data)
        self.list_height = item_height * self.item_number

        #canvas
        self.canvas = tk.Canvas(self, background = 'red', scrollregion=(0,0,self.winfo_width(),self.list_height))
        self.canvas.pack(expand = True, fill = 'both')

        #display frame
        self.frame = ctk.CTkFrame(self)
        for item in self.text_data:
            self.create_item(item).pack(expand = True, fill = 'both')

        #scrollbar
        self.vert_scrollbar = ctk.CTkScrollbar(self, orientation = 'vertical', command = self.canvas.yview)
        self.canvas.configure(yscrollcommand = self.vert_scrollbar.set)
        self.vert_scrollbar.place(relx = 1, rely = 0, relheight=1, anchor = 'ne')

        self.canvas.bind_all('<MouseWheel>', lambda event: self.canvas.yview_scroll(-event.delta, "units"))
        #Configure will bind every time we update the size of the list frame. It also run when we create it for the first time
        self.bind('<Configure>', self.update_size)

The method create_window is called inside the function update_size. This method makes it so that the canvas holds the widget specified in the parameter window.

    def update_size(self, event):
    #if the container is larger than the list the scrolling stops working
    #if that happens we want to stretch the list to cover the entire height of the container
        if self.list_height >= self.winfo_height():
            height = self.list_height
            #let's enable scrolling in case it was disabled before
            self.canvas.bind_all('<MouseWheel>', lambda event: self.canvas.yview_scroll(-event.delta, "units"))
            #let's place the scrollbar again in case it was hidden before
            self.vert_scrollbar = ctk.CTkScrollbar(self, orientation = 'vertical', command = self.canvas.yview)
            self.canvas.configure(yscrollcommand = self.vert_scrollbar.set)
            self.vert_scrollbar.place(relx = 1, rely = 0, relheight=1, anchor = 'ne')
        else:
            height = self.winfo_height()
            #if we scroll we still get some weird behavior, let's disable scrolling
            self.canvas.unbind_all('<MouseWheel>')
            #hide the scrollbar
            self.vert_scrollbar.place_forget()
        #we create the window here because only this way the parameter winfo_width will be set correctly.
        #winfo_width contains the width of the widgets, we want to use it to update the width of the frame inside the canvas as the window width changes
        self.canvas.create_window((0,0), window = self.frame, anchor = 'nw', width = self.winfo_width(), height = height)

    def create_item(self, item):
        frame = ctk.CTkFrame(self.frame)
    
        frame.rowconfigure(0, weight = 1)
        frame.columnconfigure(0, weight = 1)

        #widgets
        ctk.CTkLabel(frame, text = item).grid(row = 0, column = 0)
        ctk.CTkButton(frame, text = 'DELETE').grid(row = 0, column = 1)

        return frame

Solution

  • Turns out that customtkinter's scrollbar isn't compatible with the rest of the code and it generates this anomaly. By using ttk.Scrollbar instead, the expected behaviour can be observed