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