When Tkinter Frame class too long, event "configure" stop tracking it - python

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?

Related

Tkinter buttons getting hidden behind other frames

I am trying to create a battlemap for dnd (picture) with adjustable grid and movable enemy/creature tokens. The idea is to drag one of the token from the right onto the map on the left.
The window is made of 3 frames. The frame for the map, the frame for the "new map" button and slider. And then frame for the tokens, which are buttons tiled using button.grid()
I found a drag and drop system here that I'm using to drag the tokens. However, when I bring them over the map, they go behind it and you can't see them (I know they go behind because they can be partially visible between the two frames). Is there any way to bring them to the front?
import tkinter as tk
class DragManager():
def add_dragable(self, widget):
widget.bind("<ButtonPress-1>", self.on_start)
widget.bind("<B1-Motion>", self.on_drag)
widget.bind("<ButtonRelease-1>", self.on_drop)
widget.configure(cursor="hand1")
def on_start(self, event):
# you could use this method to create a floating window
# that represents what is being dragged.
pass
def on_drag(self, event):
# you could use this method to move a floating window that
# represents what you're dragging
event.widget.place(x=event.x_root + event.x, y= event.y_root + event.y)
#when button is dropped, create a new one where this one originally was
def on_drop(self, event):
# find the widget under the cursor
x,y = event.widget.winfo_pointerxy()
target = event.widget.winfo_containing(x,y)
try:
target.configure(image=event.widget.cget("image"))
except:
pass
if x > window.winfo_screenwidth() - 200:
del event.widget
return
if not event.widget.pure:
return
button = tk.Button(master=entity_select_frame, text = "dragable", borderwidth=1, compound="top")
#avoiding garbage collection
button.gridx = event.widget.gridx
button.gridy = event.widget.gridy
button.grid(row = event.widget.gridx, column = event.widget.gridy)
button.grid()
button.pure = True
dnd.add_dragable(button)
window = tk.Tk()
window.geometry("1000x800")
map_frame = tk.Frame()
controls_frame = tk.Frame(width=200, borderwidth=1, relief=tk.RAISED)
tk.Label(master=controls_frame, text="controls here").pack()
entity_select_frame = tk.Frame(width=200, relief=tk.RAISED, borderwidth=1)
dnd = DragManager()
button = tk.Button(master=entity_select_frame, text = "dragable", borderwidth=1)
button.gridx = 0
button.gridy = 0
button.grid(row = 0, column = 0)
button.pure = True
dnd.add_dragable(button)
map_frame.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
controls_frame.pack(fill=tk.BOTH)
entity_select_frame.pack(fill=tk.BOTH)
window.mainloop()
I played around a little bit and used stuff from this post. I did not structure it as a class and I used the picture frame as my root-frame and put the control-frame inside that. I'm not sure how this would be best combined with your "draw-grid", "token" functionalities etc., however I hope it helps. I did not find a way to drag widgets across frames though (tried to set a new master for the button, recreate it after dropping it etc.). Get the image used in my code from here.
from tkinter import Tk, Frame, Label, Button, Canvas, font
from tkinter import ttk
from PIL import Image, ImageTk
root = Tk()
""" ####################### Configuration parameters ###################### """
image_file_path = "Island_AngelaMaps-1024x768.jpg"
resize_img = False # set to True if you want to resize the image > window size
resize_to = (600, 600) # resolution to rescale image to
""" ####################### Drag and drop functionality ################### """
def make_draggable(widget):
widget.bind("<Button-1>", on_drag_start)
widget.bind("<B1-Motion>", on_drag_motion)
def on_drag_start(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def on_drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
""" ################################# Layout ############################## """
# picture frame with picture as background
picture_frame = Frame(root)
picture_frame.pack(side="left", anchor="w", fill="both", expand=True)
# load the image
if resize_img:
img = ImageTk.PhotoImage(Image.open(image_file_path).resize(resize_to, Image.ANTIALIAS))
else:
img = ImageTk.PhotoImage(Image.open(image_file_path))
# create canvas, set canvas background to the image
canvas = Canvas(picture_frame, width=img.width(), height=img.height())
canvas.pack(side="left")
canvas.background = img # Keep a reference in case this code is put in a function.
bg = canvas.create_image(0, 0, anchor="nw", image=img)
# subframe inside picture frame for controls
ctrl_subframe = Frame(picture_frame)
ctrl_subframe.pack(side="right", anchor="n")
# separator between picture and controls, inside picture frame
ttk.Separator(picture_frame, orient="vertical").pack(side="right", fill="y")
# underlined label 'Controls' in subframe
ctrl_header = Label(ctrl_subframe, text="Controls", font=("Arial", 10, "bold"))
f = font.Font(ctrl_header, ctrl_header.cget("font"))
f.configure(underline=True)
ctrl_header.configure(font=f)
ctrl_header.pack(side="top", pady=2)
# update window to get proper sizes from widgets
root.update()
# a draggable button, placed below ctrl_header
# (based on X of ctrl_subframe and height of ctrl_header, plus padding)
drag_button = Button(picture_frame, text="Drag me", bg="green", width=6)
drag_button.place(x=ctrl_subframe.winfo_x()+2, y=ctrl_header.winfo_height()+10)
make_draggable(drag_button)
""" ################################ Mainloop ############################# """
root.mainloop()

performing operations on images uploaded by tkinter

I am new to Tkinter and python. I am trying to upload two images and then perform some operations on them. The problem is that the Window class is loading all at once or the code in running parallel, so the images uploaded after that have been already assigned to None since they were uploaded later in the ScrollableFrame class and did not have a value earlier.
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk, ImageOps
import os
# ************************
# Scrollable Frame Class
# ************************
row=0
column=0
imagePaths = []
#Class to generate a frame to add to the GUI with vertical and horizontal scroll bars
class ScrollableFrame(Frame):
#The Constructor method for the class
def __init__(self, parent , *args, **kw):
Frame.__init__(self, parent, *args, **kw)
#Defining the position of the frame grid
self.grid(row = row , column = column)
self.image = None
self.imageFile = None
#Defining the vertical scroll bar
vscrollbar = Scrollbar(self, orient=VERTICAL)
vscrollbar.grid(row=row, column=column+1, sticky=N+S)
#Defining the horizontal scroll bar
hscrollbar = Scrollbar(self, orient = 'horizontal')
hscrollbar.grid(row=row+1, column=column, sticky=E+W)
#Defining the canvas to put the scroll bars on
canvas = Canvas(self, bd=0, highlightthickness=0, yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set)
canvas.grid(row=row, column=column, sticky = N+S+E+W)
canvas.config( width=800, height = 800 )
#Defining the scrolling commands (vertically and horizontally )
vscrollbar.config(command=canvas.yview)
hscrollbar.config(command=canvas.xview)
#Defining the scroll region where the scrolling is active
canvas.config(scrollregion= (0,0,1280,1024))
self.canvas = canvas
def openImage(self):
#Getting the path of the image
imageFile = filedialog.askopenfilename(initialdir=os.getcwd(),title="Select BMP File",filetypes=[("BMP Files",("*.bmp",".png",".jpg",".jpeg",".tif",".tiff"))])
#Assigning the image value to this frame object
self.imageFile = imageFile
if not imageFile:
return
def showImage(self):
#Getting the path of the image
imageFile = filedialog.askopenfilename(initialdir=os.getcwd(),title="Select BMP File",filetypes=[("BMP Files",("*.bmp",".png",".jpg",".jpeg",".tif",".tiff"))])
#Assigning the image value to this frame object
self.imageFile = imageFile
if not imageFile:
return
#Checking for the extension of the image
filename, file_extension = os.path.splitext(imageFile)
#If it is a .bmp, this means that it is an HD image, where we can directly display it
if file_extension == '.bmp':
imageToDisplay = Image.open(imageFile)
#border = (0, 0, 0, 66) #Decide on the area you want to crop in terms of no. pixels: left, up, right, bottom
#ImageOps.crop(imageToDisplay, border)
img = ImageTk.PhotoImage(imageToDisplay)
self.image = img
#print ("Done conversion")
self.canvas.create_image(row, column, image=self.image, anchor=NW)
class Window(Frame):
def __init__(self, master=None):
global row, column,imagePaths
Frame.__init__(self, master)
self.master = master
self.pos = []
self.master.title("BMP Image GUI")
self.pack(fill=BOTH, expand=1)
self.label = Label(self, text="Instructions: \n 1. Open the HD image. \n 2. Open the EBSD image. \n 3. Open the Color Map image.", anchor=W, justify=LEFT)
self.label.place(x=1640, y=0)
menu = Menu(self.master)
self.master.config(menu=menu)
self.frame1 = ScrollableFrame(self)
row=0
column=1
self.frame2 = ScrollableFrame(self)
# File Bar
file = Menu(menu)
file.add_command(label="Open HD image", command=self.frame1.showImage)
img = Image.open("original.bmp")
HD = self.frame2.imageFile
file.add_command(label="Open EBSD image", command=self.frame2.openImage)
EBSD = self.frame2.imageFile
print (HD)
print (EBSD)
root = tk.Tk()
root.geometry("%dx%d" % (1670, 1024))
root.title("BMP Image GUI")
app = Window(root)
app.pack(fill=tk.BOTH, expand=1)
#print (HD)
root.mainloop()
So printing the HD and EBSD images is giving None. What I am aiming to to make them get the actual value assigned after the upload.
This is a lot of code and it doesn't run. It also has a few problems. When you are coding complex applications it is best to do it one little piece at a time or you'll have problems finding the problems. Here are a few:
Don't use global variables in an object oriented application. The names row, column and imagePaths should belong to either of the two classes.
The menu doesn't work because you have not implemented it correctly:
file = Menu(menu)
menu.add_cascade(label='File', menu=file) # You need this for it to work
file.add_command(label="Open HD image", command=self.frame1.showImage)
# etc...
You are packing app twice, once in it's __init__() function and once after it's been created (in the global scope).
The scrollable frames are packed in front of the Label with instructions so you can't see it.
Try fixing these problems by writing components, and when each component works then combine them. If there is a problem with any of the components, or if everything works but for one thing, come back here and we will be able to give you a better answer.

Scrollable Frame with fill in Python Tkinter

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.

resizeable scrollable canvas with tkinter

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

Python/Tkinter: expanding fontsize dynamically to fill frame

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

Categories

Resources