having an issue with some tkinter code and I believe I am just too close to it and can't see the issue in front of my face. I am loading checkboxes into a frame and attaching a scrollbar to that location.
This works well until I get to a little over 1000 checkboxes. It then seems to cut off and even though the frame extends a height appropriate for all checkboxes it is not showing them in the gui. You can see in the image here where they stop showing Checkbox Malfunction
Here is my code: (Please excuse how messy it looks, it is a subset of a much larger code set, I've just isolated the error)
from tkinter import *
build_vars = {}
build_Radios = []
parent = Tk()
center_container = Frame(parent, width=5, height=5)
center_container.grid(row=1, sticky="nsew")
# Center Row Columns
center_center_container = Frame(center_container, width=150, height=200)
center_center_container.grid(row=0, column=2, sticky="ns")
build_canvas = Canvas(center_center_container, background='green')
build_canvas.grid(row=0, column=0, sticky=N+E+W+S)
# Create a vertical scrollbar linked to the canvas.
vsbar = Scrollbar(center_center_container, orient=VERTICAL, command=build_canvas.yview)
vsbar.grid(row=0, column=1, sticky=NS)
build_canvas.configure(yscrollcommand=vsbar.set)
# Create a frame on the canvas to contain the buttons.
frame_buttons = Frame(build_canvas, bd=2, background='red')
def create_build_radios():
# for index, item in enumerate(filtered_builds):
for index, item in enumerate(list(range(3000))):
build_vars[item] = IntVar()
radio = Checkbutton(frame_buttons, text=item, variable=build_vars[item], onvalue=1,
offvalue=0,
command=lambda item=item: sel(item))
radio.grid(row=index, column=0, sticky=W)
build_Radios.append(radio)
# Create canvas window to hold the buttons_frame.
build_canvas.create_window((0, 0), window=frame_buttons, anchor=NW)
build_canvas.update_idletasks() # Needed to make bbox info available.
bbox = build_canvas.bbox(ALL) # Get bounding box of canvas with Buttons.
build_canvas.configure(scrollregion=bbox, width=150, height=400)
def sel(item):
print(item)
create_build_radios()
parent.mainloop()
So here is the better solution (way better than the other, can easily put way more widgets on this, tho note that there probably is some kind of limit (at least the CPU's capabilities):
from tkinter import Tk, Canvas, Frame, Label, Scrollbar, Button, DoubleVar, StringVar, Entry
from tkinter.ttk import Progressbar
class PagedScrollFrame(Frame):
def __init__(self, master, items_per_page=100, **kwargs):
Frame.__init__(self, master, **kwargs)
self.master = master
self.items_per_page = items_per_page
self.pages = None
self.id_list = []
self.bbox_tag = 'all'
self._loading_frame = Frame(self)
self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
self.__percent_tracker = StringVar(master=self.master, value='0.00%')
self.frame = Frame(self)
self.frame.pack(side='top', padx=20, pady=20)
self.canvas = Canvas(self.frame)
self.canvas.pack(side='left')
self.bg_label = Label(self.canvas)
self.bg_label.place(x=0, y=0, relwidth=1, relheight=1)
self.scrollbar = Scrollbar(self.frame, orient='vertical', command=self.canvas.yview)
self.scrollbar.pack(side='right', fill='y')
self.canvas.config(yscrollcommand=self.scrollbar.set)
self.canvas.bind('<Configure>',
lambda e: self.canvas.config(
scrollregion=self.canvas.bbox(self.bbox_tag)
))
self.button_frame = Frame(self)
self.button_frame.pack(fill='x', side='bottom', padx=20, pady=20)
self.canvas_frame = Frame(self.button_frame)
self.button_canvas = Canvas(self.canvas_frame, height=20)
self.button_canvas.pack(expand=True)
self.inner_frame = Frame(self.button_canvas)
self.button_canvas.create_window(0, 0, window=self.inner_frame, anchor='nw')
self.button_scrollbar = Scrollbar(self.canvas_frame,
orient='horizontal',
command=self.button_canvas.xview)
self.button_scrollbar.pack(fill='x')
self.button_canvas.config(xscrollcommand=self.button_scrollbar.set)
self.button_canvas.bind(
'<Configure>', lambda e: self.button_canvas.config(
scrollregion=self.button_canvas.bbox('all')
)
)
def pack_items(self):
if not self.pages:
return
self._loading_frame.place(x=0, y=0, relwidth=1, relheight=1)
self._loading_frame.lift()
self._loading_frame.update_idletasks()
self.after(100, self._pack_items)
def _pack_items(self):
Label(self._loading_frame, text='Loading...').pack(expand=True)
Progressbar(self._loading_frame,
orient='horizontal',
variable=self.__load_progress_tracker,
length=self._loading_frame.winfo_width()
- self._loading_frame.winfo_width() // 10).pack(expand=True)
Label(self._loading_frame, textvariable=self.__percent_tracker).pack(expand=True)
self.update_idletasks()
widgets = [widget for page in self.pages for widget in page.winfo_children()]
length = len(widgets)
self.after(100, self.__pack_items, widgets, 0, length)
def __pack_items(self, widgets, index, length):
if index >= length:
self._loading_frame.destroy()
self.canvas.config(scrollregion=self.canvas.bbox('all'))
return
widgets[index].pack()
percent = (index + 1) * 100 / length
self.__load_progress_tracker.set(value=percent)
self.__percent_tracker.set(value=f'{percent: .2f}%')
self.after(1, self.__pack_items, widgets, index + 1, length)
def change_frame(self, index):
if not self.pages:
return
self.bbox_tag = self.id_list[index]
self.canvas.config(scrollregion=self.canvas.bbox(self.bbox_tag))
self.bg_label.lift()
self.pages[index].lift()
def create_pages(self, num_of_items, items_per_page=None):
self.pages = None
if not items_per_page:
items_per_page = self.items_per_page
num_of_pages = num_of_items // items_per_page
if num_of_items % items_per_page != 0:
num_of_pages += 1
start_indexes = [items_per_page * page_num for page_num in range(num_of_pages)]
end_indexes = [num + items_per_page for num in start_indexes]
end_indexes[-1] += (num_of_items % items_per_page
- (items_per_page if num_of_items % items_per_page != 0 else 0))
self.pages = [Frame(self.canvas) for _ in range(num_of_pages)]
self.id_list = []
for page, frame in enumerate(self.pages):
self.id_list.append(self.canvas.create_window(0, 0, window=frame, anchor='nw'))
self.pages[0].lift()
if num_of_pages >= 2:
Button(self.button_frame, text='1',
command=lambda: self.change_frame(0)).pack(
side='left', expand=True, fill='both', ipadx=5
)
if num_of_pages > 2:
self.canvas_frame.pack(fill='x', expand=True, side='left')
for page_num in range(1, num_of_pages - 1):
Button(self.inner_frame, text=page_num + 1,
command=lambda index=page_num: self.change_frame(index)).pack(
expand=True, fill='both', side='left', ipadx=5
)
Button(self.button_frame, text=num_of_pages,
command=lambda: self.change_frame(num_of_pages - 1)).pack(
side='right', fill='both', expand=True, ipadx=5
)
return zip(start_indexes, end_indexes, self.pages)
def create_paged_canvas():
scroll = PagedScrollFrame(root)
scroll.pack()
lst = tuple(range(3000))
for start, end, parent in scroll.create_pages(len(lst)):
for i in lst[start:end]:
frame_ = Frame(parent)
Label(frame_, text=str(i).zfill(4)).pack(side='left')
scroll.pack_items()
root = Tk()
root.protocol('WM_DELETE_WINDOW', exit)
create_paged_canvas()
root.mainloop()
Main info:
Basically this creates paged scrollable canvas. All that is needed is to adjust the inner loop and the iterable in the create_paged_canvas() function. You can also adjust how many items per page to show (that also allows for later configuration for example in a menu you could call a similar to create_paged_canvas() function and change the items_per_page argument to sth else (will have to load everything again but... tkinter is tkinter, it is pretty slow and even worse it doesn't allow directly using threads, not even talking about processes (that stuff would speed things up very much but simply can't be done)))
Important (suggestion):
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.
Misc:
for better performance it would be better to instead of creating labels simply create text directly on canvas (using the other solution) or use a listbox, that is in case if you need to display huge amounts of data because it will speed things up since no widgets have to be created (it also means you can only view the data pretty much)
If you have any questions, be sure to ask those!
Here comes the solution:
Very Important
There is some bizzare bug that I have no idea how to fix (yet at least) regarding performance and stuff, which makes this solution pretty unusable, I mean theoretically you will be able to put more widgets on this and scroll than the usual method but depending on the CPU it may work or it may not work that well. I will simply write a pagination answer
from tkinter import (Tk, Frame, Label, Scrollbar, Canvas,
DoubleVar, Entry, Button, StringVar, TclError)
from tkinter.ttk import Progressbar
class EndlessScroll(Canvas):
def __init__(self, master, update_mode='passive', scrollbar=None, **kwargs):
"""update_mode:
'passive' (recommended and default): widgets have to be accessed (Text, Entry...)
or using Frame, DON'T use if only single widget per line,
USE Frame with this
'active': recommended only if single widget
per line (best with Label
but can use Button too)"""
Canvas.__init__(self, master, **kwargs)
self.master = master
self._widget_heights = []
self._start_index = 0
self._widgets = []
self.temp_frame = None
self.__got_height = False
self.__initialised_placement = False
self.__height_counter = 0
self.yscrollincrement = None
self._update_mode = update_mode
self.scrollbar = scrollbar
self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
self.__percent_tracker = StringVar(master=self.master, value='0.0%')
self._allow_mouse_control = False
self.bind('<Enter>', lambda e: setattr(self, '_allow_mouse_control', True))
self.bind('<Leave>', lambda e: setattr(self, '_allow_mouse_control', False))
self.bind('<MouseWheel>', self.__scroll_with_mouse)
for key, value in kwargs.items():
setattr(self, key, value)
def __scroll_with_mouse(self, event):
if not self._allow_mouse_control:
return
self.yscroll('scroll', (-1 * (event.delta/120)), 'units')
def set_scrollbar(self, scrollbar):
self.scrollbar = scrollbar
def init_after_widgets(self):
self.temp_frame = Frame(self)
self._widgets = self.winfo_children()[:-1]
self.temp_frame.place(x=0, y=0, relwidth=1, relheight=1)
self.update()
Label(self.temp_frame, text='Loading...').pack(expand=True)
Progressbar(self.temp_frame,
orient='horizontal',
variable=self.__load_progress_tracker,
length=self.temp_frame.winfo_width()
- self.temp_frame.winfo_width() // 10).pack(expand=True)
Label(self.temp_frame, textvariable=self.__percent_tracker).pack(expand=True)
self.after(100, self._get_widget_heights, 0)
def _get_widget_heights(self, index):
length = len(self._widgets)
self.after(100, self.__get_widget_heights, index, self._widgets, length)
def __get_widget_heights(self, index, widgets, length):
if index >= length:
self.__got_height = True
self.temp_frame.place_forget()
self._initial_placement(widgets)
self.update_idletasks()
self.__update()
self.yscroll(None, 0)
return
id_ = self.create_window(0, 0, window=widgets[index], anchor='nw')
self.update()
try:
self._widget_heights.append(widgets[index].winfo_height())
self.delete(id_)
except TclError:
pass
percent = (index + 1) * 100 / length
self.__load_progress_tracker.set(value=percent)
self.__percent_tracker.set(value=f'{percent: .2f}%')
self.after(1, self.__get_widget_heights, index + 1, widgets, length)
def yscroll(self, *args):
if not self.__initialised_placement or not self.scrollbar:
return
type_, fraction = args[0], args[1]
parent_height = self.winfo_height()
if type_ == 'scroll':
units = args[2]
k = float(fraction)
if k < -1.0:
k = -1.0
elif k > 1.0:
k = 1.0
top, bottom, *_ = self.scrollbar.get()
length = (bottom - top) if not self.yscrollincrement else self.yscrollincrement
if (top == 0.0 and k < 0) or (bottom == 1.0 and k > 0):
return
if 0 < top < length:
length = top
if 0 < (1 - bottom) < length:
length = 1 - bottom
if units == 'units':
fraction = top + (k * length)
elif units == 'pages':
return
# fraction = top + (k * (0.9 * parent_height) * length)
sum_height = sum(self._widget_heights)
scroll_height = float(fraction) * sum_height
height_counter = 0
for index_, height in enumerate(self._widget_heights):
height_counter += height
if height_counter > scroll_height:
self._start_index = index_
self.__height_counter = -(height - (height_counter - scroll_height))
break
self.scrollbar.set(float(fraction), float(fraction) + parent_height / sum_height)
if self._update_mode == 'passive':
self.__update()
def __update(self, _call_from_self=False):
if _call_from_self and self._update_mode == 'passive':
self.after(10, self.__update, True)
return
parent_height = self.winfo_height()
height_counter = self.__height_counter
self.delete('all')
for height, widget in zip(self._widget_heights[self._start_index:], self._widgets[self._start_index:]):
if height_counter > parent_height:
break
self.create_window(0, height_counter, window=widget, anchor='nw')
height_counter += height
self.after(10, self.__update, True)
def _initial_placement(self, widgets):
parent_height = self.winfo_height()
height_counter = 0
for height, widget in zip(self._widget_heights, widgets):
if height_counter > parent_height:
self.__initialised_placement = True
return
self.create_window(0, height_counter, window=widget, anchor='nw')
height_counter += height
def create_scroller():
endless_scroll = EndlessScroll(root, yscrollincrement=0.1)
endless_scroll.pack(side='left')
for i in range(100):
frame = Frame(endless_scroll)
Label(frame, text=str(i).zfill(4)).pack()
scrollbar = Scrollbar(root, command=endless_scroll.yscroll)
scrollbar.pack(side='right', fill='y')
endless_scroll.set_scrollbar(scrollbar)
endless_scroll.init_after_widgets()
root = Tk()
create_scroller()
root.mainloop()
MAIN_EDIT:
I polished the widget a bit, rest of the instructions are still valid, but I fixed the below issue with having a widget so that you can see the last widget added (was an index problem), now you can see all of them. Added mouse scrolling and scrolling with the scrollbar's arrows (still can't move it by clicking on scrollbar, no idea how to do that either (yet)). Also now shows percentage, reduced some waiting time. Also now it catches the error which gets raised if you close the window while loading is in progress.
Basically you just have to go to the create_scroller() function definition and adjust the loop for your needs.
Important (suggestion)
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.
The main instruction here is that you have to place the .init_after_widgets() method after you have added widgets to the EndlessScroll widget (don't pack the or anything just add them (as you can see in the example)). Note the special widget, will try to fix that too so that it works properly and also very important is that it currently only works with dragging the slider of the scrollbar (will fix that too (at least try)).
The thing is that it now works, it can theoretically show any number of widgets (tried with 50000 but that was very laggy and I had to close it but with the 3000 it worked like a charm), also great thing is that the window is responsive while "loading" the widgets (loading is necessary for getting the height of the widgets which is crucial) so yeah, you can try improving this yourself but I will also try doing that myself too but a bit later.
EDIT1:
For example if you wanted to have more widgets together you can use frames:
for i in range(100):
frame = Frame(endless_scroll)
Label(frame, text=f'Entry {i}:').pack(side='left')
Entry(frame).pack(side='right')
Solved this to an extent. See edit below.
Tho this specific example has a slight issue with the entries because of the .__update() loop, otherwise widgets that are static should work completely fine (Buttons have less of an issue but it might be harder to press them, Entry and Text widgets are probably unusable)
EDIT2:
added option to change update mode to deal with the above mentioned problem (read docstring in the EndlessScroll class). The issue is using Frame, it basically breaks the now "active" mode loop (probably because of amount of data) so with it you have to use "passive" mode however that means less smooth updates (also "passive" mode horribly updates if you don't use a Frame). So you can also use "active" mode but only when you have one widget per line (hopefully this will be solved by #TheLizzard by making some kind of custom .grid() method or sth, in comments) and then it updates very smoothly. So currently unless there really is a need to have a lot of widgets in a column and have them extend to other columns, I would suggest using the usual methods of adding scrolling. That is of course unless you are fine with a little bit of flickering when dragging the slider, other methods don't produce that issue.
If you have any questions, ask them!
I am aware that you cannot use different types of geometry managers within the same Tkinter window, such as .grid() and .pack(). I have a window that has been laid out using .grid() and I am now trying to add a status bar that would be snapped to the bottom of the window. The only method I have found online for this is to use .pack(side = BOTTOM), which will not work since the rest of the window uses .grid().
Is there a way that I can select the bottom of the window to place widgets from when using .grid()?
from tkinter import *
from tkinter.ttk import *
import tkinter as tk
class sample(Frame):
def __init__(self,master=None):
Frame.__init__(self, master)
self.status = StringVar()
self.status.set("Initializing")
statusbar = Label(root,textvariable = self.status,relief = SUNKEN, anchor = W)
statusbar.pack(side = BOTTOM, fill = X)
self.parent1 = Frame()
self.parent1.pack(side = TOP)
self.createwidgets()
def createwidgets(self):
Label(self.parent1,text = "Grid 1,1").grid(row = 1, column = 1)
Label(self.parent1,text = "Grid 1,2").grid(row = 1, column = 2)
Label(self.parent1,text = "Grid 2,1").grid(row = 2, column = 1)
Label(self.parent1,text = "Grid 2,2").grid(row = 2, column = 2)
if __name__ == '__main__':
root = Tk()
app = sample(master=root)
app.mainloop()
So using labels since I was kinda lazy to do other stuff, you can do frames to ensure that each section of your window can be packed/grid as required. Frames will be a useful tool for you to use when trying to arrange your widgets. Note that using a class can make things a little easier when deciding your parents. So imagine each frame is a parent and their children can be packed as required. So I would recommend drawing out your desired GUI and see how you will arrange them. Also if you want to add another frame within a frame simply do:
self.level2 = Frame(self.parent1)
You can check out additional settings in the docs
http://effbot.org/tkinterbook/frame.htm
PS: I am using a class hence the self, if you don't want to use classes then its okay to just change it to be without a class. Classes make it nicer to read though
Just give it a row argument that is larger than any other row. Then, give a weight to at least one of the rows before it.
Even better is to use frames to organize your code. Pack the scrollbar on the bottom and a frame above it. Then, use grid for everything inside the frame.
Example:
# layout of the root window
main = tk.Frame(root)
statusbar = tk.Label(root, text="this is the statusbar", anchor="w")
statusbar.pack(side="bottom", fill="x")
main.pack(side="top", fill="both", expand=True)
# layout of the main window
for row in range(1, 10):
label = tk.Label(main, text=f"R{row}")
label.grid(row=row, sticky="nsew")
main.grid_rowconfigure(row, weight=1)
...
I want to make a window in Tk that has a custom titlebar and frame. I have seen many questions on this website dealing with this, but what I'm looking for is to actually render the frame using a canvas, and then to add the contents to the canvas. I cannot use a frame to do this, as the border is gradiented.
According to this website: http://effbot.org/tkinterbook/canvas.htm#Tkinter.Canvas.create_window-method, I cannot put any other canvas items on top of a widget (using the create_window method), but I need to do so, as some of my widgets are rendered using a canvas.
Any suggestions on how to do this? I'm clueless here.
EDIT: Bryan Oakley confirmed that rendering with a canvas would be impossible. Would it then be possible to have a frame with a custom border color? And if so, could someone give a quick example? I'm sort of new with python.
You can use the canvas as if it were a frame in order to draw your own window borders. Like you said, however, you cannot draw canvas items on top of widgets embedded in a canvas; widgets always have the highest stacking order. There is no way around that, though it's not clear if you really need to do that or not.
Here's a quick and dirty example to show how to create a window with a gradient for a custom border. To keep the example short I didn't add any code to allow you to move or resize the window. Also, it uses a fixed color for the gradient.
import Tkinter as tk
class GradientFrame(tk.Canvas):
'''A gradient frame which uses a canvas to draw the background'''
def __init__(self, parent, borderwidth=1, relief="sunken"):
tk.Canvas.__init__(self, parent, borderwidth=borderwidth, relief=relief)
self._color1 = "red"
self._color2 = "black"
self.bind("<Configure>", self._draw_gradient)
def _draw_gradient(self, event=None):
'''Draw the gradient'''
self.delete("gradient")
width = self.winfo_width()
height = self.winfo_height()
limit = width
(r1,g1,b1) = self.winfo_rgb(self._color1)
(r2,g2,b2) = self.winfo_rgb(self._color2)
r_ratio = float(r2-r1) / limit
g_ratio = float(g2-g1) / limit
b_ratio = float(b2-b1) / limit
for i in range(limit):
nr = int(r1 + (r_ratio * i))
ng = int(g1 + (g_ratio * i))
nb = int(b1 + (b_ratio * i))
color = "#%4.4x%4.4x%4.4x" % (nr,ng,nb)
self.create_line(i,0,i,height, tags=("gradient",), fill=color)
self.lower("gradient")
class SampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.wm_overrideredirect(True)
gradient_frame = GradientFrame(self)
gradient_frame.pack(side="top", fill="both", expand=True)
inner_frame = tk.Frame(gradient_frame)
inner_frame.pack(side="top", fill="both", expand=True, padx=8, pady=(16,8))
b1 = tk.Button(inner_frame, text="Close",command=self.destroy)
t1 = tk.Text(inner_frame, width=40, height=10)
b1.pack(side="top")
t1.pack(side="top", fill="both", expand=True)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Here is a rough example where the frame, titlebar and close button are made with canvas rectangles:
import Tkinter as tk
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# Get rid of the os' titlebar and frame
self.overrideredirect(True)
self.mCan = tk.Canvas(self, height=768, width=768)
self.mCan.pack()
# Frame and close button
self.lFrame = self.mCan.create_rectangle(0,0,9,769,
outline='lightgrey', fill='lightgrey')
self.rFrame = self.mCan.create_rectangle(760,0,769,769,
outline='lightgrey', fill='lightgrey')
self.bFrame = self.mCan.create_rectangle(0,760,769,769,
outline='lightgrey', fill='lightgrey')
self.titleBar = self.mCan.create_rectangle(0,0,769,20,
outline='lightgrey', fill='lightgrey')
self.closeButton = self.mCan.create_rectangle(750,4,760, 18,
activefill='red', fill='darkgrey')
# Binds
self.bind('<1>', self.left_mouse)
self.bind('<Escape>', self.close_win)
# Center the window
self.update_idletasks()
xp = (self.winfo_screenwidth() / 2) - (self.winfo_width() / 2)
yp = (self.winfo_screenheight() / 2) - (self.winfo_height() / 2)
self.geometry('{0}x{1}+{2}+{3}'.format(self.winfo_width(),
self.winfo_height(),
xp, yp))
def left_mouse(self, event=None):
obj = self.mCan.find_closest(event.x,event.y)
if obj[0] == self.closeButton:
self.destroy()
def close_win(self, event=None):
self.destroy()
app = Application()
app.mainloop()
If I were going to make a custom GUI frame I would consider creating it with images,
made with a program like Photoshop, instead of rendering canvas objects.
Images can be placed on a canvas like this:
self.ti = tk.PhotoImage(file='test.gif')
self.aImage = mCanvas.create_image(0,0, image=self.ti,anchor='nw')
More info →here←