I am trying to implement a scrollable frame in Python with Tkinter:
if the content changes, the size of the widget is supposed to stay constant (basically, I don't really care whether the size of the scrollbar is subtracted from the frame or added to the parent, although I do think that it would make sense if this was consistent but that does not seem to be the case currently)
if the content becomes too big a scrollbar shall appear so that one can scroll over the entire content (but not further)
if the content fits entirely into the widget the scrollbar shall disappear and it shall not be possible to scroll anymore (no need to scroll, because everything is visible)
if the req size of the content becomes smaller than the widget, the content shall fill the widget
I am surprised how difficult it seems to get this running, because it seems like a pretty basic functionality.
The first three requirements seem relatively easy but I am having a lot of trouble since trying to fill the widget.
The following implementation has the following problems:
first time a scrollbar appears, frame does not fill canvas (seems to depend on available space):
add one column. The horizontal scrollbar appears. Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as high as the scrollbar.
When adding or removing a row or column or resizing the window the red area disappears and does not seem to appear again.
size jumps:
add elements until horizontal scrollbar becomes visible. make window wider (not higher). the height [!] of the window increases with a jump.
infinite loop:
add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
What am I doing wrong?
Any help would be appreciated.
#!/usr/bin/env python
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
# I am not using something like vars(tk.Grid) because that would override too many methods.
# Methods like Grid.columnconfigure are suppossed to be executed on self, not a child.
GM_METHODS_TO_BE_CALLED_ON_CHILD = (
'pack', 'pack_configure', 'pack_forget', 'pack_info',
'grid', 'grid_configure', 'grid_forget', 'grid_remove', 'grid_info',
'place', 'place_configure', 'place_forget', 'place_info',
)
class AutoScrollbar(tk.Scrollbar):
'''
A scrollbar that hides itself if it's not needed.
Only works if you use the grid geometry manager.
'''
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
def pack(self, *args, **kwargs):
raise TclError('Cannot use pack with this widget.')
def place(self, *args, **kwargs):
raise TclError('Cannot use place with this widget.')
#TODO: first time a scrollbar appears, frame does not fill canvas (seems to depend on available space)
#TODO: size jumps: add elements until horizontal scrollbar becomes visible. make widget wider. height jumps from 276 to 316 pixels although it should stay constant.
#TODO: infinite loop is triggered by
# - add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again (depends on size)
# was in the past triggered by:
# - clicking "add row" very fast at transition from no vertical scrollbar to vertical scrollbar visible
# - add columns until horizontal scrollbar appears, remove column so that horizointal scrollbar disappears again, add rows until vertical scrollbar should appear
class ScrollableFrame(tk.Frame):
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Enter>', self._bindMousewheel)
self.canvas.bind('<Leave>', self._unbindMousewheel)
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# geometry manager
for method in GM_METHODS_TO_BE_CALLED_ON_CHILD:
setattr(self, method, getattr(self._parentFrame, method))
def _bindMousewheel(self, event):
# Windows
self.bind_all('<MouseWheel>', self._onMousewheel)
# Linux
self.bind_all('<Button-4>', self._onMousewheel)
self.bind_all('<Button-5>', self._onMousewheel)
def _unbindMousewheel(self, event):
# Windows
self.unbind_all('<MouseWheel>')
# Linux
self.unbind_all('<Button-4>')
self.unbind_all('<Button-5>')
def _onMousewheel(self, event):
if event.delta < 0 or event.num == 5:
dy = +1
elif event.delta > 0 or event.num == 4:
dy = -1
else:
assert False
if (dy < 0 and self.canvas.yview()[0] > 0.0) \
or (dy > 0 and self.canvas.yview()[1] < 1.0):
self.canvas.yview_scroll(dy, tk.UNITS)
return 'break'
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
def updateScrollregion(self):
# a function called with self.bind('<Configure>', ...) is called when resized or scrolled but *not* when widgets are added or removed (is called when real widget size changes but not when required/requested widget size changes)
# => useless for calling this function
# => this function must be called manually when adding or removing children
# The content has changed.
# Therefore I need to adapt the size of self.
# I need to update before measuring the size.
# It does not seem to make a difference whether I use update_idletasks() or update().
# Therefore according to Bryan Oakley I better use update_idletasks https://stackoverflow.com/a/29159152
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
# update scrollregion
self._updateScrollregion()
def setWidth(self, width):
print("setWidth(%s)" % width)
self.canvas.configure( width = width )
def setHeight(self, height):
print("setHeight(%s)" % width)
self.canvas.configure( height = height )
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ==================== TEST ====================
if __name__ == '__main__':
class Test(object):
BG_COLOR = 'white'
PAD_X = 1
PAD_Y = PAD_X
# ---------- initialization ----------
def __init__(self):
self.root = tk.Tk()
self.buttonFrame = tk.Frame(self.root)
self.buttonFrame.pack(side=tk.TOP)
self.scrollableFrame = ScrollableFrame(self.root, bg=self.BG_COLOR)
self.scrollableFrame.pack(side=tk.TOP, expand=tk.YES, fill=tk.BOTH)
self.scrollableFrame.grid_columnconfigure(0, weight=1)
self.scrollableFrame.grid_rowconfigure(0, weight=1)
self.contentFrame = tk.Frame(self.scrollableFrame, bg=self.BG_COLOR)
self.contentFrame.grid(row=0, column=0, sticky=tk.NSEW)
self.labelRight = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="right")
self.labelRight.grid(row=0, column=1)
self.labelBottom = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="bottom")
self.labelBottom.grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add row", command=self.addRow).grid(row=0, column=0)
tk.Button(self.buttonFrame, text="remove row", command=self.removeRow).grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add column", command=self.addColumn).grid(row=0, column=1)
tk.Button(self.buttonFrame, text="remove column", command=self.removeColumn).grid(row=1, column=1)
self.row = 0
self.col = 0
def start(self):
self.addRow()
widget = self.contentFrame.grid_slaves()[0]
width = widget.winfo_width() + 2*self.PAD_X + self.labelRight.winfo_width()
height = 4.9*( widget.winfo_height() + 2*self.PAD_Y ) + self.labelBottom.winfo_height()
#TODO: why is size saved in event different from what I specify here?
self.scrollableFrame.setSize(width, height)
# ---------- add ----------
def addRow(self):
if self.col == 0:
self.col = 1
columns = self.col
for col in range(columns):
button = self.addButton(self.row, col)
self.row += 1
self._onChange()
def addColumn(self):
if self.row == 0:
self.row = 1
rows = self.row
for row in range(rows):
button = self.addButton(row, self.col)
self.col += 1
self._onChange()
def addButton(self, row, col):
button = tk.Button(self.contentFrame, text = '--------------------- %d, %d ---------------------' % (row, col))
# note that grid(padx) seems to behave differently from grid_columnconfigure(pad):
# grid : padx = "Optional horizontal padding to place around the widget in a cell."
# grid_rowconfigure: pad = "Padding to add to the size of the largest widget in the row when setting the size of the whole row."
# http://effbot.org/tkinterbook/grid.htm
button.grid(row=row, column=col, sticky=tk.NSEW, padx=self.PAD_X, pady=self.PAD_Y)
# ---------- remove ----------
def removeRow(self):
if self.row <= 0:
return
self.row -= 1
columns = self.col
if columns == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['row'] == self.row:
button.destroy()
self._onChange()
def removeColumn(self):
if self.col <= 0:
return
self.col -= 1
rows = self.row
if rows == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['column'] == self.col:
button.destroy()
self._onChange()
# ---------- other ----------
def _onChange(self):
print("=========== user action ==========")
print("new size: %s x %s" % (self.row, self.col))
self.scrollableFrame.updateScrollregion()
def mainloop(self):
self.root.mainloop()
test = Test()
test.start()
test.mainloop()
EDIT: I do not think that this is a duplicate of this question. The answer to that question is certainly a good starting point if you don't know how to start. It explains the basic concept of how to handle scrollbars in Tkinter. That however, is not my problem. I think that I am aware of the basic idea and I think that I have implemented that.
I have noticed that the answer mentions the possibility of directly drawing on the canvas instead of putting a frame on it. However, I would like to have a reusable solution.
My problem is that when I tried to implement that the content shall fill the frame (like with pack(expand=tk.YES, fill=tk.BOTH)) if the req size is smaller than the size of the canvas the three above listed weird effects occured which I do not understand. Most importantly that is that the program runs into an infinite loop when I add and remove rows as described (without changing the window size).
EDIT 2: I have reduced the code even further:
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
class ScrollableFrame(tk.Frame):
# ---------- initialization ----------
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# ---------- setter ----------
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ---------- listen to GUI ----------
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
# ---------- listen to model ----------
def updateScrollregion(self):
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
self._updateScrollregion()
# ---------- internal ----------
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
# ==================== TEST ====================
if __name__ == '__main__':
labels = list()
def createLabel():
print("========= create label =========")
l = tk.Label(frame, text="test %s" % len(labels))
l.pack(anchor=tk.W)
labels.append(l)
frame.updateScrollregion()
def removeLabel():
print("========= remove label =========")
labels[-1].destroy()
del labels[-1]
frame.updateScrollregion()
root = tk.Tk()
tk.Button(root, text="+", command=createLabel).pack()
tk.Button(root, text="-", command=removeLabel).pack()
frame = ScrollableFrame(root, bg="white")
frame._parentFrame.pack(expand=tk.YES, fill=tk.BOTH)
createLabel()
frame.setSize(labels[0].winfo_width(), labels[0].winfo_height()*5.9)
#TODO: why is size saved in event object different from what I have specified here?
root.mainloop()
procedure to reproduce the infinite loop is unchanged:
click "+" until the vertical scrollbar appears, click "-" once so that vertical scrollbar disappears again, click "+" again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
to reproduce the jump in size:
click "+" until horizontal [!] scrollbar appears (the window height then increases by the size of the scrollbar, which is ok). Increase width of window until horizontal scrollbar disappears. The height [!] of the window increases with a jump.
to reproduce that the canvas is not filled:
comment out the line which calls frame.setSize. Click "+" until vertical scrollbar appears.
Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as wide as the scrollbar. When clicking "+" or "-" or resizing the window the red area disappears and does not seem to appear again.
Related
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 want that the frame comes back after it moves to right. how can i do that? or the frames goes to the right side and comes from the left side, to have a slide effect?
def move(steps=10, distance=0.10, distance2=0.10):
if steps > 10:
# get current position
relx = float(frame.place_info()['relx'])
# set new position
frame.place_configure(relx=relx+distance)
# repeate it after 10ms
window.after(10, move, steps-1, distance)
def on_click():
# start move
move(50, 0.02) # 50*0.02 = 1
frame = Frame(window, bg="red", width=400, height=400)
frame.place(relx=0.192, rely=0.001, relwidth=0.81, relheight=0.992)
i tried a few options but i can't find a way, to bring the frame back in the old postion after it's moving. have someone any ideas?
enter image description here
enter image description here
Try this:
import tkinter as tk
def move_frame(event=None):
# 1 is the change in x and the 0 is the change in y
canvas.move(frame_id, 1, 0)
# Look at: https://stackoverflow.com/a/2679823/11106801
x, *_ = canvas.coords(frame_id)
# Note 400 is the width of the canvas
if x > 400:
canvas.move(frame_id, -400, 0)
else:
# To adjust the speed change the 3 (it's the time in ms)
# It calls `move_frame` after 3 ms
canvas.after(3, move_frame)
root = tk.Tk()
# Create the canvas
canvas = tk.Canvas(root, width=400, height=400, bg="black")
canvas.pack()
frame = tk.Frame(canvas, width=100, height=100, bg="grey")
# Look at: https://stackoverflow.com/a/11981214/11106801
frame_id = canvas.create_window(0, 150, anchor="nw", window=frame)
# If you want it to move when clicked:
frame.bind("<Button-1>", move_frame)
# To just start the movement:
canvas.after(0, move_frame)
# Make sure you start the mainloop otherwise the loop woudn't work
root.mainloop()
It uses a tkinter loop to move the object on the canvas.
Something like the below would work. The concept is simple: The relx or rely of place keeps changing until the Frame has reached it's ultimate "rel-position". We use a little ratio math to make sure the frame is pushed completely out of the display and brought back to the desired "rel-position". I was bored so I made it work in every direction.
import tkinter as tk
class SlideFrame(tk.Frame):
def __init__(self, master, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.__slide = None
def slide_in(self, ult_rel=0, steps=20, side='left', inc=None):
#force everything to be perfect no matter what
if inc is None:
self.update_idletasks()
#determine the ratio of self to master in the proper direction
if side in ('left', 'right'):
r = self.winfo_width()/self.master.winfo_width()
elif side in ('top', 'bottom'):
r = self.winfo_height()/self.master.winfo_height()
#determine the extra amount to move beyond ratio
ex = ult_rel if side in ('left', 'top') else r-ult_rel
#divide the sum by steps to get a move increment
inc = (r+ex)/steps
#get the proper polarity to multiply by
s = -steps if side in ('left', 'top') else steps
#get new position
p = ult_rel+(s*inc)
#determine which property to apply the move to
self.place_configure(relx=p) if side in ('left', 'right') else self.place_configure(rely=p)
#as long as there are steps left, keep running this method
if steps > 0:
self.slide = self.after(20, self.slide_in, ult_rel, steps-1, side, inc)
def place_(self, ult_rel=0, side='left', steps=20, **kwargs):
#use this little trick and you can do everything on one line
self.place(**kwargs)
self.slide_in(ult_rel, steps, side)
return self
root = tk.Tk()
root.geometry('800x600+100+100')
root.configure(background='#111111')
root.update_idletasks()
dims=dict(relwidth=.5, relheight=.5)
#demo
frame1 = SlideFrame(root, bg='#ff0000').place_( 0, rely= 0, **dims)
frame2 = SlideFrame(root, bg='#00ff00').place_( 0, 'top' , relx=.5, **dims)
frame3 = SlideFrame(root, bg='#0000ff').place_(.5, 'right' , rely=.5, **dims)
frame4 = SlideFrame(root, bg='#ff8800').place_(.5, 'bottom', relx= 0, **dims)
if __name__ == '__main__':
root.mainloop()
Here my code for a very simple gui:
from Tkinter import *
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
# create frame (gray window)
self.frame=Frame(self.root,width=100,height=100)
self.frame.grid(row=0,column=0)
self.__add_scroll_bars()
self.__create_canvas()
self.__add_plot()
def __create_canvas(self):
# create white area in the window for plotting
# width and height are only the visible size of the white area, scrollregion is the area the user can see by scrolling
self.canvas = Canvas(self.frame,bg='#FFFFFF',width=300,height=300,scrollregion=(0,0,500,500))
# with this command the window is filled with the canvas
self.canvas.pack(side=LEFT,expand=True,fill=BOTH)
# position and size of the canvas is used for configuration of the scroll bars
self.canvas.config(xscrollcommand=self.hbar.set, yscrollcommand=self.vbar.set)
# add command to the scroll bars to scroll the canvas
self.hbar.config(command = self.canvas.xview)
self.vbar.config(command = self.canvas.yview)
def __add_scroll_bars(self):
# add scroll bars
self.hbar=Scrollbar(self.frame,orient=HORIZONTAL)
self.hbar.pack(side=BOTTOM,fill=X)
self.vbar=Scrollbar(self.frame,orient=VERTICAL)
self.vbar.pack(side=RIGHT,fill=Y)
def __add_plot(self):
# create a rectangle
self.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()
I have two questions:
1) I want if I resize the gui, that then the scrollbars are always on the Ends of the gui and I resize the canvas.
2) If I resize the GUI and the canvas, then the rectangle in the canvas shall be resized (for example if the new size of gui and canvas is four times the old size, then the new size of rectangle is twize the old size).
I search a solution for the first problem and for the second problem seperately.
Thanks for help.
You could use the following way to integrate my frame into your gui class:
from Tkinter import *
class ScrollableFrame(Frame):
def __init__(self, parent, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
self.canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
self.canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = self.canvas.yview)
hscrollbar.config(command = self.canvas.xview)
# set the view to 0,0 at initialization
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create an interior frame to be created inside the canvas
self.interior = interior = Frame(self.canvas)
interior_id = self.canvas.create_window(0, 0, window=interior,
anchor=NW)
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
self.canvas.config(scrollregion='0 0 %s %s' % size)
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width = interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
# create frame (gray window)
self.frame = ScrollableFrame(self.root)
self.frame.pack(fill=BOTH, expand=YES)
#self.__add_scroll_bars()
#self.__create_canvas()
self.__add_plot()
def __add_plot(self):
# create a rectangle
self.frame.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()
This should essentially solve your first problem. As for the second problem you'll need to create a function to re-render the canvas every time you resize it. In a way similar to the _configure_interior function.
You could use this following example, or integrate it in your class.
You could create a frame like this by calling.
self.frame = ScrollableFrame(self.root)
self.frame.pack(fill=BOTH, expand=YES)
Create a class like this for your frame:
from Tkinter import *
class ScrollableFrame(Frame):
'''
Creates a scrollable frame
'''
def __init__(self, parent, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = canvas.yview)
hscrollbar.config(command = canvas.xview)
# set the view to 0,0 at initialization
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create an interior frame to be created inside the canvas
self.interior = interior = Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor=NW)
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion='0 0 %s %s' % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width = interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
You could use this to obtain the result you want. Horizontal and Vertical scrolling are both enabled for this frame and scrollbar positions can be set using 'side' field.
For the second part of your question, could you elucidate further.
Reference: Gonzo's answer
Python Tkinter scrollbar for frame
This works very well, to get what I want with the minimal scrollable canvas size. But there is still the bug, when the gui was made larger and when it seems so, that one can not scroll, there is the possibility to click on the left or upper arrow of the scroll bars and so to scroll the canvas, what sould not be possible.
from Tkinter import *
class ScrollableFrame(Frame):
def __init__(self, parent, minimal_canvas_size, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
self.minimal_canvas_size = minimal_canvas_size
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
self.canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
self.canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = self.canvas.yview)
hscrollbar.config(command = self.canvas.xview)
# set the view to 0,0 at initialization
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
self.canvas.config(scrollregion='0 0 %s %s' % self.minimal_canvas_size)
# create an interior frame to be created inside the canvas
self.interior = interior = Frame(self.canvas)
interior_id = self.canvas.create_window(0, 0, window=interior,
anchor=NW)
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (max(interior.winfo_reqwidth(), self.minimal_canvas_size[0]), max(interior.winfo_reqheight(), self.minimal_canvas_size[1]))
self.canvas.config(scrollregion='0 0 %s %s' % size)
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width = interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
minimal_canvas_size = (500, 500)
# create frame (gray window)
self.frame = ScrollableFrame(self.root, minimal_canvas_size)
self.frame.pack(fill=BOTH, expand=YES)
self.__add_plot()
def __add_plot(self):
# create a rectangle
self.frame.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()
Hey guys I'm working on a image reading GUI using Tkinter, trying to make a auto resize frame in canvas, and it's scrollbar should renew when new image add into frame.
I found a really helpful example on web and I modified it to a more general one:
import Tkinter
from PIL import Image, ImageTk
root = Tkinter.Tk()
root.geometry("%dx%d+%d+%d" % (1400,807,0,0))
scrollable = ScrollableFrame(root)
scrollable.pack( fill = Tkinter.BOTH)
image_container = []
for i in range (50):
# Here's the problem! Scrollbar works properly, but after scroll down to 50000
# units, is screen stop at 50000, and when scroll up back to lower than 50000,
# it works again !!!!
img = ImageTk.PhotoImage( Image.open("%s.jpg"%(i)) ) )
image_container.append( Tkinter.Label( scrollable.interior, image = img )
image_container[ len( image_container ) -1 ].image = img
class ScrollableFrame(Tkinter.Frame):
def __init__(self, parent, *args, **kw):
self.set_parameter( kw )
Tkinter.Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
self.scrollbar = Tkinter.Scrollbar(self, orient= self.par["orient"])
self.scrollbar.pack(fill= self.par["scroll_fill"], side= self.par["scroll_side"], expand=Tkinter.FALSE)
self.canvas = Tkinter.Canvas(self, bd=0, highlightthickness=0, bg = self.par["canvas_bg"] )
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
self.canvas.config(yscrollcommand = self.scrollbar.set )
self.scrollbar.config(command=self.canvas.yview)
else:
self.canvas.config(xscrollcommand = self.scrollbar.set )
self.scrollbar.config(command=self.canvas.xview)
self.canvas.pack(side= self.par["canvas_side"], fill=Tkinter.BOTH, expand=Tkinter.TRUE)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Tkinter.Frame(self.canvas, bg = self.par["canvas_bg"])
interior_id = self.canvas.create_window(0, 0, window=interior,
anchor="nw")
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
print size, "interior"
self.canvas.config(scrollregion="0 0 %s %s" % size)
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width=interior.winfo_reqwidth())
else:
if interior.winfo_reqheight() != self.canvas.winfo_height():
# update the canvas's width to fit the inner frame
self.canvas.config(height=interior.winfo_reqheight())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
print "canvas"
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(interior_id, width=self.canvas.winfo_width())
else:
if interior.winfo_reqheight() != self.canvas.winfo_height():
# update the inner frame's height to fill the canvas
self.canvas.itemconfigure(interior_id, height=self.canvas.winfo_height())
self.canvas.bind('<Configure>', _configure_canvas)
def set_parameter(self, options):
self.par = {"orient": "vertical",
"canvas_bg":"white"}
if "orient" in options.keys():
self.par["orient"] = options["orient"]
options.pop("orient")
if "canvas_bg" in options.keys():
self.par["canvas_bg"] = options["canvas_bg"]
options.pop("canvas_bg")
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
self.par["scroll_fill"] = Tkinter.Y
self.par["scroll_side"] = Tkinter.RIGHT
self.par["canvas_side"] = Tkinter.LEFT
else:
self.par["scroll_fill"] = Tkinter.X
self.par["scroll_side"] = Tkinter.BOTTOM
self.par["canvas_side"] = Tkinter.TOP
This work fine at first, but when I load about 40 images, the frame height goes over 50000( units) and the configure don't works! Just can't find out what's wrong here, is there a system limit height?
I know you can get frame widgets to expand and fill all of the area available to them in their container via these commands: frameName.pack(fill = 'both', expand = True)
What would do the same for a text's font size? Currently my text is an attribute of a label widget. The label widget's parent is frameName.
I guess I could define my own function to call labelName.config(fontsize = N) to update the font size as the frame get's bigger, but I'm not sure how to correlate them.
This is what my program looks like right now:
Each of those blocks is a frame widget. I'd like the text to expand to fill up in some capacity the frame, and respond to resizing of the window as well.
You can use tkFont.font
When you initialize the label set the font to a variable such as:
self.font = SOME_BASE_FONT
self.labelName.config(font = self.font)
Then you can use:
self.font = tkFont.Font(size = PIXEL_HEIGHT)
This you can scale to the height of the label. You can bind a '<Configure>' Event to the widget, and make your callback function adjust the label size.
frameName.bind('<Configure>', self.resize)
def resize(self, event):
self.font = tkFont(size = widget_height)
For more info see the documentation here.
I've been trying to figure out how to get the text to automatically resize in tkinter.
The key to getting it working for me was to assign the calculated height to the size in the custom font object. Like so: self.label_font['size'] = height
Full example:
from tkinter import font
import tkinter as tk
class SimpleGUIExample:
def __init__(self, master):
self.master = master
self.master.title("A simple Label")
self.master.bind('<Configure>', self.resize)
self.label_font = font.Font(self.master, family='Arial', size=12, weight='bold')
self.label = tk.Label(self.master, text="Simple Label Resizing!")
self.label.config(font=self.label_font)
self.label.pack(fill=tk.BOTH, expand=tk.YES)
self.close_button = tk.Button(self.master, text="Close", command=master.quit)
self.close_button.pack()
def resize(self, event):
height = self.label.winfo_height()
width = self.label.winfo_width()
height = height // 2
print('height %s' % height)
print('width %s' % width)
if height < 10 or width < 200:
height = 10
elif width < 400 and height > 20:
height = 20
elif width < 600 and height > 30:
height = 30
else:
height = 40
print('height %s' % height)
self.label_font['size'] = height
print(self.label_font.actual())
root = tk.Tk()
simple_gui = SimpleGUIExample(root)
root.mainloop()